naider 1.7.0 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -46,11 +46,17 @@ naide # Start interactive REPL
46
46
  naide repl # Start interactive REPL
47
47
  naide init [dir] # Scaffold a new project
48
48
  naide build [dir] [outdir] # Transpile all files to JavaScript
49
+ naide check <files...> # Type-check without running
49
50
  naide fmt <files...> # Format NAIDE files
50
51
  naide lsp # Start Language Server (LSP)
51
52
  naide vscode # Install VS Code extension
52
53
  naide deploy [dir] # Generate Dockerfile for deployment
54
+ naide convert <files...> # Convert .nx ↔ .naide (bidirectional)
55
+ naide pkg init # Create naide.pkg.json manifest
56
+ naide pkg install <name> # Install a NAIDE package
57
+ naide pkg publish # Publish package to npm
53
58
  naide -w <file> # Watch mode (auto-restart on changes)
59
+ naide -d <file> # Debug mode (Node.js inspector)
54
60
  naide --emit <file> # Print generated JavaScript
55
61
  naide -o <out.js> <file> # Write JavaScript to file
56
62
  naide --mid <file.nx> # Show intermediate NAIDE v1 (debug X mode)
@@ -62,7 +68,7 @@ naide --tokens <file> # Print token stream
62
68
 
63
69
  ```
64
70
  $ naide
65
- NAIDE REPL v1.6.0 — type NAIDE code, see JavaScript output
71
+ NAIDE REPL v1.8.0 — type NAIDE code, see JavaScript output
66
72
  Type .exit to quit, .eval to toggle eval mode
67
73
 
68
74
  >>> str name = "hello"
@@ -72,7 +78,7 @@ const name = "hello";
72
78
  ... ret a + b
73
79
  ...
74
80
  function add(a, b) {
75
- return (a + b);
81
+ return a + b;
76
82
  }
77
83
  ```
78
84
 
@@ -497,6 +503,31 @@ test "math":
497
503
 
498
504
  `assert a == b` generates `assert.strictEqual` for better error messages. Run with `node --test`.
499
505
 
506
+ ### Mock / Spy (Test Utilities)
507
+
508
+ ```python
509
+ fn.async main():
510
+ # Create a mock function
511
+ any mock = createMock()
512
+ mock(1, 2)
513
+ mock("hello")
514
+ log mock.callCount() # 2
515
+ log mock.calledWith(1, 2) # true
516
+
517
+ # Mock with return value
518
+ mock.returns(42)
519
+ log mock() # 42
520
+
521
+ # Spy on an existing method
522
+ any spy = createSpy(obj, "method")
523
+ obj.method("arg")
524
+ log spy.callCount() # 1
525
+ spy.restore() # restores original method
526
+ ```
527
+
528
+ `createMock(fn?)` — create a mock function with `.calls`, `.callCount()`, `.calledWith(...)`, `.returns(val)`, `.impl(fn)`, `.reset()`.
529
+ `createSpy(obj, method)` — wraps an existing method with a mock. `.restore()` reverts it.
530
+
500
531
  ## Job Queue
501
532
 
502
533
  In-memory async job queue for background processing:
@@ -662,6 +693,8 @@ server app port 3000:
662
693
 
663
694
  Schemas with `db.sql` auto-use SQL storage instead of JSON files. Same API: `getAll`, `getById`, `create`, `update`, `delete`, `where`, `count`, `clear`.
664
695
 
696
+ Schema migration is automatic — when you add new fields to a schema, `ALTER TABLE ADD COLUMN` runs at startup. No manual migration needed.
697
+
665
698
  PostgreSQL:
666
699
 
667
700
  ```python
@@ -869,6 +902,110 @@ High-level keywords work in both modes: `schema`, `crud`, `auth`, `cors`, `limit
869
902
 
870
903
  NAIDE-X log shorthands: `log.e` = error, `log.w` = warn, `log.i` = info, `log.d` = debug.
871
904
 
905
+ ## Plugin System
906
+
907
+ Register and use plugins for extensibility:
908
+
909
+ ```python
910
+ registerPlugin("logger", (opts) =>
911
+ ret {log: (msg) => log "[{opts.prefix}] {msg}"}
912
+ )
913
+
914
+ any logger = usePlugin("logger", {prefix: "APP"})
915
+ logger.log("started")
916
+
917
+ list names = listPlugins()
918
+ ```
919
+
920
+ `registerPlugin(name, setup)` — registers a plugin factory. `usePlugin(name, opts?)` — initializes on first call, returns cached exports. `listPlugins()` — returns registered plugin names.
921
+
922
+ ## Convert (NX ↔ NAIDE)
923
+
924
+ Bidirectional conversion between `.nx` and `.naide`:
925
+
926
+ ```bash
927
+ naide convert file.nx # → file.naide (expand to readable syntax)
928
+ naide convert file.naide # → file.nx (compress to NX syntax)
929
+ ```
930
+
931
+ ## Source Maps & Error Remapping
932
+
933
+ Runtime errors are automatically remapped to source file line numbers:
934
+
935
+ ```
936
+ Error in app.naide:12
937
+ 10 | user = UserStore.getById(id)
938
+ 11 | if not user:
939
+ >>12 | throw "not found"
940
+ ```
941
+
942
+ The CLI tracks source-to-output line mappings and shows context from your `.naide`/`.nx` file, not the generated JavaScript.
943
+
944
+ ## Native Dependency Detection
945
+
946
+ When you run a `.naide` file, the CLI scans generated JavaScript for missing dependencies (`express`, `better-sqlite3`, `pg`, `ws`) and prints install hints:
947
+
948
+ ```
949
+ [NAIDE] Missing: express — run: npm install express
950
+ ```
951
+
952
+ ## Type Checker
953
+
954
+ Compile-time type checking without running the code:
955
+
956
+ ```bash
957
+ naide check app.naide
958
+ ```
959
+
960
+ Catches type mismatches at compile time:
961
+
962
+ ```
963
+ app.naide:3 ERROR: Type mismatch: cannot assign str to int
964
+ app.naide:7 WARN: Type warning: reassigning int variable 'count' with str
965
+ ```
966
+
967
+ The type checker understands NAIDE's type annotations (`str`, `int`, `num`, `bool`, `list`, `map`), infers types from expressions and function return values, and checks assignments for compatibility. `num` accepts `int` values. `any` and `json` accept all types.
968
+
969
+ Also available as a flag: `naide --check app.naide` or programmatically via `compile(source, { typeCheck: true })`.
970
+
971
+ ## Async Error Handling
972
+
973
+ Async route handlers are automatically wrapped with try/catch to prevent unhandled rejections:
974
+
975
+ ```python
976
+ server app port 3000:
977
+ post "/api/data" (req, res):
978
+ any data = await fetchData() # if this throws...
979
+ ret data # ...a 500 JSON error is returned automatically
980
+ ```
981
+
982
+ Generated code includes `try { ... } catch (__err) { res.status(500).json({ error: __err.message }) }` around async handlers. Routes with explicit `try/fail` blocks are left as-is.
983
+
984
+ A global `process.on('unhandledRejection')` handler is also added to server code to catch any remaining async errors.
985
+
986
+ ## Debugger
987
+
988
+ Debug NAIDE programs with the Node.js inspector:
989
+
990
+ ```bash
991
+ naide -d app.naide # starts with --inspect-brk
992
+ ```
993
+
994
+ Then open `chrome://inspect` in Chrome to connect. The program pauses at the first line so you can set breakpoints before execution.
995
+
996
+ ## Package Ecosystem
997
+
998
+ Manage NAIDE packages via npm:
999
+
1000
+ ```bash
1001
+ naide pkg init # create naide.pkg.json manifest
1002
+ naide pkg install my-plugin # install from npm + add to manifest
1003
+ naide pkg publish # publish to npm with naide-plugin keyword
1004
+ naide pkg list # list installed NAIDE packages
1005
+ ```
1006
+
1007
+ The `naide.pkg.json` manifest tracks NAIDE-specific metadata (main entry, exports, dependencies) while using npm as the underlying registry.
1008
+
872
1009
  ## Why NAIDE?
