1 module calcool.lexer;
2 import std.stdio;
3 import std.ascii;
4 import calcool.token;
5 import calcool.exceptions;
6 import std.conv : to;
7 
8 public class Lexer {
9 private:
10     File input;
11 
12     uint pos = 0;
13     string line;
14 public:
15     this(File f) {
16         input = f;
17     }
18 
19     this() {
20         this(stdin);
21     }
22 
23     Token[] nextLine(in string stringInput = null) {
24         if (stringInput is null) {
25             if (input is stdin) {
26                 version (Posix) {
27                     import core.sys.posix.unistd : isatty;
28 
29                     if (isatty(stdin.fileno))
30                         write(">> ");
31                 } else {
32                     write(">> ");
33                 }
34             }
35 
36             line = input.readln();
37             if (line is null) {
38                 return [];
39             }
40         } else {
41             line = stringInput; // ~ '\n';
42         }
43         pos = 0;
44         Token[] list;
45         for (auto t = next(); t.type != TokenType.EOL; t = next()) {
46             list ~= t;
47         }
48         list ~= Token(TokenType.EOL);
49         return list;
50     }
51 
52     Token next() {
53         skipWhiteSpace();
54         if (eol()) {
55             return Token(TokenType.EOL);
56         }
57         const ch = peek();
58         if (isDigit(ch) || ch == '.') {
59             return Token(TokenType.NUMBER, number());
60         } else if (isAlpha(ch)) {
61             const identifier = name();
62             if (identifier == "set") {
63                 return Token(TokenType.SET_VAR, identifier);
64             }
65             skipWhiteSpace();
66             if (!eol() && peek() == '(') {
67                 return Token(TokenType.FUNC, identifier);
68             }
69             return Token(TokenType.IDENTIFIER, identifier);
70         }
71         pos++;
72 
73         const value = ch.to!string;
74         switch (ch) {
75         case '(':
76             return Token(TokenType.PR_OPEN, value);
77         case ')':
78             return Token(TokenType.PR_CLOSE, value);
79         case '+':
80             return Token(TokenType.OP_ADD, value);
81         case '-':
82             return Token(TokenType.OP_MINUS, value);
83         case '*':
84             return Token(TokenType.OP_MULT, value);
85         case '/':
86             return Token(TokenType.OP_DIV, value);
87         case '^':
88             return Token(TokenType.OP_POW, value);
89         case '=':
90             return Token(TokenType.EQUALS, value);
91         default:
92             throw new UnsupportedTokenException(ch);
93         }
94     }
95 
96 private:
97     pragma(inline, true) {
98         auto eol() pure nothrow const {
99             return line.length == 0 || pos >= line.length;
100         }
101 
102         auto skipWhiteSpace() {
103             while (!eol() && isWhite(peek()))
104                 pos++;
105         }
106 
107         auto ref peek() {
108             return line[pos];
109         }
110     }
111 
112     auto name() {
113         const start = pos;
114         while (!eol() && (isAlpha(peek()) || isDigit(peek())))
115             pos++;
116         return line[start .. pos];
117     }
118 
119     string number() {
120         const start = pos;
121         bool isFloat = false;
122         bool shouldContinue = true;
123 
124         while (shouldContinue) {
125             while (!eol() && isDigit(peek())) {
126                 pos++;
127             }
128             if (!eol() && peek() == '.') {
129                 if (!isFloat) {
130                     isFloat = true;
131                     pos++;
132                 } else {
133                     throw new LexerException("Invalid number");
134                 }
135             } else {
136                 shouldContinue = false;
137             }
138         }
139 
140         if (!eol() && peek() == 'e') {
141             pos++;
142             if (!eol() && (peek() == '+' || peek() == '-')) {
143                 pos++;
144             }
145             if (!eol() && !isDigit(peek())) {
146                 throw new LexerException("Invalid number");
147             }
148             while (!eol() && isDigit(peek())) {
149                 pos++;
150             }
151         }
152 
153         if (isFloat && (pos - start) == 1)
154             throw new LexerException("Point is not a number");
155         return line[start .. pos];
156     }
157 }