Free cookie consent management tool by TermsFeed Policy Generator

source: trunk/HeuristicLab.Problems.DataAnalysis.Symbolic/3.4/Importer/InfixExpressionParser.cs @ 18082

Last change on this file since 18082 was 17902, checked in by gkronber, 4 years ago

#3073 merged reintegration branch to trunk

File size: 22.4 KB
RevLine 
[14024]1#region License Information
2/* HeuristicLab
[17180]3 * Copyright (C) Heuristic and Evolutionary Algorithms Laboratory (HEAL)
[14024]4 *
5 * This file is part of HeuristicLab.
6 *
7 * HeuristicLab is free software: you can redistribute it and/or modify
8 * it under the terms of the GNU General Public License as published by
9 * the Free Software Foundation, either version 3 of the License, or
10 * (at your option) any later version.
11 *
12 * HeuristicLab is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 * GNU General Public License for more details.
16 *
17 * You should have received a copy of the GNU General Public License
18 * along with HeuristicLab. If not, see <http://www.gnu.org/licenses/>.
19 */
20#endregion
21
22using System;
23using System.Collections.Generic;
24using System.Globalization;
25using System.Linq;
26using System.Text;
27using HeuristicLab.Collections;
[14350]28using HeuristicLab.Common;
[14024]29using HeuristicLab.Encodings.SymbolicExpressionTreeEncoding;
30
31namespace HeuristicLab.Problems.DataAnalysis.Symbolic {
32  /// <summary>
33  /// Parses mathematical expressions in infix form. E.g. x1 * (3.0 * x2 + x3)
34  /// Identifier format (functions or variables): '_' | letter { '_' | letter | digit }
[14826]35  /// Variables names and variable values can be set under quotes "" or '' because variable names might contain spaces.
36  ///   Variable = ident | " ident " | ' ident '
[14024]37  /// It is also possible to use functions e.g. log("x1") or real-valued constants e.g. 3.1415 .
38  /// Variable names are case sensitive. Function names are not case sensitive.
[14826]39  ///
40  ///
41  /// S             = Expr EOF
42  /// Expr          = ['-' | '+'] Term { '+' Term | '-' Term }
43  /// Term          = Fact { '*' Fact | '/' Fact }
[16356]44  /// Fact          = SimpleFact [ '^' SimpleFact ]
45  /// SimpleFact    = '(' Expr ')'
46  ///                 | '{' Expr '}'
47  ///                 | 'LAG' '(' varId ',' ['+' | '-' ] number ')
48  ///                 | funcId '(' ArgList ')'
49  ///                 | VarExpr
50  ///                 | number
[14826]51  /// ArgList       = Expr { ',' Expr }
52  /// VarExpr       = varId OptFactorPart
53  /// OptFactorPart = [ ('=' varVal | '[' ['+' | '-' ] number {',' ['+' | '-' ] number } ']' ) ]
54  /// varId         =  ident | ' ident ' | " ident "
55  /// varVal        =  ident | ' ident ' | " ident "
56  /// ident         =  '_' | letter { '_' | letter | digit }
[14024]57  /// </summary>
[14026]58  public sealed class InfixExpressionParser {
[14826]59    private enum TokenType { Operator, Identifier, Number, LeftPar, RightPar, LeftBracket, RightBracket, Comma, Eq, End, NA };
[14024]60    private class Token {
61      internal double doubleVal;
62      internal string strVal;
63      internal TokenType TokenType;
64    }
65
[17902]66    private class SymbolComparer : IEqualityComparer<ISymbol>, IComparer<ISymbol> {
[14024]67      public int Compare(ISymbol x, ISymbol y) {
68        return x.Name.CompareTo(y.Name);
69      }
70
71      public bool Equals(ISymbol x, ISymbol y) {
[17902]72        return x.GetType() == y.GetType();
[14024]73      }
74
75      public int GetHashCode(ISymbol obj) {
[17902]76        return obj.GetType().GetHashCode();
[14024]77      }
78    }
79    // format name <-> symbol
80    // the lookup table is also used in the corresponding formatter
81    internal static readonly BidirectionalLookup<string, ISymbol>
[17902]82      knownSymbols = new BidirectionalLookup<string, ISymbol>(StringComparer.InvariantCulture, new SymbolComparer());
[14024]83
84    private Constant constant = new Constant();
85    private Variable variable = new Variable();
[14826]86    private BinaryFactorVariable binaryFactorVar = new BinaryFactorVariable();
87    private FactorVariable factorVar = new FactorVariable();
[14024]88
89    private ProgramRootSymbol programRootSymbol = new ProgramRootSymbol();
90    private StartSymbol startSymbol = new StartSymbol();
91
92    static InfixExpressionParser() {
93      // populate bidirectional lookup
94      var dict = new Dictionary<string, ISymbol>
95      {
96        { "+", new Addition()},
97        { "/", new Division()},
98        { "*", new Multiplication()},
99        { "-", new Subtraction()},
[16356]100        { "^", new Power() },
101        { "ABS", new Absolute() },
[14024]102        { "EXP", new Exponential()},
103        { "LOG", new Logarithm()},
[16359]104        { "POW", new Power() },
[14024]105        { "ROOT", new Root()},
106        { "SQR", new Square() },
107        { "SQRT", new SquareRoot() },
[16356]108        { "CUBE", new Cube() },
109        { "CUBEROOT", new CubeRoot() },
[14024]110        { "SIN",new Sine()},
111        { "COS", new Cosine()},
112        { "TAN", new Tangent()},
[16656]113        { "TANH", new HyperbolicTangent()},
[14024]114        { "AIRYA", new AiryA()},
115        { "AIRYB", new AiryB()},
116        { "BESSEL", new Bessel()},
117        { "COSINT", new CosineIntegral()},
118        { "SININT", new SineIntegral()},
119        { "HYPCOSINT", new HyperbolicCosineIntegral()},
120        { "HYPSININT", new HyperbolicSineIntegral()},
121        { "FRESNELSININT", new FresnelSineIntegral()},
122        { "FRESNELCOSINT", new FresnelCosineIntegral()},
123        { "NORM", new Norm()},
124        { "ERF", new Erf()},
125        { "GAMMA", new Gamma()},
126        { "PSI", new Psi()},
127        { "DAWSON", new Dawson()},
128        { "EXPINT", new ExponentialIntegralEi()},
[16360]129        { "AQ", new AnalyticQuotient() },
[14024]130        { "MEAN", new Average()},
131        { "IF", new IfThenElse()},
[14347]132        { "GT", new GreaterThan()},
133        { "LT", new LessThan()},
[14024]134        { "AND", new And()},
135        { "OR", new Or()},
136        { "NOT", new Not()},
137        { "XOR", new Xor()},
138        { "DIFF", new Derivative()},
[14350]139        { "LAG", new LaggedVariable() },
[14024]140      };
141
142
143      foreach (var kvp in dict) {
144        knownSymbols.Add(kvp.Key, kvp.Value);
145      }
146    }
147
148    public ISymbolicExpressionTree Parse(string str) {
149      ISymbolicExpressionTreeNode root = programRootSymbol.CreateTreeNode();
150      ISymbolicExpressionTreeNode start = startSymbol.CreateTreeNode();
151      var allTokens = GetAllTokens(str).ToArray();
152      ISymbolicExpressionTreeNode mainBranch = ParseS(new Queue<Token>(allTokens));
153
154      // only a main branch was given => insert the main branch into the default tree template
155      root.AddSubtree(start);
156      start.AddSubtree(mainBranch);
157      return new SymbolicExpressionTree(root);
158    }
159
160    private IEnumerable<Token> GetAllTokens(string str) {
161      int pos = 0;
162      while (true) {
163        while (pos < str.Length && Char.IsWhiteSpace(str[pos])) pos++;
164        if (pos >= str.Length) {
165          yield return new Token { TokenType = TokenType.End, strVal = "" };
166          yield break;
167        }
168        if (char.IsDigit(str[pos])) {
[14347]169          // read number (=> read until white space or operator or comma)
[14024]170          var sb = new StringBuilder();
171          sb.Append(str[pos]);
172          pos++;
173          while (pos < str.Length && !char.IsWhiteSpace(str[pos])
[14319]174            && (str[pos] != '+' || str[pos - 1] == 'e' || str[pos - 1] == 'E')     // continue reading exponents
[14024]175            && (str[pos] != '-' || str[pos - 1] == 'e' || str[pos - 1] == 'E')
[14319]176            && str[pos] != '*'
[14024]177            && str[pos] != '/'
[16356]178            && str[pos] != '^'
[14347]179            && str[pos] != ')'
[14826]180            && str[pos] != ']'
[16356]181            && str[pos] != '}'
[14347]182            && str[pos] != ',') {
[14024]183            sb.Append(str[pos]);
184            pos++;
185          }
186          double dblVal;
187          if (double.TryParse(sb.ToString(), NumberStyles.Float, CultureInfo.InvariantCulture, out dblVal))
188            yield return new Token { TokenType = TokenType.Number, strVal = sb.ToString(), doubleVal = dblVal };
189          else yield return new Token { TokenType = TokenType.NA, strVal = sb.ToString() };
190        } else if (char.IsLetter(str[pos]) || str[pos] == '_') {
191          // read ident
192          var sb = new StringBuilder();
193          sb.Append(str[pos]);
194          pos++;
195          while (pos < str.Length &&
196            (char.IsLetter(str[pos]) || str[pos] == '_' || char.IsDigit(str[pos]))) {
197            sb.Append(str[pos]);
198            pos++;
199          }
200          yield return new Token { TokenType = TokenType.Identifier, strVal = sb.ToString() };
201        } else if (str[pos] == '"') {
202          // read to next "
203          pos++;
204          var sb = new StringBuilder();
205          while (pos < str.Length && str[pos] != '"') {
206            sb.Append(str[pos]);
207            pos++;
208          }
209          if (pos < str.Length && str[pos] == '"') {
210            pos++; // skip "
211            yield return new Token { TokenType = TokenType.Identifier, strVal = sb.ToString() };
212          } else
213            yield return new Token { TokenType = TokenType.NA };
214
215        } else if (str[pos] == '\'') {
216          // read to next '
217          pos++;
218          var sb = new StringBuilder();
219          while (pos < str.Length && str[pos] != '\'') {
220            sb.Append(str[pos]);
221            pos++;
222          }
223          if (pos < str.Length && str[pos] == '\'') {
224            pos++; // skip '
225            yield return new Token { TokenType = TokenType.Identifier, strVal = sb.ToString() };
226          } else
227            yield return new Token { TokenType = TokenType.NA };
228        } else if (str[pos] == '+') {
229          pos++;
230          yield return new Token { TokenType = TokenType.Operator, strVal = "+" };
231        } else if (str[pos] == '-') {
232          pos++;
233          yield return new Token { TokenType = TokenType.Operator, strVal = "-" };
234        } else if (str[pos] == '/') {
235          pos++;
236          yield return new Token { TokenType = TokenType.Operator, strVal = "/" };
237        } else if (str[pos] == '*') {
238          pos++;
239          yield return new Token { TokenType = TokenType.Operator, strVal = "*" };
[16356]240        } else if (str[pos] == '^') {
241          pos++;
242          yield return new Token { TokenType = TokenType.Operator, strVal = "^" };
[14024]243        } else if (str[pos] == '(') {
244          pos++;
245          yield return new Token { TokenType = TokenType.LeftPar, strVal = "(" };
246        } else if (str[pos] == ')') {
247          pos++;
248          yield return new Token { TokenType = TokenType.RightPar, strVal = ")" };
[14826]249        } else if (str[pos] == '[') {
250          pos++;
251          yield return new Token { TokenType = TokenType.LeftBracket, strVal = "[" };
252        } else if (str[pos] == ']') {
253          pos++;
254          yield return new Token { TokenType = TokenType.RightBracket, strVal = "]" };
[16356]255        } else if (str[pos] == '{') {
256          pos++;
257          yield return new Token { TokenType = TokenType.LeftPar, strVal = "{" };
258        } else if (str[pos] == '}') {
259          pos++;
260          yield return new Token { TokenType = TokenType.RightPar, strVal = "}" };
[14826]261        } else if (str[pos] == '=') {
262          pos++;
263          yield return new Token { TokenType = TokenType.Eq, strVal = "=" };
[14347]264        } else if (str[pos] == ',') {
265          pos++;
266          yield return new Token { TokenType = TokenType.Comma, strVal = "," };
[14319]267        } else {
268          throw new ArgumentException("Invalid character: " + str[pos]);
[14024]269        }
270      }
271    }
[14826]272    /// S             = Expr EOF
[14024]273    private ISymbolicExpressionTreeNode ParseS(Queue<Token> tokens) {
274      var expr = ParseExpr(tokens);
275
276      var endTok = tokens.Dequeue();
277      if (endTok.TokenType != TokenType.End)
278        throw new ArgumentException(string.Format("Expected end of expression (got {0})", endTok.strVal));
279
280      return expr;
281    }
[14826]282
283    /// Expr          = ['-' | '+'] Term { '+' Term | '-' Term }
[14024]284    private ISymbolicExpressionTreeNode ParseExpr(Queue<Token> tokens) {
285      var next = tokens.Peek();
286      var posTerms = new List<ISymbolicExpressionTreeNode>();
287      var negTerms = new List<ISymbolicExpressionTreeNode>();
288      bool negateFirstTerm = false;
289      if (next.TokenType == TokenType.Operator && (next.strVal == "+" || next.strVal == "-")) {
290        tokens.Dequeue();
291        if (next.strVal == "-")
292          negateFirstTerm = true;
293      }
294      var t = ParseTerm(tokens);
295      if (negateFirstTerm) negTerms.Add(t);
296      else posTerms.Add(t);
297
298      next = tokens.Peek();
299      while (next.strVal == "+" || next.strVal == "-") {
300        switch (next.strVal) {
301          case "+": {
302              tokens.Dequeue();
303              var term = ParseTerm(tokens);
304              posTerms.Add(term);
305              break;
306            }
307          case "-": {
308              tokens.Dequeue();
309              var term = ParseTerm(tokens);
310              negTerms.Add(term);
311              break;
312            }
313        }
314        next = tokens.Peek();
315      }
316
317      var sum = GetSymbol("+").CreateTreeNode();
318      foreach (var posTerm in posTerms) sum.AddSubtree(posTerm);
319      if (negTerms.Any()) {
320        if (negTerms.Count == 1) {
321          var sub = GetSymbol("-").CreateTreeNode();
322          sub.AddSubtree(negTerms.Single());
323          sum.AddSubtree(sub);
324        } else {
325          var sumNeg = GetSymbol("+").CreateTreeNode();
326          foreach (var negTerm in negTerms) sumNeg.AddSubtree(negTerm);
327
328          var constNode = (ConstantTreeNode)constant.CreateTreeNode();
329          constNode.Value = -1.0;
330          var prod = GetSymbol("*").CreateTreeNode();
331          prod.AddSubtree(constNode);
332          prod.AddSubtree(sumNeg);
333
334          sum.AddSubtree(prod);
335        }
336      }
337      if (sum.SubtreeCount == 1) return sum.Subtrees.First();
338      else return sum;
339    }
340
341    private ISymbol GetSymbol(string tok) {
342      var symb = knownSymbols.GetByFirst(tok).FirstOrDefault();
343      if (symb == null) throw new ArgumentException(string.Format("Unknown token {0} found.", tok));
344      return symb;
345    }
346
[14826]347    /// Term          = Fact { '*' Fact | '/' Fact }
[14024]348    private ISymbolicExpressionTreeNode ParseTerm(Queue<Token> tokens) {
349      var factors = new List<ISymbolicExpressionTreeNode>();
350      var firstFactor = ParseFact(tokens);
351      factors.Add(firstFactor);
352
353      var next = tokens.Peek();
354      while (next.strVal == "*" || next.strVal == "/") {
355        switch (next.strVal) {
356          case "*": {
357              tokens.Dequeue();
358              var fact = ParseFact(tokens);
359              factors.Add(fact);
360              break;
361            }
362          case "/": {
363              tokens.Dequeue();
364              var invFact = ParseFact(tokens);
365              var divNode = GetSymbol("/").CreateTreeNode(); // 1/x
366              divNode.AddSubtree(invFact);
367              factors.Add(divNode);
368              break;
369            }
370        }
371
372        next = tokens.Peek();
373      }
374      if (factors.Count == 1) return factors.First();
375      else {
376        var prod = GetSymbol("*").CreateTreeNode();
377        foreach (var f in factors) prod.AddSubtree(f);
378        return prod;
379      }
380    }
381
[16356]382    // Fact = SimpleFact ['^' SimpleFact]
383    private ISymbolicExpressionTreeNode ParseFact(Queue<Token> tokens) {
384      var expr = ParseSimpleFact(tokens);
385      var next = tokens.Peek();
386      if (next.TokenType == TokenType.Operator && next.strVal == "^") {
387        tokens.Dequeue(); // skip;
388
389        var p = GetSymbol("^").CreateTreeNode();
390        p.AddSubtree(expr);
391        p.AddSubtree(ParseSimpleFact(tokens));
392        expr = p;
393      }
394      return expr;
395    }
396
397
398    /// SimpleFact   = '(' Expr ')'
399    ///                 | '{' Expr '}'
400    ///                 | 'LAG' '(' varId ',' ['+' | '-' ] number ')'
401    ///                 | funcId '(' ArgList ')
402    ///                 | VarExpr
403    ///                 | number
[14826]404    /// ArgList       = Expr { ',' Expr }
405    /// VarExpr       = varId OptFactorPart
406    /// OptFactorPart = [ ('=' varVal | '[' ['+' | '-' ] number {',' ['+' | '-' ] number } ']' ) ]
407    /// varId         =  ident | ' ident ' | " ident "
408    /// varVal        =  ident | ' ident ' | " ident "
409    /// ident         =  '_' | letter { '_' | letter | digit }
[16356]410    private ISymbolicExpressionTreeNode ParseSimpleFact(Queue<Token> tokens) {
[14024]411      var next = tokens.Peek();
412      if (next.TokenType == TokenType.LeftPar) {
[16356]413        var initPar = tokens.Dequeue(); // match par type
[14024]414        var expr = ParseExpr(tokens);
415        var rPar = tokens.Dequeue();
416        if (rPar.TokenType != TokenType.RightPar)
[16356]417          throw new ArgumentException("expected closing parenthesis");
418        if (initPar.strVal == "(" && rPar.strVal == "}")
419          throw new ArgumentException("expected closing )");
420        if (initPar.strVal == "{" && rPar.strVal == ")")
421          throw new ArgumentException("expected closing }");
[14024]422        return expr;
423      } else if (next.TokenType == TokenType.Identifier) {
424        var idTok = tokens.Dequeue();
425        if (tokens.Peek().TokenType == TokenType.LeftPar) {
[14826]426          // function identifier or LAG
[14024]427          var funcId = idTok.strVal.ToUpperInvariant();
428
429          var funcNode = GetSymbol(funcId).CreateTreeNode();
430          var lPar = tokens.Dequeue();
431          if (lPar.TokenType != TokenType.LeftPar)
432            throw new ArgumentException("expected (");
[14347]433
[14350]434          // handle 'lag' specifically
435          if (funcNode.Symbol is LaggedVariable) {
436            var varId = tokens.Dequeue();
437            if (varId.TokenType != TokenType.Identifier) throw new ArgumentException("Identifier expected. Format for lagged variables: \"lag(x, -1)\"");
438            var comma = tokens.Dequeue();
439            if (comma.TokenType != TokenType.Comma) throw new ArgumentException("',' expected, Format for lagged variables: \"lag(x, -1)\"");
440            double sign = 1.0;
441            if (tokens.Peek().strVal == "+" || tokens.Peek().strVal == "-") {
442              // read sign
443              var signTok = tokens.Dequeue();
444              if (signTok.strVal == "-") sign = -1.0;
445            }
446            var lagToken = tokens.Dequeue();
447            if (lagToken.TokenType != TokenType.Number) throw new ArgumentException("Number expected, Format for lagged variables: \"lag(x, -1)\"");
448            if (!lagToken.doubleVal.IsAlmost(Math.Round(lagToken.doubleVal)))
449              throw new ArgumentException("Time lags must be integer values");
450            var laggedVarNode = funcNode as LaggedVariableTreeNode;
451            laggedVarNode.VariableName = varId.strVal;
452            laggedVarNode.Lag = (int)Math.Round(sign * lagToken.doubleVal);
453            laggedVarNode.Weight = 1.0;
454          } else {
455            // functions
456            var args = ParseArgList(tokens);
457            // check number of arguments
458            if (funcNode.Symbol.MinimumArity > args.Length || funcNode.Symbol.MaximumArity < args.Length) {
459              throw new ArgumentException(string.Format("Symbol {0} requires between {1} and  {2} arguments.", funcId,
460                funcNode.Symbol.MinimumArity, funcNode.Symbol.MaximumArity));
461            }
462            foreach (var arg in args) funcNode.AddSubtree(arg);
463          }
[14347]464
[14024]465          var rPar = tokens.Dequeue();
466          if (rPar.TokenType != TokenType.RightPar)
467            throw new ArgumentException("expected )");
468
[16356]469
[14024]470          return funcNode;
471        } else {
472          // variable
[14826]473          if (tokens.Peek().TokenType == TokenType.Eq) {
474            // binary factor
475            tokens.Dequeue(); // skip Eq
476            var valTok = tokens.Dequeue();
477            if (valTok.TokenType != TokenType.Identifier) throw new ArgumentException("expected identifier");
478            var binFactorNode = (BinaryFactorVariableTreeNode)binaryFactorVar.CreateTreeNode();
479            binFactorNode.Weight = 1.0;
480            binFactorNode.VariableName = idTok.strVal;
481            binFactorNode.VariableValue = valTok.strVal;
482            return binFactorNode;
483          } else if (tokens.Peek().TokenType == TokenType.LeftBracket) {
484            // factor variable
485            var factorVariableNode = (FactorVariableTreeNode)factorVar.CreateTreeNode();
486            factorVariableNode.VariableName = idTok.strVal;
487
488            tokens.Dequeue(); // skip [
489            var weights = new List<double>();
490            // at least one weight is necessary
491            var sign = 1.0;
492            if (tokens.Peek().TokenType == TokenType.Operator) {
493              var opToken = tokens.Dequeue();
494              if (opToken.strVal == "+") sign = 1.0;
495              else if (opToken.strVal == "-") sign = -1.0;
496              else throw new ArgumentException();
497            }
498            if (tokens.Peek().TokenType != TokenType.Number) throw new ArgumentException("number expected");
499            var weightTok = tokens.Dequeue();
500            weights.Add(sign * weightTok.doubleVal);
501            while (tokens.Peek().TokenType == TokenType.Comma) {
502              // skip comma
503              tokens.Dequeue();
504              if (tokens.Peek().TokenType == TokenType.Operator) {
505                var opToken = tokens.Dequeue();
506                if (opToken.strVal == "+") sign = 1.0;
507                else if (opToken.strVal == "-") sign = -1.0;
508                else throw new ArgumentException();
509              }
510              weightTok = tokens.Dequeue();
511              if (weightTok.TokenType != TokenType.Number) throw new ArgumentException("number expected");
512              weights.Add(sign * weightTok.doubleVal);
513            }
514            var rightBracketToken = tokens.Dequeue();
515            if (rightBracketToken.TokenType != TokenType.RightBracket) throw new ArgumentException("closing bracket ] expected");
516            factorVariableNode.Weights = weights.ToArray();
517            return factorVariableNode;
518          } else {
519            // variable
520            var varNode = (VariableTreeNode)variable.CreateTreeNode();
521            varNode.Weight = 1.0;
522            varNode.VariableName = idTok.strVal;
523            return varNode;
524          }
[14024]525        }
526      } else if (next.TokenType == TokenType.Number) {
527        var numTok = tokens.Dequeue();
528        var constNode = (ConstantTreeNode)constant.CreateTreeNode();
529        constNode.Value = numTok.doubleVal;
530        return constNode;
531      } else {
532        throw new ArgumentException(string.Format("unexpected token in expression {0}", next.strVal));
533      }
534    }
[14347]535
536    // ArgList = Expr { ',' Expr }
537    private ISymbolicExpressionTreeNode[] ParseArgList(Queue<Token> tokens) {
538      var exprList = new List<ISymbolicExpressionTreeNode>();
539      exprList.Add(ParseExpr(tokens));
540      while (tokens.Peek().TokenType != TokenType.RightPar) {
541        var comma = tokens.Dequeue();
542        if (comma.TokenType != TokenType.Comma) throw new ArgumentException("expected ',' ");
543        exprList.Add(ParseExpr(tokens));
544      }
545      return exprList.ToArray();
546    }
[14024]547  }
548}
Note: See TracBrowser for help on using the repository browser.