fazer-lang 1.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/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(lexer.allTokens);
143
+ super(allTokens, { recoveryEnabled: true });
64
144
  const $ = this;
65
145
 
66
- $.RULE('program', () => {
67
- const stmts = $.MANY(() => $.SUBRULE($.statement));
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('statement', () => $.OR([
72
- { ALT: () => $.SUBRULE($.assignStmt) },
73
- { ALT: () => $.SUBRULE($.functionDef) },
74
- { ALT: () => $.SUBRULE($.caseBlock) },
75
- { ALT: () => $.SUBRULE($.rescueBlock) },
76
- { ALT: () => $.SUBRULE($.expressionStmt) }
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('assignStmt', () => {
80
- const isMut = $.OPTION(() => $.CONSUME(Mut));
81
- const id = $.CONSUME(Identifier);
82
- let type = null;
83
- $.OPTION2(() => {
84
- $.CONSUME(Colon);
85
- type = $.CONSUME2(Identifier).image;
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 { type: 'assign', mut: !!isMut, name: id.image, varType: type, value };
196
+ return node("assign", {
197
+ name: idTok.image,
198
+ mut: !!mutTok,
199
+ value,
200
+ loc: locOf(idTok),
201
+ });
90
202
  });
91
203
 
92
- $.RULE('caseBlock', () => {
93
- $.CONSUME(Case);
204
+ $.RULE("caseBlock", () => {
205
+ const caseTok = $.CONSUME(Case);
94
206
  const expr = $.SUBRULE($.expression);
95
- const cases = [];
96
- $.MANY(() => {
97
- let pattern = $.OR([
207
+ const arms = [];
208
+ $.AT_LEAST_ONE(() => {
209
+ const pat = $.OR([
98
210
  { ALT: () => $.SUBRULE($.pattern) },
99
- { ALT: () => $.CONSUME(Else) ? 'else' : null }
211
+ { ALT: () => ( $.CONSUME(Else), node("else", {}) ) },
100
212
  ]);
101
213
  $.CONSUME(Arrow);
102
214
  const body = $.SUBRULE($.block);
103
- cases.push({ pattern, body });
215
+ arms.push({ pat, body });
104
216
  });
105
- return { type: 'case', expr, cases };
217
+ $.CONSUME(End);
218
+ return node("case", { expr, arms, loc: locOf(caseTok) });
106
219
  });
107
220
 
108
- $.RULE('block', () => {
109
- const stmts = $.MANY(() => $.SUBRULE($.statement));
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('expression', () => {
115
- let left = $.SUBRULE($.primaryExpr);
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($.primaryExpr);
124
- left = { type: 'binop', op, left, right };
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('primaryExpr', () => $.OR([
130
- { ALT: () => $.CONSUME(Integer).image },
131
- { ALT: () => $.CONSUME(StringLiteral).image.slice(1, -1) },
132
- { ALT: () => $.CONSUME(Identifier).image },
133
- // Ajouter list, map, call, etc.
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
- // Ajouter rules pour functionDef, rescueBlock, pattern, etc.
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
- // ── Runtime (exécution du CST) ───────────────────────────────────────────────
143
- class FazerRuntime {
144
- constructor() {
145
- this.vars = new Map(); // name → {value, mut, type}
146
- this.fns = new Map();
147
- }
148
-
149
- evalExpr(node) {
150
- if (typeof node === 'string' || typeof node === 'number') return node;
151
- if (node.type === 'binop') {
152
- const left = this.evalExpr(node.left);
153
- const right = this.evalExpr(node.right);
154
- switch (node.op) {
155
- case '+': return left + right;
156
- case '-': return left - right;
157
- case '*': return left * right;
158
- case '/': return left / right;
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 (node === 'print') {
162
- console.log(this.evalExpr(node.args));
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
- execute(stmt) {
169
- if (stmt.type === 'assign') {
170
- const value = this.evalExpr(stmt.value);
171
- this.vars.set(stmt.name, { value, mut: stmt.mut, type: stmt.varType });
172
- return value;
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
- if (stmt.type === 'case') {
176
- const exprValue = this.evalExpr(stmt.expr);
177
- for (const c of stmt.cases) {
178
- if (c.pattern === 'else' || this.matchPattern(exprValue, c.pattern)) {
179
- return this.executeBlock(c.body);
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
- // Étendre pour fn def, etc.
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
- matchPattern(value, pattern) {
189
- // Implémentation basique : pour conditions > < ==, ou patterns {key: val}
190
- return value === pattern; // Étendre
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
- executeBlock(block) {
194
- let last = null;
195
- for (const stmt of block) {
196
- last = this.execute(stmt);
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
- return last;
1046
+
1047
+ throw new FazerError(`Value is not callable`);
199
1048
  }
200
1049
 
201
- run(code) {
202
- const lexResult = lexer.tokenize(code);
203
- if (lexResult.errors.length > 0) {
204
- console.error('Lexer errors:', lexResult.errors);
205
- return;
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
- const parser = new FazerParser();
209
- parser.input = lexResult.tokens;
210
- const cst = parser.program();
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
- if (parser.errors.length > 0) {
213
- console.error('Parse errors:', parser.errors);
214
- return;
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
- this.executeBlock(cst);
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
- // ── CLI ──────────────────────────────────────────────────────────────────────
222
- const args = process.argv.slice(2);
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
- if (args[0] === 'run' && args[1]) {
225
- const filePath = path.resolve(args[1]);
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(`Fichier non trouvé : ${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));
228
1201
  process.exit(1);
229
1202
  }
230
- const code = fs.readFileSync(filePath, 'utf8');
231
- new FazerRuntime().run(code);
232
- } else {
233
- console.log('Usage: fazer run fichier.fz');
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();