fazer-lang 1.1.0 → 2.1.1
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 +200 -32
- package/apply_icon.js +28 -0
- package/apply_icon_robust.js +51 -0
- package/bin/fazer.exe +0 -0
- package/fazer.js +1156 -162
- package/package.json +38 -27
- package/standalone.bundled.js +12910 -0
- package/standalone.js +186 -0
- package/test.fz +92 -0
package/fazer.js
CHANGED
|
@@ -1,234 +1,1228 @@
|
|
|
1
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
|
+
/* ────────────────────────────────────────────────────────────────────────── */
|
|
2
140
|
|
|
3
|
-
const fs = require('fs');
|
|
4
|
-
const path = require('path');
|
|
5
|
-
const { Lexer, Parser, createToken, EmbeddedActionsParser } = require('chevrotain');
|
|
6
|
-
|
|
7
|
-
// Tokens (restent les mêmes, ajoutés Equals, NotEq, And, Or, Not pour expressions)
|
|
8
|
-
const Assign = createToken({ name: 'Assign', pattern: /:=/ });
|
|
9
|
-
const Arrow = createToken({ name: 'Arrow', pattern: /→/ });
|
|
10
|
-
const DoublePipe = createToken({ name: 'DoublePipe', pattern: /→>/ });
|
|
11
|
-
const Case = createToken({ name: 'Case', pattern: /case/ });
|
|
12
|
-
const Rescue = createToken({ name: 'Rescue', pattern: /rescue/ });
|
|
13
|
-
const Catch = createToken({ name: 'Catch', pattern: /catch/ });
|
|
14
|
-
const From = createToken({ name: 'From', pattern: /from/ });
|
|
15
|
-
const End = createToken({ name: 'End', pattern: /end/ });
|
|
16
|
-
const Await = createToken({ name: 'Await', pattern: /await/ });
|
|
17
|
-
const Mut = createToken({ name: 'Mut', pattern: /mut/ });
|
|
18
|
-
const Async = createToken({ name: 'Async', pattern: /async/ });
|
|
19
|
-
const Else = createToken({ name: 'Else', pattern: /else/ });
|
|
20
|
-
const In = createToken({ name: 'In', pattern: /in/ });
|
|
21
|
-
const Step = createToken({ name: 'Step', pattern: /step/ });
|
|
22
|
-
|
|
23
|
-
const Identifier = createToken({ name: 'Identifier', pattern: /[a-zA-Z_][a-zA-Z0-9_]*/ });
|
|
24
|
-
const Integer = createToken({ name: 'Integer', pattern: /-?\d+/ });
|
|
25
|
-
const Float = createToken({ name: 'Float', pattern: /-?\d+\.\d+/ });
|
|
26
|
-
const StringLiteral = createToken({ name: 'StringLiteral', pattern: /"([^"\\]|\\.)*"/ });
|
|
27
|
-
const True = createToken({ name: 'True', pattern: /true/ });
|
|
28
|
-
const False = createToken({ name: 'False', pattern: /false/ });
|
|
29
|
-
const Colon = createToken({ name: 'Colon', pattern: /:/ });
|
|
30
|
-
const LBracket = createToken({ name: 'LBracket', pattern: /\[/ });
|
|
31
|
-
const RBracket = createToken({ name: 'RBracket', pattern: /\]/ });
|
|
32
|
-
const LBrace = createToken({ name: 'LBrace', pattern: /{/ });
|
|
33
|
-
const RBrace = createToken({ name: 'RBrace', pattern: /}/ });
|
|
34
|
-
const Comma = createToken({ name: 'Comma', pattern: /,/ });
|
|
35
|
-
const Dot = createToken({ name: 'Dot', pattern: /\./ });
|
|
36
|
-
const Plus = createToken({ name: 'Plus', pattern: /\+/ });
|
|
37
|
-
const Minus = createToken({ name: 'Minus', pattern: /-/ });
|
|
38
|
-
const Star = createToken({ name: 'Star', pattern: /\*/ });
|
|
39
|
-
const Slash = createToken({ name: 'Slash', pattern: /\// });
|
|
40
|
-
const Percent = createToken({ name: 'Percent', pattern: /%/ });
|
|
41
|
-
const Greater = createToken({ name: 'Greater', pattern: />/ });
|
|
42
|
-
const Less = createToken({ name: 'Less', pattern: /</ });
|
|
43
|
-
const GreaterEq = createToken({ name: 'GreaterEq', pattern: />=/ });
|
|
44
|
-
const LessEq = createToken({ name: 'LessEq', pattern: /<=/ });
|
|
45
|
-
const Eq = createToken({ name: 'Eq', pattern: /==/ });
|
|
46
|
-
const NotEq = createToken({ name: 'NotEq', pattern: /!=/ });
|
|
47
|
-
const And = createToken({ name: 'And', pattern: /and/ });
|
|
48
|
-
const Or = createToken({ name: 'Or', pattern: /or/ });
|
|
49
|
-
const Not = createToken({ name: 'Not', pattern: /not/ });
|
|
50
|
-
const Comment = createToken({ name: 'Comment', pattern: /#.*/, group: Lexer.SKIPPED });
|
|
51
|
-
const WhiteSpace = createToken({ name: 'WhiteSpace', pattern: /\s+/, group: Lexer.SKIPPED });
|
|
52
|
-
|
|
53
|
-
const lexer = new Lexer([
|
|
54
|
-
Assign, Arrow, DoublePipe, Case, Rescue, Catch, From, End, Await, Mut, Async, Else, In, Step,
|
|
55
|
-
Identifier, Float, Integer, StringLiteral, True, False, Colon, LBracket, RBracket, LBrace, RBrace,
|
|
56
|
-
Comma, Dot, Plus, Minus, Star, Slash, Percent, Greater, Less, GreaterEq, LessEq, Eq, NotEq, And, Or, Not,
|
|
57
|
-
Comment, WhiteSpace
|
|
58
|
-
]);
|
|
59
|
-
|
|
60
|
-
// ── Parser avec Embedded Actions (exécution inline) ──────────────────────────
|
|
61
141
|
class FazerParser extends EmbeddedActionsParser {
|
|
62
142
|
constructor() {
|
|
63
|
-
super(
|
|
143
|
+
super(allTokens, { recoveryEnabled: true });
|
|
64
144
|
const $ = this;
|
|
65
145
|
|
|
66
|
-
|
|
67
|
-
|
|
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
|
+
});
|
|
68
154
|
return stmts;
|
|
69
155
|
});
|
|
70
156
|
|
|
71
|
-
$.RULE(
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
+
);
|
|
78
166
|
|
|
79
|
-
$.RULE(
|
|
80
|
-
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
+
});
|
|
86
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);
|
|
87
194
|
$.CONSUME(Assign);
|
|
88
195
|
const value = $.SUBRULE($.expression);
|
|
89
|
-
return
|
|
196
|
+
return node("assign", {
|
|
197
|
+
name: idTok.image,
|
|
198
|
+
mut: !!mutTok,
|
|
199
|
+
value,
|
|
200
|
+
loc: locOf(idTok),
|
|
201
|
+
});
|
|
90
202
|
});
|
|
91
203
|
|
|
92
|
-
$.RULE(
|
|
93
|
-
$.CONSUME(Case);
|
|
204
|
+
$.RULE("caseBlock", () => {
|
|
205
|
+
const caseTok = $.CONSUME(Case);
|
|
94
206
|
const expr = $.SUBRULE($.expression);
|
|
95
|
-
const
|
|
96
|
-
$.
|
|
97
|
-
|
|
207
|
+
const arms = [];
|
|
208
|
+
$.AT_LEAST_ONE(() => {
|
|
209
|
+
const pat = $.OR([
|
|
98
210
|
{ ALT: () => $.SUBRULE($.pattern) },
|
|
99
|
-
{ ALT: () => $.CONSUME(Else)
|
|
211
|
+
{ ALT: () => ( $.CONSUME(Else), node("else", {}) ) },
|
|
100
212
|
]);
|
|
101
213
|
$.CONSUME(Arrow);
|
|
102
214
|
const body = $.SUBRULE($.block);
|
|
103
|
-
|
|
215
|
+
arms.push({ pat, body });
|
|
104
216
|
});
|
|
105
|
-
|
|
217
|
+
$.CONSUME(End);
|
|
218
|
+
return node("case", { expr, arms, loc: locOf(caseTok) });
|
|
106
219
|
});
|
|
107
220
|
|
|
108
|
-
$.RULE(
|
|
109
|
-
const stmts =
|
|
221
|
+
$.RULE("block", () => {
|
|
222
|
+
const stmts = [];
|
|
223
|
+
$.MANY(() => {
|
|
224
|
+
const s = $.SUBRULE($.statement);
|
|
225
|
+
if (s) stmts.push(s);
|
|
226
|
+
});
|
|
110
227
|
$.CONSUME(End);
|
|
111
228
|
return stmts;
|
|
112
229
|
});
|
|
113
230
|
|
|
114
|
-
$.RULE(
|
|
115
|
-
|
|
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);
|
|
116
336
|
$.MANY(() => {
|
|
117
337
|
const op = $.OR([
|
|
118
338
|
{ ALT: () => $.CONSUME(Plus).image },
|
|
119
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([
|
|
120
351
|
{ ALT: () => $.CONSUME(Star).image },
|
|
121
|
-
{ ALT: () => $.CONSUME(Slash).image }
|
|
352
|
+
{ ALT: () => $.CONSUME(Slash).image },
|
|
353
|
+
{ ALT: () => $.CONSUME(Percent).image },
|
|
122
354
|
]);
|
|
123
|
-
const right = $.SUBRULE2($.
|
|
124
|
-
left = {
|
|
355
|
+
const right = $.SUBRULE2($.unaryExpr);
|
|
356
|
+
left = node("bin", { op, left, right, loc: left.loc ?? null });
|
|
125
357
|
});
|
|
126
358
|
return left;
|
|
127
359
|
});
|
|
128
360
|
|
|
129
|
-
$.RULE(
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
361
|
+
$.RULE("unaryExpr", () =>
|
|
362
|
+
$.OR([
|
|
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
|
+
});
|
|
135
515
|
|
|
136
|
-
|
|
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
|
+
},
|
|
531
|
+
]);
|
|
532
|
+
$.CONSUME(Colon);
|
|
533
|
+
const value = $.SUBRULE($.expression);
|
|
534
|
+
return { key, value };
|
|
535
|
+
});
|
|
137
536
|
|
|
138
537
|
this.performSelfAnalysis();
|
|
139
538
|
}
|
|
140
539
|
}
|
|
141
540
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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;
|
|
160
579
|
}
|
|
161
|
-
if (
|
|
162
|
-
|
|
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);
|
|
163
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;
|
|
164
848
|
}
|
|
165
|
-
return null; // Étendre
|
|
166
849
|
}
|
|
167
850
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
return
|
|
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;
|
|
173
857
|
}
|
|
858
|
+
return last;
|
|
859
|
+
}
|
|
174
860
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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);
|
|
180
869
|
}
|
|
870
|
+
return val;
|
|
871
|
+
}
|
|
872
|
+
case "exprstmt":
|
|
873
|
+
return this._eval(stmt.expr, scope);
|
|
874
|
+
|
|
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;
|
|
181
879
|
}
|
|
182
|
-
return null;
|
|
183
|
-
}
|
|
184
880
|
|
|
185
|
-
|
|
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
|
+
}
|
|
186
904
|
}
|
|
187
905
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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;
|
|
917
|
+
|
|
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;
|
|
928
|
+
}
|
|
929
|
+
|
|
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;
|
|
937
|
+
}
|
|
938
|
+
|
|
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}`);
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
|
|
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)];
|
|
984
|
+
}
|
|
985
|
+
|
|
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)];
|
|
992
|
+
}
|
|
993
|
+
|
|
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
|
+
}
|
|
1022
|
+
|
|
1023
|
+
default:
|
|
1024
|
+
throw new FazerError(`Unknown expression type: ${expr.type}`);
|
|
1025
|
+
}
|
|
191
1026
|
}
|
|
192
1027
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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;
|
|
197
1045
|
}
|
|
198
|
-
|
|
1046
|
+
|
|
1047
|
+
throw new FazerError(`Value is not callable`);
|
|
199
1048
|
}
|
|
200
1049
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
if (
|
|
204
|
-
|
|
205
|
-
|
|
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 } };
|
|
206
1059
|
}
|
|
207
1060
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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
|
+
}
|
|
211
1075
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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: {} };
|
|
215
1080
|
}
|
|
216
1081
|
|
|
217
|
-
|
|
1082
|
+
return { matched: false, bindings: {} };
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
|
|
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
|
+
};
|
|
1111
|
+
}
|
|
1112
|
+
|
|
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;
|
|
218
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);
|
|
219
1162
|
}
|
|
220
1163
|
|
|
221
|
-
|
|
222
|
-
const
|
|
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();
|
|
223
1173
|
|
|
224
|
-
|
|
225
|
-
const
|
|
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);
|
|
226
1179
|
if (!fs.existsSync(filePath)) {
|
|
227
|
-
console.error(`
|
|
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));
|
|
228
1201
|
process.exit(1);
|
|
229
1202
|
}
|
|
230
|
-
|
|
231
|
-
new FazerRuntime(
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
}
|
|
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();
|