lumos-language 1.1.2 → 2.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/FUNDING.yml +1 -0
- package/.npmrc.ci-backup +3 -0
- package/LICENSE +0 -29
- package/Lumos.png +0 -0
- package/README.md +284 -126
- package/STRUCTURE.md +216 -0
- package/examples/hello.lumos +5 -0
- package/index.cjs +125 -125
- package/index.html +120 -274
- package/package.json +20 -10
- package/src/backends/assembly/arm.js +39 -0
- package/src/backends/assembly/wasm.js +39 -0
- package/src/backends/assembly/x86.js +39 -0
- package/src/backends/compiled/c.js +39 -0
- package/src/backends/compiled/cpp.js +39 -0
- package/src/backends/compiled/csharp.js +39 -0
- package/src/backends/compiled/go.js +39 -0
- package/src/backends/compiled/java.js +39 -0
- package/src/backends/compiled/rust.js +39 -0
- package/src/backends/compiled/swift.js +39 -0
- package/src/backends/database/mongodb.js +39 -0
- package/src/backends/database/mysql.js +39 -0
- package/src/backends/database/postgresql.js +39 -0
- package/src/backends/database/sql.js +39 -0
- package/src/backends/database/sqlite.js +39 -0
- package/src/backends/functional/clojure.js +39 -0
- package/src/backends/functional/elixir.js +39 -0
- package/src/backends/functional/erlang.js +39 -0
- package/src/backends/functional/fsharp.js +39 -0
- package/src/backends/functional/haskell.js +39 -0
- package/src/backends/functional/scala.js +39 -0
- package/src/backends/interpreted/lua.js +39 -0
- package/src/backends/interpreted/perl.js +39 -0
- package/src/backends/interpreted/php.js +39 -0
- package/src/backends/interpreted/python.js +356 -0
- package/src/backends/interpreted/ruby.js +222 -0
- package/src/backends/scripting/bash.js +39 -0
- package/src/backends/scripting/javascript.js +39 -0
- package/src/backends/scripting/powershell.js +39 -0
- package/src/backends/scripting/typescript.js +39 -0
- package/src/backends/scripting/vbscript.js +39 -0
- package/src/backends/specialized/ada.js +39 -0
- package/src/backends/specialized/cobol.js +39 -0
- package/src/backends/specialized/fortran.js +39 -0
- package/src/backends/specialized/lisp.js +39 -0
- package/src/backends/specialized/mlang.js +39 -0
- package/src/backends/specialized/prolog.js +39 -0
- package/src/backends/web/css.js +39 -0
- package/src/backends/web/html.js +39 -0
- package/src/backends/web/jsx.js +39 -0
- package/src/backends/web/vue.js +39 -0
- package/src/cli/fileRunner.js +82 -0
- package/src/cli/repl.js +244 -0
- package/src/compiler/core/compiler.js +1350 -0
- package/src/compiler/framework-integrator.js +846 -0
- package/src/compiler/generators/dynamic-languages.js +620 -0
- package/src/compiler/generators/system-languages.js +1184 -0
- package/src/core/compiler.js +181 -0
- package/src/core/evaluator.js +408 -0
- package/src/core/lexer.js +251 -0
- package/src/core/parser.js +452 -0
- package/src/core/runtime.js +173 -0
- package/tests/run-tests.js +243 -0
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
class Token {
|
|
2
|
+
constructor(type, value, line, column) {
|
|
3
|
+
this.type = type;
|
|
4
|
+
this.value = value;
|
|
5
|
+
this.line = line;
|
|
6
|
+
this.column = column;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
class Lexer {
|
|
11
|
+
constructor(input) {
|
|
12
|
+
this.input = input;
|
|
13
|
+
this.position = 0;
|
|
14
|
+
this.line = 1;
|
|
15
|
+
this.column = 1;
|
|
16
|
+
this.tokens = [];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
tokenize() {
|
|
20
|
+
while (this.position < this.input.length) {
|
|
21
|
+
this.skipWhitespace();
|
|
22
|
+
|
|
23
|
+
if (this.position >= this.input.length) break;
|
|
24
|
+
|
|
25
|
+
const char = this.current();
|
|
26
|
+
|
|
27
|
+
if (this.isLetter(char)) {
|
|
28
|
+
this.tokens.push(this.readIdentifierOrKeyword());
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (this.isDigit(char)) {
|
|
33
|
+
this.tokens.push(this.readNumber());
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (char === '"' || char === "'") {
|
|
38
|
+
this.tokens.push(this.readString(char));
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (char === '/' && this.peek() === '/') {
|
|
43
|
+
this.skipLineComment();
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (char === '/' && this.peek() === '*') {
|
|
48
|
+
this.skipBlockComment();
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const operator = this.readOperator();
|
|
53
|
+
if (operator) {
|
|
54
|
+
this.tokens.push(operator);
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
throw new Error(`Unexpected character '${char}' at line ${this.line}, column ${this.column}`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
this.tokens.push(new Token('EOF', null, this.line, this.column));
|
|
62
|
+
return this.tokens;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
current() {
|
|
66
|
+
return this.input[this.position];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
peek(offset = 1) {
|
|
70
|
+
return this.input[this.position + offset];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
advance() {
|
|
74
|
+
const char = this.current();
|
|
75
|
+
this.position++;
|
|
76
|
+
if (char === '\n') {
|
|
77
|
+
this.line++;
|
|
78
|
+
this.column = 1;
|
|
79
|
+
} else {
|
|
80
|
+
this.column++;
|
|
81
|
+
}
|
|
82
|
+
return char;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
skipWhitespace() {
|
|
86
|
+
while (this.position < this.input.length && /\s/.test(this.current())) {
|
|
87
|
+
this.advance();
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
skipLineComment() {
|
|
92
|
+
while (this.position < this.input.length && this.current() !== '\n') {
|
|
93
|
+
this.advance();
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
skipBlockComment() {
|
|
98
|
+
this.advance();
|
|
99
|
+
this.advance();
|
|
100
|
+
while (this.position < this.input.length) {
|
|
101
|
+
if (this.current() === '*' && this.peek() === '/') {
|
|
102
|
+
this.advance();
|
|
103
|
+
this.advance();
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
this.advance();
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
isLetter(char) {
|
|
111
|
+
return /[a-zA-Z_]/.test(char);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
isDigit(char) {
|
|
115
|
+
return /[0-9]/.test(char);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
isAlphaNumeric(char) {
|
|
119
|
+
return this.isLetter(char) || this.isDigit(char);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
readIdentifierOrKeyword() {
|
|
123
|
+
const start = this.position;
|
|
124
|
+
const startLine = this.line;
|
|
125
|
+
const startColumn = this.column;
|
|
126
|
+
|
|
127
|
+
while (this.position < this.input.length && this.isAlphaNumeric(this.current())) {
|
|
128
|
+
this.advance();
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const value = this.input.substring(start, this.position);
|
|
132
|
+
const keywords = [
|
|
133
|
+
'let', 'const', 'var', 'def', 'function', 'return',
|
|
134
|
+
'if', 'else', 'elsif', 'elif', 'while', 'for', 'do', 'end',
|
|
135
|
+
'break', 'continue', 'times', 'to', 'in', 'of',
|
|
136
|
+
'class', 'struct', 'interface', 'trait', 'module',
|
|
137
|
+
'import', 'export', 'from', 'as', 'with',
|
|
138
|
+
'try', 'catch', 'finally', 'throw', 'raise',
|
|
139
|
+
'true', 'false', 'null', 'nil', 'undefined',
|
|
140
|
+
'and', 'or', 'not', 'is', 'new', 'delete',
|
|
141
|
+
'async', 'await', 'yield', 'lambda', 'match', 'case',
|
|
142
|
+
'public', 'private', 'protected', 'static', 'final'
|
|
143
|
+
];
|
|
144
|
+
|
|
145
|
+
const type = keywords.includes(value) ? value.toUpperCase() : 'IDENTIFIER';
|
|
146
|
+
return new Token(type, value, startLine, startColumn);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
readNumber() {
|
|
150
|
+
const start = this.position;
|
|
151
|
+
const startLine = this.line;
|
|
152
|
+
const startColumn = this.column;
|
|
153
|
+
|
|
154
|
+
while (this.position < this.input.length && this.isDigit(this.current())) {
|
|
155
|
+
this.advance();
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (this.current() === '.' && this.isDigit(this.peek())) {
|
|
159
|
+
this.advance();
|
|
160
|
+
while (this.position < this.input.length && this.isDigit(this.current())) {
|
|
161
|
+
this.advance();
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (this.current() === 'e' || this.current() === 'E') {
|
|
166
|
+
this.advance();
|
|
167
|
+
if (this.current() === '+' || this.current() === '-') {
|
|
168
|
+
this.advance();
|
|
169
|
+
}
|
|
170
|
+
while (this.position < this.input.length && this.isDigit(this.current())) {
|
|
171
|
+
this.advance();
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const value = this.input.substring(start, this.position);
|
|
176
|
+
return new Token('NUMBER', parseFloat(value), startLine, startColumn);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
readString(quote) {
|
|
180
|
+
const startLine = this.line;
|
|
181
|
+
const startColumn = this.column;
|
|
182
|
+
this.advance();
|
|
183
|
+
|
|
184
|
+
let value = '';
|
|
185
|
+
while (this.position < this.input.length && this.current() !== quote) {
|
|
186
|
+
if (this.current() === '\\') {
|
|
187
|
+
this.advance();
|
|
188
|
+
const escapeChar = this.current();
|
|
189
|
+
const escapeMap = {
|
|
190
|
+
'n': '\n',
|
|
191
|
+
't': '\t',
|
|
192
|
+
'r': '\r',
|
|
193
|
+
'\\': '\\',
|
|
194
|
+
'"': '"',
|
|
195
|
+
"'": "'"
|
|
196
|
+
};
|
|
197
|
+
value += escapeMap[escapeChar] || escapeChar;
|
|
198
|
+
this.advance();
|
|
199
|
+
} else {
|
|
200
|
+
value += this.current();
|
|
201
|
+
this.advance();
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (this.current() !== quote) {
|
|
206
|
+
throw new Error(`Unterminated string at line ${startLine}, column ${startColumn}`);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
this.advance();
|
|
210
|
+
return new Token('STRING', value, startLine, startColumn);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
readOperator() {
|
|
214
|
+
const startLine = this.line;
|
|
215
|
+
const startColumn = this.column;
|
|
216
|
+
const char = this.current();
|
|
217
|
+
const next = this.peek();
|
|
218
|
+
|
|
219
|
+
const twoCharOps = {
|
|
220
|
+
'==': 'EQ', '!=': 'NEQ', '<=': 'LTE', '>=': 'GTE',
|
|
221
|
+
'&&': 'AND', '||': 'OR', '++': 'INCREMENT', '--': 'DECREMENT',
|
|
222
|
+
'+=': 'PLUS_ASSIGN', '-=': 'MINUS_ASSIGN', '*=': 'MULT_ASSIGN', '/=': 'DIV_ASSIGN',
|
|
223
|
+
'=>': 'ARROW', '->': 'ARROW', '::': 'SCOPE', '..': 'RANGE'
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
const twoChar = char + next;
|
|
227
|
+
if (twoCharOps[twoChar]) {
|
|
228
|
+
this.advance();
|
|
229
|
+
this.advance();
|
|
230
|
+
return new Token(twoCharOps[twoChar], twoChar, startLine, startColumn);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const oneCharOps = {
|
|
234
|
+
'+': 'PLUS', '-': 'MINUS', '*': 'MULT', '/': 'DIV', '%': 'MOD',
|
|
235
|
+
'=': 'ASSIGN', '<': 'LT', '>': 'GT', '!': 'NOT',
|
|
236
|
+
'(': 'LPAREN', ')': 'RPAREN', '{': 'LBRACE', '}': 'RBRACE',
|
|
237
|
+
'[': 'LBRACKET', ']': 'RBRACKET', ',': 'COMMA', ';': 'SEMICOLON',
|
|
238
|
+
':': 'COLON', '.': 'DOT', '|': 'PIPE', '&': 'AMPERSAND',
|
|
239
|
+
'^': 'XOR', '~': 'TILDE', '?': 'QUESTION'
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
if (oneCharOps[char]) {
|
|
243
|
+
this.advance();
|
|
244
|
+
return new Token(oneCharOps[char], char, startLine, startColumn);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return null;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
module.exports = Lexer;
|
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
class ASTNode {
|
|
2
|
+
constructor(type, attributes = {}) {
|
|
3
|
+
this.type = type;
|
|
4
|
+
Object.assign(this, attributes);
|
|
5
|
+
}
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
class Parser {
|
|
9
|
+
constructor(tokens) {
|
|
10
|
+
this.tokens = tokens;
|
|
11
|
+
this.position = 0;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
parse() {
|
|
15
|
+
const statements = [];
|
|
16
|
+
while (!this.isAtEnd()) {
|
|
17
|
+
if (this.match('EOF')) break;
|
|
18
|
+
statements.push(this.statement());
|
|
19
|
+
}
|
|
20
|
+
return new ASTNode('Program', { statements });
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
statement() {
|
|
24
|
+
if (this.match('LET', 'CONST', 'VAR')) return this.variableDeclaration();
|
|
25
|
+
if (this.match('DEF', 'FUNCTION')) return this.functionDeclaration();
|
|
26
|
+
if (this.match('IF')) return this.ifStatement();
|
|
27
|
+
if (this.match('WHILE')) return this.whileStatement();
|
|
28
|
+
if (this.match('FOR')) return this.forStatement();
|
|
29
|
+
if (this.match('RETURN')) return this.returnStatement();
|
|
30
|
+
if (this.match('BREAK')) return new ASTNode('Break');
|
|
31
|
+
if (this.match('CONTINUE')) return new ASTNode('Continue');
|
|
32
|
+
if (this.match('CLASS')) return this.classDeclaration();
|
|
33
|
+
if (this.match('IMPORT')) return this.importStatement();
|
|
34
|
+
if (this.match('TRY')) return this.tryStatement();
|
|
35
|
+
|
|
36
|
+
return this.expressionStatement();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
variableDeclaration() {
|
|
40
|
+
const keyword = this.previous().value;
|
|
41
|
+
const name = this.consume('IDENTIFIER', 'Expected variable name').value;
|
|
42
|
+
|
|
43
|
+
let initializer = null;
|
|
44
|
+
if (this.match('ASSIGN')) {
|
|
45
|
+
initializer = this.expression();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
this.consumeOptional('SEMICOLON');
|
|
49
|
+
return new ASTNode('VariableDeclaration', { keyword, name, initializer });
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
functionDeclaration() {
|
|
53
|
+
const name = this.consume('IDENTIFIER', 'Expected function name').value;
|
|
54
|
+
this.consume('LPAREN', 'Expected ( after function name');
|
|
55
|
+
|
|
56
|
+
const parameters = [];
|
|
57
|
+
if (!this.check('RPAREN')) {
|
|
58
|
+
do {
|
|
59
|
+
parameters.push(this.consume('IDENTIFIER', 'Expected parameter name').value);
|
|
60
|
+
} while (this.match('COMMA'));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
this.consume('RPAREN', 'Expected ) after parameters');
|
|
64
|
+
this.consume('LBRACE', 'Expected { before function body');
|
|
65
|
+
|
|
66
|
+
const body = this.block();
|
|
67
|
+
|
|
68
|
+
return new ASTNode('FunctionDeclaration', { name, parameters, body });
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
classDeclaration() {
|
|
72
|
+
const name = this.consume('IDENTIFIER', 'Expected class name').value;
|
|
73
|
+
|
|
74
|
+
let superclass = null;
|
|
75
|
+
if (this.match('LT')) {
|
|
76
|
+
superclass = this.consume('IDENTIFIER', 'Expected superclass name').value;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
this.consume('LBRACE', 'Expected { before class body');
|
|
80
|
+
|
|
81
|
+
const methods = [];
|
|
82
|
+
const properties = [];
|
|
83
|
+
|
|
84
|
+
while (!this.check('RBRACE') && !this.isAtEnd()) {
|
|
85
|
+
if (this.match('DEF', 'FUNCTION')) {
|
|
86
|
+
methods.push(this.functionDeclaration());
|
|
87
|
+
} else if (this.match('LET', 'CONST', 'VAR')) {
|
|
88
|
+
properties.push(this.variableDeclaration());
|
|
89
|
+
} else {
|
|
90
|
+
this.advance();
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
this.consume('RBRACE', 'Expected } after class body');
|
|
95
|
+
|
|
96
|
+
return new ASTNode('ClassDeclaration', { name, superclass, methods, properties });
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
importStatement() {
|
|
100
|
+
const specifiers = [];
|
|
101
|
+
|
|
102
|
+
if (this.match('LBRACE')) {
|
|
103
|
+
do {
|
|
104
|
+
const name = this.consume('IDENTIFIER', 'Expected import specifier').value;
|
|
105
|
+
let alias = name;
|
|
106
|
+
if (this.match('AS')) {
|
|
107
|
+
alias = this.consume('IDENTIFIER', 'Expected alias').value;
|
|
108
|
+
}
|
|
109
|
+
specifiers.push({ name, alias });
|
|
110
|
+
} while (this.match('COMMA'));
|
|
111
|
+
this.consume('RBRACE', 'Expected }');
|
|
112
|
+
} else if (this.match('MULT')) {
|
|
113
|
+
this.consume('AS', 'Expected as after *');
|
|
114
|
+
const alias = this.consume('IDENTIFIER', 'Expected alias').value;
|
|
115
|
+
specifiers.push({ name: '*', alias });
|
|
116
|
+
} else {
|
|
117
|
+
const name = this.consume('IDENTIFIER', 'Expected import name').value;
|
|
118
|
+
specifiers.push({ name, alias: name });
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
this.consume('FROM', 'Expected from');
|
|
122
|
+
const source = this.consume('STRING', 'Expected module path').value;
|
|
123
|
+
|
|
124
|
+
this.consumeOptional('SEMICOLON');
|
|
125
|
+
return new ASTNode('ImportStatement', { specifiers, source });
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
tryStatement() {
|
|
129
|
+
this.consume('LBRACE', 'Expected { after try');
|
|
130
|
+
const tryBlock = this.block();
|
|
131
|
+
|
|
132
|
+
let catchClause = null;
|
|
133
|
+
if (this.match('CATCH')) {
|
|
134
|
+
let parameter = null;
|
|
135
|
+
if (this.match('LPAREN')) {
|
|
136
|
+
parameter = this.consume('IDENTIFIER', 'Expected parameter name').value;
|
|
137
|
+
this.consume('RPAREN', 'Expected )');
|
|
138
|
+
}
|
|
139
|
+
this.consume('LBRACE', 'Expected {');
|
|
140
|
+
const catchBlock = this.block();
|
|
141
|
+
catchClause = { parameter, body: catchBlock };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
let finallyBlock = null;
|
|
145
|
+
if (this.match('FINALLY')) {
|
|
146
|
+
this.consume('LBRACE', 'Expected {');
|
|
147
|
+
finallyBlock = this.block();
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return new ASTNode('TryStatement', { tryBlock, catchClause, finallyBlock });
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
ifStatement() {
|
|
154
|
+
this.consume('LPAREN', 'Expected ( after if');
|
|
155
|
+
const condition = this.expression();
|
|
156
|
+
this.consume('RPAREN', 'Expected ) after condition');
|
|
157
|
+
this.consume('LBRACE', 'Expected { after condition');
|
|
158
|
+
|
|
159
|
+
const thenBranch = this.block();
|
|
160
|
+
|
|
161
|
+
const elifBranches = [];
|
|
162
|
+
while (this.match('ELSIF', 'ELIF')) {
|
|
163
|
+
this.consume('LPAREN', 'Expected (');
|
|
164
|
+
const elifCondition = this.expression();
|
|
165
|
+
this.consume('RPAREN', 'Expected )');
|
|
166
|
+
this.consume('LBRACE', 'Expected {');
|
|
167
|
+
const elifBody = this.block();
|
|
168
|
+
elifBranches.push({ condition: elifCondition, body: elifBody });
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
let elseBranch = null;
|
|
172
|
+
if (this.match('ELSE')) {
|
|
173
|
+
this.consume('LBRACE', 'Expected {');
|
|
174
|
+
elseBranch = this.block();
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return new ASTNode('IfStatement', { condition, thenBranch, elifBranches, elseBranch });
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
whileStatement() {
|
|
181
|
+
this.consume('LPAREN', 'Expected ( after while');
|
|
182
|
+
const condition = this.expression();
|
|
183
|
+
this.consume('RPAREN', 'Expected ) after condition');
|
|
184
|
+
this.consume('LBRACE', 'Expected { after condition');
|
|
185
|
+
const body = this.block();
|
|
186
|
+
|
|
187
|
+
return new ASTNode('WhileStatement', { condition, body });
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
forStatement() {
|
|
191
|
+
const iterator = this.consume('IDENTIFIER', 'Expected iterator variable').value;
|
|
192
|
+
this.consume('ASSIGN', 'Expected =');
|
|
193
|
+
const start = this.expression();
|
|
194
|
+
this.consume('TO', 'Expected to');
|
|
195
|
+
const end = this.expression();
|
|
196
|
+
|
|
197
|
+
let step = new ASTNode('Literal', { value: 1 });
|
|
198
|
+
if (this.match('IDENTIFIER') && this.previous().value === 'step') {
|
|
199
|
+
step = this.expression();
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
this.consume('LBRACE', 'Expected {');
|
|
203
|
+
const body = this.block();
|
|
204
|
+
|
|
205
|
+
return new ASTNode('ForStatement', { iterator, start, end, step, body });
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
returnStatement() {
|
|
209
|
+
let value = null;
|
|
210
|
+
if (!this.check('SEMICOLON') && !this.check('RBRACE')) {
|
|
211
|
+
value = this.expression();
|
|
212
|
+
}
|
|
213
|
+
this.consumeOptional('SEMICOLON');
|
|
214
|
+
return new ASTNode('ReturnStatement', { value });
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
expressionStatement() {
|
|
218
|
+
const expr = this.expression();
|
|
219
|
+
this.consumeOptional('SEMICOLON');
|
|
220
|
+
return new ASTNode('ExpressionStatement', { expression: expr });
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
block() {
|
|
224
|
+
const statements = [];
|
|
225
|
+
while (!this.check('RBRACE') && !this.isAtEnd()) {
|
|
226
|
+
statements.push(this.statement());
|
|
227
|
+
}
|
|
228
|
+
this.consume('RBRACE', 'Expected } after block');
|
|
229
|
+
return statements;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
expression() {
|
|
233
|
+
return this.assignment();
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
assignment() {
|
|
237
|
+
const expr = this.logicalOr();
|
|
238
|
+
|
|
239
|
+
if (this.match('ASSIGN', 'PLUS_ASSIGN', 'MINUS_ASSIGN', 'MULT_ASSIGN', 'DIV_ASSIGN')) {
|
|
240
|
+
const operator = this.previous().value;
|
|
241
|
+
const value = this.assignment();
|
|
242
|
+
return new ASTNode('Assignment', { target: expr, operator, value });
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return expr;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
logicalOr() {
|
|
249
|
+
let expr = this.logicalAnd();
|
|
250
|
+
|
|
251
|
+
while (this.match('OR')) {
|
|
252
|
+
const operator = this.previous().value;
|
|
253
|
+
const right = this.logicalAnd();
|
|
254
|
+
expr = new ASTNode('BinaryExpression', { left: expr, operator, right });
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return expr;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
logicalAnd() {
|
|
261
|
+
let expr = this.equality();
|
|
262
|
+
|
|
263
|
+
while (this.match('AND')) {
|
|
264
|
+
const operator = this.previous().value;
|
|
265
|
+
const right = this.equality();
|
|
266
|
+
expr = new ASTNode('BinaryExpression', { left: expr, operator, right });
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return expr;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
equality() {
|
|
273
|
+
let expr = this.comparison();
|
|
274
|
+
|
|
275
|
+
while (this.match('EQ', 'NEQ')) {
|
|
276
|
+
const operator = this.previous().value;
|
|
277
|
+
const right = this.comparison();
|
|
278
|
+
expr = new ASTNode('BinaryExpression', { left: expr, operator, right });
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return expr;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
comparison() {
|
|
285
|
+
let expr = this.term();
|
|
286
|
+
|
|
287
|
+
while (this.match('GT', 'GTE', 'LT', 'LTE')) {
|
|
288
|
+
const operator = this.previous().value;
|
|
289
|
+
const right = this.term();
|
|
290
|
+
expr = new ASTNode('BinaryExpression', { left: expr, operator, right });
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return expr;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
term() {
|
|
297
|
+
let expr = this.factor();
|
|
298
|
+
|
|
299
|
+
while (this.match('PLUS', 'MINUS')) {
|
|
300
|
+
const operator = this.previous().value;
|
|
301
|
+
const right = this.factor();
|
|
302
|
+
expr = new ASTNode('BinaryExpression', { left: expr, operator, right });
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return expr;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
factor() {
|
|
309
|
+
let expr = this.unary();
|
|
310
|
+
|
|
311
|
+
while (this.match('MULT', 'DIV', 'MOD')) {
|
|
312
|
+
const operator = this.previous().value;
|
|
313
|
+
const right = this.unary();
|
|
314
|
+
expr = new ASTNode('BinaryExpression', { left: expr, operator, right });
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
return expr;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
unary() {
|
|
321
|
+
if (this.match('NOT', 'MINUS', 'PLUS')) {
|
|
322
|
+
const operator = this.previous().value;
|
|
323
|
+
const operand = this.unary();
|
|
324
|
+
return new ASTNode('UnaryExpression', { operator, operand });
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
return this.postfix();
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
postfix() {
|
|
331
|
+
let expr = this.primary();
|
|
332
|
+
|
|
333
|
+
while (true) {
|
|
334
|
+
if (this.match('LPAREN')) {
|
|
335
|
+
const args = [];
|
|
336
|
+
if (!this.check('RPAREN')) {
|
|
337
|
+
do {
|
|
338
|
+
args.push(this.expression());
|
|
339
|
+
} while (this.match('COMMA'));
|
|
340
|
+
}
|
|
341
|
+
this.consume('RPAREN', 'Expected ) after arguments');
|
|
342
|
+
expr = new ASTNode('CallExpression', { callee: expr, arguments: args });
|
|
343
|
+
} else if (this.match('LBRACKET')) {
|
|
344
|
+
const index = this.expression();
|
|
345
|
+
this.consume('RBRACKET', 'Expected ]');
|
|
346
|
+
expr = new ASTNode('IndexExpression', { object: expr, index });
|
|
347
|
+
} else if (this.match('DOT')) {
|
|
348
|
+
const property = this.consume('IDENTIFIER', 'Expected property name').value;
|
|
349
|
+
expr = new ASTNode('MemberExpression', { object: expr, property });
|
|
350
|
+
} else {
|
|
351
|
+
break;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
return expr;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
primary() {
|
|
359
|
+
if (this.match('TRUE')) return new ASTNode('Literal', { value: true });
|
|
360
|
+
if (this.match('FALSE')) return new ASTNode('Literal', { value: false });
|
|
361
|
+
if (this.match('NULL', 'NIL')) return new ASTNode('Literal', { value: null });
|
|
362
|
+
|
|
363
|
+
if (this.match('NUMBER')) {
|
|
364
|
+
return new ASTNode('Literal', { value: this.previous().value });
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
if (this.match('STRING')) {
|
|
368
|
+
return new ASTNode('Literal', { value: this.previous().value });
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
if (this.match('IDENTIFIER')) {
|
|
372
|
+
return new ASTNode('Identifier', { name: this.previous().value });
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
if (this.match('LPAREN')) {
|
|
376
|
+
const expr = this.expression();
|
|
377
|
+
this.consume('RPAREN', 'Expected ) after expression');
|
|
378
|
+
return expr;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
if (this.match('LBRACKET')) {
|
|
382
|
+
const elements = [];
|
|
383
|
+
if (!this.check('RBRACKET')) {
|
|
384
|
+
do {
|
|
385
|
+
elements.push(this.expression());
|
|
386
|
+
} while (this.match('COMMA'));
|
|
387
|
+
}
|
|
388
|
+
this.consume('RBRACKET', 'Expected ]');
|
|
389
|
+
return new ASTNode('ArrayLiteral', { elements });
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
if (this.match('LBRACE')) {
|
|
393
|
+
const properties = [];
|
|
394
|
+
if (!this.check('RBRACE')) {
|
|
395
|
+
do {
|
|
396
|
+
const key = this.consume('IDENTIFIER', 'Expected property name').value;
|
|
397
|
+
this.consume('COLON', 'Expected :');
|
|
398
|
+
const value = this.expression();
|
|
399
|
+
properties.push({ key, value });
|
|
400
|
+
} while (this.match('COMMA'));
|
|
401
|
+
}
|
|
402
|
+
this.consume('RBRACE', 'Expected }');
|
|
403
|
+
return new ASTNode('ObjectLiteral', { properties });
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
throw new Error(`Unexpected token: ${this.peek().type} at line ${this.peek().line}`);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
match(...types) {
|
|
410
|
+
for (const type of types) {
|
|
411
|
+
if (this.check(type)) {
|
|
412
|
+
this.advance();
|
|
413
|
+
return true;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
return false;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
check(type) {
|
|
420
|
+
if (this.isAtEnd()) return false;
|
|
421
|
+
return this.peek().type === type;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
advance() {
|
|
425
|
+
if (!this.isAtEnd()) this.position++;
|
|
426
|
+
return this.previous();
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
isAtEnd() {
|
|
430
|
+
return this.position >= this.tokens.length;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
peek() {
|
|
434
|
+
return this.tokens[this.position];
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
previous() {
|
|
438
|
+
return this.tokens[this.position - 1];
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
consume(type, message) {
|
|
442
|
+
if (this.check(type)) return this.advance();
|
|
443
|
+
const token = this.peek();
|
|
444
|
+
throw new Error(`${message} at line ${token.line}, column ${token.column}`);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
consumeOptional(type) {
|
|
448
|
+
if (this.check(type)) this.advance();
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
module.exports = Parser;
|