fazer-lang 0.1.0 → 2.1.0
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/README.md +137 -0
- package/apply_icon.js +28 -0
- package/apply_icon_robust.js +51 -0
- package/bin/fazer.exe +0 -0
- package/fazer.js +1192 -139
- package/package.json +38 -44
- package/package.json.bak +44 -0
- package/standalone.bundled.js +12910 -0
- package/standalone.js +186 -0
- package/test.fz +92 -0
package/fazer.js
CHANGED
|
@@ -1,175 +1,1228 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
const
|
|
12
|
-
const
|
|
13
|
-
const
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
const
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
const
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
const
|
|
29
|
-
const
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
Fazer v2 — interpreter "batteries included"
|
|
6
|
+
- Chevrotain lexer/parser → AST
|
|
7
|
+
- Runtime with scopes, functions, pipes, lists/maps, property/index access
|
|
8
|
+
- Stdlib: fs/path/crypto/encoding/ui
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const fs = require("fs");
|
|
12
|
+
const path = require("path");
|
|
13
|
+
const crypto = require("crypto");
|
|
14
|
+
const { Lexer, createToken, EmbeddedActionsParser } = require("chevrotain");
|
|
15
|
+
|
|
16
|
+
/* ────────────────────────────────────────────────────────────────────────── */
|
|
17
|
+
/* Tokens */
|
|
18
|
+
/* ────────────────────────────────────────────────────────────────────────── */
|
|
19
|
+
|
|
20
|
+
const WhiteSpace = createToken({ name: "WhiteSpace", pattern: /[ \t\r\n]+/, group: Lexer.SKIPPED });
|
|
21
|
+
const Comment = createToken({ name: "Comment", pattern: /#[^\n]*/, group: Lexer.SKIPPED });
|
|
22
|
+
|
|
23
|
+
const Assign = createToken({ name: "Assign", pattern: /:=/ });
|
|
24
|
+
|
|
25
|
+
const Arrow = createToken({ name: "Arrow", pattern: /→/ });
|
|
26
|
+
const DoublePipe = createToken({ name: "DoublePipe", pattern: /→>/ });
|
|
27
|
+
|
|
28
|
+
const Case = createToken({ name: "Case", pattern: /case\b/ });
|
|
29
|
+
const Else = createToken({ name: "Else", pattern: /else\b/ });
|
|
30
|
+
const End = createToken({ name: "End", pattern: /end\b/ });
|
|
31
|
+
|
|
32
|
+
const Fn = createToken({ name: "Fn", pattern: /fn\b/ });
|
|
33
|
+
const Return = createToken({ name: "Return", pattern: /return\b/ });
|
|
34
|
+
const Mut = createToken({ name: "Mut", pattern: /mut\b/ });
|
|
35
|
+
|
|
36
|
+
const True = createToken({ name: "True", pattern: /true\b/ });
|
|
37
|
+
const False = createToken({ name: "False", pattern: /false\b/ });
|
|
38
|
+
const Null = createToken({ name: "Null", pattern: /null\b/ });
|
|
39
|
+
|
|
40
|
+
const And = createToken({ name: "And", pattern: /and\b/ });
|
|
41
|
+
const Or = createToken({ name: "Or", pattern: /or\b/ });
|
|
42
|
+
const Not = createToken({ name: "Not", pattern: /not\b/ });
|
|
43
|
+
|
|
44
|
+
const GreaterEq = createToken({ name: "GreaterEq", pattern: />=/ });
|
|
45
|
+
const LessEq = createToken({ name: "LessEq", pattern: /<=/ });
|
|
46
|
+
const Eq = createToken({ name: "Eq", pattern: /==/ });
|
|
47
|
+
const NotEq = createToken({ name: "NotEq", pattern: /!=/ });
|
|
48
|
+
const Greater = createToken({ name: "Greater", pattern: />/ });
|
|
49
|
+
const Less = createToken({ name: "Less", pattern: /</ });
|
|
50
|
+
|
|
51
|
+
const LParen = createToken({ name: "LParen", pattern: /\(/ });
|
|
52
|
+
const RParen = createToken({ name: "RParen", pattern: /\)/ });
|
|
53
|
+
const LBracket = createToken({ name: "LBracket", pattern: /\[/ });
|
|
54
|
+
const RBracket = createToken({ name: "RBracket", pattern: /\]/ });
|
|
55
|
+
const LBrace = createToken({ name: "LBrace", pattern: /{/ });
|
|
56
|
+
const RBrace = createToken({ name: "RBrace", pattern: /}/ });
|
|
57
|
+
|
|
58
|
+
const Colon = createToken({ name: "Colon", pattern: /:/ });
|
|
59
|
+
const Comma = createToken({ name: "Comma", pattern: /,/ });
|
|
60
|
+
const Dot = createToken({ name: "Dot", pattern: /\./ });
|
|
61
|
+
|
|
62
|
+
const Plus = createToken({ name: "Plus", pattern: /\+/ });
|
|
63
|
+
const Minus = createToken({ name: "Minus", pattern: /-/ });
|
|
64
|
+
const Star = createToken({ name: "Star", pattern: /\*/ });
|
|
65
|
+
const Slash = createToken({ name: "Slash", pattern: /\// });
|
|
66
|
+
const Percent = createToken({ name: "Percent", pattern: /%/ });
|
|
67
|
+
|
|
68
|
+
const Float = createToken({ name: "Float", pattern: /-?\d+\.\d+/ });
|
|
69
|
+
const Integer = createToken({ name: "Integer", pattern: /-?\d+/ });
|
|
70
|
+
const StringLiteral = createToken({
|
|
71
|
+
name: "StringLiteral",
|
|
72
|
+
pattern: /"([^"\\]|\\.)*"/,
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const Identifier = createToken({
|
|
76
|
+
name: "Identifier",
|
|
77
|
+
pattern: /[a-zA-Z_][a-zA-Z0-9_]*/,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const allTokens = [
|
|
81
|
+
WhiteSpace,
|
|
82
|
+
Comment,
|
|
83
|
+
|
|
84
|
+
Assign,
|
|
85
|
+
|
|
86
|
+
DoublePipe,
|
|
87
|
+
Arrow,
|
|
88
|
+
|
|
89
|
+
Case,
|
|
90
|
+
Else,
|
|
91
|
+
End,
|
|
92
|
+
|
|
93
|
+
Fn,
|
|
94
|
+
Return,
|
|
95
|
+
Mut,
|
|
96
|
+
|
|
97
|
+
True,
|
|
98
|
+
False,
|
|
99
|
+
Null,
|
|
100
|
+
|
|
101
|
+
And,
|
|
102
|
+
Or,
|
|
103
|
+
Not,
|
|
104
|
+
|
|
105
|
+
GreaterEq,
|
|
106
|
+
LessEq,
|
|
107
|
+
Eq,
|
|
108
|
+
NotEq,
|
|
109
|
+
Greater,
|
|
110
|
+
Less,
|
|
111
|
+
|
|
112
|
+
LParen,
|
|
113
|
+
RParen,
|
|
114
|
+
LBracket,
|
|
115
|
+
RBracket,
|
|
116
|
+
LBrace,
|
|
117
|
+
RBrace,
|
|
118
|
+
|
|
119
|
+
Colon,
|
|
120
|
+
Comma,
|
|
121
|
+
Dot,
|
|
122
|
+
|
|
123
|
+
Plus,
|
|
124
|
+
Minus,
|
|
125
|
+
Star,
|
|
126
|
+
Slash,
|
|
127
|
+
Percent,
|
|
128
|
+
|
|
129
|
+
Float,
|
|
130
|
+
Integer,
|
|
131
|
+
StringLiteral,
|
|
132
|
+
Identifier,
|
|
133
|
+
];
|
|
134
|
+
|
|
135
|
+
const lexer = new Lexer(allTokens, { positionTracking: "full" });
|
|
136
|
+
|
|
137
|
+
/* ────────────────────────────────────────────────────────────────────────── */
|
|
138
|
+
/* Parser → AST */
|
|
139
|
+
/* ────────────────────────────────────────────────────────────────────────── */
|
|
140
|
+
|
|
141
|
+
class FazerParser extends EmbeddedActionsParser {
|
|
41
142
|
constructor() {
|
|
42
|
-
super(
|
|
143
|
+
super(allTokens, { recoveryEnabled: true });
|
|
43
144
|
const $ = this;
|
|
44
|
-
|
|
145
|
+
|
|
146
|
+
const node = (type, props) => ({ type, ...props });
|
|
147
|
+
|
|
148
|
+
$.RULE("program", () => {
|
|
149
|
+
const stmts = [];
|
|
150
|
+
$.MANY(() => {
|
|
151
|
+
const s = $.SUBRULE($.statement);
|
|
152
|
+
if (s) stmts.push(s);
|
|
153
|
+
});
|
|
154
|
+
return stmts;
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
$.RULE("statement", () =>
|
|
158
|
+
$.OR([
|
|
159
|
+
{ ALT: () => $.SUBRULE($.fnDef) },
|
|
160
|
+
{ ALT: () => $.SUBRULE($.returnStmt) },
|
|
161
|
+
{ ALT: () => $.SUBRULE($.assignStmt) },
|
|
162
|
+
{ ALT: () => $.SUBRULE($.caseBlock) },
|
|
163
|
+
{ ALT: () => $.SUBRULE($.exprStmt) },
|
|
164
|
+
])
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
$.RULE("fnDef", () => {
|
|
168
|
+
$.CONSUME(Fn);
|
|
169
|
+
const nameTok = $.CONSUME(Identifier);
|
|
170
|
+
$.CONSUME(LParen);
|
|
171
|
+
const params = [];
|
|
172
|
+
$.OPTION(() => {
|
|
173
|
+
params.push($.CONSUME2(Identifier).image);
|
|
174
|
+
$.MANY(() => {
|
|
175
|
+
$.CONSUME(Comma);
|
|
176
|
+
params.push($.CONSUME3(Identifier).image);
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
$.CONSUME(RParen);
|
|
180
|
+
$.CONSUME(Arrow);
|
|
181
|
+
const body = $.SUBRULE($.block);
|
|
182
|
+
return node("fn", { name: nameTok.image, params, body, loc: locOf(nameTok) });
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
$.RULE("returnStmt", () => {
|
|
186
|
+
const tok = $.CONSUME(Return);
|
|
187
|
+
const value = $.OPTION(() => $.SUBRULE($.expression));
|
|
188
|
+
return node("return", { value: value ?? node("null", {}), loc: locOf(tok) });
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
$.RULE("assignStmt", () => {
|
|
192
|
+
const mutTok = $.OPTION(() => $.CONSUME(Mut));
|
|
193
|
+
const idTok = $.CONSUME(Identifier);
|
|
194
|
+
$.CONSUME(Assign);
|
|
195
|
+
const value = $.SUBRULE($.expression);
|
|
196
|
+
return node("assign", {
|
|
197
|
+
name: idTok.image,
|
|
198
|
+
mut: !!mutTok,
|
|
199
|
+
value,
|
|
200
|
+
loc: locOf(idTok),
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
$.RULE("caseBlock", () => {
|
|
205
|
+
const caseTok = $.CONSUME(Case);
|
|
206
|
+
const expr = $.SUBRULE($.expression);
|
|
207
|
+
const arms = [];
|
|
208
|
+
$.AT_LEAST_ONE(() => {
|
|
209
|
+
const pat = $.OR([
|
|
210
|
+
{ ALT: () => $.SUBRULE($.pattern) },
|
|
211
|
+
{ ALT: () => ( $.CONSUME(Else), node("else", {}) ) },
|
|
212
|
+
]);
|
|
213
|
+
$.CONSUME(Arrow);
|
|
214
|
+
const body = $.SUBRULE($.block);
|
|
215
|
+
arms.push({ pat, body });
|
|
216
|
+
});
|
|
217
|
+
$.CONSUME(End);
|
|
218
|
+
return node("case", { expr, arms, loc: locOf(caseTok) });
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
$.RULE("block", () => {
|
|
222
|
+
const stmts = [];
|
|
223
|
+
$.MANY(() => {
|
|
224
|
+
const s = $.SUBRULE($.statement);
|
|
225
|
+
if (s) stmts.push(s);
|
|
226
|
+
});
|
|
227
|
+
$.CONSUME(End);
|
|
228
|
+
return stmts;
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
$.RULE("exprStmt", () => {
|
|
232
|
+
const expr = $.SUBRULE($.expression);
|
|
233
|
+
if (!expr) return node("noop", {});
|
|
234
|
+
return node("exprstmt", { expr, loc: expr.loc ?? null });
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
$.RULE("pattern", () =>
|
|
238
|
+
$.OR([
|
|
239
|
+
{ ALT: () => $.SUBRULE($.patternCompare) },
|
|
240
|
+
{ ALT: () => $.SUBRULE($.literal) },
|
|
241
|
+
{ ALT: () => node("identPat", { name: $.CONSUME(Identifier).image }) }, // binds
|
|
242
|
+
])
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
$.RULE("patternCompare", () => {
|
|
246
|
+
// Examples:
|
|
247
|
+
// > 10
|
|
248
|
+
// <= 3
|
|
249
|
+
const op = $.OR([
|
|
250
|
+
{ ALT: () => $.CONSUME(GreaterEq).image },
|
|
251
|
+
{ ALT: () => $.CONSUME(LessEq).image },
|
|
252
|
+
{ ALT: () => $.CONSUME(Greater).image },
|
|
253
|
+
{ ALT: () => $.CONSUME(Less).image },
|
|
254
|
+
{ ALT: () => $.CONSUME(Eq).image },
|
|
255
|
+
{ ALT: () => $.CONSUME(NotEq).image },
|
|
256
|
+
]);
|
|
257
|
+
const rhs = $.SUBRULE($.expression);
|
|
258
|
+
return node("cmpPat", { op, rhs });
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
/* Expression precedence:
|
|
262
|
+
pipe →> (lowest)
|
|
263
|
+
or
|
|
264
|
+
and
|
|
265
|
+
equality
|
|
266
|
+
relational
|
|
267
|
+
additive
|
|
268
|
+
multiplicative
|
|
269
|
+
unary
|
|
270
|
+
postfix (calls, dot, index)
|
|
271
|
+
primary
|
|
272
|
+
*/
|
|
273
|
+
|
|
274
|
+
$.RULE("expression", () => $.SUBRULE($.pipeExpr));
|
|
275
|
+
|
|
276
|
+
$.RULE("pipeExpr", () => {
|
|
277
|
+
let left = $.SUBRULE($.orExpr);
|
|
278
|
+
$.MANY(() => {
|
|
279
|
+
$.CONSUME(DoublePipe);
|
|
280
|
+
const right = $.SUBRULE2($.orExpr);
|
|
281
|
+
left = node("pipe", { left, right, loc: left.loc ?? null });
|
|
282
|
+
});
|
|
283
|
+
return left;
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
$.RULE("orExpr", () => {
|
|
287
|
+
let left = $.SUBRULE($.andExpr);
|
|
288
|
+
$.MANY(() => {
|
|
289
|
+
$.CONSUME(Or);
|
|
290
|
+
const right = $.SUBRULE2($.andExpr);
|
|
291
|
+
left = node("bin", { op: "or", left, right, loc: left.loc ?? null });
|
|
292
|
+
});
|
|
293
|
+
return left;
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
$.RULE("andExpr", () => {
|
|
297
|
+
let left = $.SUBRULE($.eqExpr);
|
|
298
|
+
$.MANY(() => {
|
|
299
|
+
$.CONSUME(And);
|
|
300
|
+
const right = $.SUBRULE2($.eqExpr);
|
|
301
|
+
left = node("bin", { op: "and", left, right, loc: left.loc ?? null });
|
|
302
|
+
});
|
|
303
|
+
return left;
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
$.RULE("eqExpr", () => {
|
|
307
|
+
let left = $.SUBRULE($.relExpr);
|
|
308
|
+
$.MANY(() => {
|
|
309
|
+
const op = $.OR([
|
|
310
|
+
{ ALT: () => $.CONSUME(Eq).image },
|
|
311
|
+
{ ALT: () => $.CONSUME(NotEq).image },
|
|
312
|
+
]);
|
|
313
|
+
const right = $.SUBRULE2($.relExpr);
|
|
314
|
+
left = node("bin", { op, left, right, loc: left.loc ?? null });
|
|
315
|
+
});
|
|
316
|
+
return left;
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
$.RULE("relExpr", () => {
|
|
320
|
+
let left = $.SUBRULE($.addExpr);
|
|
321
|
+
$.MANY(() => {
|
|
322
|
+
const op = $.OR([
|
|
323
|
+
{ ALT: () => $.CONSUME(GreaterEq).image },
|
|
324
|
+
{ ALT: () => $.CONSUME(LessEq).image },
|
|
325
|
+
{ ALT: () => $.CONSUME(Greater).image },
|
|
326
|
+
{ ALT: () => $.CONSUME(Less).image },
|
|
327
|
+
]);
|
|
328
|
+
const right = $.SUBRULE2($.addExpr);
|
|
329
|
+
left = node("bin", { op, left, right, loc: left.loc ?? null });
|
|
330
|
+
});
|
|
331
|
+
return left;
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
$.RULE("addExpr", () => {
|
|
335
|
+
let left = $.SUBRULE($.mulExpr);
|
|
336
|
+
$.MANY(() => {
|
|
337
|
+
const op = $.OR([
|
|
338
|
+
{ ALT: () => $.CONSUME(Plus).image },
|
|
339
|
+
{ ALT: () => $.CONSUME(Minus).image },
|
|
340
|
+
]);
|
|
341
|
+
const right = $.SUBRULE2($.mulExpr);
|
|
342
|
+
left = node("bin", { op, left, right, loc: left.loc ?? null });
|
|
343
|
+
});
|
|
344
|
+
return left;
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
$.RULE("mulExpr", () => {
|
|
348
|
+
let left = $.SUBRULE($.unaryExpr);
|
|
349
|
+
$.MANY(() => {
|
|
350
|
+
const op = $.OR([
|
|
351
|
+
{ ALT: () => $.CONSUME(Star).image },
|
|
352
|
+
{ ALT: () => $.CONSUME(Slash).image },
|
|
353
|
+
{ ALT: () => $.CONSUME(Percent).image },
|
|
354
|
+
]);
|
|
355
|
+
const right = $.SUBRULE2($.unaryExpr);
|
|
356
|
+
left = node("bin", { op, left, right, loc: left.loc ?? null });
|
|
357
|
+
});
|
|
358
|
+
return left;
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
$.RULE("unaryExpr", () =>
|
|
45
362
|
$.OR([
|
|
46
|
-
{
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
{
|
|
54
|
-
|
|
55
|
-
|
|
363
|
+
{
|
|
364
|
+
ALT: () => {
|
|
365
|
+
const tok = $.CONSUME(Not);
|
|
366
|
+
const expr = $.SUBRULE($.unaryExpr);
|
|
367
|
+
return node("un", { op: "not", expr, loc: locOf(tok) });
|
|
368
|
+
},
|
|
369
|
+
},
|
|
370
|
+
{
|
|
371
|
+
ALT: () => {
|
|
372
|
+
const tok = $.CONSUME(Minus);
|
|
373
|
+
const expr = $.SUBRULE2($.unaryExpr);
|
|
374
|
+
return node("un", { op: "-", expr, loc: locOf(tok) });
|
|
375
|
+
},
|
|
376
|
+
},
|
|
377
|
+
{ ALT: () => $.SUBRULE($.postfixExpr) },
|
|
378
|
+
])
|
|
379
|
+
);
|
|
380
|
+
|
|
381
|
+
$.RULE("postfixExpr", () => {
|
|
382
|
+
let base = $.SUBRULE($.primaryExpr);
|
|
383
|
+
|
|
384
|
+
$.MANY(() => {
|
|
385
|
+
$.OR([
|
|
386
|
+
{
|
|
387
|
+
ALT: () => {
|
|
388
|
+
// call: foo(a,b)
|
|
389
|
+
$.CONSUME(LParen);
|
|
390
|
+
const args = [];
|
|
391
|
+
$.OPTION(() => {
|
|
392
|
+
args.push($.SUBRULE($.expression));
|
|
393
|
+
$.MANY2(() => {
|
|
394
|
+
$.CONSUME(Comma);
|
|
395
|
+
args.push($.SUBRULE2($.expression));
|
|
396
|
+
});
|
|
397
|
+
});
|
|
398
|
+
$.CONSUME(RParen);
|
|
399
|
+
base = node("call", { callee: base, args, loc: base.loc ?? null });
|
|
400
|
+
},
|
|
401
|
+
},
|
|
402
|
+
{
|
|
403
|
+
ALT: () => {
|
|
404
|
+
// dot: obj.key
|
|
405
|
+
$.CONSUME(Dot);
|
|
406
|
+
const keyTok = $.CONSUME(Identifier);
|
|
407
|
+
base = node("get", { obj: base, key: node("str", { value: keyTok.image }), loc: base.loc ?? null });
|
|
408
|
+
},
|
|
409
|
+
},
|
|
410
|
+
{
|
|
411
|
+
ALT: () => {
|
|
412
|
+
// index: obj[expr]
|
|
413
|
+
$.CONSUME(LBracket);
|
|
414
|
+
const idx = $.SUBRULE3($.expression);
|
|
415
|
+
$.CONSUME(RBracket);
|
|
416
|
+
base = node("idx", { obj: base, idx, loc: base.loc ?? null });
|
|
417
|
+
},
|
|
418
|
+
},
|
|
419
|
+
]);
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
return base;
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
$.RULE("primaryExpr", () =>
|
|
426
|
+
$.OR([
|
|
427
|
+
{ ALT: () => $.SUBRULE($.literal) },
|
|
428
|
+
{
|
|
429
|
+
ALT: () => {
|
|
430
|
+
const tok = $.CONSUME(Identifier);
|
|
431
|
+
return node("ident", { name: tok.image, loc: locOf(tok) });
|
|
432
|
+
},
|
|
433
|
+
},
|
|
434
|
+
{
|
|
435
|
+
ALT: () => {
|
|
436
|
+
$.CONSUME(LParen);
|
|
437
|
+
const e = $.SUBRULE($.expression);
|
|
438
|
+
$.CONSUME(RParen);
|
|
439
|
+
return e;
|
|
440
|
+
},
|
|
441
|
+
},
|
|
442
|
+
{ ALT: () => $.SUBRULE($.listLiteral) },
|
|
443
|
+
{ ALT: () => $.SUBRULE($.mapLiteral) },
|
|
444
|
+
])
|
|
445
|
+
);
|
|
446
|
+
|
|
447
|
+
$.RULE("literal", () =>
|
|
448
|
+
$.OR([
|
|
449
|
+
{
|
|
450
|
+
ALT: () => {
|
|
451
|
+
const tok = $.CONSUME(Float);
|
|
452
|
+
return node("num", { value: Number(tok.image), loc: locOf(tok) });
|
|
453
|
+
},
|
|
454
|
+
},
|
|
455
|
+
{
|
|
456
|
+
ALT: () => {
|
|
457
|
+
const tok = $.CONSUME(Integer);
|
|
458
|
+
return node("num", { value: Number(tok.image), loc: locOf(tok) });
|
|
459
|
+
},
|
|
460
|
+
},
|
|
461
|
+
{
|
|
462
|
+
ALT: () => {
|
|
463
|
+
const tok = $.CONSUME(StringLiteral);
|
|
464
|
+
return node("str", { value: unescapeString(tok.image), loc: locOf(tok) });
|
|
465
|
+
},
|
|
466
|
+
},
|
|
467
|
+
{
|
|
468
|
+
ALT: () => {
|
|
469
|
+
const tok = $.CONSUME(True);
|
|
470
|
+
return node("bool", { value: true, loc: locOf(tok) });
|
|
471
|
+
},
|
|
472
|
+
},
|
|
473
|
+
{
|
|
474
|
+
ALT: () => {
|
|
475
|
+
const tok = $.CONSUME(False);
|
|
476
|
+
return node("bool", { value: false, loc: locOf(tok) });
|
|
477
|
+
},
|
|
478
|
+
},
|
|
479
|
+
{
|
|
480
|
+
ALT: () => {
|
|
481
|
+
const tok = $.CONSUME(Null);
|
|
482
|
+
return node("null", { loc: locOf(tok) });
|
|
483
|
+
},
|
|
484
|
+
},
|
|
485
|
+
])
|
|
486
|
+
);
|
|
487
|
+
|
|
488
|
+
$.RULE("listLiteral", () => {
|
|
489
|
+
const tok = $.CONSUME(LBracket);
|
|
490
|
+
const items = [];
|
|
491
|
+
$.OPTION(() => {
|
|
492
|
+
items.push($.SUBRULE($.expression));
|
|
493
|
+
$.MANY(() => {
|
|
494
|
+
$.CONSUME(Comma);
|
|
495
|
+
items.push($.SUBRULE2($.expression));
|
|
496
|
+
});
|
|
497
|
+
});
|
|
498
|
+
$.CONSUME(RBracket);
|
|
499
|
+
return node("list", { items, loc: locOf(tok) });
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
$.RULE("mapLiteral", () => {
|
|
503
|
+
const tok = $.CONSUME(LBrace);
|
|
504
|
+
const entries = [];
|
|
505
|
+
$.OPTION(() => {
|
|
506
|
+
entries.push($.SUBRULE($.mapEntry));
|
|
507
|
+
$.MANY(() => {
|
|
508
|
+
$.CONSUME(Comma);
|
|
509
|
+
entries.push($.SUBRULE2($.mapEntry));
|
|
510
|
+
});
|
|
511
|
+
});
|
|
512
|
+
$.CONSUME(RBrace);
|
|
513
|
+
return node("map", { entries, loc: locOf(tok) });
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
$.RULE("mapEntry", () => {
|
|
517
|
+
// key can be Identifier or String
|
|
518
|
+
const key = $.OR([
|
|
519
|
+
{
|
|
520
|
+
ALT: () => {
|
|
521
|
+
const t = $.CONSUME(StringLiteral);
|
|
522
|
+
return node("str", { value: unescapeString(t.image), loc: locOf(t) });
|
|
523
|
+
},
|
|
524
|
+
},
|
|
525
|
+
{
|
|
526
|
+
ALT: () => {
|
|
527
|
+
const t = $.CONSUME(Identifier);
|
|
528
|
+
return node("str", { value: t.image, loc: locOf(t) });
|
|
529
|
+
},
|
|
530
|
+
},
|
|
56
531
|
]);
|
|
532
|
+
$.CONSUME(Colon);
|
|
533
|
+
const value = $.SUBRULE($.expression);
|
|
534
|
+
return { key, value };
|
|
57
535
|
});
|
|
58
|
-
|
|
536
|
+
|
|
59
537
|
this.performSelfAnalysis();
|
|
60
538
|
}
|
|
61
539
|
}
|
|
62
|
-
const parser = new FazerParser();
|
|
63
540
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
541
|
+
/* ────────────────────────────────────────────────────────────────────────── */
|
|
542
|
+
/* Runtime */
|
|
543
|
+
/* ────────────────────────────────────────────────────────────────────────── */
|
|
544
|
+
|
|
545
|
+
class FazerError extends Error {
|
|
546
|
+
constructor(message, meta = {}) {
|
|
547
|
+
super(message);
|
|
548
|
+
this.name = "FazerError";
|
|
549
|
+
this.meta = meta;
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
class ReturnSignal {
|
|
554
|
+
constructor(value) {
|
|
555
|
+
this.value = value;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
class Scope {
|
|
560
|
+
constructor(parent = null) {
|
|
561
|
+
this.parent = parent;
|
|
562
|
+
this.vars = new Map(); // name -> {value, mut}
|
|
563
|
+
}
|
|
564
|
+
hasHere(name) { return this.vars.has(name); }
|
|
565
|
+
get(name) {
|
|
566
|
+
if (this.vars.has(name)) return this.vars.get(name);
|
|
567
|
+
if (this.parent) return this.parent.get(name);
|
|
568
|
+
return null;
|
|
569
|
+
}
|
|
570
|
+
set(name, value, mut = false) {
|
|
571
|
+
this.vars.set(name, { value, mut });
|
|
572
|
+
}
|
|
573
|
+
assign(name, value) {
|
|
574
|
+
if (this.vars.has(name)) {
|
|
575
|
+
const cell = this.vars.get(name);
|
|
576
|
+
if (!cell.mut) throw new FazerError(`Variable '${name}' is immutable (use mut)`);
|
|
577
|
+
cell.value = value;
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
if (this.parent) return this.parent.assign(name, value);
|
|
581
|
+
throw new FazerError(`Undefined variable '${name}'`);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
class FazerRuntime {
|
|
586
|
+
constructor({ argv = process.argv.slice(2), filename = "<stdin>", code = "" } = {}) {
|
|
587
|
+
this.filename = filename;
|
|
588
|
+
this.code = code;
|
|
589
|
+
this.global = new Scope(null);
|
|
590
|
+
this.fns = new Map(); // name -> {params, body, closure}
|
|
591
|
+
this._installStdlib(argv);
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
_installStdlib(argv) {
|
|
595
|
+
const { execFileSync } = require("child_process");
|
|
596
|
+
|
|
597
|
+
const ANSI = {
|
|
598
|
+
reset: "\x1b[0m",
|
|
599
|
+
bold: "\x1b[1m",
|
|
600
|
+
dim: "\x1b[2m",
|
|
601
|
+
red: "\x1b[31m",
|
|
602
|
+
green: "\x1b[32m",
|
|
603
|
+
yellow: "\x1b[33m",
|
|
604
|
+
blue: "\x1b[34m",
|
|
605
|
+
magenta: "\x1b[35m",
|
|
606
|
+
cyan: "\x1b[36m",
|
|
607
|
+
gray: "\x1b[90m",
|
|
608
|
+
};
|
|
609
|
+
|
|
610
|
+
const style = (s, c) => (ANSI[c] || "") + String(s) + ANSI.reset;
|
|
611
|
+
|
|
612
|
+
const stripAnsi = (s) => String(s).replace(/\x1b\[[0-9;]*m/g, "");
|
|
613
|
+
|
|
614
|
+
const box = (title, lines) => {
|
|
615
|
+
const arr = Array.isArray(lines) ? lines.map(String) : [String(lines)];
|
|
616
|
+
const all = [String(title), ...arr];
|
|
617
|
+
|
|
618
|
+
const visibleLen = (s) => stripAnsi(s).length;
|
|
619
|
+
const w = Math.max(...all.map(visibleLen)) + 4;
|
|
620
|
+
|
|
621
|
+
const top = "┌" + "─".repeat(w - 2) + "┐";
|
|
622
|
+
const bot = "└" + "─".repeat(w - 2) + "┘";
|
|
623
|
+
|
|
624
|
+
const mid = all.map((l) => {
|
|
625
|
+
const vl = visibleLen(l);
|
|
626
|
+
const pad = w - 4 - vl;
|
|
627
|
+
return "│ " + l + " ".repeat(Math.max(0, pad)) + " │";
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
console.log(top);
|
|
631
|
+
for (const m of mid) console.log(m);
|
|
632
|
+
console.log(bot);
|
|
633
|
+
return null;
|
|
634
|
+
};
|
|
635
|
+
|
|
636
|
+
const toBuf = (x, enc = "utf8") => {
|
|
637
|
+
if (Buffer.isBuffer(x)) return x;
|
|
638
|
+
if (typeof x === "string") return Buffer.from(x, enc);
|
|
639
|
+
return Buffer.from(String(x), enc);
|
|
640
|
+
};
|
|
641
|
+
|
|
642
|
+
const readText = (p) => fs.readFileSync(path.resolve(String(p)), "utf8");
|
|
643
|
+
const writeText = (p, s) => { fs.writeFileSync(path.resolve(String(p)), String(s), "utf8"); return null; };
|
|
644
|
+
|
|
645
|
+
const readBytesB64 = (p) => fs.readFileSync(path.resolve(String(p))).toString("base64");
|
|
646
|
+
const writeBytesB64 = (p, b64) => { fs.writeFileSync(path.resolve(String(p)), Buffer.from(String(b64), "base64")); return null; };
|
|
647
|
+
|
|
648
|
+
const hex = (buf) => Buffer.from(buf).toString("hex");
|
|
649
|
+
const b64 = (buf) => Buffer.from(buf).toString("base64");
|
|
650
|
+
const fromHex = (h) => Buffer.from(String(h), "hex");
|
|
651
|
+
const fromB64 = (b) => Buffer.from(String(b), "base64");
|
|
652
|
+
|
|
653
|
+
const sha256 = (x) => hex(crypto.createHash("sha256").update(toBuf(x)).digest());
|
|
654
|
+
|
|
655
|
+
// AES-256-GCM(payload): [salt16|iv12|tag16|ct...], base64-encoded
|
|
656
|
+
const encText = (plaintext, password) => {
|
|
657
|
+
const salt = crypto.randomBytes(16);
|
|
658
|
+
const key = crypto.scryptSync(String(password), salt, 32);
|
|
659
|
+
const iv = crypto.randomBytes(12);
|
|
660
|
+
const cipher = crypto.createCipheriv("aes-256-gcm", key, iv);
|
|
661
|
+
const ct = Buffer.concat([cipher.update(String(plaintext), "utf8"), cipher.final()]);
|
|
662
|
+
const tag = cipher.getAuthTag();
|
|
663
|
+
return b64(Buffer.concat([salt, iv, tag, ct]));
|
|
664
|
+
};
|
|
665
|
+
|
|
666
|
+
const decText = (payloadB64, password) => {
|
|
667
|
+
const payload = fromB64(payloadB64);
|
|
668
|
+
if (payload.length < 16 + 12 + 16) throw new FazerError("Invalid encrypted payload");
|
|
669
|
+
const salt = payload.subarray(0, 16);
|
|
670
|
+
const iv = payload.subarray(16, 28);
|
|
671
|
+
const tag = payload.subarray(28, 44);
|
|
672
|
+
const ct = payload.subarray(44);
|
|
673
|
+
const key = crypto.scryptSync(String(password), salt, 32);
|
|
674
|
+
const decipher = crypto.createDecipheriv("aes-256-gcm", key, iv);
|
|
675
|
+
decipher.setAuthTag(tag);
|
|
676
|
+
const pt = Buffer.concat([decipher.update(ct), decipher.final()]);
|
|
677
|
+
return pt.toString("utf8");
|
|
678
|
+
};
|
|
679
|
+
|
|
680
|
+
const encB64 = (b64data, password) => encText(String(b64data), password);
|
|
681
|
+
const decB64 = (payloadB64, password) => decText(payloadB64, password);
|
|
682
|
+
|
|
683
|
+
const pathJoin = (...xs) => path.join(...xs.map(String));
|
|
684
|
+
const pathAbs = (p) => path.resolve(String(p));
|
|
685
|
+
const pathDir = (p) => path.dirname(String(p));
|
|
686
|
+
const pathBase = (p) => path.basename(String(p));
|
|
687
|
+
|
|
688
|
+
const nowMs = () => Date.now();
|
|
689
|
+
const len = (x) => (x == null ? 0 : (typeof x === "string" || Array.isArray(x)) ? x.length : (typeof x === "object" ? Object.keys(x).length : 0));
|
|
690
|
+
|
|
691
|
+
const keys = (o) => {
|
|
692
|
+
if (o == null || typeof o !== "object") return [];
|
|
693
|
+
return Object.keys(o);
|
|
694
|
+
};
|
|
695
|
+
|
|
696
|
+
const get = (o, k) => {
|
|
697
|
+
if (o == null) return null;
|
|
698
|
+
if (Array.isArray(o)) return o[Number(k)];
|
|
699
|
+
return o[String(k)];
|
|
700
|
+
};
|
|
701
|
+
|
|
702
|
+
const set = (o, k, v) => {
|
|
703
|
+
if (o == null || typeof o !== "object") throw new FazerError("set(obj,key,val) expects object/list");
|
|
704
|
+
if (Array.isArray(o)) { o[Number(k)] = v; return o; }
|
|
705
|
+
o[String(k)] = v; return o;
|
|
706
|
+
};
|
|
707
|
+
|
|
708
|
+
const argvFn = () => argv.slice();
|
|
709
|
+
const envFn = (k) => process.env[String(k)] ?? null;
|
|
710
|
+
const cwdFn = () => process.cwd();
|
|
711
|
+
|
|
712
|
+
const http = require("http");
|
|
713
|
+
const child_process = require("child_process");
|
|
714
|
+
|
|
715
|
+
// register builtins in global scope
|
|
716
|
+
const builtins = {
|
|
717
|
+
println: (x = "") => (console.log(String(x)), null),
|
|
718
|
+
print: (x = "") => (process.stdout.write(String(x)), null),
|
|
719
|
+
ask: (prompt = "") => {
|
|
720
|
+
process.stdout.write(String(prompt));
|
|
721
|
+
const buf = Buffer.alloc(1);
|
|
722
|
+
let str = "";
|
|
723
|
+
try {
|
|
724
|
+
while (true) {
|
|
725
|
+
const n = fs.readSync(0, buf, 0, 1, null);
|
|
726
|
+
if (n === 0) break;
|
|
727
|
+
const c = buf.toString();
|
|
728
|
+
if (c === "\r") continue;
|
|
729
|
+
if (c === "\n") break;
|
|
730
|
+
str += c;
|
|
731
|
+
}
|
|
732
|
+
return str;
|
|
733
|
+
} catch (e) {
|
|
734
|
+
return null;
|
|
735
|
+
}
|
|
736
|
+
},
|
|
737
|
+
|
|
738
|
+
style: (s, color) => style(s, String(color || "reset")),
|
|
739
|
+
box: (title, ...lines) => box(title, lines),
|
|
740
|
+
|
|
741
|
+
readText,
|
|
742
|
+
writeText,
|
|
743
|
+
saveText: (s, p) => { fs.writeFileSync(path.resolve(String(p)), String(s), "utf8"); return null; },
|
|
744
|
+
exists: (p) => fs.existsSync(path.resolve(String(p))),
|
|
745
|
+
ls: (p) => { try { return fs.readdirSync(path.resolve(String(p || "."))); } catch(e) { return []; } },
|
|
746
|
+
rm: (p) => { try { fs.rmSync(path.resolve(String(p)), { recursive: true, force: true }); return true; } catch(e) { return false; } },
|
|
747
|
+
mkdir: (p) => { try { fs.mkdirSync(path.resolve(String(p)), { recursive: true }); return true; } catch(e) { return false; } },
|
|
748
|
+
|
|
749
|
+
readB64: readBytesB64,
|
|
750
|
+
writeB64: writeBytesB64,
|
|
751
|
+
saveB64: (s, p) => { fs.writeFileSync(path.resolve(String(p)), Buffer.from(String(s), "base64")); return null; },
|
|
752
|
+
|
|
753
|
+
sha256,
|
|
754
|
+
encText,
|
|
755
|
+
decText,
|
|
756
|
+
encB64,
|
|
757
|
+
decB64,
|
|
758
|
+
|
|
759
|
+
hex,
|
|
760
|
+
b64,
|
|
761
|
+
fromHex,
|
|
762
|
+
fromB64,
|
|
763
|
+
|
|
764
|
+
join: pathJoin,
|
|
765
|
+
abs: pathAbs,
|
|
766
|
+
dir: pathDir,
|
|
767
|
+
base: pathBase,
|
|
768
|
+
|
|
769
|
+
nowMs,
|
|
770
|
+
len,
|
|
771
|
+
keys,
|
|
772
|
+
get,
|
|
773
|
+
set,
|
|
774
|
+
|
|
775
|
+
json: (x) => JSON.stringify(x, null, 2),
|
|
776
|
+
parseJson: (s) => JSON.parse(s),
|
|
777
|
+
|
|
778
|
+
exec: (cmd) => {
|
|
779
|
+
try {
|
|
780
|
+
return child_process.execSync(String(cmd)).toString();
|
|
781
|
+
} catch (e) {
|
|
782
|
+
return null;
|
|
783
|
+
}
|
|
784
|
+
},
|
|
785
|
+
|
|
786
|
+
server: (port, handlerName) => {
|
|
787
|
+
const srv = http.createServer((req, res) => {
|
|
788
|
+
// We need to call the Fazer function `handlerName`
|
|
789
|
+
// But `this._call` requires scope context.
|
|
790
|
+
// This is tricky in a sync interpreter loop.
|
|
791
|
+
// For now, simple static server or basic response?
|
|
792
|
+
// To do it properly, we need to invoke the runtime from the callback.
|
|
793
|
+
// Since we are inside the class, we can do it!
|
|
794
|
+
|
|
795
|
+
// We need to find the function in `this.fns`
|
|
796
|
+
const fn = this.fns.get(handlerName);
|
|
797
|
+
if (!fn) {
|
|
798
|
+
res.writeHead(500);
|
|
799
|
+
res.end("Handler not found");
|
|
800
|
+
return;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
// Construct request object
|
|
804
|
+
const reqObj = {
|
|
805
|
+
method: req.method,
|
|
806
|
+
url: req.url,
|
|
807
|
+
headers: req.headers
|
|
808
|
+
};
|
|
809
|
+
|
|
810
|
+
// Call handler(req) -> { status, body, headers }
|
|
811
|
+
try {
|
|
812
|
+
const inner = new Scope(fn.closure);
|
|
813
|
+
inner.set(fn.params[0], reqObj, true);
|
|
814
|
+
const result = this._execBlock(fn.body, inner);
|
|
815
|
+
const out = (result instanceof ReturnSignal) ? result.value : result;
|
|
816
|
+
|
|
817
|
+
const status = (out && out.status) || 200;
|
|
818
|
+
const body = (out && out.body) || "";
|
|
819
|
+
const headers = (out && out.headers) || {};
|
|
820
|
+
|
|
821
|
+
res.writeHead(status, headers);
|
|
822
|
+
res.end(String(body));
|
|
823
|
+
} catch (e) {
|
|
824
|
+
console.error(e);
|
|
825
|
+
res.writeHead(500);
|
|
826
|
+
res.end("Internal Server Error");
|
|
827
|
+
}
|
|
828
|
+
});
|
|
829
|
+
srv.listen(Number(port));
|
|
830
|
+
console.log(`Server listening on port ${port}`);
|
|
831
|
+
},
|
|
832
|
+
|
|
833
|
+
argv: argvFn,
|
|
834
|
+
env: envFn,
|
|
835
|
+
cwd: cwdFn,
|
|
836
|
+
};
|
|
837
|
+
|
|
838
|
+
this.global.set("__builtins__", builtins, false);
|
|
839
|
+
// Also expose builtins as top-level identifiers (fast path)
|
|
840
|
+
for (const [k, v] of Object.entries(builtins)) this.global.set(k, v, false);
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
run(ast) {
|
|
844
|
+
try {
|
|
845
|
+
return this._execBlock(ast, this.global);
|
|
846
|
+
} catch (e) {
|
|
847
|
+
throw e;
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
_execBlock(stmts, scope) {
|
|
852
|
+
let last = null;
|
|
853
|
+
for (const s of stmts) {
|
|
854
|
+
const v = this._execStmt(s, scope);
|
|
855
|
+
if (v instanceof ReturnSignal) return v;
|
|
856
|
+
last = v;
|
|
857
|
+
}
|
|
858
|
+
return last;
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
_execStmt(stmt, scope) {
|
|
862
|
+
switch (stmt.type) {
|
|
863
|
+
case "assign": {
|
|
864
|
+
const val = this._eval(stmt.value, scope);
|
|
865
|
+
if (scope.hasHere(stmt.name)) {
|
|
866
|
+
scope.assign(stmt.name, val);
|
|
867
|
+
} else {
|
|
868
|
+
scope.set(stmt.name, val, stmt.mut);
|
|
869
|
+
}
|
|
870
|
+
return val;
|
|
99
871
|
}
|
|
872
|
+
case "exprstmt":
|
|
873
|
+
return this._eval(stmt.expr, scope);
|
|
100
874
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
this.cooldowns[name] = Date.now() + time;
|
|
106
|
-
console.log(`Cooldown set for ${name}: ${timeStr}`);
|
|
875
|
+
case "fn": {
|
|
876
|
+
this.fns.set(stmt.name, { params: stmt.params, body: stmt.body, closure: scope });
|
|
877
|
+
scope.set(stmt.name, { __fnref__: stmt.name }, false);
|
|
878
|
+
return null;
|
|
107
879
|
}
|
|
108
880
|
|
|
109
|
-
|
|
881
|
+
case "return": {
|
|
882
|
+
const v = stmt.value ? this._eval(stmt.value, scope) : null;
|
|
883
|
+
return new ReturnSignal(v);
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
case "case": {
|
|
887
|
+
const val = this._eval(stmt.expr, scope);
|
|
888
|
+
for (const arm of stmt.arms) {
|
|
889
|
+
const { matched, bindings } = this._matchPattern(val, arm.pat, scope);
|
|
890
|
+
if (matched) {
|
|
891
|
+
const inner = new Scope(scope);
|
|
892
|
+
for (const [k, v] of Object.entries(bindings)) inner.set(k, v, true);
|
|
893
|
+
const out = this._execBlock(arm.body, inner);
|
|
894
|
+
if (out instanceof ReturnSignal) return out;
|
|
895
|
+
return out;
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
return null;
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
default:
|
|
902
|
+
throw new FazerError(`Unknown statement type: ${stmt.type}`);
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
_eval(expr, scope) {
|
|
907
|
+
if (!expr) return null;
|
|
908
|
+
|
|
909
|
+
switch (expr.type) {
|
|
910
|
+
case "num":
|
|
911
|
+
case "str":
|
|
912
|
+
case "bool":
|
|
913
|
+
return expr.value;
|
|
914
|
+
|
|
915
|
+
case "null":
|
|
916
|
+
return null;
|
|
110
917
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
918
|
+
case "list":
|
|
919
|
+
return expr.items.map((it) => this._eval(it, scope));
|
|
920
|
+
|
|
921
|
+
case "map": {
|
|
922
|
+
const o = {};
|
|
923
|
+
for (const ent of expr.entries) {
|
|
924
|
+
const k = this._eval(ent.key, scope);
|
|
925
|
+
o[String(k)] = this._eval(ent.value, scope);
|
|
926
|
+
}
|
|
927
|
+
return o;
|
|
116
928
|
}
|
|
117
929
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
930
|
+
case "ident": {
|
|
931
|
+
const cell = scope.get(expr.name);
|
|
932
|
+
if (!cell) throw new FazerError(`Undefined variable '${expr.name}'`);
|
|
933
|
+
const v = cell.value;
|
|
934
|
+
// fnref is stored as {__fnref__: name} to avoid collisions with user objects
|
|
935
|
+
if (v && typeof v === "object" && v.__fnref__) return v;
|
|
936
|
+
return v;
|
|
123
937
|
}
|
|
124
938
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
if (
|
|
129
|
-
|
|
130
|
-
|
|
939
|
+
case "un": {
|
|
940
|
+
const v = this._eval(expr.expr, scope);
|
|
941
|
+
if (expr.op === "not") return !truthy(v);
|
|
942
|
+
if (expr.op === "-") return -Number(v);
|
|
943
|
+
throw new FazerError(`Unknown unary op ${expr.op}`);
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
case "bin": {
|
|
947
|
+
// short-circuit for and/or
|
|
948
|
+
if (expr.op === "and") {
|
|
949
|
+
const l = this._eval(expr.left, scope);
|
|
950
|
+
if (!truthy(l)) return l;
|
|
951
|
+
return this._eval(expr.right, scope);
|
|
952
|
+
}
|
|
953
|
+
if (expr.op === "or") {
|
|
954
|
+
const l = this._eval(expr.left, scope);
|
|
955
|
+
if (truthy(l)) return l;
|
|
956
|
+
return this._eval(expr.right, scope);
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
const l = this._eval(expr.left, scope);
|
|
960
|
+
const r = this._eval(expr.right, scope);
|
|
961
|
+
|
|
962
|
+
switch (expr.op) {
|
|
963
|
+
case "+": return (typeof l === "string" || typeof r === "string") ? String(l) + String(r) : Number(l) + Number(r);
|
|
964
|
+
case "-": return Number(l) - Number(r);
|
|
965
|
+
case "*": return Number(l) * Number(r);
|
|
966
|
+
case "/": return Number(l) / Number(r);
|
|
967
|
+
case "%": return Number(l) % Number(r);
|
|
968
|
+
|
|
969
|
+
case "==": return deepEqual(l, r);
|
|
970
|
+
case "!=": return !deepEqual(l, r);
|
|
971
|
+
case ">": return Number(l) > Number(r);
|
|
972
|
+
case "<": return Number(l) < Number(r);
|
|
973
|
+
case ">=": return Number(l) >= Number(r);
|
|
974
|
+
case "<=": return Number(l) <= Number(r);
|
|
975
|
+
default:
|
|
976
|
+
throw new FazerError(`Unknown binary op ${expr.op}`);
|
|
131
977
|
}
|
|
132
978
|
}
|
|
133
979
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
await this.sendToDiscord(channelId, this.evaluate(message));
|
|
980
|
+
case "get": {
|
|
981
|
+
const obj = this._eval(expr.obj, scope);
|
|
982
|
+
const key = this._eval(expr.key, scope);
|
|
983
|
+
return (obj == null) ? null : obj[String(key)];
|
|
139
984
|
}
|
|
140
985
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
const
|
|
144
|
-
if (
|
|
145
|
-
if (
|
|
146
|
-
|
|
986
|
+
case "idx": {
|
|
987
|
+
const obj = this._eval(expr.obj, scope);
|
|
988
|
+
const idx = this._eval(expr.idx, scope);
|
|
989
|
+
if (obj == null) return null;
|
|
990
|
+
if (Array.isArray(obj)) return obj[Number(idx)];
|
|
991
|
+
return obj[String(idx)];
|
|
147
992
|
}
|
|
148
993
|
|
|
149
|
-
|
|
994
|
+
case "call": {
|
|
995
|
+
const callee = this._eval(expr.callee, scope);
|
|
996
|
+
const args = expr.args.map((a) => this._eval(a, scope));
|
|
997
|
+
return this._call(callee, args, scope);
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
case "pipe": {
|
|
1001
|
+
// left →> right
|
|
1002
|
+
// If right is a call: pass left as first arg
|
|
1003
|
+
// If right is an identifier: treat as function name, call with left
|
|
1004
|
+
// Otherwise: if right evaluates to function, call it with left
|
|
1005
|
+
const leftVal = this._eval(expr.left, scope);
|
|
1006
|
+
const rightNode = expr.right;
|
|
1007
|
+
|
|
1008
|
+
if (rightNode.type === "call") {
|
|
1009
|
+
const callee = this._eval(rightNode.callee, scope);
|
|
1010
|
+
const args = [leftVal, ...rightNode.args.map((a) => this._eval(a, scope))];
|
|
1011
|
+
return this._call(callee, args, scope);
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
if (rightNode.type === "ident") {
|
|
1015
|
+
const fn = this._eval(rightNode, scope);
|
|
1016
|
+
return this._call(fn, [leftVal], scope);
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
const fn = this._eval(rightNode, scope);
|
|
1020
|
+
return this._call(fn, [leftVal], scope);
|
|
1021
|
+
}
|
|
150
1022
|
|
|
151
|
-
|
|
1023
|
+
default:
|
|
1024
|
+
throw new FazerError(`Unknown expression type: ${expr.type}`);
|
|
152
1025
|
}
|
|
153
|
-
return output;
|
|
154
1026
|
}
|
|
155
1027
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
1028
|
+
_call(callee, args, scope) {
|
|
1029
|
+
// builtin JS function
|
|
1030
|
+
if (typeof callee === "function") return callee(...args);
|
|
1031
|
+
|
|
1032
|
+
// function reference stored as {__fnref__: "name"}
|
|
1033
|
+
if (callee && typeof callee === "object" && callee.__fnref__) {
|
|
1034
|
+
const name = callee.__fnref__;
|
|
1035
|
+
const fn = this.fns.get(name);
|
|
1036
|
+
if (!fn) throw new FazerError(`Unknown function '${name}'`);
|
|
1037
|
+
if (args.length !== fn.params.length) {
|
|
1038
|
+
throw new FazerError(`Arity mismatch: ${name} expects ${fn.params.length}, got ${args.length}`);
|
|
1039
|
+
}
|
|
1040
|
+
const inner = new Scope(fn.closure);
|
|
1041
|
+
for (let i = 0; i < fn.params.length; i++) inner.set(fn.params[i], args[i], true);
|
|
1042
|
+
const out = this._execBlock(fn.body, inner);
|
|
1043
|
+
if (out instanceof ReturnSignal) return out.value;
|
|
1044
|
+
return out;
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
throw new FazerError(`Value is not callable`);
|
|
160
1048
|
}
|
|
161
1049
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
1050
|
+
_matchPattern(value, pat, scope) {
|
|
1051
|
+
// returns { matched, bindings }
|
|
1052
|
+
if (!pat) return { matched: false, bindings: {} };
|
|
1053
|
+
|
|
1054
|
+
if (pat.type === "else") return { matched: true, bindings: {} };
|
|
1055
|
+
|
|
1056
|
+
if (pat.type === "identPat") {
|
|
1057
|
+
// binding: case x name → ...
|
|
1058
|
+
return { matched: true, bindings: { [pat.name]: value } };
|
|
166
1059
|
}
|
|
167
|
-
|
|
168
|
-
|
|
1060
|
+
|
|
1061
|
+
if (pat.type === "cmpPat") {
|
|
1062
|
+
const rhs = this._eval(pat.rhs, scope);
|
|
1063
|
+
const l = value;
|
|
1064
|
+
const r = rhs;
|
|
1065
|
+
switch (pat.op) {
|
|
1066
|
+
case "==": return { matched: deepEqual(l, r), bindings: {} };
|
|
1067
|
+
case "!=": return { matched: !deepEqual(l, r), bindings: {} };
|
|
1068
|
+
case ">": return { matched: Number(l) > Number(r), bindings: {} };
|
|
1069
|
+
case "<": return { matched: Number(l) < Number(r), bindings: {} };
|
|
1070
|
+
case ">=": return { matched: Number(l) >= Number(r), bindings: {} };
|
|
1071
|
+
case "<=": return { matched: Number(l) <= Number(r), bindings: {} };
|
|
1072
|
+
default: return { matched: false, bindings: {} };
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
// literal patterns
|
|
1077
|
+
if (pat.type === "num" || pat.type === "str" || pat.type === "bool" || pat.type === "null") {
|
|
1078
|
+
const p = pat.type === "null" ? null : pat.value;
|
|
1079
|
+
return { matched: deepEqual(value, p), bindings: {} };
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
return { matched: false, bindings: {} };
|
|
169
1083
|
}
|
|
1084
|
+
}
|
|
170
1085
|
|
|
171
|
-
|
|
1086
|
+
/* ────────────────────────────────────────────────────────────────────────── */
|
|
1087
|
+
/* Utilities */
|
|
1088
|
+
/* ────────────────────────────────────────────────────────────────────────── */
|
|
1089
|
+
|
|
1090
|
+
function unescapeString(rawTokenImage) {
|
|
1091
|
+
// rawTokenImage includes quotes
|
|
1092
|
+
const s = rawTokenImage.slice(1, -1);
|
|
1093
|
+
// minimal escapes
|
|
1094
|
+
return s
|
|
1095
|
+
.replace(/\\n/g, "\n")
|
|
1096
|
+
.replace(/\\r/g, "\r")
|
|
1097
|
+
.replace(/\\t/g, "\t")
|
|
1098
|
+
.replace(/\\"/g, '"')
|
|
1099
|
+
.replace(/\\\\/g, "\\");
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
function locOf(tok) {
|
|
1103
|
+
return {
|
|
1104
|
+
offset: tok.startOffset,
|
|
1105
|
+
line: tok.startLine,
|
|
1106
|
+
col: tok.startColumn,
|
|
1107
|
+
endOffset: tok.endOffset,
|
|
1108
|
+
endLine: tok.endLine,
|
|
1109
|
+
endCol: tok.endColumn,
|
|
1110
|
+
};
|
|
172
1111
|
}
|
|
173
1112
|
|
|
174
|
-
|
|
175
|
-
|
|
1113
|
+
function truthy(v) {
|
|
1114
|
+
return !!v;
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
function deepEqual(a, b) {
|
|
1118
|
+
if (a === b) return true;
|
|
1119
|
+
if (a == null || b == null) return a === b;
|
|
1120
|
+
if (typeof a !== typeof b) return false;
|
|
1121
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
1122
|
+
if (a.length !== b.length) return false;
|
|
1123
|
+
for (let i = 0; i < a.length; i++) if (!deepEqual(a[i], b[i])) return false;
|
|
1124
|
+
return true;
|
|
1125
|
+
}
|
|
1126
|
+
if (typeof a === "object" && typeof b === "object") {
|
|
1127
|
+
const ka = Object.keys(a);
|
|
1128
|
+
const kb = Object.keys(b);
|
|
1129
|
+
if (ka.length !== kb.length) return false;
|
|
1130
|
+
for (const k of ka) if (!deepEqual(a[k], b[k])) return false;
|
|
1131
|
+
return true;
|
|
1132
|
+
}
|
|
1133
|
+
return false;
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
function prettyError(err, filename, code) {
|
|
1137
|
+
const lines = String(code).split(/\r?\n/);
|
|
1138
|
+
const meta = err && err.meta ? err.meta : null;
|
|
1139
|
+
if (!meta || !meta.line) return `${err.name}: ${err.message}`;
|
|
1140
|
+
|
|
1141
|
+
const lineNo = meta.line;
|
|
1142
|
+
const col = meta.col || 1;
|
|
1143
|
+
const line = lines[lineNo - 1] ?? "";
|
|
1144
|
+
const caret = " ".repeat(Math.max(0, col - 1)) + "^";
|
|
1145
|
+
|
|
1146
|
+
return [
|
|
1147
|
+
`${err.name}: ${err.message}`,
|
|
1148
|
+
`at ${filename}:${lineNo}:${col}`,
|
|
1149
|
+
line,
|
|
1150
|
+
caret,
|
|
1151
|
+
].join("\n");
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
/* ────────────────────────────────────────────────────────────────────────── */
|
|
1155
|
+
/* CLI */
|
|
1156
|
+
/* ────────────────────────────────────────────────────────────────────────── */
|
|
1157
|
+
|
|
1158
|
+
function usage() {
|
|
1159
|
+
console.log("Usage:");
|
|
1160
|
+
console.log(" fazer run <file.fz> [-- args...]");
|
|
1161
|
+
process.exit(1);
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
function main() {
|
|
1165
|
+
const argv = process.argv.slice(2);
|
|
1166
|
+
if (argv.length === 0) usage();
|
|
1167
|
+
|
|
1168
|
+
const cmd = argv[0];
|
|
1169
|
+
|
|
1170
|
+
if (cmd !== "run") usage();
|
|
1171
|
+
const fileArg = argv[1];
|
|
1172
|
+
if (!fileArg) usage();
|
|
1173
|
+
|
|
1174
|
+
// forward args after "--" to the script
|
|
1175
|
+
const sep = argv.indexOf("--");
|
|
1176
|
+
const forwarded = sep >= 0 ? argv.slice(sep + 1) : [];
|
|
1177
|
+
|
|
1178
|
+
const filePath = path.resolve(fileArg);
|
|
1179
|
+
if (!fs.existsSync(filePath)) {
|
|
1180
|
+
console.error(`File not found: ${filePath}`);
|
|
1181
|
+
process.exit(1);
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
const code = fs.readFileSync(filePath, "utf8");
|
|
1185
|
+
const lex = lexer.tokenize(code);
|
|
1186
|
+
if (lex.errors.length) {
|
|
1187
|
+
console.error("Lexer error:", lex.errors[0].message || String(lex.errors[0]));
|
|
1188
|
+
process.exit(1);
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
const parser = new FazerParser();
|
|
1192
|
+
parser.input = lex.tokens;
|
|
1193
|
+
const ast = parser.program();
|
|
1194
|
+
|
|
1195
|
+
if (parser.errors.length) {
|
|
1196
|
+
const e = parser.errors[0];
|
|
1197
|
+
const tok = e.token || (lex.tokens.length ? lex.tokens[0] : null);
|
|
1198
|
+
const meta = tok ? locOf(tok) : null;
|
|
1199
|
+
const ferr = new FazerError(e.message, meta || {});
|
|
1200
|
+
console.error(prettyError(ferr, filePath, code));
|
|
1201
|
+
process.exit(1);
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
const rt = new FazerRuntime({ argv: forwarded, filename: filePath, code });
|
|
1205
|
+
try {
|
|
1206
|
+
rt.run(ast);
|
|
1207
|
+
} catch (err) {
|
|
1208
|
+
if (err instanceof FazerError) {
|
|
1209
|
+
console.error(prettyError(err, filePath, code));
|
|
1210
|
+
} else {
|
|
1211
|
+
console.error(err && err.stack ? err.stack : String(err));
|
|
1212
|
+
}
|
|
1213
|
+
process.exit(1);
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
module.exports = {
|
|
1218
|
+
Lexer,
|
|
1219
|
+
createToken,
|
|
1220
|
+
FazerParser,
|
|
1221
|
+
FazerRuntime,
|
|
1222
|
+
FazerError,
|
|
1223
|
+
lexer,
|
|
1224
|
+
prettyError,
|
|
1225
|
+
locOf
|
|
1226
|
+
};
|
|
1227
|
+
|
|
1228
|
+
if (require.main === module) main();
|