1 module calcool.lexer;
2 import std.stdio;
3 import std.ascii;
4 import calcool.token;
5 import calcool.exceptions;
6 
7 public class Lexer {
8 private:
9     File input;
10 
11     uint pos = 0;
12     string line;
13 public:
14     this(File f) {
15         input = f;
16     }
17 
18     this() {
19         this(stdin);
20     }
21 
22     Token[] nextLine(in string stringInput = null) {
23         if (stringInput is null) {
24             if (input is stdin) {
25                 version (Posix) {
26                     import core.sys.posix.unistd : isatty;
27 
28                     if (isatty(stdin.fileno))
29                         write(">> ");
30                 } else {
31                     write(">> ");
32                 }
33             }
34 
35             line = input.readln();
36             if (line is null) {
37                 return [];
38             }
39         } else {
40             line = stringInput; // ~ '\n';
41         }
42         pos = 0;
43         Token[] list;
44         for (auto t = next(); t.type != TokenType.EOL; t = next()) {
45             list ~= t;
46         }
47         list ~= Token(TokenType.EOL);
48         return list;
49     }
50 
51     Token next() {
52         while (!eol() && isWhite(peek()))
53             pos++;
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             return Token(TokenType.FUNC, name());
62         pos++;
63         switch (ch) {
64             import std.conv : to;
65 
66         case '(':
67             return Token(TokenType.PR_OPEN, ch.to!string);
68         case ')':
69             return Token(TokenType.PR_CLOSE, ch.to!string);
70         case '+':
71             return Token(TokenType.OP_ADD, ch.to!string);
72         case '-':
73             return Token(TokenType.OP_MINUS, ch.to!string);
74         case '*':
75             return Token(TokenType.OP_MULT, ch.to!string);
76         case '/':
77             return Token(TokenType.OP_DIV, ch.to!string);
78         case '^':
79             return Token(TokenType.OP_POW, ch.to!string);
80         default:
81             throw new UnsupportedTokenException(ch);
82         }
83     }
84 
85 private:
86     pragma(inline, true) {
87         auto eol() pure nothrow const {
88             return line.length == 0 || pos >= line.length;
89         }
90 
91         auto ref peek() {
92             return line[pos];
93         }
94     }
95 
96     auto name() {
97         const start = pos;
98         while (!eol() && (isAlpha(peek()) || isDigit(peek())))
99             pos++;
100         return line[start .. pos];
101     }
102 
103     string number() {
104         const start = pos;
105         bool isFloat = false;
106     read_digits:
107         while (!eol() && isDigit(peek())) {
108             pos++;
109         }
110         if (!eol() && peek() == '.') {
111             if (!isFloat) {
112                 isFloat = true;
113                 pos++;
114                 goto read_digits;
115             } else {
116                 throw new LexerException("Unknown number passed");
117             }
118         }
119 
120         if (isFloat && (pos - start) == 1)
121             throw new LexerException("Point is not a number");
122         return line[start .. pos];
123     }
124 }