larkhre 11.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.
Files changed (3) hide show
  1. package/README.md +17 -0
  2. package/larkhre.js +2094 -0
  3. package/package.json +15 -0
package/larkhre.js ADDED
@@ -0,0 +1,2094 @@
1
+ // Larkhré — moteur officiel (généré depuis docs/code/index.html par construire.js, ne pas modifier à la main)
2
+ /* ============================================================
3
+ Larkhré v1.0 — Moteur JavaScript (port du larkhre.py)
4
+ Créé par Ladji — fonctionne dans tout navigateur.
5
+ API :
6
+ Larkhre.run(source, { onPrint(texte), onInput(prompt) -> Promise<string>, shouldStop() -> bool })
7
+ ============================================================ */
8
+ 'use strict';
9
+
10
+ const Larkhre = (function () {
11
+
12
+ // ---------------- ERREURS ----------------
13
+ class LazError extends Error {
14
+ constructor(message, line) {
15
+ super(message);
16
+ this.lazMessage = message;
17
+ this.line = line;
18
+ }
19
+ toString() {
20
+ if (this.line) return `✘ Erreur Larkhré (ligne ${this.line}) : ${this.lazMessage}`;
21
+ return `✘ Erreur Larkhré : ${this.lazMessage}`;
22
+ }
23
+ }
24
+ class ReturnEx { constructor(value) { this.value = value; } }
25
+ class BreakEx { }
26
+ class ContinueEx { }
27
+ class StopEx { }
28
+
29
+ // ---------------- LEXER ----------------
30
+ const KEYWORDS = new Set(['laz', 'fonk', 'rend', 'kan', 'sinon', 'tanke', 'pou', 'dan',
31
+ 'vrai', 'faux', 'walu', 'et', 'ou', 'non', 'kase', 'swiv',
32
+ 'klas', 'herite', 'importe', 'essaie', 'rattrape', 'garde']);
33
+
34
+ // v5.0 : les langues — mêmes mots-clés, dans ta langue (#langue: anglais)
35
+ const LANGUES = {
36
+ anglais: {
37
+ laz: 'let', fonk: 'func', rend: 'give', kan: 'when', sinon: 'else',
38
+ tanke: 'while', pou: 'for', dan: 'in', vrai: 'true', faux: 'false',
39
+ walu: 'null', et: 'and', ou: 'or', non: 'not', kase: 'stop',
40
+ swiv: 'next', klas: 'class', herite: 'extends', importe: 'load',
41
+ essaie: 'try', rattrape: 'catch', garde: 'keep',
42
+ },
43
+ // v7.1 : le pack "français académique" — orthographe correcte
44
+ francais: {
45
+ laz: 'soit', fonk: 'fonction', rend: 'retourne', kan: 'si', sinon: 'sinon',
46
+ tanke: 'tantque', pou: 'pour', dan: 'dans', vrai: 'vrai', faux: 'faux',
47
+ walu: 'rien', et: 'et', ou: 'ou', non: 'non', kase: 'casse',
48
+ swiv: 'continue', klas: 'classe', herite: 'herite', importe: 'importe',
49
+ essaie: 'essaie', rattrape: 'rattrape', garde: 'garde',
50
+ },
51
+ bambara: { // BROUILLON à faire valider par des locuteurs natifs
52
+ laz: 'bila', fonk: 'baara', rend: 'segin', kan: 'ni', sinon: 'note',
53
+ tanke: 'foo', pou: 'ye', dan: 'la', vrai: 'tien', faux: 'galon',
54
+ walu: 'foyi', et: 'ani', ou: 'walima', non: 'te', kase: 'tige',
55
+ swiv: 'taa', klas: 'kulu', herite: 'ciden', importe: 'tala',
56
+ essaie: 'kekan', rattrape: 'minna', garde: 'mara',
57
+ },
58
+ wolof: { // BROUILLON à faire valider par des locuteurs natifs
59
+ laz: 'teg', fonk: 'liggeey', rend: 'delloo', kan: 'su', sinon: 'walla',
60
+ tanke: 'liye', pou: 'ngir', dan: 'ci', vrai: 'degg', faux: 'fen',
61
+ walu: 'dara', et: 'ak', ou: 'mbaa', non: 'du', kase: 'taxaw',
62
+ swiv: 'topp', klas: 'mbooloo', herite: 'donn', importe: 'yeb',
63
+ essaie: 'jeema', rattrape: 'japp', garde: 'denc',
64
+ },
65
+ };
66
+
67
+ const LANGUE_RE = /#\s*langue\s*:\s*([a-zA-Z]+)/i;
68
+
69
+ function detecteLangue(source) {
70
+ for (const l of source.split('\n').slice(0, 3)) {
71
+ const m = l.match(LANGUE_RE);
72
+ if (m) {
73
+ const nom = m[1].toLowerCase();
74
+ if (['larkhre', 'lazarus', 'classique'].includes(nom)) return null; // « lazarus » : l'ancien nom reste accepté
75
+ if (!(nom in LANGUES)) {
76
+ throw new LazError(`langue inconnue « ${nom} » (disponibles : larkhre, ${Object.keys(LANGUES).sort().join(', ')})`, 1);
77
+ }
78
+ const rmap = {};
79
+ for (const [k, v] of Object.entries(LANGUES[nom])) rmap[v] = k;
80
+ return rmap;
81
+ }
82
+ }
83
+ return null;
84
+ }
85
+ const TWO_CHAR = new Set(['==', '!=', '<=', '>=', '&&', '||', '..', '+=', '-=', '*=', '/=']);
86
+ const ONE_CHAR = new Set(['+', '-', '*', '/', '%', '<', '>', '=', '(', ')',
87
+ '{', '}', '[', ']', ',', '!', ';', '.', ':']);
88
+
89
+ function isDigit(c) { return c >= '0' && c <= '9'; }
90
+ function isAlpha(c) { return /[\p{L}_]/u.test(c); }
91
+ function isAlnum(c) { return /[\p{L}\p{N}_]/u.test(c); }
92
+
93
+ function tokenize(source, langueMap) {
94
+ const tokens = [];
95
+ let i = 0, line = 1, parenDepth = 0;
96
+ const n = source.length;
97
+
98
+ while (i < n) {
99
+ const c = source[i];
100
+
101
+ if (c === ' ' || c === '\t' || c === '\r') { i++; continue; }
102
+
103
+ if (c === '\n') {
104
+ if (parenDepth === 0) tokens.push(['NEWLINE', null, line]);
105
+ line++; i++; continue;
106
+ }
107
+
108
+ if (c === '#' || (c === '/' && source[i + 1] === '/')) {
109
+ while (i < n && source[i] !== '\n') i++;
110
+ continue;
111
+ }
112
+
113
+ if (c === '"') {
114
+ i++;
115
+ const startLine = line;
116
+ let buf = '';
117
+ while (i < n && source[i] !== '"') {
118
+ const ch = source[i];
119
+ if (ch === '\n') throw new LazError('chaîne de caractères non fermée (il manque un ")', startLine);
120
+ if (ch === '\\' && i + 1 < n) {
121
+ const nxt = source[i + 1];
122
+ const esc = { n: '\n', t: '\t', '"': '"', '\\': '\\' };
123
+ buf += (esc[nxt] !== undefined ? esc[nxt] : nxt);
124
+ i += 2;
125
+ } else { buf += ch; i++; }
126
+ }
127
+ if (i >= n) throw new LazError('chaîne de caractères non fermée (il manque un ")', startLine);
128
+ i++;
129
+ tokens.push(['STRING', buf, startLine]);
130
+ continue;
131
+ }
132
+
133
+ if (isDigit(c)) {
134
+ const start = i;
135
+ while (i < n && isDigit(source[i])) i++;
136
+ if (i < n && source[i] === '.' && isDigit(source[i + 1])) {
137
+ i++;
138
+ while (i < n && isDigit(source[i])) i++;
139
+ }
140
+ tokens.push(['NUMBER', parseFloat(source.slice(start, i)), line]);
141
+ continue;
142
+ }
143
+
144
+ if (isAlpha(c)) {
145
+ const start = i;
146
+ while (i < n && isAlnum(source[i])) i++;
147
+ const word = source.slice(start, i);
148
+ if (KEYWORDS.has(word)) tokens.push([word.toUpperCase(), word, line]);
149
+ else if (langueMap && word in langueMap) tokens.push([langueMap[word].toUpperCase(), word, line]);
150
+ else tokens.push(['IDENT', word, line]);
151
+ continue;
152
+ }
153
+
154
+ const two = source.slice(i, i + 2);
155
+ if (TWO_CHAR.has(two)) { tokens.push(['OP', two, line]); i += 2; continue; }
156
+
157
+ if (ONE_CHAR.has(c)) {
158
+ if (c === '(' || c === '[') parenDepth++;
159
+ else if (c === ')' || c === ']') parenDepth = Math.max(0, parenDepth - 1);
160
+ if (c === ';') tokens.push(['NEWLINE', null, line]);
161
+ else tokens.push(['OP', c, line]);
162
+ i++; continue;
163
+ }
164
+
165
+ throw new LazError(`caractère inconnu : '${c}'`, line);
166
+ }
167
+ tokens.push(['NEWLINE', null, line]);
168
+ tokens.push(['EOF', null, line]);
169
+ return tokens;
170
+ }
171
+
172
+ // ---------------- PARSER ----------------
173
+ const NICE = { NEWLINE: 'une fin de ligne', EOF: 'la fin du fichier' };
174
+
175
+ class Parser {
176
+ constructor(tokens) { this.tokens = tokens; this.pos = 0; }
177
+ peek() { return this.tokens[this.pos]; }
178
+ next() { return this.tokens[this.pos++]; }
179
+ check(t, v) {
180
+ const tok = this.peek();
181
+ if (tok[0] !== t) return false;
182
+ if (v !== undefined && tok[1] !== v) return false;
183
+ return true;
184
+ }
185
+ accept(t, v) { if (this.check(t, v)) return this.next(); return null; }
186
+ expect(t, v, what) {
187
+ const tok = this.peek();
188
+ if (!this.check(t, v)) {
189
+ const attendu = what || v || t;
190
+ let trouve = tok[1] !== null ? tok[1] : tok[0];
191
+ trouve = NICE[trouve] || trouve;
192
+ throw new LazError(`j'attendais « ${attendu} » mais j'ai trouvé « ${trouve} »`, tok[2]);
193
+ }
194
+ return this.next();
195
+ }
196
+ skipNewlines() { while (this.check('NEWLINE')) this.next(); }
197
+
198
+ parseProgram() {
199
+ const stmts = [];
200
+ this.skipNewlines();
201
+ while (!this.check('EOF')) {
202
+ stmts.push(this.parseStatement());
203
+ this.skipNewlines();
204
+ }
205
+ return ['block', stmts];
206
+ }
207
+
208
+ parseBlock() {
209
+ this.skipNewlines();
210
+ this.expect('OP', '{', '{');
211
+ const stmts = [];
212
+ this.skipNewlines();
213
+ while (!this.check('OP', '}')) {
214
+ if (this.check('EOF')) throw new LazError('il manque une accolade fermante }', this.peek()[2]);
215
+ stmts.push(this.parseStatement());
216
+ this.skipNewlines();
217
+ }
218
+ this.expect('OP', '}', '}');
219
+ return ['block', stmts];
220
+ }
221
+
222
+ parseStatement() {
223
+ const tok = this.peek();
224
+ const line = tok[2];
225
+
226
+ if (this.accept('LAZ')) {
227
+ const name = this.expect('IDENT', undefined, 'un nom de variable')[1];
228
+ this.expect('OP', '=', '=');
229
+ return ['declare', name, this.parseExpression(), line];
230
+ }
231
+ if (this.accept('GARDE')) {
232
+ const name = this.expect('IDENT', undefined, 'un nom de variable')[1];
233
+ this.expect('OP', '=', '=');
234
+ return ['garde', name, this.parseExpression(), line];
235
+ }
236
+ if (this.accept('FONK')) {
237
+ const name = this.expect('IDENT', undefined, 'un nom de fonction')[1];
238
+ this.expect('OP', '(', '(');
239
+ const params = [];
240
+ if (!this.check('OP', ')')) {
241
+ params.push(this.expect('IDENT', undefined, 'un paramètre')[1]);
242
+ while (this.accept('OP', ',')) params.push(this.expect('IDENT', undefined, 'un paramètre')[1]);
243
+ }
244
+ this.expect('OP', ')', ')');
245
+ return ['fonk', name, params, this.parseBlock(), line];
246
+ }
247
+ if (this.accept('KAN')) return this.parseKan(line);
248
+ if (this.accept('TANKE')) {
249
+ const cond = this.parseExpression();
250
+ return ['tanke', cond, this.parseBlock(), line];
251
+ }
252
+ if (this.accept('POU')) {
253
+ const v = this.expect('IDENT', undefined, 'un nom de variable')[1];
254
+ this.expect('DAN', undefined, 'dan');
255
+ const iterable = this.parseExpression();
256
+ return ['pou', v, iterable, this.parseBlock(), line];
257
+ }
258
+ if (this.accept('REND')) {
259
+ if (this.check('NEWLINE') || this.check('OP', '}') || this.check('EOF')) return ['rend', null, line];
260
+ return ['rend', this.parseExpression(), line];
261
+ }
262
+ if (this.accept('KASE')) return ['kase', line];
263
+ if (this.accept('SWIV')) return ['swiv', line];
264
+
265
+ if (this.accept('KLAS')) {
266
+ const name = this.expect('IDENT', undefined, 'un nom de classe')[1];
267
+ let parent = null;
268
+ if (this.accept('HERITE')) parent = this.expect('IDENT', undefined, 'un nom de classe parente')[1];
269
+ this.skipNewlines();
270
+ this.expect('OP', '{', '{');
271
+ const methods = [];
272
+ this.skipNewlines();
273
+ while (!this.check('OP', '}')) {
274
+ if (this.check('EOF')) throw new LazError('il manque une accolade fermante } pour la klas', this.peek()[2]);
275
+ if (!this.check('FONK')) {
276
+ throw new LazError('dans une klas, on ne met que des fonctions (fonk nom(moi, ...) { ... })', this.peek()[2]);
277
+ }
278
+ this.next();
279
+ const mline = this.peek()[2];
280
+ const mname = this.expect('IDENT', undefined, 'un nom de fonction')[1];
281
+ this.expect('OP', '(', '(');
282
+ const params = [];
283
+ if (!this.check('OP', ')')) {
284
+ params.push(this.expect('IDENT', undefined, 'un paramètre')[1]);
285
+ while (this.accept('OP', ',')) params.push(this.expect('IDENT', undefined, 'un paramètre')[1]);
286
+ }
287
+ this.expect('OP', ')', ')');
288
+ if (params.length === 0) {
289
+ throw new LazError(`la fonction « ${mname} » d'une klas doit avoir « moi » comme premier paramètre`, mline);
290
+ }
291
+ const body = this.parseBlock();
292
+ methods.push([mname, params, body, mline]);
293
+ this.skipNewlines();
294
+ }
295
+ this.expect('OP', '}', '}');
296
+ return ['klas', name, parent, methods, line];
297
+ }
298
+
299
+ if (this.accept('IMPORTE')) {
300
+ const tok2 = this.peek();
301
+ if (tok2[0] !== 'STRING') {
302
+ throw new LazError('importe demande un nom de fichier entre guillemets : importe "outils.laz"', line);
303
+ }
304
+ this.next();
305
+ return ['importe', tok2[1], line];
306
+ }
307
+
308
+ if (this.accept('ESSAIE')) {
309
+ const body = this.parseBlock();
310
+ this.skipNewlines();
311
+ this.expect('RATTRAPE', undefined, 'rattrape');
312
+ const errname = this.expect('IDENT', undefined, "un nom pour l'erreur (ex : rattrape probleme { ... })")[1];
313
+ const handler = this.parseBlock();
314
+ return ['essaie', body, errname, handler, line];
315
+ }
316
+
317
+ const expr = this.parseExpression();
318
+ const tok2 = this.peek();
319
+ const estCompose = tok2[0] === 'OP' && ['+=', '-=', '*=', '/='].includes(tok2[1]);
320
+ if (this.check('OP', '=') || estCompose) {
321
+ const op = this.next()[1];
322
+ let value = this.parseExpression();
323
+ if (estCompose) {
324
+ value = ['binop', op[0], expr, value, line];
325
+ }
326
+ if (expr[0] === 'var') return ['assign', expr[1], value, line];
327
+ if (expr[0] === 'index') return ['assign_index', expr[1], expr[2], value, line];
328
+ if (expr[0] === 'attr') return ['assign_attr', expr[1], expr[2], value, line];
329
+ throw new LazError("on ne peut affecter une valeur qu'à une variable, un élément de liste/dico ou une propriété d'objet", line);
330
+ }
331
+ return ['expr', expr, line];
332
+ }
333
+
334
+ parseKan(line) {
335
+ const cond = this.parseExpression();
336
+ const body = this.parseBlock();
337
+ let elseBranch = null;
338
+ const save = this.pos;
339
+ this.skipNewlines();
340
+ if (this.accept('SINON')) {
341
+ if (this.accept('KAN')) elseBranch = ['block', [this.parseKan(this.peek()[2])]];
342
+ else elseBranch = this.parseBlock();
343
+ } else {
344
+ this.pos = save;
345
+ }
346
+ return ['kan', cond, body, elseBranch, line];
347
+ }
348
+
349
+ parseExpression() { return this.parseOr(); }
350
+
351
+ parseOr() {
352
+ let left = this.parseAnd();
353
+ for (;;) {
354
+ if (this.accept('OU') || this.accept('OP', '||')) left = ['or', left, this.parseAnd()];
355
+ else break;
356
+ }
357
+ return left;
358
+ }
359
+ parseAnd() {
360
+ let left = this.parseNot();
361
+ for (;;) {
362
+ if (this.accept('ET') || this.accept('OP', '&&')) left = ['and', left, this.parseNot()];
363
+ else break;
364
+ }
365
+ return left;
366
+ }
367
+ parseNot() {
368
+ if (this.accept('NON') || this.accept('OP', '!')) return ['not', this.parseNot()];
369
+ return this.parseComparison();
370
+ }
371
+ parseComparison() {
372
+ const left = this.parseRange();
373
+ const tok = this.peek();
374
+ if (tok[0] === 'OP' && ['==', '!=', '<', '>', '<=', '>='].includes(tok[1])) {
375
+ const op = this.next()[1];
376
+ return ['cmp', op, left, this.parseRange(), tok[2]];
377
+ }
378
+ return left;
379
+ }
380
+ parseRange() {
381
+ const left = this.parseAdditive();
382
+ const tok = this.peek();
383
+ if (tok[0] === 'OP' && tok[1] === '..') {
384
+ this.next();
385
+ return ['range', left, this.parseAdditive(), tok[2]];
386
+ }
387
+ return left;
388
+ }
389
+ parseAdditive() {
390
+ let left = this.parseTerm();
391
+ for (;;) {
392
+ const tok = this.peek();
393
+ if (tok[0] === 'OP' && (tok[1] === '+' || tok[1] === '-')) {
394
+ const op = this.next()[1];
395
+ left = ['binop', op, left, this.parseTerm(), tok[2]];
396
+ } else break;
397
+ }
398
+ return left;
399
+ }
400
+ parseTerm() {
401
+ let left = this.parseUnary();
402
+ for (;;) {
403
+ const tok = this.peek();
404
+ if (tok[0] === 'OP' && ['*', '/', '%'].includes(tok[1])) {
405
+ const op = this.next()[1];
406
+ left = ['binop', op, left, this.parseUnary(), tok[2]];
407
+ } else break;
408
+ }
409
+ return left;
410
+ }
411
+ parseUnary() {
412
+ const tok = this.peek();
413
+ if (tok[0] === 'OP' && tok[1] === '-') {
414
+ this.next();
415
+ return ['neg', this.parseUnary(), tok[2]];
416
+ }
417
+ return this.parsePostfix();
418
+ }
419
+ parsePostfix() {
420
+ let expr = this.parsePrimary();
421
+ for (;;) {
422
+ const tok = this.peek();
423
+ if (tok[0] === 'OP' && tok[1] === '(') {
424
+ this.next();
425
+ const args = [];
426
+ if (!this.check('OP', ')')) {
427
+ args.push(this.parseExpression());
428
+ while (this.accept('OP', ',')) args.push(this.parseExpression());
429
+ }
430
+ this.expect('OP', ')', ')');
431
+ expr = ['call', expr, args, tok[2]];
432
+ } else if (tok[0] === 'OP' && tok[1] === '[') {
433
+ this.next();
434
+ const index = this.parseExpression();
435
+ this.expect('OP', ']', ']');
436
+ expr = ['index', expr, index, tok[2]];
437
+ } else if (tok[0] === 'OP' && tok[1] === '.') {
438
+ this.next();
439
+ const name = this.expect('IDENT', undefined, 'un nom de propriété')[1];
440
+ expr = ['attr', expr, name, tok[2]];
441
+ } else break;
442
+ }
443
+ return expr;
444
+ }
445
+ parsePrimary() {
446
+ const tok = this.peek();
447
+ if (tok[0] === 'NUMBER') { this.next(); return ['num', tok[1]]; }
448
+ if (tok[0] === 'STRING') { this.next(); return ['str', tok[1]]; }
449
+ if (tok[0] === 'VRAI') { this.next(); return ['bool', true]; }
450
+ if (tok[0] === 'FAUX') { this.next(); return ['bool', false]; }
451
+ if (tok[0] === 'WALU') { this.next(); return ['walu']; }
452
+ if (tok[0] === 'IDENT') { this.next(); return ['var', tok[1], tok[2]]; }
453
+ if (tok[0] === 'OP' && tok[1] === '(') {
454
+ this.next();
455
+ const expr = this.parseExpression();
456
+ this.expect('OP', ')', ')');
457
+ return expr;
458
+ }
459
+ if (tok[0] === 'OP' && tok[1] === '[') {
460
+ this.next();
461
+ const items = [];
462
+ this.skipNewlines();
463
+ if (!this.check('OP', ']')) {
464
+ items.push(this.parseExpression());
465
+ while (this.accept('OP', ',')) {
466
+ this.skipNewlines();
467
+ items.push(this.parseExpression());
468
+ }
469
+ this.skipNewlines();
470
+ }
471
+ this.expect('OP', ']', ']');
472
+ return ['list', items, tok[2]];
473
+ }
474
+ if (tok[0] === 'OP' && tok[1] === '{') {
475
+ this.next();
476
+ const pairs = [];
477
+ this.skipNewlines();
478
+ if (!this.check('OP', '}')) {
479
+ for (;;) {
480
+ this.skipNewlines();
481
+ const key = this.parseExpression();
482
+ this.skipNewlines();
483
+ this.expect('OP', ':', ':');
484
+ this.skipNewlines();
485
+ const value = this.parseExpression();
486
+ pairs.push([key, value]);
487
+ this.skipNewlines();
488
+ if (!this.accept('OP', ',')) break;
489
+ }
490
+ this.skipNewlines();
491
+ }
492
+ this.expect('OP', '}', '}');
493
+ return ['dict', pairs, tok[2]];
494
+ }
495
+ let trouve = tok[1] !== null ? tok[1] : tok[0];
496
+ trouve = NICE[trouve] || trouve;
497
+ throw new LazError(`expression invalide, je ne comprends pas « ${trouve} » (il manque peut-être une valeur)`, tok[2]);
498
+ }
499
+ }
500
+
501
+ // ---------------- ENVIRONNEMENT ----------------
502
+ class Env {
503
+ constructor(parent) { this.vars = new Map(); this.parent = parent || null; }
504
+ get(name, line) {
505
+ let e = this;
506
+ while (e) { if (e.vars.has(name)) return e.vars.get(name); e = e.parent; }
507
+ throw new LazError(MODE_PYTHON.actif
508
+ ? `la variable « ${name} » n'existe pas (déclare-la d'abord avec : ${name} = ...)`
509
+ : `la variable « ${name} » n'existe pas (déclare-la avec : laz ${name} = ...)`, line);
510
+ }
511
+ declare(name, value) { this.vars.set(name, value); }
512
+ assign(name, value, line) {
513
+ let e = this;
514
+ while (e) { if (e.vars.has(name)) { e.vars.set(name, value); return; } e = e.parent; }
515
+ throw new LazError(MODE_PYTHON.actif
516
+ ? `la variable « ${name} » n'existe pas (déclare-la d'abord avec : ${name} = ...)`
517
+ : `la variable « ${name} » n'existe pas (déclare-la avec : laz ${name} = ...)`, line);
518
+ }
519
+ }
520
+
521
+ class LazFunction {
522
+ constructor(name, params, body, env) {
523
+ this.name = name; this.params = params; this.body = body; this.env = env;
524
+ }
525
+ }
526
+
527
+ class LazClass {
528
+ constructor(name, methods, parent) {
529
+ this.name = name; this.methods = methods; this.parent = parent || null;
530
+ }
531
+ findMethod(name) {
532
+ let k = this;
533
+ while (k) { if (k.methods.has(name)) return k.methods.get(name); k = k.parent; }
534
+ return null;
535
+ }
536
+ }
537
+
538
+ class LazInstance {
539
+ constructor(klass) { this.klass = klass; this.fields = new Map(); }
540
+ }
541
+
542
+ class BoundMethod {
543
+ constructor(fn, instance) { this.fn = fn; this.instance = instance; }
544
+ }
545
+
546
+ // ---------------- VALEURS ----------------
547
+ function numText(v) {
548
+ if (Number.isInteger(v)) return String(v);
549
+ return String(Math.round(v * 1e10) / 1e10);
550
+ }
551
+ function toText(v) {
552
+ if (v === null || v === undefined) return 'walu';
553
+ if (v === true) return 'vrai';
554
+ if (v === false) return 'faux';
555
+ if (typeof v === 'number') return numText(v);
556
+ if (Array.isArray(v)) {
557
+ return '[' + v.map(x => typeof x === 'string' ? `"${x}"` : toText(x)).join(', ') + ']';
558
+ }
559
+ if (v instanceof Map) {
560
+ const parts = [];
561
+ for (const [k, val] of v.entries()) {
562
+ const ks = typeof k === 'string' ? `"${k}"` : toText(k);
563
+ const vs = typeof val === 'string' ? `"${val}"` : toText(val);
564
+ parts.push(`${ks}: ${vs}`);
565
+ }
566
+ return '{' + parts.join(', ') + '}';
567
+ }
568
+ if (v instanceof LazFunction) return `<fonk ${v.name}>`;
569
+ if (v instanceof LazClass) return `<klas ${v.name}>`;
570
+ if (v instanceof LazInstance) return `<objet ${v.klass.name}>`;
571
+ if (v instanceof BoundMethod) return `<fonk ${v.fn.name}>`;
572
+ return String(v);
573
+ }
574
+ function isTruthy(v) {
575
+ if (v === null || v === undefined || v === false) return false;
576
+ if (v === true) return true;
577
+ if (typeof v === 'number') return v !== 0;
578
+ if (typeof v === 'string' || Array.isArray(v)) return v.length > 0;
579
+ if (v instanceof Map) return v.size > 0;
580
+ return true;
581
+ }
582
+ const INTERP_RE = /\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
583
+
584
+ function interpolate(s, env) {
585
+ if (!s.includes('{')) return s;
586
+ let s2 = s.split('{{').join('\x00').split('}}').join('\x01');
587
+ s2 = s2.replace(INTERP_RE, (whole, name) => {
588
+ let e = env;
589
+ while (e) {
590
+ if (e.vars.has(name)) return toText(e.vars.get(name));
591
+ e = e.parent;
592
+ }
593
+ return whole;
594
+ });
595
+ return s2.split('\x00').join('{').split('\x01').join('}');
596
+ }
597
+
598
+ function checkNumber(v, line, contexte) {
599
+ if (typeof v !== 'number' || Number.isNaN(v)) {
600
+ throw new LazError(`${(contexte || 'cette opération')} demande un nombre, pas « ${toText(v)} »`, line);
601
+ }
602
+ return v;
603
+ }
604
+
605
+ // ---------------- INTERPRÉTEUR ----------------
606
+ const MAX_STEPS = 5000000;
607
+
608
+
609
+ // ============================================================
610
+ // v11.0 : LE MODE PYTHON BIENVEILLANT (lecteur Python -> AST Larkhré)
611
+ // ============================================================
612
+ const PY_MOTS = new Set(['def', 'return', 'if', 'elif', 'else', 'while', 'for', 'in',
613
+ 'break', 'continue', 'pass', 'and', 'or', 'not', 'True', 'False', 'None',
614
+ 'try', 'except', 'as', 'import', 'from']);
615
+ const PY_FONCTIONS = { print: 'vox', input: 'demand', len: 'taille', str: 'texte', float: 'nombre', int: '__ent', round: 'arondi', sorted: 'tri', abs: '__abs' };
616
+ const PY_METHODES = { append: 'ajoute', pop: 'retire', upper: 'majus', lower: 'minus', split: 'koupe', replace: 'remplace', join: 'colle' };
617
+ const PY_OPS2 = new Set(['**', '//', '==', '!=', '<=', '>=', '+=', '-=', '*=', '/=']);
618
+ const PY_MODE_RE = /#\s*langue\s*:\s*python/i;
619
+ const MODE_PYTHON = { actif: false };
620
+
621
+ function estModePython(source) {
622
+ return source.split('\n').slice(0, 3).some((l) => PY_MODE_RE.test(l));
623
+ }
624
+
625
+ function pyTokenize(source) {
626
+ const tokens = [];
627
+ const indents = [0];
628
+ const lignes = source.split('\n');
629
+ let profondeur = 0;
630
+ for (let num = 1; num <= lignes.length; num++) {
631
+ const brute = lignes[num - 1];
632
+ const depouillee = brute.trim();
633
+ if (profondeur === 0) {
634
+ if (depouillee === '' || depouillee.startsWith('#')) continue;
635
+ let larg = 0;
636
+ for (const c of brute) {
637
+ if (c === ' ') larg += 1;
638
+ else if (c === '\t') larg += 4;
639
+ else break;
640
+ }
641
+ if (larg > indents[indents.length - 1]) {
642
+ indents.push(larg);
643
+ tokens.push(['INDENT', null, num]);
644
+ }
645
+ while (larg < indents[indents.length - 1]) {
646
+ indents.pop();
647
+ tokens.push(['DEDENT', null, num]);
648
+ }
649
+ if (larg !== indents[indents.length - 1]) {
650
+ throw new LazError('indentation incohérente : aligne cette ligne avec le bloc auquel elle appartient', num);
651
+ }
652
+ }
653
+ let i = 0;
654
+ const n = brute.length;
655
+ while (i < n) {
656
+ let c = brute[i];
657
+ if (c === ' ' || c === '\t') { i++; continue; }
658
+ if (c === '#') break;
659
+ let estF = false;
660
+ if ((c === 'f' || c === 'F') && i + 1 < n && (brute[i + 1] === '"' || brute[i + 1] === "'")) {
661
+ estF = true;
662
+ i++;
663
+ c = brute[i];
664
+ }
665
+ if (c === '"' || c === "'") {
666
+ const quote = c;
667
+ i++;
668
+ let buf = '';
669
+ let ferme = false;
670
+ while (i < n) {
671
+ const ch = brute[i];
672
+ if (ch === '\\' && i + 1 < n) {
673
+ const nxt = brute[i + 1];
674
+ const esc = { n: '\n', t: '\t', '"': '"', "'": "'", '\\': '\\' };
675
+ buf += (esc[nxt] !== undefined ? esc[nxt] : nxt);
676
+ i += 2;
677
+ continue;
678
+ }
679
+ if (ch === quote) { ferme = true; i++; break; }
680
+ buf += ch;
681
+ i++;
682
+ }
683
+ if (!ferme) throw new LazError(`chaîne de caractères non fermée (il manque un ${quote} )`, num);
684
+ if (!estF) buf = buf.replace(/\{/g, '{{').replace(/\}/g, '}}');
685
+ tokens.push(['STRING', buf, num]);
686
+ continue;
687
+ }
688
+ if (isDigit(c) || (c === '.' && i + 1 < n && isDigit(brute[i + 1]))) {
689
+ const debut = i;
690
+ while (i < n && (isDigit(brute[i]) || brute[i] === '.')) i++;
691
+ tokens.push(['NUMBER', parseFloat(brute.slice(debut, i)), num]);
692
+ continue;
693
+ }
694
+ if (/[A-Za-z_]/.test(c)) {
695
+ const debut = i;
696
+ while (i < n && /[A-Za-z0-9_]/.test(brute[i])) i++;
697
+ const mot = brute.slice(debut, i);
698
+ tokens.push([PY_MOTS.has(mot) ? 'KW' : 'IDENT', mot, num]);
699
+ continue;
700
+ }
701
+ const deux = brute.slice(i, i + 2);
702
+ if (PY_OPS2.has(deux)) { tokens.push(['OP', deux, num]); i += 2; continue; }
703
+ if ('+-*/%=<>()[]{},:.'.includes(c)) {
704
+ if ('([{'.includes(c)) profondeur++;
705
+ else if (')]}'.includes(c)) profondeur = Math.max(0, profondeur - 1);
706
+ tokens.push(['OP', c, num]);
707
+ i++;
708
+ continue;
709
+ }
710
+ if (c === ';') throw new LazError('pas de point-virgule en Python — une instruction par ligne', num);
711
+ throw new LazError(`caractère inconnu : '${c}'`, num);
712
+ }
713
+ if (profondeur === 0 && tokens.length && !['NEWLINE', 'INDENT', 'DEDENT'].includes(tokens[tokens.length - 1][0])) {
714
+ tokens.push(['NEWLINE', null, num]);
715
+ }
716
+ }
717
+ while (indents.length > 1) {
718
+ indents.pop();
719
+ tokens.push(['DEDENT', null, lignes.length]);
720
+ }
721
+ tokens.push(['EOF', null, lignes.length]);
722
+ return tokens;
723
+ }
724
+
725
+ const PY_NICE = { NEWLINE: 'une fin de ligne', EOF: 'la fin du fichier', INDENT: 'une indentation', DEDENT: 'une fin de bloc' };
726
+
727
+ class PyParser {
728
+ constructor(tokens) {
729
+ this.tokens = tokens;
730
+ this.pos = 0;
731
+ this.scopes = [new Set()];
732
+ }
733
+ peek() { return this.tokens[this.pos]; }
734
+ next() { return this.tokens[this.pos++]; }
735
+ check(t, v) {
736
+ const tok = this.peek();
737
+ if (tok[0] !== t) return false;
738
+ if (v !== undefined && tok[1] !== v) return false;
739
+ return true;
740
+ }
741
+ accept(t, v) { if (this.check(t, v)) return this.next(); return null; }
742
+ expect(t, v, quoi) {
743
+ const tok = this.peek();
744
+ if (!this.check(t, v)) {
745
+ const attendu = quoi || v || t;
746
+ const trouve = tok[1] !== null ? tok[1] : (PY_NICE[tok[0]] || tok[0]);
747
+ throw new LazError(`j'attendais « ${attendu} » mais j'ai trouvé « ${trouve} »`, tok[2]);
748
+ }
749
+ return this.next();
750
+ }
751
+ sauteLignes() { while (this.check('NEWLINE')) this.next(); }
752
+
753
+ parseProgram() {
754
+ const stmts = [];
755
+ this.sauteLignes();
756
+ while (!this.check('EOF')) {
757
+ stmts.push(this.parseStatement());
758
+ this.sauteLignes();
759
+ }
760
+ return ['block', stmts];
761
+ }
762
+
763
+ parseBloc(line) {
764
+ this.expect('OP', ':', "les deux-points ':' à la fin de la ligne");
765
+ this.expect('NEWLINE', undefined, 'un passage à la ligne après les deux-points');
766
+ this.sauteLignes();
767
+ if (!this.check('INDENT')) {
768
+ throw new LazError('le bloc doit être indenté (décalé vers la droite, par exemple de 4 espaces)', this.peek()[2]);
769
+ }
770
+ this.next();
771
+ const stmts = [];
772
+ this.sauteLignes();
773
+ while (!this.check('DEDENT') && !this.check('EOF')) {
774
+ stmts.push(this.parseStatement());
775
+ this.sauteLignes();
776
+ }
777
+ this.accept('DEDENT');
778
+ return ['block', stmts];
779
+ }
780
+
781
+ declareOuAssigne(nom) {
782
+ const scope = this.scopes[this.scopes.length - 1];
783
+ if (scope.has(nom)) return 'assign';
784
+ scope.add(nom);
785
+ return 'declare';
786
+ }
787
+
788
+ parseStatement() {
789
+ const tok = this.peek();
790
+ const line = tok[2];
791
+
792
+ if (this.accept('KW', 'def')) {
793
+ const nom = this.expect('IDENT', undefined, 'un nom de fonction')[1];
794
+ this.expect('OP', '(', '(');
795
+ const params = [];
796
+ if (!this.check('OP', ')')) {
797
+ params.push(this.expect('IDENT', undefined, 'un nom de paramètre')[1]);
798
+ while (this.accept('OP', ',')) params.push(this.expect('IDENT', undefined, 'un nom de paramètre')[1]);
799
+ }
800
+ this.expect('OP', ')', ')');
801
+ this.scopes[this.scopes.length - 1].add(nom);
802
+ this.scopes.push(new Set(params));
803
+ const corps = this.parseBloc(line);
804
+ this.scopes.pop();
805
+ return ['fonk', nom, params, corps, line];
806
+ }
807
+
808
+ if (this.accept('KW', 'if')) return this.parseIf(line);
809
+
810
+ if (this.accept('KW', 'while')) {
811
+ const cond = this.parseExpression();
812
+ return ['tanke', cond, this.parseBloc(line), line];
813
+ }
814
+
815
+ if (this.accept('KW', 'for')) {
816
+ const v = this.expect('IDENT', undefined, 'un nom de variable')[1];
817
+ this.expect('KW', 'in', 'in');
818
+ const iterable = this.parseExpression();
819
+ this.scopes[this.scopes.length - 1].add(v);
820
+ return ['pou', v, iterable, this.parseBloc(line), line];
821
+ }
822
+
823
+ if (this.accept('KW', 'return')) {
824
+ if (this.check('NEWLINE')) return ['rend', null, line];
825
+ return ['rend', this.parseExpression(), line];
826
+ }
827
+ if (this.accept('KW', 'break')) return ['kase', line];
828
+ if (this.accept('KW', 'continue')) return ['swiv', line];
829
+ if (this.accept('KW', 'pass')) return ['expr', ['walu'], line];
830
+
831
+ if (this.accept('KW', 'try')) {
832
+ const corps = this.parseBloc(line);
833
+ this.sauteLignes();
834
+ this.expect('KW', 'except', 'except (après un try, il faut un except)');
835
+ let nomErreur = 'erreur';
836
+ if (this.check('IDENT')) {
837
+ this.next();
838
+ if (this.accept('KW', 'as')) nomErreur = this.expect('IDENT', undefined, "un nom pour l'erreur")[1];
839
+ }
840
+ this.scopes[this.scopes.length - 1].add(nomErreur);
841
+ const gestion = this.parseBloc(line);
842
+ return ['essaie', corps, nomErreur, gestion, line];
843
+ }
844
+
845
+ if (this.accept('KW', 'import')) {
846
+ const module = this.expect('IDENT', undefined, 'un nom de module')[1];
847
+ if (module !== 'random' && module !== 'math') {
848
+ throw new LazError(`le module « ${module} » n'est pas disponible dans le mode Python pédagogique (disponibles : random, math)`, line);
849
+ }
850
+ return ['expr', ['walu'], line];
851
+ }
852
+ if (this.accept('KW', 'from')) {
853
+ throw new LazError('« from ... import ... » n\'est pas disponible — utilise « import random » et random.randint(...)', line);
854
+ }
855
+
856
+ const expr = this.parseExpression();
857
+ const optok = this.peek();
858
+ if (optok[0] === 'OP' && ['=', '+=', '-=', '*=', '/='].includes(optok[1])) {
859
+ this.next();
860
+ let valeur = this.parseExpression();
861
+ if (optok[1] !== '=') valeur = ['binop', optok[1][0], expr, valeur, line];
862
+ if (expr[0] === 'var') {
863
+ const genre = this.declareOuAssigne(expr[1]);
864
+ return [genre, expr[1], valeur, line];
865
+ }
866
+ if (expr[0] === 'index') return ['assign_index', expr[1], expr[2], valeur, line];
867
+ throw new LazError("on ne peut affecter qu'une variable ou une case de liste/dictionnaire", line);
868
+ }
869
+ return ['expr', expr, line];
870
+ }
871
+
872
+ parseIf(line) {
873
+ const cond = this.parseExpression();
874
+ const corps = this.parseBloc(line);
875
+ let sinon = null;
876
+ const posSauv = this.pos;
877
+ this.sauteLignes();
878
+ if (this.accept('KW', 'elif')) {
879
+ const l2 = this.tokens[this.pos - 1][2];
880
+ sinon = ['block', [this.parseIf(l2)]];
881
+ } else if (this.accept('KW', 'else')) {
882
+ const l2 = this.tokens[this.pos - 1][2];
883
+ sinon = this.parseBloc(l2);
884
+ } else {
885
+ this.pos = posSauv;
886
+ }
887
+ return ['kan', cond, corps, sinon, line];
888
+ }
889
+
890
+ parseExpression() { return this.parseOr(); }
891
+ parseOr() {
892
+ let g = this.parseAnd();
893
+ while (this.check('KW', 'or')) {
894
+ const line = this.next()[2];
895
+ g = ['or', g, this.parseAnd(), line];
896
+ }
897
+ return g;
898
+ }
899
+ parseAnd() {
900
+ let g = this.parseNot();
901
+ while (this.check('KW', 'and')) {
902
+ const line = this.next()[2];
903
+ g = ['and', g, this.parseNot(), line];
904
+ }
905
+ return g;
906
+ }
907
+ parseNot() {
908
+ if (this.check('KW', 'not')) {
909
+ const line = this.next()[2];
910
+ return ['not', this.parseNot(), line];
911
+ }
912
+ return this.parseComparaison();
913
+ }
914
+ parseComparaison() {
915
+ let g = this.parseAddition();
916
+ while (this.peek()[0] === 'OP' && ['==', '!=', '<', '>', '<=', '>='].includes(this.peek()[1])) {
917
+ const op = this.next();
918
+ g = ['cmp', op[1], g, this.parseAddition(), op[2]];
919
+ }
920
+ return g;
921
+ }
922
+ parseAddition() {
923
+ let g = this.parseMultiplication();
924
+ while (this.peek()[0] === 'OP' && (this.peek()[1] === '+' || this.peek()[1] === '-')) {
925
+ const op = this.next();
926
+ g = ['binop', op[1], g, this.parseMultiplication(), op[2]];
927
+ }
928
+ return g;
929
+ }
930
+ parseMultiplication() {
931
+ let g = this.parseUnaire();
932
+ while (this.peek()[0] === 'OP' && ['*', '/', '//', '%'].includes(this.peek()[1])) {
933
+ const op = this.next();
934
+ g = ['binop', op[1], g, this.parseUnaire(), op[2]];
935
+ }
936
+ return g;
937
+ }
938
+ parseUnaire() {
939
+ if (this.check('OP', '-')) {
940
+ const line = this.next()[2];
941
+ return ['neg', this.parseUnaire(), line];
942
+ }
943
+ return this.parsePuissance();
944
+ }
945
+ parsePuissance() {
946
+ const base = this.parsePostfixe();
947
+ if (this.check('OP', '**')) {
948
+ const line = this.next()[2];
949
+ return ['binop', '**', base, this.parseUnaire(), line];
950
+ }
951
+ return base;
952
+ }
953
+ parsePostfixe() {
954
+ let expr = this.parsePrimaire();
955
+ for (;;) {
956
+ const tok = this.peek();
957
+ if (this.accept('OP', '(')) {
958
+ const args = [];
959
+ if (!this.check('OP', ')')) {
960
+ args.push(this.parseExpression());
961
+ while (this.accept('OP', ',')) args.push(this.parseExpression());
962
+ }
963
+ this.expect('OP', ')', ')');
964
+ expr = this.traduitAppel(expr, args, tok[2]);
965
+ continue;
966
+ }
967
+ if (this.accept('OP', '[')) {
968
+ const index = this.parseExpression();
969
+ this.expect('OP', ']', ']');
970
+ expr = ['index', expr, index, tok[2]];
971
+ continue;
972
+ }
973
+ if (this.accept('OP', '.')) {
974
+ const nom = this.expect('IDENT', undefined, 'un nom de méthode')[1];
975
+ expr = ['pymeth', expr, nom, tok[2]];
976
+ continue;
977
+ }
978
+ break;
979
+ }
980
+ if (expr[0] === 'pymeth') {
981
+ throw new LazError(`la méthode .${expr[2]} doit être appelée avec des parenthèses`, expr[3]);
982
+ }
983
+ return expr;
984
+ }
985
+ traduitAppel(callee, args, line) {
986
+ if (callee[0] === 'pymeth') {
987
+ const [, objet, nom, l2] = callee;
988
+ if (objet[0] === 'var' && objet[1] === 'random') {
989
+ if (nom === 'randint') return ['call', ['var', 'hasard', l2], args, line];
990
+ throw new LazError(`random.${nom} n'est pas disponible (disponible : random.randint(a, b))`, line);
991
+ }
992
+ if (objet[0] === 'var' && objet[1] === 'math') {
993
+ if (nom === 'sqrt') return ['binop', '**', args[0], ['num', 0.5], line];
994
+ throw new LazError(`math.${nom} n'est pas disponible (disponible : math.sqrt(x))`, line);
995
+ }
996
+ if (nom in PY_METHODES) {
997
+ if (nom === 'join') return ['call', ['var', 'colle', l2], [args[0], objet], line];
998
+ if (nom === 'pop') return ['call', ['var', 'retire', l2], [objet, args.length ? args[0] : ['num', -1]], line];
999
+ if (nom === 'split') return ['call', ['var', 'koupe', l2], [objet, args.length ? args[0] : ['str', ' ']], line];
1000
+ return ['call', ['var', PY_METHODES[nom], l2], [objet, ...args], line];
1001
+ }
1002
+ const dispo = Object.keys(PY_METHODES).sort().join(', ');
1003
+ throw new LazError(`la méthode .${nom} n'est pas disponible dans le mode Python pédagogique (disponibles : ${dispo})`, line);
1004
+ }
1005
+ if (callee[0] === 'var') {
1006
+ const nom = callee[1];
1007
+ if (nom === 'range') return ['call', ['var', '__plage', callee[2]], args, line];
1008
+ if (nom in PY_FONCTIONS) return ['call', ['var', PY_FONCTIONS[nom], callee[2]], args, line];
1009
+ }
1010
+ return ['call', callee, args, line];
1011
+ }
1012
+ parsePrimaire() {
1013
+ const tok = this.peek();
1014
+ const line = tok[2];
1015
+ if (tok[0] === 'NUMBER') { this.next(); return ['num', tok[1]]; }
1016
+ if (tok[0] === 'STRING') { this.next(); return ['str', tok[1]]; }
1017
+ if (this.accept('KW', 'True')) return ['bool', true];
1018
+ if (this.accept('KW', 'False')) return ['bool', false];
1019
+ if (this.accept('KW', 'None')) return ['walu'];
1020
+ if (tok[0] === 'IDENT') { this.next(); return ['var', tok[1], line]; }
1021
+ if (this.accept('OP', '(')) {
1022
+ const e = this.parseExpression();
1023
+ this.expect('OP', ')', ')');
1024
+ return e;
1025
+ }
1026
+ if (this.accept('OP', '[')) {
1027
+ const items = [];
1028
+ if (!this.check('OP', ']')) {
1029
+ items.push(this.parseExpression());
1030
+ while (this.accept('OP', ',')) {
1031
+ if (this.check('OP', ']')) break;
1032
+ items.push(this.parseExpression());
1033
+ }
1034
+ }
1035
+ this.expect('OP', ']', ']');
1036
+ return ['list', items, line];
1037
+ }
1038
+ if (this.accept('OP', '{')) {
1039
+ const paires = [];
1040
+ if (!this.check('OP', '}')) {
1041
+ let k = this.parseExpression();
1042
+ this.expect('OP', ':', ':');
1043
+ paires.push([k, this.parseExpression()]);
1044
+ while (this.accept('OP', ',')) {
1045
+ if (this.check('OP', '}')) break;
1046
+ k = this.parseExpression();
1047
+ this.expect('OP', ':', ':');
1048
+ paires.push([k, this.parseExpression()]);
1049
+ }
1050
+ }
1051
+ this.expect('OP', '}', '}');
1052
+ return ['dict', paires, line];
1053
+ }
1054
+ const trouve = tok[1] !== null ? tok[1] : (PY_NICE[tok[0]] || tok[0]);
1055
+ throw new LazError(`expression attendue, mais j'ai trouvé « ${trouve} »`, line);
1056
+ }
1057
+ }
1058
+
1059
+ function parsePython(source) {
1060
+ return new PyParser(pyTokenize(source)).parseProgram();
1061
+ }
1062
+
1063
+ class Interpreter {
1064
+ constructor(io) {
1065
+ this.io = io;
1066
+ this.fs = (io && io.fs) || new Map();
1067
+ this.dessin = null;
1068
+ this.memoire = (io && io.memoire) || {};
1069
+ this.gardeNoms = new Set();
1070
+ this.vitesse = 0;
1071
+ this.histoire = [];
1072
+ this.globals = new Env(null);
1073
+ this.steps = 0;
1074
+ this.frameFn = null; // v6 : la fonction appelée à chaque image du jeu
1075
+ this.jeuFini = false; // v6 : arrete_jeu() a été appelé
1076
+ this.aInterface = false; // v7 : des widgets ont été créés
1077
+ this.actionsBoutons = new Map(); // v7 : id de bouton -> fonction Larkhré
1078
+ this.compteurWidget = 0; // v7 : générateur d'identifiants
1079
+ this.makeBuiltins();
1080
+ }
1081
+
1082
+ noteHistoire(line, name, value) {
1083
+ this.histoire.push([line, name, toText(value)]);
1084
+ if (this.histoire.length > 6) this.histoire.shift();
1085
+ }
1086
+
1087
+ snapshot() {
1088
+ const out = [];
1089
+ for (const [k, v] of this.globals.vars.entries()) {
1090
+ if (v && (v.builtin || v instanceof LazFunction || v instanceof LazClass)) continue;
1091
+ out.push([k, toText(v)]);
1092
+ if (out.length >= 12) break;
1093
+ }
1094
+ return out;
1095
+ }
1096
+
1097
+ tick(line) {
1098
+ this.steps++;
1099
+ if (this.steps > MAX_STEPS) {
1100
+ throw new LazError('programme trop long — boucle infinie ?', line);
1101
+ }
1102
+ if (this.io.shouldStop && this.io.shouldStop()) throw new StopEx();
1103
+ }
1104
+
1105
+ need(args, count, name, line) {
1106
+ if (args.length < count) {
1107
+ throw new LazError(`${name}() demande au moins ${count} argument(s), reçu ${args.length}`, line);
1108
+ }
1109
+ }
1110
+
1111
+ makeBuiltins() {
1112
+ const g = this.globals;
1113
+ const self = this;
1114
+ const B = (name, fn) => g.declare(name, { builtin: name, fn });
1115
+
1116
+ B('vox', async (args) => { self.io.onPrint(args.map(toText).join(' ')); return null; });
1117
+ B('demand', async (args) => {
1118
+ const p = args.length ? toText(args[0]) : '';
1119
+ const answer = await self.io.onInput(p);
1120
+ return answer === null || answer === undefined ? '' : String(answer);
1121
+ });
1122
+ B('nombre', async (args, line) => {
1123
+ self.need(args, 1, 'nombre', line);
1124
+ const v = args[0];
1125
+ if (v === true) return 1;
1126
+ if (v === false) return 0;
1127
+ if (typeof v === 'number') return v;
1128
+ const f = parseFloat(String(v).trim().replace(',', '.'));
1129
+ if (Number.isNaN(f) || !/^-?[\d.,]+$/.test(String(v).trim())) {
1130
+ throw new LazError(`impossible de convertir « ${toText(v)} » en nombre`, line);
1131
+ }
1132
+ return f;
1133
+ });
1134
+ B('texte', async (args, line) => { self.need(args, 1, 'texte', line); return toText(args[0]); });
1135
+ B('taille', async (args, line) => {
1136
+ self.need(args, 1, 'taille', line);
1137
+ const v = args[0];
1138
+ if (typeof v === 'string' || Array.isArray(v)) return v.length;
1139
+ if (v instanceof Map) return v.size;
1140
+ throw new LazError('taille() fonctionne avec un texte, une liste ou un dictionnaire', line);
1141
+ });
1142
+ B('ajoute', async (args, line) => {
1143
+ self.need(args, 2, 'ajoute', line);
1144
+ if (!Array.isArray(args[0])) throw new LazError('ajoute() demande une liste en premier argument', line);
1145
+ args[0].push(args[1]);
1146
+ return args[0];
1147
+ });
1148
+ B('retire', async (args, line) => {
1149
+ self.need(args, 2, 'retire', line);
1150
+ const c = args[0];
1151
+ if (c instanceof Map) {
1152
+ const key = checkDictKey(args[1], line);
1153
+ if (!c.has(key)) throw new LazError(`la clé « ${toText(key)} » n'existe pas dans le dictionnaire`, line);
1154
+ const v = c.get(key);
1155
+ c.delete(key);
1156
+ return v;
1157
+ }
1158
+ if (!Array.isArray(c)) throw new LazError('retire() demande une liste ou un dictionnaire en premier argument', line);
1159
+ let idx = Math.trunc(checkNumber(args[1], line, 'retire()'));
1160
+ if (idx < -c.length || idx >= c.length) {
1161
+ throw new LazError(`position ${idx} hors de la liste (taille ${c.length})`, line);
1162
+ }
1163
+ if (idx < 0) idx += c.length;
1164
+ return c.splice(idx, 1)[0];
1165
+ });
1166
+ B('hasard', async (args, line) => {
1167
+ self.need(args, 2, 'hasard', line);
1168
+ const a = Math.trunc(checkNumber(args[0], line, 'hasard()'));
1169
+ const b = Math.trunc(checkNumber(args[1], line, 'hasard()'));
1170
+ const lo = Math.min(a, b), hi = Math.max(a, b);
1171
+ return lo + Math.floor(Math.random() * (hi - lo + 1));
1172
+ });
1173
+ B('arondi', async (args, line) => {
1174
+ self.need(args, 1, 'arondi', line);
1175
+ const v = checkNumber(args[0], line, 'arondi()');
1176
+ const nd = args.length > 1 ? Math.trunc(checkNumber(args[1], line, 'arondi()')) : 0;
1177
+ const m = Math.pow(10, nd);
1178
+ return Math.round(v * m) / m;
1179
+ });
1180
+ B('majus', async (args, line) => { self.need(args, 1, 'majus', line); return toText(args[0]).toUpperCase(); });
1181
+ B('minus', async (args, line) => { self.need(args, 1, 'minus', line); return toText(args[0]).toLowerCase(); });
1182
+ B('koupe', async (args, line) => { self.need(args, 2, 'koupe', line); return toText(args[0]).split(toText(args[1])); });
1183
+ B('tri', async (args, line) => {
1184
+ self.need(args, 1, 'tri', line);
1185
+ if (!Array.isArray(args[0])) throw new LazError('tri() demande une liste', line);
1186
+ const arr = args[0];
1187
+ const allNum = arr.every(x => typeof x === 'number');
1188
+ const allStr = arr.every(x => typeof x === 'string');
1189
+ if (!allNum && !allStr && arr.length > 0) {
1190
+ throw new LazError('tri() ne peut pas trier des types mélangés', line);
1191
+ }
1192
+ const copy = arr.slice();
1193
+ if (allNum) copy.sort((a, b) => a - b);
1194
+ else copy.sort();
1195
+ return copy;
1196
+ });
1197
+ B('tip', async (args, line) => {
1198
+ self.need(args, 1, 'tip', line);
1199
+ const v = args[0];
1200
+ if (v === null || v === undefined) return 'walu';
1201
+ if (v === true || v === false) return 'buli';
1202
+ if (typeof v === 'number') return 'nombre';
1203
+ if (typeof v === 'string') return 'texte';
1204
+ if (Array.isArray(v)) return 'liste';
1205
+ if (v instanceof Map) return 'dico';
1206
+ if (v instanceof LazInstance) return v.klass.name;
1207
+ if (v instanceof LazClass) return 'klas';
1208
+ if (v instanceof LazFunction || v instanceof BoundMethod) return 'fonk';
1209
+ return 'inconnu';
1210
+ });
1211
+
1212
+ // --- nouveautés v2.0 ---
1213
+ B('cles', async (args, line) => {
1214
+ self.need(args, 1, 'cles', line);
1215
+ if (!(args[0] instanceof Map)) throw new LazError('cles() demande un dictionnaire', line);
1216
+ return [...args[0].keys()];
1217
+ });
1218
+ B('valeurs', async (args, line) => {
1219
+ self.need(args, 1, 'valeurs', line);
1220
+ if (!(args[0] instanceof Map)) throw new LazError('valeurs() demande un dictionnaire', line);
1221
+ return [...args[0].values()];
1222
+ });
1223
+ B('contient', async (args, line) => {
1224
+ self.need(args, 2, 'contient', line);
1225
+ const c = args[0], x = args[1];
1226
+ if (c instanceof Map) return c.has(x);
1227
+ if (Array.isArray(c)) return c.some(v => lazEquals(v, x));
1228
+ if (typeof c === 'string') return c.includes(toText(x));
1229
+ throw new LazError('contient() demande un texte, une liste ou un dictionnaire en premier argument', line);
1230
+ });
1231
+ B('colle', async (args, line) => {
1232
+ self.need(args, 2, 'colle', line);
1233
+ if (!Array.isArray(args[0])) throw new LazError('colle() demande une liste en premier argument', line);
1234
+ return args[0].map(toText).join(toText(args[1]));
1235
+ });
1236
+ B('remplace', async (args, line) => {
1237
+ self.need(args, 3, 'remplace', line);
1238
+ return toText(args[0]).split(toText(args[1])).join(toText(args[2]));
1239
+ });
1240
+ B('lis_fichier', async (args, line) => {
1241
+ self.need(args, 1, 'lis_fichier', line);
1242
+ const chemin = toText(args[0]);
1243
+ if (!self.fs.has(chemin)) throw new LazError(`fichier introuvable : ${chemin}`, line);
1244
+ return self.fs.get(chemin);
1245
+ });
1246
+ B('ecris_fichier', async (args, line) => {
1247
+ self.need(args, 2, 'ecris_fichier', line);
1248
+ self.fs.set(toText(args[0]), toText(args[1]));
1249
+ return null;
1250
+ });
1251
+ B('ajoute_fichier', async (args, line) => {
1252
+ self.need(args, 2, 'ajoute_fichier', line);
1253
+ const chemin = toText(args[0]);
1254
+ self.fs.set(chemin, (self.fs.get(chemin) || '') + toText(args[1]));
1255
+ return null;
1256
+ });
1257
+ B('fichier_existe', async (args, line) => {
1258
+ self.need(args, 1, 'fichier_existe', line);
1259
+ return self.fs.has(toText(args[0]));
1260
+ });
1261
+
1262
+ // --- nouveautés v3.0 : la couleur ! ---
1263
+ const COULEURS = {
1264
+ rouge: '31', vert: '32', jaune: '33', bleu: '34',
1265
+ violet: '35', cyan: '36', blanc: '37', or: '93',
1266
+ gris: '90', rose: '95', noir: '30',
1267
+ };
1268
+ const STYLES = { gras: '1', souligne: '4', 'souligné': '4' };
1269
+
1270
+ B('vox_couleur', async (args, line) => {
1271
+ self.need(args, 2, 'vox_couleur', line);
1272
+ const couleur = toText(args[args.length - 1]).toLowerCase();
1273
+ if (!(couleur in COULEURS)) {
1274
+ throw new LazError(`couleur inconnue « ${couleur} » (disponibles : ${Object.keys(COULEURS).sort().join(', ')})`, line);
1275
+ }
1276
+ const texte = args.slice(0, -1).map(toText).join(' ');
1277
+ self.io.onPrint(`\x1b[${COULEURS[couleur]}m${texte}\x1b[0m`);
1278
+ return null;
1279
+ });
1280
+ B('stylise', async (args, line) => {
1281
+ self.need(args, 2, 'stylise', line);
1282
+ const style = toText(args[1]).toLowerCase();
1283
+ const code = COULEURS[style] || STYLES[style];
1284
+ if (!code) {
1285
+ throw new LazError(`style inconnu « ${style} » (disponibles : ${Object.keys(COULEURS).sort().concat(['gras', 'souligne']).join(', ')})`, line);
1286
+ }
1287
+ return `\x1b[${code}m${toText(args[0])}\x1b[0m`;
1288
+ });
1289
+ B('efface_ecran', async () => {
1290
+ if (self.io.onClear) self.io.onClear();
1291
+ return null;
1292
+ });
1293
+
1294
+ // --- nouveautés v3.1 : le mode dessin ! ---
1295
+ const DESSIN_COULEURS = {
1296
+ rouge: '#f87171', vert: '#4ade80', jaune: '#facc15',
1297
+ bleu: '#60a5fa', violet: '#c084fc', cyan: '#22d3ee',
1298
+ blanc: '#e6edf3', or: '#f0b429', gris: '#8b949e',
1299
+ rose: '#f9a8d4', noir: '#0d1117',
1300
+ };
1301
+ const couleurCss = (nom, line) => {
1302
+ nom = toText(nom).toLowerCase();
1303
+ if (nom.startsWith('#')) return nom;
1304
+ if (!(nom in DESSIN_COULEURS)) {
1305
+ throw new LazError(`couleur inconnue « ${nom} » (disponibles : ${Object.keys(DESSIN_COULEURS).sort().join(', ')}, ou un code #rrggbb)`, line);
1306
+ }
1307
+ return DESSIN_COULEURS[nom];
1308
+ };
1309
+ const toileRequise = (line) => {
1310
+ if (!self.dessin) throw new LazError('appelle d\'abord toile(largeur, hauteur) pour créer ta zone de dessin', line);
1311
+ return self.dessin;
1312
+ };
1313
+ const draw = (cmd) => { if (self.io.onDraw) self.io.onDraw(cmd); };
1314
+
1315
+ B('toile', async (args, line) => {
1316
+ self.need(args, 2, 'toile', line);
1317
+ const w = checkNumber(args[0], line, 'toile()');
1318
+ const h = checkNumber(args[1], line, 'toile()');
1319
+ if (w < 1 || h < 1 || w > 2000 || h > 2000) throw new LazError('toile() : dimensions entre 1 et 2000', line);
1320
+ self.dessin = { w, h, formes: [] };
1321
+ draw({ type: 'toile', w, h });
1322
+ return null;
1323
+ });
1324
+ B('fond', async (args, line) => {
1325
+ self.need(args, 1, 'fond', line);
1326
+ const t = toileRequise(line);
1327
+ const c = couleurCss(args[0], line);
1328
+ t.formes.push(['fond', c]);
1329
+ draw({ type: 'fond', c });
1330
+ return null;
1331
+ });
1332
+ B('trace_ligne', async (args, line) => {
1333
+ self.need(args, 5, 'trace_ligne', line);
1334
+ const t = toileRequise(line);
1335
+ const [x1, y1, x2, y2] = args.slice(0, 4).map(a => checkNumber(a, line, 'trace_ligne()'));
1336
+ const c = couleurCss(args[4], line);
1337
+ t.formes.push(['ligne', x1, y1, x2, y2, c]);
1338
+ draw({ type: 'ligne', x1, y1, x2, y2, c });
1339
+ return null;
1340
+ });
1341
+ const rectF = (nom, plein) => async (args, line) => {
1342
+ self.need(args, 5, nom, line);
1343
+ const t = toileRequise(line);
1344
+ const [x, y, w, h] = args.slice(0, 4).map(a => checkNumber(a, line, nom + '()'));
1345
+ const c = couleurCss(args[4], line);
1346
+ t.formes.push(['rect', x, y, w, h, c, plein]);
1347
+ draw({ type: 'rect', x, y, l: w, h, c, plein });
1348
+ return null;
1349
+ };
1350
+ B('trace_rect', rectF('trace_rect', false));
1351
+ B('rect_plein', rectF('rect_plein', true));
1352
+ const cercleF = (nom, plein) => async (args, line) => {
1353
+ self.need(args, 4, nom, line);
1354
+ const t = toileRequise(line);
1355
+ const [x, y, r] = args.slice(0, 3).map(a => checkNumber(a, line, nom + '()'));
1356
+ const c = couleurCss(args[3], line);
1357
+ t.formes.push(['cercle', x, y, r, c, plein]);
1358
+ draw({ type: 'cercle', x, y, r, c, plein });
1359
+ return null;
1360
+ };
1361
+ B('trace_cercle', cercleF('trace_cercle', false));
1362
+ B('cercle_plein', cercleF('cercle_plein', true));
1363
+ B('trace_texte', async (args, line) => {
1364
+ self.need(args, 4, 'trace_texte', line);
1365
+ const t = toileRequise(line);
1366
+ const x = checkNumber(args[0], line, 'trace_texte()');
1367
+ const y = checkNumber(args[1], line, 'trace_texte()');
1368
+ const txt = toText(args[2]);
1369
+ const c = couleurCss(args[3], line);
1370
+ t.formes.push(['texte', x, y, txt, c]);
1371
+ draw({ type: 'texte', x, y, t: txt, c });
1372
+ return null;
1373
+ });
1374
+ B('sauve_dessin', async (args, line) => {
1375
+ self.need(args, 1, 'sauve_dessin', line);
1376
+ const t = toileRequise(line);
1377
+ self.fs.set(toText(args[0]), svgDuDessin(t));
1378
+ return null;
1379
+ });
1380
+
1381
+ // --- nouveautés v4.0 ---
1382
+ B('echoue', async (args, line) => {
1383
+ self.need(args, 1, 'echoue', line);
1384
+ throw new LazError(toText(args[0]), line);
1385
+ });
1386
+
1387
+ // --- nouveautés v5.0 ---
1388
+ B('ralenti', async (args, line) => {
1389
+ self.need(args, 1, 'ralenti', line);
1390
+ const v = checkNumber(args[0], line, 'ralenti()');
1391
+ self.vitesse = Math.min(3, Math.max(0, v));
1392
+ return null;
1393
+ });
1394
+
1395
+ // --- nouveautés v6.0 : le mode JEU (temps réel) ---
1396
+ const SONS_CONNUS = ['clic', 'defaite', 'explosion', 'moteur', 'piece', 'saut', 'victoire'];
1397
+ B('chaque_image', async (args, line) => {
1398
+ self.need(args, 1, 'chaque_image', line);
1399
+ if (!(args[0] instanceof LazFunction)) {
1400
+ throw new LazError('chaque_image() attend une fonction : chaque_image(ma_fonction) — sans parenthèses après son nom', line);
1401
+ }
1402
+ self.frameFn = args[0];
1403
+ return null;
1404
+ });
1405
+ B('touche_pressee', async (args, line) => {
1406
+ self.need(args, 1, 'touche_pressee', line);
1407
+ const nom = toText(args[0]).toLowerCase();
1408
+ return self.io.toucheEnfoncee ? !!self.io.toucheEnfoncee(nom) : false;
1409
+ });
1410
+ B('arrete_jeu', async () => { self.jeuFini = true; return null; });
1411
+
1412
+ // --- nouveauté v8.0 : Larkhré PARLE ---
1413
+ B('dis', async (args, line) => {
1414
+ self.need(args, 1, 'dis', line);
1415
+ const texte = args.map(toText).join(' ');
1416
+ if (self.io.onDis) { try { self.io.onDis(texte); } catch (e) { } }
1417
+ return null;
1418
+ });
1419
+
1420
+ // --- nouveauté v9.0 : Larkhré ECOUTE ---
1421
+ B('ecoute', async (args) => {
1422
+ const prompt = args.length ? toText(args[0]) : '';
1423
+ if (self.io.onEcoute) {
1424
+ const r = await self.io.onEcoute(prompt);
1425
+ return r === null || r === undefined ? '' : String(r);
1426
+ }
1427
+ if (self.io.onInput) {
1428
+ const r = await self.io.onInput(prompt + ' (micro indisponible — tape ta réponse) ');
1429
+ return r === null || r === undefined ? '' : String(r);
1430
+ }
1431
+ return '';
1432
+ });
1433
+
1434
+ // --- aides internes du mode Python (v11) ---
1435
+ B('__plage', async (args, line) => {
1436
+ self.need(args, 1, 'range', line);
1437
+ const vals = args.map(a => Math.trunc(checkNumber(a, line, 'range()')));
1438
+ let debut = 0, fin = 0, pas = 1;
1439
+ if (vals.length === 1) { fin = vals[0]; }
1440
+ else { debut = vals[0]; fin = vals[1]; if (vals.length > 2) pas = vals[2]; }
1441
+ if (pas === 0) throw new LazError('range() : le pas ne peut pas être zéro', line);
1442
+ const out = [];
1443
+ if (pas > 0) { for (let v = debut; v < fin; v += pas) out.push(v); }
1444
+ else { for (let v = debut; v > fin; v += pas) out.push(v); }
1445
+ return out;
1446
+ });
1447
+ B('__ent', async (args, line) => {
1448
+ self.need(args, 1, 'int', line);
1449
+ const v = args[0];
1450
+ if (v === true) return 1;
1451
+ if (v === false) return 0;
1452
+ if (typeof v === 'number') return Math.trunc(v);
1453
+ if (typeof v === 'string') {
1454
+ const f = parseFloat(v.trim().replace(',', '.'));
1455
+ if (Number.isNaN(f) || !/^-?[\d.,]+$/.test(v.trim())) {
1456
+ throw new LazError(`impossible de convertir « ${toText(v)} » en nombre entier`, line);
1457
+ }
1458
+ return Math.trunc(f);
1459
+ }
1460
+ throw new LazError('int() attend un nombre ou un texte', line);
1461
+ });
1462
+ B('__abs', async (args, line) => {
1463
+ self.need(args, 1, 'abs', line);
1464
+ return Math.abs(checkNumber(args[0], line, 'abs()'));
1465
+ });
1466
+
1467
+ // --- nouveautés v10.0 : Larkhré QUANTIQUE ---
1468
+ // Simulateur à vecteur d'état complet (nombres complexes [re, im]).
1469
+ const etatQ = { n: 0, amp: [] };
1470
+ const qRequis = (line) => {
1471
+ if (etatQ.n === 0) throw new LazError('appelle d\'abord qubits(n) pour créer ton registre quantique', line);
1472
+ };
1473
+ const qBit = (idx, q) => (idx >> (etatQ.n - 1 - q)) & 1;
1474
+ const qVerifie = (v, line, nom) => {
1475
+ const q = Math.floor(checkNumber(v, line, nom));
1476
+ if (q < 0 || q >= etatQ.n) throw new LazError(`${nom} : le qubit ${q} n'existe pas (registre de ${etatQ.n} qubits, numérotés de 0 à ${etatQ.n - 1})`, line);
1477
+ return q;
1478
+ };
1479
+ const porte1q = (q, m00, m01, m10, m11) => {
1480
+ const amp = etatQ.amp;
1481
+ const pas = 2 ** (etatQ.n - 1 - q);
1482
+ for (let i = 0; i < amp.length; i++) {
1483
+ if (Math.floor(i / pas) % 2 === 0) {
1484
+ const jdx = i + pas;
1485
+ const a0 = amp[i], a1 = amp[jdx];
1486
+ amp[i] = [m00 * a0[0] + m01 * a1[0], m00 * a0[1] + m01 * a1[1]];
1487
+ amp[jdx] = [m10 * a0[0] + m11 * a1[0], m10 * a0[1] + m11 * a1[1]];
1488
+ }
1489
+ }
1490
+ };
1491
+ B('qubits', async (args, line) => {
1492
+ self.need(args, 1, 'qubits', line);
1493
+ const n = Math.floor(checkNumber(args[0], line, 'qubits()'));
1494
+ if (n < 1 || n > 10) throw new LazError('qubits() : entre 1 et 10 qubits (chaque qubit DOUBLE la mémoire du simulateur !)', line);
1495
+ etatQ.n = n;
1496
+ etatQ.amp = Array.from({ length: 2 ** n }, () => [0, 0]);
1497
+ etatQ.amp[0] = [1, 0];
1498
+ return null;
1499
+ });
1500
+ B('superpose', async (args, line) => {
1501
+ self.need(args, 1, 'superpose', line);
1502
+ qRequis(line);
1503
+ const q = qVerifie(args[0], line, 'superpose()');
1504
+ const r = 1 / Math.sqrt(2);
1505
+ porte1q(q, r, r, r, -r);
1506
+ return null;
1507
+ });
1508
+ B('porte_x', async (args, line) => {
1509
+ self.need(args, 1, 'porte_x', line);
1510
+ qRequis(line);
1511
+ porte1q(qVerifie(args[0], line, 'porte_x()'), 0, 1, 1, 0);
1512
+ return null;
1513
+ });
1514
+ B('porte_z', async (args, line) => {
1515
+ self.need(args, 1, 'porte_z', line);
1516
+ qRequis(line);
1517
+ porte1q(qVerifie(args[0], line, 'porte_z()'), 1, 0, 0, -1);
1518
+ return null;
1519
+ });
1520
+ B('intrique', async (args, line) => {
1521
+ self.need(args, 2, 'intrique', line);
1522
+ qRequis(line);
1523
+ const c = qVerifie(args[0], line, 'intrique()');
1524
+ const t = qVerifie(args[1], line, 'intrique()');
1525
+ if (c === t) throw new LazError('intrique() : le qubit de contrôle et la cible doivent être différents', line);
1526
+ const amp = etatQ.amp;
1527
+ const pas = 2 ** (etatQ.n - 1 - t);
1528
+ for (let i = 0; i < amp.length; i++) {
1529
+ if (qBit(i, c) === 1 && qBit(i, t) === 0) {
1530
+ const jdx = i + pas;
1531
+ const tmp = amp[i];
1532
+ amp[i] = amp[jdx];
1533
+ amp[jdx] = tmp;
1534
+ }
1535
+ }
1536
+ return null;
1537
+ });
1538
+ B('mesure', async (args, line) => {
1539
+ self.need(args, 1, 'mesure', line);
1540
+ qRequis(line);
1541
+ const q = qVerifie(args[0], line, 'mesure()');
1542
+ const amp = etatQ.amp;
1543
+ let p1 = 0;
1544
+ for (let i = 0; i < amp.length; i++) {
1545
+ if (qBit(i, q) === 1) p1 += amp[i][0] ** 2 + amp[i][1] ** 2;
1546
+ }
1547
+ const resultat = Math.random() < p1 ? 1 : 0;
1548
+ let norme = 0;
1549
+ for (let i = 0; i < amp.length; i++) {
1550
+ if (qBit(i, q) !== resultat) amp[i] = [0, 0];
1551
+ else norme += amp[i][0] ** 2 + amp[i][1] ** 2;
1552
+ }
1553
+ if (norme > 0) {
1554
+ norme = Math.sqrt(norme);
1555
+ for (let i = 0; i < amp.length; i++) amp[i] = [amp[i][0] / norme, amp[i][1] / norme];
1556
+ }
1557
+ return resultat;
1558
+ });
1559
+ B('probabilites', async (args, line) => {
1560
+ qRequis(line);
1561
+ const d = new Map();
1562
+ for (let i = 0; i < etatQ.amp.length; i++) {
1563
+ const p = etatQ.amp[i][0] ** 2 + etatQ.amp[i][1] ** 2;
1564
+ if (p > 1e-9) d.set(i.toString(2).padStart(etatQ.n, '0'), Math.round(p * 10000) / 10000);
1565
+ }
1566
+ return d;
1567
+ });
1568
+
1569
+ // --- nouveautés v7.0 : le MODE INTERFACE ---
1570
+ const widget = (cmd) => { if (self.io.onWidget) self.io.onWidget(cmd); };
1571
+ const nouvelId = (prefixe) => { self.compteurWidget++; return prefixe + '_' + self.compteurWidget; };
1572
+ B('titre', async (args, line) => {
1573
+ self.need(args, 1, 'titre', line);
1574
+ self.aInterface = true;
1575
+ widget({ type: 'titre', texte: toText(args[0]) });
1576
+ return null;
1577
+ });
1578
+ B('etiquette', async (args) => {
1579
+ self.aInterface = true;
1580
+ const id = nouvelId('etq');
1581
+ widget({ type: 'etiquette', id, texte: args.length ? toText(args[0]) : '' });
1582
+ return id;
1583
+ });
1584
+ B('bouton', async (args, line) => {
1585
+ self.need(args, 2, 'bouton', line);
1586
+ if (!(args[1] instanceof LazFunction)) {
1587
+ throw new LazError('bouton() attend un texte puis une fonction : bouton("OK", mon_action) — sans parenthèses après le nom de la fonction', line);
1588
+ }
1589
+ self.aInterface = true;
1590
+ const id = nouvelId('btn');
1591
+ self.actionsBoutons.set(id, args[1]);
1592
+ widget({ type: 'bouton', id, texte: toText(args[0]) });
1593
+ return id;
1594
+ });
1595
+ B('champ', async (args) => {
1596
+ self.aInterface = true;
1597
+ const id = nouvelId('chp');
1598
+ widget({ type: 'champ', id, placeholder: args.length ? toText(args[0]) : '' });
1599
+ return id;
1600
+ });
1601
+ B('valeur_de', async (args, line) => {
1602
+ self.need(args, 1, 'valeur_de', line);
1603
+ const v = self.io.getChampValeur ? self.io.getChampValeur(toText(args[0])) : '';
1604
+ return v === null || v === undefined ? '' : String(v);
1605
+ });
1606
+ B('change_texte', async (args, line) => {
1607
+ self.need(args, 2, 'change_texte', line);
1608
+ widget({ type: 'maj', id: toText(args[0]), texte: toText(args[1]) });
1609
+ return null;
1610
+ });
1611
+ B('efface_interface', async () => {
1612
+ self.actionsBoutons.clear();
1613
+ widget({ type: 'efface' });
1614
+ return null;
1615
+ });
1616
+ B('joue_son', async (args, line) => {
1617
+ self.need(args, 1, 'joue_son', line);
1618
+ const nom = toText(args[0]).toLowerCase();
1619
+ if (!SONS_CONNUS.includes(nom)) {
1620
+ throw new LazError(`son inconnu « ${nom} » (disponibles : ${SONS_CONNUS.join(', ')})`, line);
1621
+ }
1622
+ if (self.io.onSon) { try { self.io.onSon(nom); } catch (e) { } }
1623
+ return null;
1624
+ });
1625
+ }
1626
+
1627
+ async run(source) {
1628
+ // v11 : le mode PYTHON BIENVEILLANT
1629
+ if (estModePython(source)) {
1630
+ MODE_PYTHON.actif = true;
1631
+ const ast = parsePython(source);
1632
+ return await this.execBlock(ast, this.globals);
1633
+ }
1634
+ MODE_PYTHON.actif = false;
1635
+ const langueMap = detecteLangue(source);
1636
+ const tokens = tokenize(source, langueMap);
1637
+ const ast = new Parser(tokens).parseProgram();
1638
+ return await this.execBlock(ast, this.globals);
1639
+ }
1640
+
1641
+ collecteGarde() {
1642
+ const jsonOk = (v) => v === null || ['number', 'string', 'boolean'].includes(typeof v)
1643
+ || (Array.isArray(v) && v.every(jsonOk));
1644
+ const out = {};
1645
+ for (const n of this.gardeNoms) {
1646
+ if (this.globals.vars.has(n)) {
1647
+ const v = this.globals.vars.get(n);
1648
+ if (jsonOk(v)) out[n] = v;
1649
+ }
1650
+ }
1651
+ return out;
1652
+ }
1653
+
1654
+ async execBlock(block, env) {
1655
+ let result = null;
1656
+ for (const stmt of block[1]) result = await this.execStmt(stmt, env);
1657
+ return result;
1658
+ }
1659
+
1660
+ async execStmt(stmt, env) {
1661
+ const kind = stmt[0];
1662
+ this.tick(stmt[stmt.length - 1]);
1663
+ // laisser le navigateur respirer (bouton Arrêter, affichage) sur les gros programmes
1664
+ if ((this.steps & 8191) === 0 && this.io.onYield) await this.io.onYield();
1665
+ // v5 : le ralenti pédagogique
1666
+ if (this.vitesse > 0) {
1667
+ if (this.io.onStep) await this.io.onStep(stmt[stmt.length - 1], this.snapshot(), this.vitesse * 1000);
1668
+ else await new Promise(r => setTimeout(r, this.vitesse * 1000));
1669
+ }
1670
+
1671
+ if (kind === 'declare') {
1672
+ const value = await this.eval(stmt[2], env);
1673
+ env.declare(stmt[1], value);
1674
+ this.noteHistoire(stmt[3], stmt[1], value);
1675
+ return null;
1676
+ }
1677
+ if (kind === 'garde') {
1678
+ const [, name, valueNode, line] = stmt;
1679
+ this.gardeNoms.add(name);
1680
+ let value;
1681
+ if (Object.prototype.hasOwnProperty.call(this.memoire, name)) value = this.memoire[name];
1682
+ else value = await this.eval(valueNode, env);
1683
+ env.declare(name, value);
1684
+ this.noteHistoire(line, name, value);
1685
+ return null;
1686
+ }
1687
+ if (kind === 'assign') {
1688
+ const value = await this.eval(stmt[2], env);
1689
+ env.assign(stmt[1], value, stmt[3]);
1690
+ this.noteHistoire(stmt[3], stmt[1], value);
1691
+ return null;
1692
+ }
1693
+ if (kind === 'assign_index') {
1694
+ const [, targetNode, indexNode, valueNode, line] = stmt;
1695
+ const target = await this.eval(targetNode, env);
1696
+ const index = await this.eval(indexNode, env);
1697
+ const value = await this.eval(valueNode, env);
1698
+ if (target instanceof Map) {
1699
+ target.set(checkDictKey(index, line), value);
1700
+ return null;
1701
+ }
1702
+ if (!Array.isArray(target)) throw new LazError('on ne peut modifier par position que les listes et les dictionnaires', line);
1703
+ let idx = Math.trunc(checkNumber(index, line, "l'indexation"));
1704
+ if (idx < -target.length || idx >= target.length) {
1705
+ throw new LazError(`position ${idx} hors de la liste (taille ${target.length})`, line);
1706
+ }
1707
+ if (idx < 0) idx += target.length;
1708
+ target[idx] = value;
1709
+ return null;
1710
+ }
1711
+ if (kind === 'assign_attr') {
1712
+ const [, objNode, name, valueNode, line] = stmt;
1713
+ const obj = await this.eval(objNode, env);
1714
+ if (!(obj instanceof LazInstance)) {
1715
+ throw new LazError('on ne peut modifier une propriété (avec le point .) que sur un objet de klas', line);
1716
+ }
1717
+ obj.fields.set(name, await this.eval(valueNode, env));
1718
+ return null;
1719
+ }
1720
+ if (kind === 'klas') {
1721
+ const [, name, parentName, methods, line] = stmt;
1722
+ let parent = null;
1723
+ if (parentName) {
1724
+ parent = env.get(parentName, line);
1725
+ if (!(parent instanceof LazClass)) {
1726
+ throw new LazError(`« ${parentName} » n'est pas une klas, impossible d'en hériter`, line);
1727
+ }
1728
+ }
1729
+ const mmap = new Map();
1730
+ for (const [mname, params, body] of methods) {
1731
+ mmap.set(mname, new LazFunction(mname, params, body, env));
1732
+ }
1733
+ env.declare(name, new LazClass(name, mmap, parent));
1734
+ return null;
1735
+ }
1736
+ if (kind === 'importe') {
1737
+ throw new LazError("importe n'est pas disponible dans le playground web — utilise la version Python (pip install larkhre)", stmt[2]);
1738
+ }
1739
+ if (kind === 'essaie') {
1740
+ const [, body, errname, handler] = stmt;
1741
+ try {
1742
+ await this.execBlock(body, env);
1743
+ } catch (e) {
1744
+ if (e instanceof LazError) {
1745
+ env.declare(errname, e.lazMessage);
1746
+ await this.execBlock(handler, env);
1747
+ } else {
1748
+ throw e;
1749
+ }
1750
+ }
1751
+ return null;
1752
+ }
1753
+ if (kind === 'fonk') {
1754
+ env.declare(stmt[1], new LazFunction(stmt[1], stmt[2], stmt[3], env));
1755
+ return null;
1756
+ }
1757
+ if (kind === 'kan') {
1758
+ const [, cond, body, elseBranch] = stmt;
1759
+ if (isTruthy(await this.eval(cond, env))) await this.execBlock(body, env);
1760
+ else if (elseBranch) await this.execBlock(elseBranch, env);
1761
+ return null;
1762
+ }
1763
+ if (kind === 'tanke') {
1764
+ const [, cond, body, line] = stmt;
1765
+ while (isTruthy(await this.eval(cond, env))) {
1766
+ this.tick(line);
1767
+ try { await this.execBlock(body, env); }
1768
+ catch (e) {
1769
+ if (e instanceof BreakEx) break;
1770
+ if (e instanceof ContinueEx) continue;
1771
+ throw e;
1772
+ }
1773
+ }
1774
+ return null;
1775
+ }
1776
+ if (kind === 'pou') {
1777
+ const [, varName, iterableNode, body, line] = stmt;
1778
+ let iterable = await this.eval(iterableNode, env);
1779
+ if (typeof iterable === 'string') iterable = iterable.split('');
1780
+ if (iterable instanceof Map) iterable = [...iterable.keys()];
1781
+ if (!Array.isArray(iterable)) {
1782
+ throw new LazError('« pou ... dan ... » demande une liste, un intervalle (1..10), un texte ou un dictionnaire', line);
1783
+ }
1784
+ env.declare(varName, null);
1785
+ for (const item of iterable) {
1786
+ this.tick(line);
1787
+ env.vars.set(varName, item);
1788
+ try { await this.execBlock(body, env); }
1789
+ catch (e) {
1790
+ if (e instanceof BreakEx) break;
1791
+ if (e instanceof ContinueEx) continue;
1792
+ throw e;
1793
+ }
1794
+ }
1795
+ return null;
1796
+ }
1797
+ if (kind === 'rend') {
1798
+ const value = stmt[1] !== null ? await this.eval(stmt[1], env) : null;
1799
+ throw new ReturnEx(value);
1800
+ }
1801
+ if (kind === 'kase') throw new BreakEx();
1802
+ if (kind === 'swiv') throw new ContinueEx();
1803
+ if (kind === 'expr') return await this.eval(stmt[1], env);
1804
+
1805
+ throw new LazError(`instruction inconnue : ${kind}`);
1806
+ }
1807
+
1808
+ async eval(node, env) {
1809
+ const kind = node[0];
1810
+ this.tick();
1811
+
1812
+ if (kind === 'num' || kind === 'bool') return node[1];
1813
+ if (kind === 'str') return interpolate(node[1], env);
1814
+ if (kind === 'walu') return null;
1815
+ if (kind === 'var') return env.get(node[1], node[2]);
1816
+ if (kind === 'list') {
1817
+ const out = [];
1818
+ for (const item of node[1]) out.push(await this.eval(item, env));
1819
+ return out;
1820
+ }
1821
+ if (kind === 'dict') {
1822
+ const [, pairs, line] = node;
1823
+ const d = new Map();
1824
+ for (const [knode, vnode] of pairs) {
1825
+ const k = checkDictKey(await this.eval(knode, env), line);
1826
+ d.set(k, await this.eval(vnode, env));
1827
+ }
1828
+ return d;
1829
+ }
1830
+ if (kind === 'attr') {
1831
+ const [, objNode, name, line] = node;
1832
+ const obj = await this.eval(objNode, env);
1833
+ if (obj instanceof LazInstance) {
1834
+ if (obj.fields.has(name)) return obj.fields.get(name);
1835
+ const m = obj.klass.findMethod(name);
1836
+ if (m) return new BoundMethod(m, obj);
1837
+ throw new LazError(`« ${name} » n'existe pas dans cet objet de klas ${obj.klass.name}`, line);
1838
+ }
1839
+ throw new LazError(`le point (.${name}) s'utilise sur un objet créé avec une klas`, line);
1840
+ }
1841
+ if (kind === 'range') {
1842
+ const [, startNode, endNode, line] = node;
1843
+ const start = Math.trunc(checkNumber(await this.eval(startNode, env), line, "l'intervalle .."));
1844
+ const end = Math.trunc(checkNumber(await this.eval(endNode, env), line, "l'intervalle .."));
1845
+ const step = end >= start ? 1 : -1;
1846
+ const out = [];
1847
+ for (let v = start; step > 0 ? v <= end : v >= end; v += step) {
1848
+ out.push(v);
1849
+ if (out.length > 1000000) throw new LazError('intervalle trop grand (plus d\'un million d\'éléments)', line);
1850
+ }
1851
+ return out;
1852
+ }
1853
+ if (kind === 'or') {
1854
+ const left = await this.eval(node[1], env);
1855
+ if (isTruthy(left)) return left;
1856
+ return await this.eval(node[2], env);
1857
+ }
1858
+ if (kind === 'and') {
1859
+ const left = await this.eval(node[1], env);
1860
+ if (!isTruthy(left)) return left;
1861
+ return await this.eval(node[2], env);
1862
+ }
1863
+ if (kind === 'not') return !isTruthy(await this.eval(node[1], env));
1864
+ if (kind === 'neg') {
1865
+ return -checkNumber(await this.eval(node[1], env), node[2], 'le signe -');
1866
+ }
1867
+ if (kind === 'cmp') {
1868
+ const [, op, leftNode, rightNode, line] = node;
1869
+ const left = await this.eval(leftNode, env);
1870
+ const right = await this.eval(rightNode, env);
1871
+ if (op === '==') return lazEquals(left, right);
1872
+ if (op === '!=') return !lazEquals(left, right);
1873
+ if (!(typeof left === 'string' && typeof right === 'string')) {
1874
+ checkNumber(left, line, `la comparaison ${op}`);
1875
+ checkNumber(right, line, `la comparaison ${op}`);
1876
+ }
1877
+ if (op === '<') return left < right;
1878
+ if (op === '>') return left > right;
1879
+ if (op === '<=') return left <= right;
1880
+ if (op === '>=') return left >= right;
1881
+ }
1882
+ if (kind === 'binop') {
1883
+ const [, op, leftNode, rightNode, line] = node;
1884
+ const left = await this.eval(leftNode, env);
1885
+ const right = await this.eval(rightNode, env);
1886
+ if (op === '+') {
1887
+ if (typeof left === 'string' || typeof right === 'string') return toText(left) + toText(right);
1888
+ if (Array.isArray(left) && Array.isArray(right)) return left.concat(right);
1889
+ checkNumber(left, line, "l'addition +");
1890
+ checkNumber(right, line, "l'addition +");
1891
+ return left + right;
1892
+ }
1893
+ checkNumber(left, line, `l'opération ${op}`);
1894
+ checkNumber(right, line, `l'opération ${op}`);
1895
+ if (op === '-') return left - right;
1896
+ if (op === '*') return left * right;
1897
+ if (op === '/') {
1898
+ if (right === 0) throw new LazError('division par zéro impossible', line);
1899
+ return left / right;
1900
+ }
1901
+ if (op === '%') {
1902
+ if (right === 0) throw new LazError('modulo par zéro impossible', line);
1903
+ // modulo « à la Python » : toujours du signe du diviseur
1904
+ return ((left % right) + right) % right;
1905
+ }
1906
+ if (op === '//') {
1907
+ if (right === 0) throw new LazError('division par zéro impossible', line);
1908
+ return Math.floor(left / right);
1909
+ }
1910
+ if (op === '**') {
1911
+ return Math.pow(left, right);
1912
+ }
1913
+ }
1914
+ if (kind === 'index') {
1915
+ const [, targetNode, indexNode, line] = node;
1916
+ const target = await this.eval(targetNode, env);
1917
+ const index = await this.eval(indexNode, env);
1918
+ if (target instanceof Map) {
1919
+ const k = checkDictKey(index, line);
1920
+ if (!target.has(k)) throw new LazError(`la clé « ${toText(k)} » n'existe pas dans le dictionnaire`, line);
1921
+ return target.get(k);
1922
+ }
1923
+ if (!(Array.isArray(target) || typeof target === 'string')) {
1924
+ throw new LazError('on ne peut indexer que les listes, les textes et les dictionnaires', line);
1925
+ }
1926
+ let idx = Math.trunc(checkNumber(index, line, "l'indexation"));
1927
+ if (idx < -target.length || idx >= target.length) {
1928
+ throw new LazError(`position ${idx} hors limites (taille ${target.length})`, line);
1929
+ }
1930
+ if (idx < 0) idx += target.length;
1931
+ return target[idx];
1932
+ }
1933
+ if (kind === 'call') {
1934
+ const [, calleeNode, argNodes, line] = node;
1935
+ const callee = await this.eval(calleeNode, env);
1936
+ const args = [];
1937
+ for (const a of argNodes) args.push(await this.eval(a, env));
1938
+
1939
+ if (callee && callee.builtin) return await callee.fn(args, line);
1940
+
1941
+ if (callee instanceof LazFunction) {
1942
+ if (args.length !== callee.params.length) {
1943
+ throw new LazError(
1944
+ `la fonction « ${callee.name} » attend ${callee.params.length} argument(s), reçu ${args.length}`, line);
1945
+ }
1946
+ return await this.callFunction(callee, args);
1947
+ }
1948
+
1949
+ if (callee instanceof BoundMethod) {
1950
+ const fn = callee.fn;
1951
+ const expected = fn.params.length - 1;
1952
+ if (args.length !== expected) {
1953
+ throw new LazError(`la fonction « ${fn.name} » attend ${expected} argument(s), reçu ${args.length}`, line);
1954
+ }
1955
+ return await this.callFunction(fn, [callee.instance, ...args]);
1956
+ }
1957
+
1958
+ if (callee instanceof LazClass) {
1959
+ const inst = new LazInstance(callee);
1960
+ const init = callee.findMethod('init');
1961
+ if (init) {
1962
+ const expected = init.params.length - 1;
1963
+ if (args.length !== expected) {
1964
+ throw new LazError(`la klas « ${callee.name} » attend ${expected} argument(s) pour init, reçu ${args.length}`, line);
1965
+ }
1966
+ await this.callFunction(init, [inst, ...args]);
1967
+ } else if (args.length) {
1968
+ throw new LazError(`la klas « ${callee.name} » n'a pas de fonction init : on l'appelle sans argument`, line);
1969
+ }
1970
+ return inst;
1971
+ }
1972
+
1973
+ throw new LazError(`« ${toText(callee)} » n'est pas une fonction`, line);
1974
+ }
1975
+ throw new LazError(`expression inconnue : ${kind}`);
1976
+ }
1977
+
1978
+ async callFunction(fn, args) {
1979
+ const callEnv = new Env(fn.env);
1980
+ for (let i = 0; i < args.length; i++) callEnv.declare(fn.params[i], args[i]);
1981
+ try {
1982
+ await this.execBlock(fn.body, callEnv);
1983
+ } catch (e) {
1984
+ if (e instanceof ReturnEx) return e.value;
1985
+ throw e;
1986
+ }
1987
+ return null;
1988
+ }
1989
+ }
1990
+
1991
+ function lazEquals(a, b) {
1992
+ if (Array.isArray(a) && Array.isArray(b)) {
1993
+ if (a.length !== b.length) return false;
1994
+ for (let i = 0; i < a.length; i++) if (!lazEquals(a[i], b[i])) return false;
1995
+ return true;
1996
+ }
1997
+ if (a instanceof Map && b instanceof Map) {
1998
+ if (a.size !== b.size) return false;
1999
+ for (const [k, v] of a.entries()) {
2000
+ if (!b.has(k) || !lazEquals(v, b.get(k))) return false;
2001
+ }
2002
+ return true;
2003
+ }
2004
+ return a === b;
2005
+ }
2006
+
2007
+ function svgDuDessin(t) {
2008
+ const fmt = (v) => numText(v);
2009
+ const W = fmt(t.w), H = fmt(t.h);
2010
+ const out = [`<svg xmlns='http://www.w3.org/2000/svg' width='${W}' height='${H}' viewBox='0 0 ${W} ${H}'>`];
2011
+ out.push(`<rect width='${W}' height='${H}' fill='#0d1117'/>`);
2012
+ for (const f of t.formes) {
2013
+ const k = f[0];
2014
+ if (k === 'fond') {
2015
+ out.push(`<rect width='${W}' height='${H}' fill='${f[1]}'/>`);
2016
+ } else if (k === 'ligne') {
2017
+ out.push(`<line x1='${fmt(f[1])}' y1='${fmt(f[2])}' x2='${fmt(f[3])}' y2='${fmt(f[4])}' stroke='${f[5]}' stroke-width='3' stroke-linecap='round'/>`);
2018
+ } else if (k === 'rect') {
2019
+ const remplir = f[6] ? f[5] : 'none';
2020
+ const contour = f[6] ? '' : ` stroke='${f[5]}' stroke-width='3'`;
2021
+ out.push(`<rect x='${fmt(f[1])}' y='${fmt(f[2])}' width='${fmt(f[3])}' height='${fmt(f[4])}' fill='${remplir}'${contour}/>`);
2022
+ } else if (k === 'cercle') {
2023
+ const remplir = f[5] ? f[4] : 'none';
2024
+ const contour = f[5] ? '' : ` stroke='${f[4]}' stroke-width='3'`;
2025
+ out.push(`<circle cx='${fmt(f[1])}' cy='${fmt(f[2])}' r='${fmt(f[3])}' fill='${remplir}'${contour}/>`);
2026
+ } else if (k === 'texte') {
2027
+ const txt = f[3].replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
2028
+ out.push(`<text x='${fmt(f[1])}' y='${fmt(f[2])}' fill='${f[4]}' font-family='monospace' font-size='16'>${txt}</text>`);
2029
+ }
2030
+ }
2031
+ out.push('</svg>');
2032
+ return out.join('\n');
2033
+ }
2034
+
2035
+ function checkDictKey(k, line) {
2036
+ if (!(typeof k === 'string' || typeof k === 'number')) {
2037
+ throw new LazError("les clés d'un dictionnaire doivent être des textes ou des nombres", line);
2038
+ }
2039
+ return k;
2040
+ }
2041
+
2042
+ // ---------------- API PUBLIQUE ----------------
2043
+ async function run(source, io) {
2044
+ const interp = new Interpreter(io || {
2045
+ onPrint: (t) => console.log(t),
2046
+ onInput: async () => '',
2047
+ shouldStop: () => false,
2048
+ });
2049
+ try {
2050
+ await interp.run(source);
2051
+ // v6/v7 : si chaque_image() a été appelé OU si une interface existe,
2052
+ // le programme reste vivant : boucle d'images + clics de boutons
2053
+ if (interp.frameFn || interp.aInterface) {
2054
+ if (interp.io.onJeuDemarre) interp.io.onJeuDemarre(interp.frameFn ? 'jeu' : 'interface');
2055
+ while (!interp.jeuFini) {
2056
+ if (interp.io.shouldStop && interp.io.shouldStop()) throw new StopEx();
2057
+ interp.steps = 0; // chaque tour repart de zéro (l'appli peut durer des heures)
2058
+ // v7 : les clics de boutons en attente
2059
+ const clics = interp.io.prendClics ? interp.io.prendClics() : [];
2060
+ for (const id of clics) {
2061
+ const action = interp.actionsBoutons.get(id);
2062
+ if (action) await interp.callFunction(action, []);
2063
+ }
2064
+ if (interp.frameFn) await interp.callFunction(interp.frameFn, []);
2065
+ if (interp.io.onFrame) await interp.io.onFrame();
2066
+ else await new Promise(r => setTimeout(r, 33));
2067
+ }
2068
+ if (interp.io.onJeuTermine) interp.io.onJeuTermine();
2069
+ }
2070
+ if (interp.gardeNoms.size && interp.io.onSauveMemoire) {
2071
+ interp.io.onSauveMemoire(interp.collecteGarde());
2072
+ }
2073
+ return { ok: true };
2074
+ } catch (e) {
2075
+ if (interp.gardeNoms.size && interp.io.onSauveMemoire) {
2076
+ try { interp.io.onSauveMemoire(interp.collecteGarde()); } catch (e2) { }
2077
+ }
2078
+ if (e instanceof StopEx) return { ok: false, stopped: true };
2079
+ if (e instanceof LazError) return { ok: false, error: e.toString(), histoire: interp.histoire };
2080
+ if (e instanceof ReturnEx) return { ok: true };
2081
+ if (e instanceof BreakEx || e instanceof ContinueEx) {
2082
+ return { ok: false, error: '✘ Erreur Larkhré : « kase » et « swiv » ne s\'utilisent que dans une boucle' };
2083
+ }
2084
+ if (e instanceof RangeError) {
2085
+ return { ok: false, error: '✘ Erreur Larkhré : récursion trop profonde (boucle infinie ?)' };
2086
+ }
2087
+ return { ok: false, error: '✘ Erreur Larkhré : ' + (e.message || String(e)) };
2088
+ }
2089
+ }
2090
+
2091
+ return { run, toText, LazError, version: '11.1' };
2092
+ })();
2093
+
2094
+ if (typeof module !== 'undefined' && module.exports) module.exports = Larkhre;