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 +139 -2
- package/bin/naide.js +330 -3
- package/lsp/server.js +36 -1
- package/package.json +1 -1
- package/src/generator.js +47 -7
- package/src/index.js +11 -3
- package/src/lexer.js +7 -2
- package/src/parser.js +6 -1
- package/src/runtime.js +78 -0
- package/src/typechecker.js +261 -0
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.
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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 +=
|
|
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
|
+
}
|