naidejs 1.2.0 → 1.4.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/SPEC-X.nx CHANGED
@@ -6,7 +6,7 @@
6
6
  -- Rule: first char of line = intent
7
7
  -- > return ? if | elif : else
8
8
  -- @ loop * while ! try !! catch
9
- -- $ server ^ model < import % match
9
+ -- $ server ^ model < import % match X patch
10
10
  -- f func ~f async + export
11
11
  -- s:i:n:b:l:m:a: typed vars
12
12
  -- ~ await ~~ await.all
@@ -138,6 +138,12 @@ $app:PORT
138
138
  static "/public"
139
139
  crud "/api/users" User
140
140
  cookie
141
+ session JWT_SECRET
142
+ cache "/api/*" "5m"
143
+ view "./views"
144
+ sse "/events"
145
+ upload "/api/upload" "avatar" (req,res):
146
+ >{file:req.file.filename}
141
147
  group "/api/v1":
142
148
  G"/status"
143
149
  >{version:"1.0"}
@@ -185,6 +191,22 @@ $app:PORT
185
191
  -- sign({id:1}) → JWT (uses auth secret)
186
192
  -- sign({id:1},"secret") → JWT (explicit)
187
193
 
194
+ -- Validate: auto-validate POST/PUT/PATCH bodies
195
+ -- validate "/api/users" User
196
+
197
+ -- Test (node:test)
198
+ test "math":
199
+ assert 1+1==2
200
+ assert 2*3==6
201
+
202
+ -- Queue (async job queue)
203
+ queue jobs:
204
+ job "notify" (data):
205
+ log data.msg
206
+
207
+ -- OpenAPI: auto-generate spec from schemas
208
+ -- openapi "/docs"
209
+
188
210
  -- Scheduled tasks
189
211
  every "5m":
190
212
  log"cleanup"
package/SPEC.naide CHANGED
@@ -183,6 +183,26 @@ server app port PORT:
183
183
  # クッキーパーサー
184
184
  cookie
185
185
 
186
+ # セッション管理 (cookie-based, ゼロ依存)
187
+ session JWT_SECRET
188
+
189
+ # レスポンスキャッシュ
190
+ cache "/api/*" "5m"
191
+
192
+ # テンプレートエンジン
193
+ view "./views"
194
+
195
+ # Server-Sent Events
196
+ sse "/events"
197
+
198
+ # ファイルアップロード (ゼロ依存)
199
+ upload "/api/upload" "avatar" (req, res):
200
+ ret {file: req.file.filename, size: req.file.size}
201
+
202
+ # 名前付きミドルウェア適用
203
+ # mid myMiddleware
204
+ # mid myMiddleware "/api"
205
+
186
206
  # ルートグループ
187
207
  group "/api/v1":
188
208
  get "/status":
@@ -230,6 +250,41 @@ server app port PORT:
230
250
  # any raw = await api.raw(url, opts)
231
251
 
232
252
 
253
+ # ---- HTTPメソッド ----
254
+ # get, post, put, del, patch がサーバー内で使える
255
+ # patch "/api/users/:id" (req, res):
256
+ # ret {updated: true}
257
+
258
+
259
+ # ---- リクエストバリデーション ----
260
+ # validate パス スキーマ名 → POST/PUT/PATCHのボディをスキーマで自動検証
261
+ # server app port 3000:
262
+ # validate "/api/users" User
263
+
264
+
265
+ # ---- テスト (node:test ベース) ----
266
+ test "math operations":
267
+ assert 1 + 1 == 2
268
+ assert 10 - 3 == 7
269
+
270
+ test "string operations":
271
+ str name = "NAIDE"
272
+ assert name == "NAIDE"
273
+
274
+
275
+ # ---- ジョブキュー (インメモリ非同期) ----
276
+ queue jobs:
277
+ job "sendEmail" (data):
278
+ log "sending to {data.to}"
279
+ job "resize" (data):
280
+ log "resizing {data.path}"
281
+
282
+
283
+ # ---- OpenAPI 自動生成 ----
284
+ # server app port 3000:
285
+ # openapi "/docs" # スキーマからOpenAPI仕様を自動生成して配信
286
+
287
+
233
288
  # ---- レスポンスヘルパー ----