873
1010
 
874
1011
  AI code generation speed depends on:
package/bin/naide.js CHANGED
@@ -5,6 +5,63 @@ import { resolve, basename, extname, join, relative } from 'path';
5
5
  import { compile } from '../src/index.js';
6
6
  import { spawn } from 'child_process';
7
7
 
8
+ import { createRequire } from 'module';
9
+ const _require = createRequire(import.meta.url);
10
+
11
+ function checkDependencies(jsCode) {
12
+ const depMap = {
13
+ "from 'express'": { pkg: 'express', reason: 'server' },
14
+ "from 'better-sqlite3'": { pkg: 'better-sqlite3', reason: 'db.sql "sqlite"' },
15
+ "from 'pg'": { pkg: 'pg', reason: 'db.sql "postgres"' },
16
+ "from 'ws'": { pkg: 'ws', reason: 'WebSocket (ws)' },
17
+ };
18
+ const missing = [];
19
+ for (const [pattern, info] of Object.entries(depMap)) {
20
+ if (jsCode.includes(pattern)) {
21
+ try { _require.resolve(info.pkg); } catch {
22
+ missing.push(info);
23
+ }
24
+ }
25
+ }
26
+ if (missing.length > 0) {
27
+ console.log('\n [NAIDE] Missing dependencies detected:\n');
28
+ for (const m of missing) {
29
+ console.log(` npm install ${m.pkg} # required for: ${m.reason}`);
30
+ }
31
+ console.log(`\n Run: npm install ${missing.map(m => m.pkg).join(' ')}\n`);
32
+ }
33
+ }
34
+
35
+ function remapError(err, sourceMap, sourceFile, sourceCode) {
36
+ if (!err.stack || !sourceMap || sourceMap.length === 0) return;
37
+ const lines = sourceCode.split('\n');
38
+ const tempPattern = /\.naide_tmp_[^:]+\.mjs:(\d+)/g;
39
+ let match;
40
+ const remapped = [];
41
+ while ((match = tempPattern.exec(err.stack)) !== null) {
42
+ const jsLine = parseInt(match[1]) - 1;
43
+ const srcLine = sourceMap[jsLine];
44
+ if (srcLine && srcLine > 0) {
45
+ remapped.push(srcLine);
46
+ }
47
+ }
48
+ if (remapped.length > 0) {
49
+ const srcLine = remapped[0];
50
+ const context = [];
51
+ const start = Math.max(0, srcLine - 3);
52
+ const end = Math.min(lines.length, srcLine + 2);
53
+ for (let i = start; i < end; i++) {
54
+ const marker = i + 1 === srcLine ? ' >> ' : ' ';
55
+ context.push(`${marker}${i + 1} | ${lines[i]}`);
56
+ }
57
+ console.error(`\n [NAIDE Error] ${sourceFile}:${srcLine}\n`);
58
+ console.error(context.join('\n'));
59
+ console.error(`\n ${err.message}\n`);
60
+ return true;
61
+ }
62
+ return false;
63
+ }
64
+
8
65
  const args = process.argv.slice(2);
9
66
 
10
67
  const flags = {
@@ -17,6 +74,8 @@ const flags = {
17
74
  mode: null,
18
75
  mid: false,
19
76
  watch: false,
77
+ debug: false,
78
+ check: false,
20
79
  };
21
80
 
22
81
  const files = [];
@@ -32,6 +91,8 @@ for (let i = 0; i < args.length; i++) {
32
91
  case '--x': case '-x': flags.mode = 'x'; break;
33
92
  case '--mid': flags.mid = true; flags.run = false; break;
34
93
  case '--watch': case '-w': flags.watch = true; break;
94
+ case '--debug': case '-d': flags.debug = true; break;
95
+ case '--check': flags.check = true; flags.run = false; break;
35
96
  default: files.push(arg);
36
97
  }
37
98
  }
