1. using System;
  2. using System.Collections.Generic;
  3. using System.IO;
  4. using System.Text;
  5.  
  6. namespace Symlang {    /// <summary>Token type.</summary>
  7.     public enum TokenType {
  8.         Ident,     // [a-zA-Z_]+
  9.         Number,    // [0..9]+
  10.         String,    // "..."
  11.         // Structural operators.
  12.         LParen,    // (
  13.         RParen,    // )
  14.         LBrace,    // {
  15.         RBrace,    // }
  16.         LBracket,  // [
  17.         RBracket,  // ]
  18.         Colon,     // :
  19.         SemiColon, // ;
  20.         Comma,     // ,
  21.         Then,      // ->
  22.         Loop,      // =>
  23.         // Arithmetic/logic operators.
  24.         Exponent,  // ^
  25.         Multiply,  // *
  26.         Divide,    // /
  27.         Modulus,   // %
  28.         Add,       // +
  29.         Subtract,  // -
  30.         And,       // &
  31.         Or,        // |
  32.         Not,       // ~
  33.         Xor,       // .
  34.         LessThan,  // <
  35.         LtEq,      // <=
  36.         EqualTo,   // =
  37.         NotEqual,  // /=
  38.         GtEq,      // >=
  39.         GrtrThan,  // >
  40.         // Miscellaneous operators.
  41.         Assign     // <-
  42.     }
  43.  
  44.     /// <summary>Token.</summary>
  45.     public class Token {
  46.         /// <summary>The type of token.</summary>
  47.         public readonly TokenType Type;
  48.         /// <summary>The token value.</summary>
  49.         public readonly string Value;
  50.         /// <summary>The name of the file the token was in.</summary>
  51.         public readonly string Filename;
  52.         /// <summary>The line the token was on.</summary>
  53.         public readonly int Line;
  54.         /// <summary>The column the token started at.</summary>
  55.         public readonly int Column;
  56.  
  57.         /// <summary>Constructs <see cref="Symlang.Token"/>.</summary>
  58.         /// <param name='type'>The token type.</param>
  59.         /// <param name='val'>The token value (identifier/number/string only).
  60.         /// </param>
  61.         /// <param name='filename'>The file in which the token was read.</param>
  62.         /// <param name='line'>The line on which the token was read.</param>
  63.         /// <param name='column'>The column at which the token started.</param>
  64.         public Token(TokenType type, string val, string filename, int line,
  65.                      int column)
  66.         {
  67.             this.Type = type;
  68.             this.Value = val;
  69.             this.Filename = filename;
  70.             this.Line = line;
  71.             this.Column = column;
  72.         }
  73.  
  74.         /// <summary>Returns a <see cref="System.String"/> that represents the
  75.         /// <see cref="Symlang.Token"/>.</summary>
  76.         /// <returns>A <see cref="System.String"/> that represents the
  77.         /// <see cref="Symlang.Token"/>.</returns>
  78.         public override string ToString()
  79.         {
  80.             StringBuilder sb = new StringBuilder();
  81.             sb.Append(string.Format("{0}", this.Type.ToString().ToUpper()));
  82.             if ((this.Type == TokenType.Ident) || (this.Type == TokenType.Number))
  83.                 sb.Append(string.Format("({0})", this.Value));
  84.             else if (this.Type == TokenType.String)
  85.                 sb.Append(string.Format("(\"{0}\")", this.Value));
  86.             return sb.ToString();
  87.         }
  88.     }
  89.  
  90.     /// <summary>Lexer.</summary>
  91.     public class Lexer {
  92.         /// <summary>The filename.</summary>
  93.         private string Filename;
  94.         /// <summary>The code.</summary>
  95.         private List<string> Code;
  96.         /// <summary>The tokens.</summary>
  97.         private List<Token> Tokens;
  98.         /// <summary>The line.</summary>
  99.         private int Line;
  100.         /// <summary>The column.</summary>
  101.         private int Column;
  102.         /// <summary>The tokenised.</summary>
  103.         private bool Tokenised;
  104.  
  105.         /// <summary>Constructs <see cref="Symlang.Lexer"/>.</summary>
  106.         public Lexer()
  107.         {
  108.             this.Code = new List<string>();
  109.             this.Tokenised = false;
  110.         }
  111.  
  112.         /// <summary>Constructs <see cref="Symlang.Lexer"/> from a filename and
  113.         /// a list of lines of code.</summary>
  114.         /// <param name='filename'>The name of the file the code was read from.
  115.         /// </param>
  116.         /// <param name='code'>The list of lines of code.</param>
  117.         public Lexer(string filename, List<string> code)
  118.         {
  119.             this.Filename = String.Format("{0}.{1}", Path.GetFileName(filename),
  120.                                           Path.GetExtension(filename));
  121.             this.Code = code;
  122.             this.Tokenised = false;
  123.         }
  124.  
  125.         /// <summary> Constructs <see cref="Symlang.Lexer"/> from a filename and
  126.         /// an array of lines of code.</summary>
  127.         /// <param name='filename'>The name of the file the code was read from.
  128.         /// </param>
  129.         /// <param name='code'>The array of lines of code.</param>
  130.         public Lexer(string filename, string[] code)
  131.         : this(filename, new List<string>(code))
  132.         {
  133.             // Do nothing.
  134.         }
  135.  
  136.         /// <summary>Constructs <see cref="Symlang.Lexer"/> from a filename and
  137.         /// a string containing code.</summary>
  138.         /// <param name='filename'>The name of the file the code was read from.
  139.         /// </param>
  140.         /// <param name='code'>The string containing the code.</param>
  141.         public Lexer(string filename, string code)
  142.         : this(filename, code.Split('\n'))
  143.         {
  144.             // Do nothing.
  145.         }
  146.  
  147.         /// <summary>Constructs <see cref="Symlang.Lexer"/> from a filename and
  148.         /// an open StreamReader.</summary>
  149.         /// <param name='filename'>The name of the stream/file.</param>
  150.         /// <param name='stream'>The StreamReader to read code from.</param>
  151.         public Lexer(string filename, StreamReader stream)
  152.         : this(filename, stream.ReadToEnd())
  153.         {
  154.             // Do nothing.
  155.         }
  156.  
  157.         /// <summary>Constructs <see cref="Symlang.Lexer"/> from a filename and
  158.         /// an open stream.</summary>
  159.         /// <param name='filename'>The name of the stream/file.</param>
  160.         /// <param name='stream'>The stream to read code from.</param>
  161.         public Lexer(string filename, Stream stream)
  162.         : this(filename, new StreamReader(stream))
  163.         {
  164.             // Do nothing.
  165.         }
  166.  
  167.         /// <summary>Constructs <see cref="Symlang.Lexer"/> from a filename.
  168.         /// </summary>
  169.         /// <param name='filename'>The name of a file to read code from.</param>
  170.         public Lexer(string filename)
  171.         : this(filename, new StreamReader(filename))
  172.         {
  173.             // Do nothing.
  174.         }
  175.  
  176.         /// <summary>Appends a line to the code.</summary>
  177.         public void AppendLine(string line)
  178.         {
  179.             this.Code.Add(line);
  180.             this.Tokenised = false;
  181.         }
  182.  
  183.         /// <summary>Adds a token.</summary>
  184.         /// <param name='type'>The token type.</param>
  185.         /// <param name='val'>The value.</param>
  186.         private void AddToken(TokenType type, string val = "")
  187.         {
  188.             this.Tokens.Add(new Token(type, val, this.Filename, this.Line,
  189.                                       this.Column));
  190.         }
  191.  
  192.         /// <summary>Adds an ident.</summary>
  193.         /// <param name='line'>The current line.</param>
  194.         private void AddIdent(string line)
  195.         {
  196.             StringBuilder sb = new StringBuilder();
  197.             int i;
  198.             for (i = this.Column; (i < line.Length) &&
  199.                           ((Char.IsLetter(line[i])) || (line[i] == '_')); ++i) {
  200.                 sb.Append(line[i]);
  201.             }
  202.             this.AddToken(TokenType.Ident, sb.ToString());
  203.             this.Column = i;
  204.         }
  205.  
  206.         /// <summary>Adds a number.</summary>
  207.         /// <param name='line'>The current line.</param>
  208.         private void AddNumber(string line)
  209.         {
  210.             StringBuilder sb = new StringBuilder();
  211.             bool hasDecimal = false;
  212.             int i;
  213.             for (i = this.Column; (i < line.Length) &&
  214.                            ((Char.IsDigit(line[i])) || (line[i] == '.')); ++i) {
  215.                 if ((hasDecimal) && (line[i] == '.')) {
  216.                     this.Column += 2;
  217.                     new SyntaxError(this.Filename, this.Line, this.Column,
  218.                                     line, "Unexpected '.'");
  219.                     return;
  220.                 } else if (line[i] == '.') {
  221.                     hasDecimal = true;
  222.                 }
  223.                 sb.Append(line[i]);
  224.             }
  225.             this.AddToken(TokenType.Number, sb.ToString());
  226.             this.Column = i;
  227.         }
  228.  
  229.         /// <summary>Adds a string.</summary>
  230.         /// <param name='line'>The current line.</param>
  231.         private void AddString(string line)
  232.         {
  233.             StringBuilder sb = new StringBuilder();
  234.             int i;
  235.             for (i = this.Column; (i < line.Length) && (line[i] != '"'); ++i)
  236.                 sb.Append(line[i]);
  237.             if (line[i] != '"')
  238.                 new SyntaxError(this.Filename, this.Line, this.Column,
  239.                                 line, "Expected '\"' before end-of-line.");
  240.             this.AddToken(TokenType.String, sb.ToString());
  241.             this.Column = i;
  242.         }
  243.  
  244.         /// <summary>Tokenises the code.</summary>
  245.         /// <param name='retokenise'>Whether to retokenise the code if it's been
  246.         /// done before. The default behaviour is to return the result of the
  247.         /// previous tokenisation.</param>
  248.         public List<Token> Tokenise(bool retokenise = false)
  249.         {
  250.             if ((retokenise) || (this.Tokens != null) || !(this.Tokenised))
  251.                 return this.Tokens;
  252.             bool inComment = false;
  253.             this.Tokens = new List<Token>();
  254.             foreach (string line in this.Code) {
  255.                 for (this.Column = 0; this.Column < line.Length; ++this.Column) {
  256.                     char symbol = line[this.Column];
  257.                     char nextSymbol = (this.Column + 1 < line.Length)
  258.                                       ? line[this.Column + 1] : '\0';
  259.                     if (inComment)
  260.                         break;
  261.                     switch (symbol) {
  262.                         case '\n': case '\t': case ' ':
  263.                             break;
  264.                         case '(':
  265.                             this.AddToken(TokenType.LParen);
  266.                             break;
  267.                         case ')':
  268.                             this.AddToken(TokenType.RParen);
  269.                             break;
  270.                         case '{':
  271.                             this.AddToken(TokenType.LBrace);
  272.                             break;
  273.                         case '}':
  274.                             this.AddToken(TokenType.RBrace);
  275.                             break;
  276.                         case '[':
  277.                             this.AddToken(TokenType.LBracket);
  278.                             break;
  279.                         case ']':
  280.                             this.AddToken(TokenType.RBracket);
  281.                             break;
  282.                         case ':':
  283.                             this.AddToken(TokenType.Colon);
  284.                             break;
  285.                         case ';':
  286.                             this.AddToken(TokenType.SemiColon);
  287.                             break;
  288.                         case ',':
  289.                             this.AddToken(TokenType.Comma);
  290.                             break;
  291.                         case '^':
  292.                             this.AddToken(TokenType.Exponent);
  293.                             break;
  294.                         case '*':
  295.                             this.AddToken(TokenType.Multiply);
  296.                             break;
  297.                         case '/':
  298.                             if (nextSymbol == '/') {
  299.                                 inComment = true;
  300.                                 ++this.Column;
  301.                             } else if (nextSymbol == '=') {
  302.                                 this.AddToken(TokenType.NotEqual);
  303.                                 ++Column;
  304.                             } else {
  305.                                 this.AddToken(TokenType.Divide);
  306.                             }
  307.                             break;
  308.                         case '%':
  309.                             this.AddToken(TokenType.Modulus);
  310.                             break;
  311.                         case '+':
  312.                             this.AddToken(TokenType.Add);
  313.                             break;
  314.                         case '-':
  315.                             if (nextSymbol == '>') {
  316.                                 this.AddToken(TokenType.Then);
  317.                                 ++this.Column;
  318.                             } else {
  319.                                 this.AddToken(TokenType.Subtract);
  320.                             }
  321.                             break;
  322.                         case '&':
  323.                             this.AddToken(TokenType.And);
  324.                             break;
  325.                         case '|':
  326.                             this.AddToken(TokenType.Or);
  327.                             break;
  328.                         case '~':
  329.                             this.AddToken(TokenType.Not);
  330.                             break;
  331.                         case '.':
  332.                             this.AddToken(TokenType.Xor);
  333.                             break;
  334.                         case '<':
  335.                             if (nextSymbol == '-') {
  336.                                 this.AddToken(TokenType.Assign);
  337.                                 ++this.Column;
  338.                             } else if (nextSymbol == '=') {
  339.                                 this.AddToken(TokenType.LtEq);
  340.                                 ++this.Column;
  341.                             } else {
  342.                                 this.AddToken(TokenType.LessThan);
  343.                             }
  344.                             break;
  345.                         case '=':
  346.                             if (nextSymbol == '>') {
  347.                                 this.AddToken(TokenType.Loop);
  348.                                 ++this.Column;
  349.                             } else {
  350.                                 this.AddToken(TokenType.EqualTo);
  351.                             }
  352.                             break;
  353.                         case '>':
  354.                             if (nextSymbol == '=') {
  355.                                 this.AddToken(TokenType.GtEq);
  356.                             } else {
  357.                                 this.AddToken(TokenType.GrtrThan);
  358.                             }
  359.                             break;
  360.                         default:
  361.                             if (char.IsLower(symbol)) {
  362.                                 this.AddIdent(line);
  363.                                 --this.Column;
  364.                             } else if (char.IsDigit(symbol)) {
  365.                                 this.AddNumber(line);
  366.                                 --this.Column;
  367.                             } else if (symbol == '"') {
  368.                                 ++this.Column;
  369.                                 this.AddString(line);
  370.                             } else {
  371.                                 new SyntaxError(this.Filename, this.Line,
  372.                                             this.Column, line,
  373.                                             "Unexpected '{0}'", symbol);
  374.                             }
  375.                             break;
  376.                     }
  377.                 }
  378.                 inComment = false;
  379.                 ++this.Line;
  380.             }
  381.             this.Tokenised = true;
  382.             return this.Tokens;
  383.         }
  384.     }
  385. }