fazer-lang 0.1.0 → 2.1.0

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