@@ -90,7 +151,7 @@ if (files[0] === 'repl' || (files.length === 0 && !flags.help)) {
90
151
  const { createInterface } = await import('readline');
91
152
  const { transpile } = await import('../src/index.js');
92
153
 
93
- console.log(`\n NAIDE REPL v1.6.0 — type NAIDE code, see JavaScript output`);
154
+ console.log(`\n NAIDE REPL v1.8.0 — type NAIDE code, see JavaScript output`);
94
155
  console.log(` Type .exit to quit, .eval to toggle eval mode\n`);
95
156
 
96
157
  const rl = createInterface({
@@ -229,7 +290,7 @@ if (files[0] === 'init') {
229
290
  writeFileSync(resolve(dir, 'package.json'), JSON.stringify({
230
291
  name, version: '1.0.0', type: 'module',
231
292
  scripts: { start: 'naide app.naide', dev: 'naide -w app.naide', build: 'naide --emit app.naide -o dist/app.mjs' },
232
- dependencies: { naider: '^1.7.0' }
293
+ dependencies: { naider: '^1.8.0' }
233
294
  }, null, 2) + '\n');
234
295
  }
235
296
 
@@ -315,6 +376,232 @@ if (files[0] === 'build') {
315
376
  process.exit(errors > 0 ? 1 : 0);
316
377
  }
317
378
 
379
+ // ── Convert (NX ↔ NAIDE) ──
380
+ if (files[0] === 'convert') {
381
+ const targets = files.slice(1);
382
+ if (targets.length === 0) {
383
+ console.log(' Usage: naide convert <file.nx|file.naide>');
384
+ console.log(' .nx → .naide Expand NX shorthand to readable NAIDE');
385
+ console.log(' .naide → .nx Compress NAIDE to AI-optimized NX');
386
+ process.exit(0);
387
+ }
388
+
389
+ for (const file of targets) {
390
+ const filePath = resolve(file);
391
+ const ext = extname(file);
392
+ try {
393
+ const source = readFileSync(filePath, 'utf-8');
394
+
395
+ if (ext === '.nx') {
396
+ const { preprocess } = await import('../src/preprocess.js');
397
+ const naide = preprocess(source);
398
+ const outPath = filePath.replace(/\.nx$/, '.naide');
399
+ writeFileSync(outPath, naide);
400
+ console.log(` ${file} → ${basename(outPath)}`);
401
+ } else if (ext === '.naide') {
402
+ const lines = source.split('\n');
403
+ const nxLines = [];
404
+ for (const line of lines) {
405
+ let nx = line;
406
+ nx = nx.replace(/^(\s*)#\s?(.*)/, '$1-- $2');
407
+ nx = nx.replace(/^(\s*)fn\.async\s+/, '$1~f ');
408
+ nx = nx.replace(/^(\s*)fn\s+/, '$1f ');
409
+ nx = nx.replace(/^(\s*)use\s+\{(.+?)\}\s+from\s+/, '$1<{$2}');
410
+ nx = nx.replace(/^(\s*)use\s+/, '$1<');
411
+ nx = nx.replace(/\bret\b/, '>');
412
+ nx = nx.replace(/\beach\s+(\w+)\s+in\s+/, '@$1<');
413
+ nx = nx.replace(/\bawait\s+/, '~');
414
+ nx = nx.replace(/\bstr\b/g, 's');
415
+ nx = nx.replace(/\bint\b/g, 'i');
416
+ nx = nx.replace(/\bnum\b/g, 'n');
417
+ nx = nx.replace(/\bbool\b/g, 'b');
418
+ nx = nx.replace(/\blist\b/g, 'l');
419
+ nx = nx.replace(/\bmap\b/g, 'm');
420
+ nx = nx.replace(/\bany\b/g, 'a');
421
+ nx = nx.replace(/\bconsole\.log\b/, 'log');
422
+ nxLines.push(nx);
423
+ }
424
+ const outPath = filePath.replace(/\.naide$/, '.nx');
425
+ writeFileSync(outPath, nxLines.join('\n'));
426
+ console.log(` ${file} → ${basename(outPath)}`);
427
+ } else {
428
+ console.error(` Unsupported: ${file} (use .naide or .nx)`);
429
+ }
430
+ } catch (e) {
431
+ console.error(` Error: ${file} — ${e.message}`);
432
+ }
433
+ }
434
+ process.exit(0);
435
+ }
436
+
437
+ // ── Type Check ──
438
+ if (files[0] === 'check') {
439
+ const targets = files.slice(1);
440
+ if (targets.length === 0) {
441
+ console.log(' Usage: naide check <file.naide|file.nx> [files...]');
442
+ process.exit(0);
443
+ }
444
+
445
+ let totalErrors = 0;
446
+ let totalWarnings = 0;
447
+ for (const file of targets) {
448
+ const filePath = resolve(file);
449
+ try {
450
+ const source = readFileSync(filePath, 'utf-8');
451
+ const ext = extname(file);
452
+ const mode = ext === '.nx' ? 'x' : 'naide';
453
+ const result = compile(source, { mode, typeCheck: true });
454
+ const { typeErrors } = result;
455
+
456
+ if (typeErrors) {
457
+ for (const e of typeErrors.errors) {
458
+ console.log(` ${file}:${e.line} ERROR: ${e.message}`);
459
+ totalErrors++;
460
+ }
461
+ for (const w of typeErrors.warnings) {
462
+ console.log(` ${file}:${w.line} WARN: ${w.message}`);
463
+ totalWarnings++;
464
+ }
465
+ }
466
+
467
+ if (!typeErrors || (typeErrors.errors.length === 0 && typeErrors.warnings.length === 0)) {
468
+ console.log(` ${file}: OK`);
469
+ }
470
+ } catch (e) {
471
+ console.error(` ${file}: ${e.message.split('\n')[0]}`);
472
+ totalErrors++;
473
+ }
474
+ }
475
+
476
+ console.log(`\n ${totalErrors} error(s), ${totalWarnings} warning(s)`);
477
+ process.exit(totalErrors > 0 ? 1 : 0);
478
+ }
479
+
480
+ // ── Package Ecosystem ──
481
+ if (files[0] === 'pkg') {
482
+ const subcmd = files[1];
483
+
484
+ if (!subcmd || subcmd === 'help') {
485
+ console.log(`
486
+ NAIDE Package Manager
487
+
488
+ Usage:
489
+ naide pkg init Create naide.pkg.json manifest
490
+ naide pkg install <name> Install a NAIDE package from npm
491
+ naide pkg publish Publish current package to npm
492
+ naide pkg list List installed NAIDE packages
493
+ `);
494
+ process.exit(0);
495
+ }
496
+
497
+ const pkgManifestPath = resolve('naide.pkg.json');
498
+
499
+ if (subcmd === 'init') {
500
+ if (existsSync(pkgManifestPath)) {
501
+ console.log(' naide.pkg.json already exists.');
502
+ process.exit(0);
503
+ }
504
+ const npmPkgPath = resolve('package.json');
505
+ let name = 'my-naide-pkg';
506
+ if (existsSync(npmPkgPath)) {
507
+ try { name = JSON.parse(readFileSync(npmPkgPath, 'utf-8')).name || name; } catch {}
508
+ }
509
+ const manifest = {
510
+ name,
511
+ version: '1.0.0',
512
+ description: '',
513
+ main: 'index.naide',
514
+ keywords: ['naide', 'naide-plugin'],
515
+ exports: {},
516
+ dependencies: {},
517
+ };
518
+ writeFileSync(pkgManifestPath, JSON.stringify(manifest, null, 2) + '\n');
519
+ console.log(`\n Created naide.pkg.json\n`);
520
+ process.exit(0);
521
+ }
522
+
523
+ if (subcmd === 'install') {
524
+ const pkgName = files[2];
525
+ if (!pkgName) {
526
+ console.log(' Usage: naide pkg install <package-name>');
527
+ process.exit(1);
528
+ }
529
+
530
+ console.log(` Installing ${pkgName}...`);
531
+ const child = spawn('npm', ['install', pkgName], { stdio: 'inherit', shell: true });
532
+ child.on('close', (code) => {
533
+ if (code === 0) {
534
+ if (existsSync(pkgManifestPath)) {
535
+ try {
536
+ const manifest = JSON.parse(readFileSync(pkgManifestPath, 'utf-8'));
537
+ const npmPkg = resolve('node_modules', pkgName, 'package.json');
538
+ if (existsSync(npmPkg)) {
539
+ const ver = JSON.parse(readFileSync(npmPkg, 'utf-8')).version;
540
+ manifest.dependencies[pkgName] = `^${ver}`;
541
+ writeFileSync(pkgManifestPath, JSON.stringify(manifest, null, 2) + '\n');
542
+ }
543
+ } catch {}
544
+ }
545
+ console.log(`\n Installed ${pkgName}`);
546
+ }
547
+ process.exit(code);
548
+ });
549
+ await new Promise(() => {});
550
+ }
551
+
552
+ if (subcmd === 'publish') {
553
+ if (!existsSync(pkgManifestPath)) {
554
+ console.log(' No naide.pkg.json found. Run: naide pkg init');
555
+ process.exit(1);
556
+ }
557
+
558
+ const manifest = JSON.parse(readFileSync(pkgManifestPath, 'utf-8'));
559
+ const npmPkgPath = resolve('package.json');
560
+ if (!existsSync(npmPkgPath)) {
561
+ writeFileSync(npmPkgPath, JSON.stringify({
562
+ name: manifest.name,
563
+ version: manifest.version,
564
+ description: manifest.description,
565
+ type: 'module',
566
+ main: manifest.main,
567
+ keywords: manifest.keywords,
568
+ files: ['*.naide', '*.nx', 'src/', 'naide.pkg.json'],
569
+ }, null, 2) + '\n');
570
+ }
571
+
572
+ console.log(` Publishing ${manifest.name}@${manifest.version}...`);
573
+ const child = spawn('npm', ['publish', '--access', 'public'], { stdio: 'inherit', shell: true });
574
+ child.on('close', (code) => {
575
+ if (code === 0) console.log(`\n Published ${manifest.name}@${manifest.version}`);
576
+ process.exit(code);
577
+ });
578
+ await new Promise(() => {});
579
+ }
580
+
581
+ if (subcmd === 'list') {
582
+ if (!existsSync(pkgManifestPath)) {
583
+ console.log(' No naide.pkg.json found.');
584
+ process.exit(0);
585
+ }
586
+ const manifest = JSON.parse(readFileSync(pkgManifestPath, 'utf-8'));
587
+ const deps = Object.entries(manifest.dependencies || {});
588
+ if (deps.length === 0) {
589
+ console.log(' No NAIDE packages installed.');
590
+ } else {
591
+ console.log('\n NAIDE packages:\n');
592
+ for (const [name, ver] of deps) {
593
+ console.log(` ${name} ${ver}`);
594
+ }
595
+ console.log('');
596
+ }
597
+ process.exit(0);
598
+ }
599
+
600
+ console.log(` Unknown subcommand: naide pkg ${subcmd}`);
601
+ console.log(' Run: naide pkg help');
602
+ process.exit(1);
603
+ }
604
+
318
605
  // ── Deploy ──
319
606
  if (files[0] === 'deploy') {
320
607
  const dir = resolve(files[1] || '.');
@@ -377,15 +664,22 @@ if (flags.help) {
377
664
  naide <file.nx> Run a NAIDE-X file (auto-detected)
378
665
  naide init [dir] Create a new NAIDE project
379
666
  naide build [dir] [outdir] Transpile all files to JavaScript
667
+ naide check <files...> Type-check without running
380
668
  naide repl Start interactive REPL
381
669
  naide lsp Start language server (LSP)
382
670
  naide vscode Install VS Code extension
383
671
  naide deploy [dir] Generate Dockerfile for deployment
672
+ naide convert <files...> Convert between .naide and .nx formats
384
673
  naide fmt <files...> Format NAIDE files
674
+ naide pkg init Create naide.pkg.json manifest
675
+ naide pkg install <name> Install a NAIDE package
676
+ naide pkg publish Publish package to npm
677
+ naide pkg list List NAIDE dependencies
385
678
  naide --emit <file> Output generated JavaScript
386
679
  naide --mid <file.nx> Output intermediate NAIDE v1 (debug)
387
680
  naide -x <file.naide> Force NAIDE-X mode
388
681
  naide -w <file.naide> Watch mode (auto-restart on changes)
682
+ naide -d <file> Debug mode (Node.js inspector)
389
683
 
390
684
  Modes:
391
685
  .naide Standard NAIDE (~40% fewer tokens than JS)
@@ -396,6 +690,8 @@ if (flags.help) {
396
690
  -o, --output Write generated JavaScript to file
397
691
  -x Force NAIDE-X mode
398
692
  -w, --watch Watch mode: restart on file changes
693
+ -d, --debug Start with Node.js debugger (--inspect-brk)
694
+ --check Type-check files without running
399
695
  --mid Show intermediate NAIDE v1 (X mode only)
400
696
  --ast Print AST
401
697
  --tokens Print tokens
@@ -476,7 +772,17 @@ for (const file of files) {
476
772
  continue;
477
773
  }
478
774
 
479
- const result = compile(source, { mode, runtimePath });
775
+ const result = compile(source, { mode, runtimePath, sourceFile: file, typeCheck: flags.check });
776
+
777
+ if (flags.check) {
778
+ const { typeErrors } = result;
779
+ if (typeErrors) {
780
+ for (const e of typeErrors.errors) console.log(` ${file}:${e.line} ERROR: ${e.message}`);
781
+ for (const w of typeErrors.warnings) console.log(` ${file}:${w.line} WARN: ${w.message}`);
782
+ if (typeErrors.errors.length === 0 && typeErrors.warnings.length === 0) console.log(` ${file}: OK`);
783
+ }
784
+ continue;
785
+ }
480
786
 
481
787
  if (flags.tokens) {
482
788
  console.log(JSON.stringify(result.tokens, null, 2));
@@ -503,8 +809,29 @@ for (const file of files) {
503
809
  const tempFile = resolve(`.naide_tmp_${basename(file, ext)}.mjs`);
504
810
  writeFileSync(tempFile, result.js, 'utf-8');
505
811
 
812
+ if (flags.debug) {
813
+ console.log(`\n [NAIDE] Debugger starting — ${file}`);
814
+ console.log(' Open Chrome → chrome://inspect to connect\n');
815
+ const debugChild = spawn(process.execPath, ['--inspect-brk', tempFile], { stdio: 'inherit' });
816
+ debugChild.on('close', (code) => {
817
+ try { unlinkSync(tempFile); } catch {}
818
+ process.exit(code || 0);
819
+ });
820
+ process.on('SIGINT', () => {
821
+ debugChild.kill();
822
+ try { unlinkSync(tempFile); } catch {}
823
+ process.exit(0);
824
+ });
825
+ await new Promise(() => {});
826
+ }
827
+
506
828
  try {
507
829
  await import('file:///' + tempFile.replace(/\\/g, '/'));
830
+ } catch (runErr) {
831
+ if (!remapError(runErr, result.sourceMap, file, source)) {
832
+ console.error(`\n${runErr.message}`);
833
+ }
834
+ if (process.env.NAIDE_DEBUG) console.error(runErr.stack);
508
835
  } finally {
509
836
  try {
510
837
  const { unlinkSync } = await import('fs');
package/lsp/server.js CHANGED
@@ -89,7 +89,38 @@ function validateDocument(uri) {
89
89
  const diagnostics = [];
90
90
 
91
91
  try {
92
- compile(text, { mode });
92
+ const result = compile(text, { mode });
93
+
94
+ const lines = text.split('\n');
95
+ for (let i = 0; i < lines.length; i++) {
96
+ const line = lines[i].trim();
97
+ if (!line || line.startsWith('#') || line.startsWith('--')) continue;
98
+
99
+ const typeMatch = line.match(/^(str|int|num|bool)\s+\w+\s*=\s*(.+)/);
100
+ if (typeMatch) {
101
+ const declType = typeMatch[1];
102
+ const val = typeMatch[2].trim();
103
+ if (declType === 'int' && /^["']/.test(val)) {
104
+ diagnostics.push({
105
+ range: { start: { line: i, character: 0 }, end: { line: i, character: lines[i].length } },
106
+ severity: 2, source: 'naide',
107
+ message: `Type hint: assigning string to int variable`
108
+ });
109
+ } else if (declType === 'str' && /^\d+$/.test(val)) {
110
+ diagnostics.push({
111
+ range: { start: { line: i, character: 0 }, end: { line: i, character: lines[i].length } },
112
+ severity: 2, source: 'naide',
113
+ message: `Type hint: assigning number to str variable`
114
+ });
115
+ } else if (declType === 'bool' && !['true', 'false'].includes(val) && !/\b(not|and|or|==|!=|>|<)\b/.test(val)) {
116
+ diagnostics.push({
117
+ range: { start: { line: i, character: 0 }, end: { line: i, character: lines[i].length } },
118
+ severity: 2, source: 'naide',
119
+ message: `Type hint: value may not be boolean`
120
+ });
121
+ }
122
+ }
123
+ }
93
124
  } catch (e) {
94
125
  const lineMatch = e.message.match(/line (\d+):(\d+)/);
95
126
  const line = lineMatch ? parseInt(lineMatch[1]) - 1 : 0;
@@ -131,6 +162,10 @@ function getCompletions() {
131
162
  { label: 'api.get(url)', detail: 'HTTP GET', insertText: 'api.get(' },
132
163
  { label: 'api.post(url, body)', detail: 'HTTP POST', insertText: 'api.post(' },
133
164
  { label: 'prompt', detail: 'Reusable prompt template', insertText: 'prompt ' },
165
+ { label: 'createMock(fn)', detail: 'Create mock function', insertText: 'createMock(' },
166
+ { label: 'createSpy(obj, method)', detail: 'Spy on method', insertText: 'createSpy(' },
167
+ { label: 'registerPlugin(name, setup)', detail: 'Register plugin', insertText: 'registerPlugin(' },
168
+ { label: 'usePlugin(name)', detail: 'Use registered plugin', insertText: 'usePlugin(' },
134
169
  ];
135
170
 
136
171
  return [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "naider",
3
- "version": "1.7.0",
3
+ "version": "1.8.0",
4
4
  "description": "NAIDE - Node AI Development Environment. AI-specialized language with built-in server, auth, DB, SQL, AI/LLM, WebSocket, and more — transpiles to Node.js.",
5
5
  "main": "src/index.js",
6
6
  "exports": {
package/src/generator.js CHANGED
@@ -16,6 +16,9 @@ export class Generator {
16
16
  this.hasAsserts = false;
17
17
  this.dbSql = false;
18
18
  this.sqlDriver = null;
19
+ this.sourceMap = [];
20
+ this.currentSourceLine = 0;
21
+ this.sourceFile = options.sourceFile || null;
19
22
  }
20
23
 
21
24
  generate(ast) {
@@ -46,6 +49,11 @@ export class Generator {
46
49
  preamble.push('');
47
50
  }
48
51
 
52
+ if (this.usesExpress) {
53
+ this.output.push('');
54
+ this.output.push("process.on('unhandledRejection', (err) => { console.error('[NAIDE] Unhandled async error:', err.message || err); });");
55
+ }
56
+
49
57
  if (preamble.length > 0) {
50
58
  this.output.unshift(...preamble);
51
59
  }
@@ -55,10 +63,12 @@ export class Generator {
55
63
 
56
64
  emit(line) {
57
65
  this.output.push(' '.repeat(this.indent) + line);
66
+ this.sourceMap.push(this.currentSourceLine);
58
67
  }
59
68
 
60
69
  emitRaw(line) {
61
70
  this.output.push(line);
71
+ this.sourceMap.push(this.currentSourceLine);
62
72
  }
63
73
 
64
74
  visitProgram(node) {
@@ -67,7 +77,15 @@ export class Generator {
67
77
  }
68
78
  }
69
79
 
80
+ normalizeRoutePath(pathStr) {
81
+ return pathStr.replace(/(["'])(.+?)\1/g, (m, q, p) => {
82
+ const normalized = p.replace(/(?<!\()(?<!\.\*)\*(?!\))/g, '(.*)');
83
+ return q + normalized + q;
84
+ });
85
+ }
86
+
70
87
  visitStatement(node) {
88
+ if (node._line) this.currentSourceLine = node._line;
71
89
  switch (node.type) {
72
90
  case 'Use': return this.visitUse(node);
73
91
  case 'UseDestructured': return this.visitUseDestructured(node);
@@ -321,6 +339,7 @@ export class Generator {
321
339
  }
322
340
 
323
341
  visitServer(node) {
342
+ this.usesExpress = true;
324
343
  const hasWs = node.routes.some(r => r.type === 'WsDecl');
325
344
 
326
345
  this.emit(`import express from 'express';`);
@@ -423,6 +442,12 @@ export class Generator {
423
442
  this.indent++;
424
443
 
425
444
  const hasRes = params.includes('res');
445
+ const hasTryCatch = route.body.some(s => s.type === 'Try');
446
+
447
+ if (needsAsync && !hasTryCatch) {
448
+ this.emit('try {');
449
+ this.indent++;
450
+ }
426
451
 
427
452
  for (let i = 0; i < route.body.length; i++) {
428
453
  const stmt = route.body[i];
@@ -437,6 +462,15 @@ export class Generator {
437
462
  }
438
463
  }
439
464
 
465
+ if (needsAsync && !hasTryCatch) {
466
+ this.indent--;
467
+ this.emit('} catch (__err) {');
468
+ this.indent++;
469
+ this.emit('if (!res.headersSent) res.status(500).json({ error: __err.message });');
470
+ this.indent--;
471
+ this.emit('}');
472
+ }
473
+
440
474
  this.indent--;
441
475
  this.emit('});');
442
476
  this.emitRaw('');
@@ -679,7 +713,7 @@ export class Generator {
679
713
  const optStr = options.length > 0 ? `, { ${options.join(', ')} }` : '';
680
714
 
681
715
  if (node.protectedPaths.length > 0) {
682
- const path = this.stringValue(node.protectedPaths[0]);
716
+ const path = this.normalizeRoutePath(this.stringValue(node.protectedPaths[0]));
683
717
  this.emit(`${appName}.use(${path}, jwtAuth(${secret}${optStr}));`);
684
718
  } else {
685
719
  this.emit(`${appName}.use(jwtAuth(${secret}${optStr}));`);
@@ -710,7 +744,7 @@ export class Generator {
710
744
  visitLimit(appName, node) {
711
745
  this.runtimeImports.add('rateLimit');
712
746
 
713
- const path = this.stringValue(node.path);
747
+ const path = this.normalizeRoutePath(this.stringValue(node.path));
714
748
  const max = this.expr(node.max);
715
749
  const window = this.expr(node.window);
716
750
  this.emit(`${appName}.use(${path}, rateLimit(${max}, ${window}));`);
@@ -901,7 +935,7 @@ export class Generator {
901
935
 
902
936
  visitCache(appName, node) {
903
937
  this.runtimeImports.add('cacheMiddleware');
904
- const path = this.stringValue(node.path);
938
+ const path = this.normalizeRoutePath(this.stringValue(node.path));
905
939
  const duration = this.expr(node.duration);
906
940
  this.emit(`${appName}.use(${path}, cacheMiddleware(${duration}));`);
907
941
  this.emitRaw('');
@@ -940,7 +974,7 @@ export class Generator {
940
974
 
941
975
  visitValidate(appName, node) {
942
976
  this.runtimeImports.add('validateMiddleware');
943
- const path = this.stringValue(node.path);
977
+ const path = this.normalizeRoutePath(this.stringValue(node.path));
944
978
  this.emit(`${appName}.use(${path}, validateMiddleware(${node.schemaName}Schema));`);
945
979
  this.emitRaw('');
946
980
  }
@@ -1092,8 +1126,12 @@ export class Generator {
1092
1126
  case 'Self': return 'this';
1093
1127
  case 'Identifier': return node.name;
1094
1128
 
1095
- case 'Binary':
1096
- return `(${this.expr(node.left)} ${node.op} ${this.expr(node.right)})`;
1129
+ case 'Binary': {
1130
+ const l = this.expr(node.left);
1131
+ const r = this.expr(node.right);
1132
+ const simple = node.left.type !== 'Binary' && node.right.type !== 'Binary';
1133
+ return simple ? `${l} ${node.op} ${r}` : `(${l} ${node.op} ${r})`;
1134
+ }
1097
1135
 
1098
1136
  case 'Unary':
1099
1137
  return `${node.op}${this.expr(node.expr)}`;
@@ -1174,7 +1212,9 @@ export class Generator {
1174
1212
  }
1175
1213
 
1176
1214
  generateCall(node) {
1177
- const AUTO_IMPORT = { 'hash': 'hash', 'verify': 'verify', 'uuid': 'uuid' };
1215
+ const AUTO_IMPORT = { 'hash': 'hash', 'verify': 'verify', 'uuid': 'uuid',
1216
+ 'createMock': 'createMock', 'createSpy': 'createSpy',
1217
+ 'registerPlugin': 'registerPlugin', 'usePlugin': 'usePlugin' };
1178
1218
 
1179
1219
  if (node.callee.type === 'Identifier' && AUTO_IMPORT[node.callee.name]) {
1180
1220
  const runtimeFn = AUTO_IMPORT[node.callee.name];
package/src/index.js CHANGED
@@ -2,8 +2,9 @@ import { Lexer } from './lexer.js';
2
2
  import { Parser } from './parser.js';
3
3
  import { Generator } from './generator.js';
4
4
  import { preprocess } from './preprocess.js';
5
+ import { TypeChecker } from './typechecker.js';
5
6
 
6
- export function compile(source, { mode = 'naide', runtimePath } = {}) {
7
+ export function compile(source, { mode = 'naide', runtimePath, sourceFile, typeCheck = false } = {}) {
7
8
  let processedSource = source;
8
9
  if (mode === 'x') {
9
10
  processedSource = preprocess(source);
@@ -13,9 +14,16 @@ export function compile(source, { mode = 'naide', runtimePath } = {}) {
13
14
  const tokens = lexer.tokenize();
14
15
  const parser = new Parser(tokens);
15
16
  const ast = parser.parse();
16
- const generator = new Generator({ runtimePath });
17
+
18
+ let typeErrors = null;
19
+ if (typeCheck) {
20
+ const checker = new TypeChecker();
21
+ typeErrors = checker.check(ast);
22
+ }
23
+
24
+ const generator = new Generator({ runtimePath, sourceFile });
17
25
  const js = generator.generate(ast);
18
- return { js, ast, tokens, naide: mode === 'x' ? processedSource : null };
26
+ return { js, ast, tokens, sourceMap: generator.sourceMap, naide: mode === 'x' ? processedSource : null, typeErrors };
19
27
  } catch (e) {
20
28
  const lineMatch = e.message.match(/line (\d+)/);
21
29
  if (lineMatch) {
package/src/lexer.js CHANGED
@@ -18,6 +18,8 @@ export class Lexer {
18
18
  this.tokens = [];
19
19
  this.indentStack = [0];
20
20
  this.atLineStart = true;
21
+ this.indentUnit = 0;
22
+ this.tabSize = 4;
21
23
  }
22
24
 
23
25
  peek() {
@@ -125,7 +127,7 @@ export class Lexer {
125
127
  this.pos++;
126
128
  this.col++;
127
129
  } else if (ch === '\t') {
128
- indent += 2;
130
+ indent += this.tabSize;
129
131
  this.pos++;
130
132
  this.col++;
131
133
  } else {
@@ -133,7 +135,6 @@ export class Lexer {
133
135
  }
134
136
  }
135
137
 
136
- // Skip blank lines and comment-only lines
137
138
  if (this.pos >= this.source.length || this.source[this.pos] === '\n' || this.source[this.pos] === '#') {
138
139
  return;
139
140
  }
@@ -141,6 +142,10 @@ export class Lexer {
141
142
  const currentIndent = this.indentStack[this.indentStack.length - 1];
142
143
 
143
144
  if (indent > currentIndent) {
145
+ if (!this.indentUnit) {
146
+ this.indentUnit = indent - currentIndent;
147
+ this.tabSize = this.indentUnit;
148
+ }
144
149
  this.indentStack.push(indent);
145
150
  this.tokens.push(this.makeToken(T.INDENT, indent));
146
151
  } else if (indent < currentIndent) {
package/src/parser.js CHANGED
@@ -1,8 +1,9 @@
1
1
  import { T, TYPE_TOKENS } from './tokens.js';
2
2
 
3
3
  class ASTNode {
4
- constructor(type, props = {}) {
4
+ constructor(type, props = {}, line = 0) {
5
5
  this.type = type;
6
+ this._line = line;
6
7
  Object.assign(this, props);
7
8
  }
8
9
  }
@@ -119,6 +120,10 @@ export class Parser {
119
120
  throw this.error(`Expected property name but got ${tok.type} ('${tok.value}')`, tok);
120
121
  }
121
122
 
123
+ node(type, props = {}) {
124
+ return new ASTNode(type, props, this.peek().line);
125
+ }
126
+
122
127
  error(msg, tok) {
123
128
  const t = tok || this.peek();
124
129
  return new Error(`[NAIDE Parse Error] ${msg} at line ${t.line}:${t.col}`);
package/src/runtime.js CHANGED
@@ -833,6 +833,7 @@ export function createSqliteStore(db, tableName, schema) {
833
833
  }).join(', ');
834
834
 
835
835
  db.exec(`CREATE TABLE IF NOT EXISTS "${tableName}" (${columns})`);
836
+ migrateSqliteSchema(db, tableName, schema);
836
837
 
837
838
  function toRow(data) {
838
839
  const row = { ...data };
@@ -952,6 +953,83 @@ export function createPrompt(template, defaults = {}) {
952
953
  };
953
954
  }
954
955
 
956
+ // ===== Schema Migration (auto ALTER TABLE for SQLite) =====
957
+ export function migrateSqliteSchema(db, tableName, schema) {
958
+ const fields = Object.entries(schema.fields);
959
+ const info = db.prepare(`PRAGMA table_info("${tableName}")`).all();
960
+ const existing = new Set(info.map(c => c.name));
961
+ let changed = 0;
962
+
963
+ for (const [name, rules] of fields) {
964
+ if (!existing.has(name)) {
965
+ let type = 'TEXT';
966
+ if (rules.type === 'integer') type = 'INTEGER';
967
+ if (rules.type === 'number') type = 'REAL';
968
+ if (rules.type === 'boolean') type = 'INTEGER';
969
+ let def = '';
970
+ if (rules.default !== undefined) {
971
+ const d = typeof rules.default === 'boolean' ? (rules.default ? 1 : 0) :
972
+ typeof rules.default === 'string' ? `'${rules.default}'` : rules.default;
973
+ def = ` DEFAULT ${d}`;
974
+ }
975
+ db.exec(`ALTER TABLE "${tableName}" ADD COLUMN "${name}" ${type}${def}`);
976
+ changed++;
977
+ }
978
+ }
979
+ return changed;
980
+ }
981
+
982
+ // ===== Mock / Spy (test utilities) =====
983
+ export function createMock(fn) {
984
+ const calls = [];
985
+ let impl = fn || (() => undefined);
986
+ const mock = function(...args) {
987
+ calls.push({ args, timestamp: Date.now() });
988
+ return impl.apply(this, args);
989
+ };
990
+ mock.calls = calls;
991
+ mock.callCount = () => calls.length;
992
+ mock.calledWith = (...expected) => calls.some(c =>
993
+ c.args.length === expected.length && c.args.every((a, i) => a === expected[i])
994
+ );
995
+ mock.returns = (val) => { impl = () => val; return mock; };
996
+ mock.impl = (f) => { impl = f; return mock; };
997
+ mock.reset = () => { calls.length = 0; return mock; };
998
+ return mock;
999
+ }
1000
+
1001
+ export function createSpy(obj, method) {
1002
+ const original = obj[method];
1003
+ const mock = createMock(original.bind(obj));
1004
+ obj[method] = mock;
1005
+ mock.restore = () => { obj[method] = original; };
1006
+ return mock;
1007
+ }
1008
+
1009
+ // ===== Plugin System =====
1010
+ const _plugins = new Map();
1011
+
1012
+ export function registerPlugin(name, setup) {
1013
+ _plugins.set(name, { name, setup, initialized: false, exports: {} });
1014
+ }
1015
+
1016
+ export function usePlugin(name, options = {}) {
1017
+ const plugin = _plugins.get(name);
1018
+ if (!plugin) throw new Error(`Plugin not found: ${name}`);
1019
+ if (!plugin.initialized) {
1020
+ const result = plugin.setup(options);
1021
+ if (result && typeof result === 'object') {
1022
+ plugin.exports = result;
1023
+ }
1024
+ plugin.initialized = true;
1025
+ }
1026
+ return plugin.exports;
1027
+ }
1028
+
1029
+ export function listPlugins() {
1030
+ return [..._plugins.keys()];
1031
+ }
1032
+
955
1033
  // ===== Helpers =====
956
1034
  function parseMs(str) {
957
1035
  if (typeof str === 'number') return str;
@@ -0,0 +1,261 @@
1
+ export class TypeChecker {
2
+ constructor() {
3
+ this.scopes = [new Map()];
4
+ this.errors = [];
5
+ this.warnings = [];
6
+ this.functions = new Map();
7
+ }
8
+
9
+ pushScope() { this.scopes.push(new Map()); }
10
+ popScope() { this.scopes.pop(); }
11
+
12
+ setType(name, type) {
13
+ this.scopes[this.scopes.length - 1].set(name, type);
14
+ }
15
+
16
+ getType(name) {
17
+ for (let i = this.scopes.length - 1; i >= 0; i--) {
18
+ if (this.scopes[i].has(name)) return this.scopes[i].get(name);
19
+ }
20
+ return null;
21
+ }
22
+
23
+ error(msg, line) {
24
+ this.errors.push({ message: msg, line: line || 0, severity: 'error' });
25
+ }
26
+
27
+ warn(msg, line) {
28
+ this.warnings.push({ message: msg, line: line || 0, severity: 'warning' });
29
+ }
30
+
31
+ check(ast) {
32
+ this.visitProgram(ast);
33
+ return { errors: this.errors, warnings: this.warnings };
34
+ }
35
+
36
+ visitProgram(node) {
37
+ for (const stmt of node.body) {
38
+ this.visitStatement(stmt);
39
+ }
40
+ }
41
+
42
+ visitStatement(node) {
43
+ const line = node._line || 0;
44
+ switch (node.type) {
45
+ case 'TypedVar': return this.visitTypedVar(node, line);
46
+ case 'Function': return this.visitFunction(node, line);
47
+ case 'Assignment': return this.visitAssignment(node, line);
48
+ case 'Return': return this.visitReturn(node, line);
49
+ case 'If': return this.visitIf(node, line);
50
+ case 'Each': return this.visitEach(node, line);
51
+ case 'For': return this.visitFor(node, line);
52
+ case 'While': return this.visitWhile(node, line);
53
+ case 'Try': return this.visitTry(node, line);
54
+ case 'Server': return this.visitBlock(node.body, line);
55
+ default: return;
56
+ }
57
+ }
58
+
59
+ visitBlock(body, _line) {
60
+ if (!body) return;
61
+ this.pushScope();
62
+ for (const stmt of body) {
63
+ this.visitStatement(stmt);
64
+ }
65
+ this.popScope();
66
+ }
67
+
68
+ visitTypedVar(node, line) {
69
+ const declaredType = node.varType;
70
+ const inferredType = this.inferType(node.value);
71
+
72
+ if (declaredType && inferredType && inferredType !== 'any' && declaredType !== 'any') {
73
+ const compatible = this.typesCompatible(declaredType, inferredType);
74
+ if (!compatible) {
75
+ this.error(`Type mismatch: cannot assign ${inferredType} to ${declaredType}`, line);
76
+ }
77
+ }
78
+
79
+ this.setType(node.name, declaredType || inferredType || 'any');
80
+ }
81
+
82
+ visitFunction(node, line) {
83
+ const paramTypes = {};
84
+ for (const p of node.params) {
85
+ paramTypes[p.name] = p.varType || 'any';
86
+ }
87
+
88
+ this.functions.set(node.name, {
89
+ params: node.params.map(p => ({ name: p.name, type: p.varType || 'any' })),
90
+ returnType: node.returnType || 'any',
91
+ isAsync: node.isAsync || false,
92
+ });
93
+
94
+ this.setType(node.name, 'function');
95
+
96
+ this.pushScope();
97
+ for (const p of node.params) {
98
+ this.setType(p.name, p.varType || 'any');
99
+ }
100
+ for (const stmt of node.body) {
101
+ this.visitStatement(stmt);
102
+ }
103
+ this.popScope();
104
+ }
105
+
106
+ visitAssignment(node, line) {
107
+ if (node.target.type === 'Identifier') {
108
+ const existing = this.getType(node.target.name);
109
+ const newType = this.inferType(node.value);
110
+ if (existing && existing !== 'any' && newType && newType !== 'any') {
111
+ if (!this.typesCompatible(existing, newType)) {
112
+ this.warn(`Type warning: reassigning ${existing} variable '${node.target.name}' with ${newType}`, line);
113
+ }
114
+ }
115
+ if (!existing) {
116
+ this.setType(node.target.name, newType || 'any');
117
+ }
118
+ }
119
+ }
120
+
121
+ visitReturn(node, line) {}
122
+
123
+ visitIf(node, line) {
124
+ this.visitBlock(node.body, line);
125
+ for (const elif of (node.elifs || [])) {
126
+ this.visitBlock(elif.body, line);
127
+ }
128
+ if (node.elseBody) {
129
+ this.visitBlock(node.elseBody, line);
130
+ }
131
+ }
132
+
133
+ visitEach(node, line) {
134
+ this.pushScope();
135
+ this.setType(node.variable, 'any');
136
+ if (node.keyVar) this.setType(node.keyVar, 'any');
137
+ for (const stmt of node.body) {
138
+ this.visitStatement(stmt);
139
+ }
140
+ this.popScope();
141
+ }
142
+
143
+ visitFor(node, line) {
144
+ this.pushScope();
145
+ this.setType(node.variable, 'int');
146
+ for (const stmt of node.body) {
147
+ this.visitStatement(stmt);
148
+ }
149
+ this.popScope();
150
+ }
151
+
152
+ visitWhile(node, line) {
153
+ this.visitBlock(node.body, line);
154
+ }
155
+
156
+ visitTry(node, line) {
157
+ this.visitBlock(node.body, line);
158
+ if (node.catchBody) {
159
+ this.pushScope();
160
+ if (node.catchVar) this.setType(node.catchVar, 'any');
161
+ for (const stmt of node.catchBody) {
162
+ this.visitStatement(stmt);
163
+ }
164
+ this.popScope();
165
+ }
166
+ if (node.ensureBody) {
167
+ this.visitBlock(node.ensureBody, line);
168
+ }
169
+ }
170
+
171
+ inferType(node) {
172
+ if (!node) return 'any';
173
+ switch (node.type) {
174
+ case 'Number': {
175
+ const n = typeof node.value === 'string' ? Number(node.value) : node.value;
176
+ return Number.isInteger(n) && !String(node.value).includes('.') ? 'int' : 'num';
177
+ }
178
+ case 'String': return 'str';
179
+ case 'InterpolatedString': return 'str';
180
+ case 'Boolean': return 'bool';
181
+ case 'Null': return 'any';
182
+ case 'Array': return 'list';
183
+ case 'Object': return 'map';
184
+ case 'Identifier': return this.getType(node.name) || 'any';
185
+ case 'Binary': return this.inferBinaryType(node);
186
+ case 'Unary': {
187
+ if (node.op === 'not') return 'bool';
188
+ if (node.op === 'typeof') return 'str';
189
+ return this.inferType(node.operand);
190
+ }
191
+ case 'Call': return this.inferCallType(node);
192
+ case 'MemberAccess': return 'any';
193
+ case 'Ternary': {
194
+ const t = this.inferType(node.consequent);
195
+ const f = this.inferType(node.alternate);
196
+ return t === f ? t : 'any';
197
+ }
198
+ case 'Await': return this.inferType(node.expression);
199
+ case 'ArrowFunction': return 'function';
200
+ default: return 'any';
201
+ }
202
+ }
203
+
204
+ inferBinaryType(node) {
205
+ const op = node.op;
206
+ if (['==', '!=', '>', '<', '>=', '<=', 'and', 'or'].includes(op)) return 'bool';
207
+ if (['+', '-', '*', '/', '%'].includes(op)) {
208
+ const l = this.inferType(node.left);
209
+ const r = this.inferType(node.right);
210
+ if (l === 'str' || r === 'str') {
211
+ if (op === '+') return 'str';
212
+ }
213
+ if (l === 'num' || r === 'num') return 'num';
214
+ if (l === 'int' && r === 'int') return 'int';
215
+ return 'num';
216
+ }
217
+ return 'any';
218
+ }
219
+
220
+ inferCallType(node) {
221
+ if (node.callee.type === 'Identifier') {
222
+ const fn = this.functions.get(node.callee.name);
223
+ if (fn) return fn.returnType;
224
+
225
+ const builtins = {
226
+ 'uuid': 'str', 'hash': 'str', 'verify': 'bool',
227
+ 'sign': 'str', 'parseInt': 'int', 'parseFloat': 'num',
228
+ 'String': 'str', 'Number': 'num', 'Boolean': 'bool',
229
+ 'createMock': 'function', 'createSpy': 'function',
230
+ };
231
+ if (builtins[node.callee.name]) return builtins[node.callee.name];
232
+ }
233
+
234
+ if (node.callee.type === 'MemberAccess') {
235
+ const obj = node.callee.object;
236
+ const prop = node.callee.property;
237
+ if (obj.type === 'Identifier' && obj.name === 'ai') {
238
+ if (prop === 'ask' || prop === 'chat') return 'str';
239
+ if (prop === 'json') return 'map';
240
+ if (prop === 'embed') return 'list';
241
+ if (prop === 'similarity') return 'num';
242
+ }
243
+ if (obj.type === 'Identifier' && obj.name === 'api') return 'any';
244
+ if (prop === 'length') return 'int';
245
+ if (['map', 'filter', 'slice'].includes(prop)) return 'list';
246
+ if (['join', 'toString', 'trim', 'toLowerCase', 'toUpperCase'].includes(prop)) return 'str';
247
+ if (['includes', 'startsWith', 'endsWith', 'some', 'every'].includes(prop)) return 'bool';
248
+ if (['indexOf', 'findIndex'].includes(prop)) return 'int';
249
+ }
250
+
251
+ return 'any';
252
+ }
253
+
254
+ typesCompatible(declared, actual) {
255
+ if (declared === actual) return true;
256
+ if (declared === 'any' || actual === 'any') return true;
257
+ if (declared === 'num' && actual === 'int') return true;
258
+ if (declared === 'json' && ['str', 'int', 'num', 'bool', 'list', 'map'].includes(actual)) return true;
259
+ return false;
260
+ }
261
+ }