234
289
  # ret {data} → JSON応答
235
290
  # ret.status 404 {error} → ステータスコード付き
@@ -237,6 +292,7 @@ server app port PORT:
237
292
  # ret.html "<h1>Hi</h1>" → HTML応答
238
293
  # ret.text "pong" → テキスト応答
239
294
  # ret.file "/path/to/file" → ファイル送信
295
+ # ret.render "template" {data} → テンプレート描画
240
296
 
241
297
 
242
298
  # ---- 組み込み関数 (自動インポート) ----
package/bin/naide.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { readFileSync, writeFileSync, unlinkSync, watch as fsWatch } from 'fs';
4
- import { resolve, basename, extname } from 'path';
3
+ import { readFileSync, writeFileSync, unlinkSync, watch as fsWatch, existsSync, mkdirSync, readdirSync, statSync } from 'fs';
4
+ 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
 
@@ -36,6 +36,101 @@ for (let i = 0; i < args.length; i++) {
36
36
  }
37
37
  }
38
38
 
39
+ if (files[0] === 'init') {
40
+ const dir = files[1] ? resolve(files[1]) : process.cwd();
41
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
42
+ const name = basename(dir === process.cwd() ? dir : dir);
43
+
44
+ if (!existsSync(resolve(dir, 'package.json'))) {
45
+ writeFileSync(resolve(dir, 'package.json'), JSON.stringify({
46
+ name, version: '1.0.0', type: 'module',
47
+ scripts: { start: 'naide app.naide', dev: 'naide -w app.naide', build: 'naide --emit app.naide -o dist/app.mjs' },
48
+ dependencies: { naidejs: '^1.3.0' }
49
+ }, null, 2) + '\n');
50
+ }
51
+
52
+ if (!existsSync(resolve(dir, 'app.naide'))) {
53
+ writeFileSync(resolve(dir, 'app.naide'), `db "data/"
54
+
55
+ env:
56
+ PORT int default(3000)
57
+
58
+ schema Item:
59
+ id auto
60
+ name str required min(1) max(100)
61
+ done bool default(false)
62
+
63
+ server app port PORT:
64
+ cors "*"
65
+ cookie
66
+ static "/public"
67
+ crud "/api/items" Item
68
+
69
+ get "/":
70
+ ret.text "NAIDE server running"
71
+
72
+ get "/api/health":
73
+ ret {status: "ok", items: ItemStore.count()}
74
+ `);
75
+ }
76
+
77
+ if (!existsSync(resolve(dir, 'public'))) mkdirSync(resolve(dir, 'public'), { recursive: true });
78
+
79
+ console.log(`\n NAIDE project initialized!
80
+
81
+ ${dir === process.cwd() ? '' : ` cd ${basename(dir)}\n`} npm install
82
+ npm run dev
83
+ `);
84
+ process.exit(0);
85
+ }
86
+
87
+ if (files[0] === 'build') {
88
+ const dir = resolve(files[1] || '.');
89
+ const outDir = files[2] ? resolve(files[2]) : null;
90
+
91
+ function walkDir(d) {
92
+ const found = [];
93
+ for (const entry of readdirSync(d)) {
94
+ const full = join(d, entry);
95
+ if (statSync(full).isDirectory()) {
96
+ if (entry === 'node_modules' || entry === '.git' || entry === 'dist') continue;
97
+ found.push(...walkDir(full));
98
+ } else if (entry.endsWith('.naide') || entry.endsWith('.nx')) {
99
+ found.push(full);
100
+ }
101
+ }
102
+ return found;
103
+ }
104
+
105
+ const sourceFiles = walkDir(dir);
106
+ if (sourceFiles.length === 0) {
107
+ console.log(' No .naide or .nx files found.');
108
+ process.exit(0);
109
+ }
110
+
111
+ console.log(`\n NAIDE build — ${sourceFiles.length} file(s)\n`);
112
+ let errors = 0;
113
+ for (const srcFile of sourceFiles) {
114
+ const rel = relative(dir, srcFile);
115
+ const mode = srcFile.endsWith('.nx') ? 'x' : 'naide';
116
+ try {
117
+ const source = readFileSync(srcFile, 'utf-8');
118
+ const { js } = compile(source, { mode });
119
+ const outName = rel.replace(/\.(naide|nx)$/, '.mjs');
120
+ const outPath = outDir ? join(outDir, outName) : join(dir, outName);
121
+ const outDirPath = resolve(outPath, '..');
122
+ if (!existsSync(outDirPath)) mkdirSync(outDirPath, { recursive: true });
123
+ writeFileSync(outPath, js);
124
+ console.log(` ${rel} → ${outDir ? join(relative('.', outDir), outName) : outName}`);
125
+ } catch (e) {
126
+ console.error(` FAIL ${rel}: ${e.message.split('\n')[0]}`);
127
+ errors++;
128
+ }
129
+ }
130
+ console.log(`\n Done. ${sourceFiles.length - errors} compiled, ${errors} failed.`);
131
+ process.exit(errors > 0 ? 1 : 0);
132
+ }
133
+
39
134
  if (flags.help || files.length === 0) {
40
135
  console.log(`
41
136
  NAIDE - Node AI Development Environment
@@ -44,6 +139,8 @@ if (flags.help || files.length === 0) {
44
139
  Usage:
45
140
  naide <file.naide> Run a NAIDE file
46
141
  naide <file.nx> Run a NAIDE-X file (auto-detected)
142
+ naide init [dir] Create a new NAIDE project
143
+ naide build [dir] [outdir] Transpile all files to JavaScript
47
144
  naide --emit <file.nx> Output generated JavaScript
48
145
  naide --mid <file.nx> Output intermediate NAIDE v1 (debug)
49
146
  naide -x <file.naide> Force NAIDE-X mode
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "naidejs",
3
- "version": "1.2.0",
3
+ "version": "1.4.0",
4
4
  "description": "NAIDE - Node AI Development Environment. AI-specialized language with built-in server, auth, DB, WebSocket, and more — transpiles to Node.js. Standard mode (~40% fewer tokens) and X mode (~80% fewer tokens).",
5
5
  "main": "src/index.js",
6
6
  "exports": {
package/src/generator.js CHANGED
@@ -12,6 +12,8 @@ export class Generator {
12
12
  this.authSecret = null;
13
13
  this.hasWs = false;
14
14
  this.wsNodes = [];
15
+ this.hasTests = false;
16
+ this.hasAsserts = false;
15
17
  }
16
18
 
17
19
  generate(ast) {
@@ -29,6 +31,14 @@ export class Generator {
29
31
  preamble.push('');
30
32
  }
31
33
 
34
+ if (this.hasTests) {
35
+ preamble.push("import { test } from 'node:test';");
36
+ }
37
+ if (this.hasTests || this.hasAsserts) {
38
+ preamble.push("import assert from 'node:assert/strict';");
39
+ if (this.hasTests) preamble.push('');
40
+ }
41
+
32
42
  if (this.needsEventBus) {
33
43
  preamble.push('const __eventBus = createEventBus();');
34
44
  preamble.push('');
@@ -96,6 +106,18 @@ export class Generator {
96
106
  case 'GroupDecl': return this.visitGroupTopLevel(node);
97
107
  case 'ErrorHandler': return this.visitErrorHandlerTopLevel(node);
98
108
  case 'CookieDecl': return this.visitCookieTopLevel(node);
109
+ case 'UploadDecl': return this.visitUploadTopLevel(node);
110
+ case 'SessionDecl': return this.visitSessionTopLevel(node);
111
+ case 'ViewDecl': return this.visitViewTopLevel(node);
112
+ case 'SseDecl': return this.visitSseTopLevel(node);
113
+ case 'CacheDecl': return this.visitCacheTopLevel(node);
114
+ case 'MiddlewareRef': return this.visitMiddlewareRefTopLevel(node);
115
+ case 'ReturnRender': return this.visitReturnRender(node);
116
+ case 'ValidateDecl': return this.visitValidateTopLevel(node);
117
+ case 'TestDecl': return this.visitTest(node);
118
+ case 'AssertStmt': return this.visitAssert(node);
119
+ case 'QueueDecl': return this.visitQueue(node);
120
+ case 'OpenapiDecl': return this.visitOpenapiTopLevel(node);
99
121
  default:
100
122
  this.emit(`/* unknown: ${node.type} */`);
101
123
  }
@@ -324,6 +346,24 @@ export class Generator {
324
346
  errorHandlers.push(child);
325
347
  } else if (child.type === 'CookieDecl') {
326
348
  this.visitCookie(node.name, child);
349
+ } else if (child.type === 'UploadDecl') {
350
+ this.visitUpload(node.name, child);
351
+ } else if (child.type === 'SessionDecl') {
352
+ this.visitSession(node.name, child);
353
+ } else if (child.type === 'ViewDecl') {
354
+ this.visitView(node.name, child);
355
+ } else if (child.type === 'SseDecl') {
356
+ this.visitSse(node.name, child);
357
+ } else if (child.type === 'CacheDecl') {
358
+ this.visitCache(node.name, child);
359
+ } else if (child.type === 'MiddlewareRef') {
360
+ this.visitMiddlewareRef(node.name, child);
361
+ } else if (child.type === 'ValidateDecl') {
362
+ this.visitValidate(node.name, child);
363
+ } else if (child.type === 'OpenapiDecl') {
364
+ this.visitOpenapi(node.name, child);
365
+ } else if (child.type === 'QueueDecl') {
366
+ this.visitQueue(child);
327
367
  } else {
328
368
  this.visitStatement(child);
329
369
  }
@@ -759,6 +799,165 @@ export class Generator {
759
799
  this.visitCookie('app', node);
760
800
  }
761
801
 
802
+ visitUpload(appName, node) {
803
+ this.runtimeImports.add('uploadMiddleware');
804
+ const path = this.stringValue(node.path);
805
+ const field = this.stringValue(node.fieldName);
806
+ const params = node.params.length > 0 ? node.params.join(', ') : 'req, res';
807
+ const needsAsync = this.bodyUsesAwait(node.body);
808
+ const asyncPrefix = needsAsync ? 'async ' : '';
809
+ this.emit(`${appName}.post(${path}, uploadMiddleware(${field}), ${asyncPrefix}(${params}) => {`);
810
+ this.indent++;
811
+ for (const stmt of node.body) {
812
+ if (stmt.type === 'Return' && stmt.value !== null) {
813
+ this.emit(`res.json(${this.expr(stmt.value)});`);
814
+ } else {
815
+ this.visitStatement(stmt);
816
+ }
817
+ }
818
+ this.indent--;
819
+ this.emit('});');
820
+ this.emitRaw('');
821
+ }
822
+
823
+ visitUploadTopLevel(node) {
824
+ this.visitUpload('app', node);
825
+ }
826
+
827
+ visitSession(appName, node) {
828
+ this.runtimeImports.add('sessionMiddleware');
829
+ const secret = this.expr(node.secret);
830
+ this.emit(`${appName}.use(sessionMiddleware(${secret}));`);
831
+ this.emitRaw('');
832
+ }
833
+
834
+ visitSessionTopLevel(node) {
835
+ this.visitSession('app', node);
836
+ }
837
+
838
+ visitView(appName, node) {
839
+ this.runtimeImports.add('createRenderer');
840
+ const dir = this.stringValue(node.dir);
841
+ this.emit(`const __render = createRenderer(${dir});`);
842
+ this.emitRaw('');
843
+ }
844
+
845
+ visitViewTopLevel(node) {
846
+ this.visitView('app', node);
847
+ }
848
+
849
+ visitSse(appName, node) {
850
+ this.runtimeImports.add('createSseManager');
851
+ const path = this.stringValue(node.path);
852
+ this.emit(`const sse = createSseManager();`);
853
+ this.emit(`${appName}.get(${path}, sse.handler());`);
854
+ this.emitRaw('');
855
+ }
856
+
857
+ visitSseTopLevel(node) {
858
+ this.visitSse('app', node);
859
+ }
860
+
861
+ visitCache(appName, node) {
862
+ this.runtimeImports.add('cacheMiddleware');
863
+ const path = this.stringValue(node.path);
864
+ const duration = this.expr(node.duration);
865
+ this.emit(`${appName}.use(${path}, cacheMiddleware(${duration}));`);
866
+ this.emitRaw('');
867
+ }
868
+
869
+ visitCacheTopLevel(node) {
870
+ this.visitCache('app', node);
871
+ }
872
+
873
+ visitMiddlewareRef(appName, node) {
874
+ if (node.path) {
875
+ this.emit(`${appName}.use(${this.stringValue(node.path)}, ${node.name});`);
876
+ } else {
877
+ this.emit(`${appName}.use(${node.name});`);
878
+ }
879
+ this.emitRaw('');
880
+ }
881
+
882
+ visitMiddlewareRefTopLevel(node) {
883
+ this.visitMiddlewareRef('app', node);
884
+ }
885
+
886
+ visitReturnRender(node) {
887
+ const template = this.expr(node.template);
888
+ const data = node.data ? this.expr(node.data) : '{}';
889
+ this.emit(`return res.type('html').send(__render(${template}, ${data}));`);
890
+ }
891
+
892
+ visitValidate(appName, node) {
893
+ this.runtimeImports.add('validateMiddleware');
894
+ const path = this.stringValue(node.path);
895
+ this.emit(`${appName}.use(${path}, validateMiddleware(${node.schemaName}Schema));`);
896
+ this.emitRaw('');
897
+ }
898
+
899
+ visitValidateTopLevel(node) {
900
+ this.visitValidate('app', node);
901
+ }
902
+
903
+ visitTest(node) {
904
+ this.hasTests = true;
905
+ const name = this.expr(node.name);
906
+ const needsAsync = this.bodyUsesAwait(node.body);
907
+ const asyncPrefix = needsAsync ? 'async ' : '';
908
+ this.emit(`test(${name}, ${asyncPrefix}() => {`);
909
+ this.indent++;
910
+ for (const stmt of node.body) this.visitStatement(stmt);
911
+ this.indent--;
912
+ this.emit('});');
913
+ this.emitRaw('');
914
+ }
915
+
916
+ visitAssert(node) {
917
+ this.hasAsserts = true;
918
+ const exprNode = node.expr;
919
+ if (exprNode.type === 'Binary' && exprNode.op === '===') {
920
+ this.emit(`assert.strictEqual(${this.expr(exprNode.left)}, ${this.expr(exprNode.right)});`);
921
+ } else if (exprNode.type === 'Binary' && exprNode.op === '!==') {
922
+ this.emit(`assert.notStrictEqual(${this.expr(exprNode.left)}, ${this.expr(exprNode.right)});`);
923
+ } else {
924
+ this.emit(`assert.ok(${this.expr(exprNode)});`);
925
+ }
926
+ }
927
+
928
+ visitQueue(node) {
929
+ this.runtimeImports.add('createQueue');
930
+ this.emit(`const ${node.name} = createQueue();`);
931
+ for (const job of node.jobs) {
932
+ const params = job.params.length > 0 ? job.params.join(', ') : 'data';
933
+ const needsAsync = this.bodyUsesAwait(job.body);
934
+ const asyncPrefix = needsAsync ? 'async ' : '';
935
+ this.emit(`${node.name}.register(${this.generateString(job.name)}, ${asyncPrefix}(${params}) => {`);
936
+ this.indent++;
937
+ for (const stmt of job.body) this.visitStatement(stmt);
938
+ this.indent--;
939
+ this.emit('});');
940
+ }
941
+ this.emitRaw('');
942
+ }
943
+
944
+ visitOpenapi(appName, node) {
945
+ this.runtimeImports.add('buildOpenApiSpec');
946
+ const path = this.stringValue(node.path);
947
+ const schemaNames = [...this.schemas.keys()];
948
+ const schemaArgs = schemaNames.map(n => `${n}Schema`).join(', ');
949
+ this.emit(`${appName}.get(${path}, (req, res) => {`);
950
+ this.indent++;
951
+ this.emit(`res.json(buildOpenApiSpec([${schemaArgs}]));`);
952
+ this.indent--;
953
+ this.emit('});');
954
+ this.emitRaw('');
955
+ }
956
+
957
+ visitOpenapiTopLevel(node) {
958
+ this.visitOpenapi('app', node);
959
+ }
960
+
762
961
  visitEnv(node) {
763
962
  this.runtimeImports.add('loadEnv');
764
963
 
package/src/index.js CHANGED
@@ -4,16 +4,34 @@ import { Generator } from './generator.js';
4
4
  import { preprocess } from './preprocess.js';
5
5
 
6
6
  export function compile(source, { mode = 'naide', runtimePath } = {}) {
7
+ let processedSource = source;
7
8
  if (mode === 'x') {
8
- source = preprocess(source);
9
+ processedSource = preprocess(source);
10
+ }
11
+ try {
12
+ const lexer = new Lexer(processedSource);
13
+ const tokens = lexer.tokenize();
14
+ const parser = new Parser(tokens);
15
+ const ast = parser.parse();
16
+ const generator = new Generator({ runtimePath });
17
+ const js = generator.generate(ast);
18
+ return { js, ast, tokens, naide: mode === 'x' ? processedSource : null };
19
+ } catch (e) {
20
+ const lineMatch = e.message.match(/line (\d+)/);
21
+ if (lineMatch) {
22
+ const lineNum = parseInt(lineMatch[1]);
23
+ const lines = processedSource.split('\n');
24
+ const start = Math.max(0, lineNum - 3);
25
+ const end = Math.min(lines.length, lineNum + 2);
26
+ const context = lines.slice(start, end).map((l, i) => {
27
+ const num = start + i + 1;
28
+ const marker = num === lineNum ? ' >> ' : ' ';
29
+ return `${marker}${num} | ${l}`;
30
+ }).join('\n');
31
+ e.message += `\n\n${context}\n`;
32
+ }
33
+ throw e;
9
34
  }
10
- const lexer = new Lexer(source);
11
- const tokens = lexer.tokenize();
12
- const parser = new Parser(tokens);
13
- const ast = parser.parse();
14
- const generator = new Generator({ runtimePath });
15
- const js = generator.generate(ast);
16
- return { js, ast, tokens, naide: mode === 'x' ? source : null };
17
35
  }
18
36
 
19
37
  export function transpile(source, opts) {
package/src/lexer.js CHANGED
@@ -11,7 +11,7 @@ class Token {
11
11
 
12
12
  export class Lexer {
13
13
  constructor(source) {
14
- this.source = source;
14
+ this.source = source.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
15
15
  this.pos = 0;
16
16
  this.line = 1;
17
17
  this.col = 1;
@@ -78,6 +78,11 @@ export class Lexer {
78
78
  continue;
79
79
  }
80
80
 
81
+ if (ch === ';') {
82
+ this.advance();
83
+ continue;
84
+ }
85
+
81
86
  if (ch === '"' || ch === "'") {
82
87
  this.readString(ch);
83
88
  continue;
@@ -195,7 +200,11 @@ export class Lexer {
195
200
  }
196
201
  }
197
202
 
198
- if (this.peek() === quote) this.advance(); // skip closing quote
203
+ if (this.peek() === quote) {
204
+ this.advance();
205
+ } else {
206
+ throw new Error(`[NAIDE Lexer Error] Unterminated string starting at line ${startLine}:${startCol}`);
207
+ }
199
208
 
200
209
  if (current) parts.push({ type: 'text', value: current });
201
210
 
@@ -221,7 +230,11 @@ export class Lexer {
221
230
  value += this.advance();
222
231
  }
223
232
  }
224
- if (this.peek() === '`') this.advance();
233
+ if (this.peek() === '`') {
234
+ this.advance();
235
+ } else {
236
+ throw new Error(`[NAIDE Lexer Error] Unterminated template string starting at line ${startLine}:${startCol}`);
237
+ }
225
238
  this.tokens.push(new Token(T.STRING, { parts: [{ type: 'text', value }], raw: value }, startLine, startCol));
226
239
  }
227
240
 
@@ -371,7 +384,7 @@ export class Lexer {
371
384
  case ':': this.tokens.push(new Token(T.COLON, ':', startLine, startCol)); break;
372
385
  case ',': this.tokens.push(new Token(T.COMMA, ',', startLine, startCol)); break;
373
386
  default:
374
- throw new Error(`Unexpected character '${ch}' at line ${startLine}:${startCol}`);
387
+ throw new Error(`[NAIDE Lexer Error] Unexpected character '${ch}' at line ${startLine}:${startCol}`);
375
388
  }
376
389
  }
377
390