novac 2.0.1 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (161) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +1574 -597
  3. package/bin/novac +468 -171
  4. package/bin/nvc +522 -0
  5. package/bin/nvml +78 -17
  6. package/demo.nv +0 -0
  7. package/demo_builtins.nv +0 -0
  8. package/demo_http.nv +0 -0
  9. package/examples/bf.nv +69 -0
  10. package/examples/math.nv +21 -0
  11. package/kits/birdAPI/kitdef.js +954 -0
  12. package/kits/kitRNG/kitdef.js +740 -0
  13. package/kits/kitSSH/kitdef.js +1272 -0
  14. package/kits/kitadb/kitdef.js +606 -0
  15. package/kits/kitai/kitdef.js +2185 -0
  16. package/kits/kitansi/kitdef.js +1402 -0
  17. package/kits/kitcanvas/kitdef.js +914 -0
  18. package/kits/kitclippy/kitdef.js +925 -0
  19. package/kits/kitformat/kitdef.js +1485 -0
  20. package/kits/kitgps/kitdef.js +1862 -0
  21. package/kits/kitlibproc/kitdef.js +3 -2
  22. package/kits/kitmatrix/ex.js +19 -0
  23. package/kits/kitmatrix/kitdef.js +960 -0
  24. package/kits/kitmorse/kitdef.js +229 -0
  25. package/kits/kitmpatch/kitdef.js +906 -0
  26. package/kits/kitnet/kitdef.js +1401 -0
  27. package/kits/kitnovacweb/README.md +1416 -143
  28. package/kits/kitnovacweb/kitdef.js +92 -2
  29. package/kits/kitnovacweb/nvml/executor.js +578 -176
  30. package/kits/kitnovacweb/nvml/index.js +2 -2
  31. package/kits/kitnovacweb/nvml/lexer.js +72 -69
  32. package/kits/kitnovacweb/nvml/parser.js +328 -159
  33. package/kits/kitnovacweb/nvml/renderer.js +770 -270
  34. package/kits/kitparse/kitdef.js +1688 -0
  35. package/kits/kitproto/kitdef.js +613 -0
  36. package/kits/kitqr/kitdef.js +637 -0
  37. package/kits/kitregex++/kitdef.js +1353 -0
  38. package/kits/kitrequire/kitdef.js +1599 -0
  39. package/kits/kitx11/kitdef.js +1 -0
  40. package/kits/kitx11/kitx11.js +2472 -0
  41. package/kits/kitx11/kitx11_conn.js +948 -0
  42. package/kits/kitx11/kitx11_worker.js +121 -0
  43. package/kits/libtea/kitdef.js +2691 -0
  44. package/kits/libterm/ex.js +285 -0
  45. package/kits/libterm/kitdef.js +1927 -0
  46. package/novac/LICENSE +21 -0
  47. package/novac/README.md +1823 -0
  48. package/novac/bin/novac +950 -0
  49. package/novac/bin/nvc +522 -0
  50. package/novac/bin/nvml +542 -0
  51. package/novac/demo.nv +245 -0
  52. package/novac/demo_builtins.nv +209 -0
  53. package/novac/demo_http.nv +62 -0
  54. package/novac/examples/bf.nv +69 -0
  55. package/novac/examples/math.nv +21 -0
  56. package/novac/kits/kitai/kitdef.js +2185 -0
  57. package/novac/kits/kitansi/kitdef.js +1402 -0
  58. package/novac/kits/kitformat/kitdef.js +1485 -0
  59. package/novac/kits/kitgps/kitdef.js +1862 -0
  60. package/novac/kits/kitlibfs/kitdef.js +231 -0
  61. package/{examples/example-project/nova_modules → novac/kits}/kitlibproc/kitdef.js +3 -2
  62. package/novac/kits/kitmatrix/ex.js +19 -0
  63. package/novac/kits/kitmatrix/kitdef.js +960 -0
  64. package/novac/kits/kitmpatch/kitdef.js +906 -0
  65. package/novac/kits/kitnovacweb/README.md +1572 -0
  66. package/novac/kits/kitnovacweb/demo.nv +12 -0
  67. package/novac/kits/kitnovacweb/demo.nvml +71 -0
  68. package/novac/kits/kitnovacweb/index.nova +12 -0
  69. package/novac/kits/kitnovacweb/kitdef.js +692 -0
  70. package/novac/kits/kitnovacweb/nova.kit.json +8 -0
  71. package/novac/kits/kitnovacweb/nvml/executor.js +739 -0
  72. package/novac/kits/kitnovacweb/nvml/index.js +67 -0
  73. package/novac/kits/kitnovacweb/nvml/lexer.js +263 -0
  74. package/novac/kits/kitnovacweb/nvml/parser.js +508 -0
  75. package/novac/kits/kitnovacweb/nvml/renderer.js +924 -0
  76. package/novac/kits/kitparse/kitdef.js +1688 -0
  77. package/novac/kits/kitregex++/kitdef.js +1353 -0
  78. package/novac/kits/kitrequire/kitdef.js +1599 -0
  79. package/novac/kits/kitx11/kitdef.js +1 -0
  80. package/novac/kits/kitx11/kitx11.js +2472 -0
  81. package/novac/kits/kitx11/kitx11_conn.js +948 -0
  82. package/novac/kits/kitx11/kitx11_worker.js +121 -0
  83. package/novac/kits/libtea/tf.js +2691 -0
  84. package/novac/kits/libterm/ex.js +285 -0
  85. package/novac/kits/libterm/kitdef.js +1927 -0
  86. package/novac/node_modules/chalk/license +9 -0
  87. package/novac/node_modules/chalk/package.json +83 -0
  88. package/novac/node_modules/chalk/readme.md +297 -0
  89. package/novac/node_modules/chalk/source/index.d.ts +325 -0
  90. package/novac/node_modules/chalk/source/index.js +225 -0
  91. package/novac/node_modules/chalk/source/utilities.js +33 -0
  92. package/novac/node_modules/chalk/source/vendor/ansi-styles/index.d.ts +236 -0
  93. package/novac/node_modules/chalk/source/vendor/ansi-styles/index.js +223 -0
  94. package/novac/node_modules/chalk/source/vendor/supports-color/browser.d.ts +1 -0
  95. package/novac/node_modules/chalk/source/vendor/supports-color/browser.js +34 -0
  96. package/novac/node_modules/chalk/source/vendor/supports-color/index.d.ts +55 -0
  97. package/novac/node_modules/chalk/source/vendor/supports-color/index.js +190 -0
  98. package/novac/node_modules/commander/LICENSE +22 -0
  99. package/novac/node_modules/commander/Readme.md +1176 -0
  100. package/novac/node_modules/commander/esm.mjs +16 -0
  101. package/novac/node_modules/commander/index.js +24 -0
  102. package/novac/node_modules/commander/lib/argument.js +150 -0
  103. package/novac/node_modules/commander/lib/command.js +2777 -0
  104. package/novac/node_modules/commander/lib/error.js +39 -0
  105. package/novac/node_modules/commander/lib/help.js +747 -0
  106. package/novac/node_modules/commander/lib/option.js +380 -0
  107. package/novac/node_modules/commander/lib/suggestSimilar.js +101 -0
  108. package/novac/node_modules/commander/package-support.json +19 -0
  109. package/novac/node_modules/commander/package.json +82 -0
  110. package/novac/node_modules/commander/typings/esm.d.mts +3 -0
  111. package/novac/node_modules/commander/typings/index.d.ts +1113 -0
  112. package/novac/node_modules/node-addon-api/LICENSE.md +9 -0
  113. package/novac/node_modules/node-addon-api/README.md +95 -0
  114. package/novac/node_modules/node-addon-api/common.gypi +21 -0
  115. package/novac/node_modules/node-addon-api/except.gypi +25 -0
  116. package/novac/node_modules/node-addon-api/index.js +14 -0
  117. package/novac/node_modules/node-addon-api/napi-inl.deprecated.h +186 -0
  118. package/novac/node_modules/node-addon-api/napi-inl.h +7165 -0
  119. package/novac/node_modules/node-addon-api/napi.h +3364 -0
  120. package/novac/node_modules/node-addon-api/node_addon_api.gyp +42 -0
  121. package/novac/node_modules/node-addon-api/node_api.gyp +9 -0
  122. package/novac/node_modules/node-addon-api/noexcept.gypi +26 -0
  123. package/novac/node_modules/node-addon-api/package-support.json +21 -0
  124. package/novac/node_modules/node-addon-api/package.json +480 -0
  125. package/novac/node_modules/node-addon-api/tools/README.md +73 -0
  126. package/novac/node_modules/node-addon-api/tools/check-napi.js +99 -0
  127. package/novac/node_modules/node-addon-api/tools/clang-format.js +71 -0
  128. package/novac/node_modules/node-addon-api/tools/conversion.js +301 -0
  129. package/novac/node_modules/serialize-javascript/LICENSE +27 -0
  130. package/novac/node_modules/serialize-javascript/README.md +149 -0
  131. package/novac/node_modules/serialize-javascript/index.js +297 -0
  132. package/novac/node_modules/serialize-javascript/package.json +33 -0
  133. package/novac/package.json +27 -0
  134. package/novac/scripts/update-bin.js +24 -0
  135. package/novac/src/core/bstd.js +1035 -0
  136. package/novac/src/core/config.js +155 -0
  137. package/novac/src/core/describe.js +187 -0
  138. package/novac/src/core/emitter.js +499 -0
  139. package/novac/src/core/error.js +86 -0
  140. package/novac/src/core/executor.js +5606 -0
  141. package/novac/src/core/formatter.js +686 -0
  142. package/novac/src/core/lexer.js +1026 -0
  143. package/novac/src/core/nova_builtins.js +717 -0
  144. package/novac/src/core/nova_thread_worker.js +166 -0
  145. package/novac/src/core/parser.js +2181 -0
  146. package/novac/src/core/types.js +112 -0
  147. package/novac/src/index.js +28 -0
  148. package/novac/src/runtime/stdlib.js +244 -0
  149. package/package.json +6 -3
  150. package/scripts/update-bin.js +0 -0
  151. package/src/core/bstd.js +838 -362
  152. package/src/core/executor.js +2578 -170
  153. package/src/core/lexer.js +502 -54
  154. package/src/core/nova_builtins.js +21 -3
  155. package/src/core/parser.js +413 -72
  156. package/src/core/types.js +30 -2
  157. package/src/index.js +0 -0
  158. package/examples/example-project/README.md +0 -3
  159. package/examples/example-project/src/main.nova +0 -3
  160. package/src/core/environment.js +0 -0
  161. /package/{examples/example-project/bin/example-project.nv → novac/node_modules/node-addon-api/nothing.c} +0 -0
@@ -0,0 +1,1026 @@
1
+ /**
2
+ * Nova Lexer v2
3
+ * Removed: backtick template literals
4
+ * Added: f"..." strings, full operator set, type/struct/interface/enum/trait keywords,
5
+ * numeric separators (1_000_000), &&= ||= ??= logical-assign, get/set keywords,
6
+ * multi-char escape sequences, \ESCAPE{arg1,arg2} argument escapes,
7
+ * 40+ built-in escapes including \color{name}, \textType{type}, full ANSI terminal escapes
8
+ */
9
+
10
+ // ── ANSI Terminal Escape Engine ────────────────────────────────────────────
11
+ // All escape sequences resolve to raw strings at lex time.
12
+ // Custom escapes registered via #register escape NAME pattern
13
+ // or the `register_escape` Classic statement.
14
+
15
+ // Foreground colors (standard 8 + bright 8 = 16)
16
+ const FG = {
17
+ black: '\x1b[30m', red: '\x1b[31m', green: '\x1b[32m',
18
+ yellow: '\x1b[33m', blue: '\x1b[34m', magenta: '\x1b[35m',
19
+ cyan: '\x1b[36m', white: '\x1b[37m',
20
+ // bright
21
+ brightblack: '\x1b[90m', brightred: '\x1b[91m', brightgreen: '\x1b[92m',
22
+ brightyellow: '\x1b[93m', brightblue: '\x1b[94m', brightmagenta: '\x1b[95m',
23
+ brightcyan: '\x1b[96m', brightwhite: '\x1b[97m',
24
+ // aliases
25
+ grey: '\x1b[90m', gray: '\x1b[90m', orange: '\x1b[33m',
26
+ pink: '\x1b[95m', purple: '\x1b[35m', violet: '\x1b[35m',
27
+ lime: '\x1b[92m', teal: '\x1b[36m', navy: '\x1b[34m',
28
+ maroon: '\x1b[31m', olive: '\x1b[33m', aqua: '\x1b[36m',
29
+ silver: '\x1b[37m', gold: '\x1b[33m', indigo: '\x1b[34m',
30
+ coral: '\x1b[91m', salmon: '\x1b[91m', khaki: '\x1b[33m',
31
+ lavender: '\x1b[95m', turquoise: '\x1b[96m', crimson: '\x1b[31m',
32
+ ivory: '\x1b[97m', beige: '\x1b[97m', tan: '\x1b[33m',
33
+ sienna: '\x1b[31m', amber: '\x1b[33m', emerald: '\x1b[32m',
34
+ ruby: '\x1b[31m', sapphire: '\x1b[34m', jade: '\x1b[32m',
35
+ default: '\x1b[39m',
36
+ };
37
+
38
+ // Background colors
39
+ const BG = {
40
+ black: '\x1b[40m', red: '\x1b[41m', green: '\x1b[42m',
41
+ yellow: '\x1b[43m', blue: '\x1b[44m', magenta: '\x1b[45m',
42
+ cyan: '\x1b[46m', white: '\x1b[47m',
43
+ brightblack: '\x1b[100m',brightred: '\x1b[101m',brightgreen: '\x1b[102m',
44
+ brightyellow: '\x1b[103m',brightblue: '\x1b[104m',brightmagenta: '\x1b[105m',
45
+ brightcyan: '\x1b[106m',brightwhite: '\x1b[107m',
46
+ grey: '\x1b[100m',gray: '\x1b[100m',orange: '\x1b[43m',
47
+ pink: '\x1b[105m',purple: '\x1b[45m', default: '\x1b[49m',
48
+ };
49
+
50
+ // SGR text style codes \textType{type}
51
+ const TEXT_TYPES = {
52
+ reset: '\x1b[0m', bold: '\x1b[1m', dim: '\x1b[2m',
53
+ italic: '\x1b[3m', underline: '\x1b[4m', blink: '\x1b[5m',
54
+ rapidblink: '\x1b[6m', reverse: '\x1b[7m', hidden: '\x1b[8m',
55
+ strikethrough: '\x1b[9m',
56
+ // reset variants
57
+ resetbold: '\x1b[21m', resetdim: '\x1b[22m', resetitalic: '\x1b[23m',
58
+ resetunderline:'\x1b[24m', resetblink: '\x1b[25m', resetreverse: '\x1b[27m',
59
+ resethidden: '\x1b[28m', resetstrike: '\x1b[29m',
60
+ // extra
61
+ overline: '\x1b[53m', resetoverline: '\x1b[55m',
62
+ doubleunderline:'\x1b[21m',
63
+ // aliases
64
+ b: '\x1b[1m', i: '\x1b[3m', u: '\x1b[4m',
65
+ s: '\x1b[9m', r: '\x1b[0m', inv: '\x1b[7m',
66
+ };
67
+
68
+ // All built-in named escape sequences (no args)
69
+ // \name → value
70
+ const ESCAPE_SEQUENCES = {
71
+ // classic single-char
72
+ 'n': '\n', 't': '\t', 'r': '\r', '\\': '\\',
73
+ "'": "'", '"': '"', '0': '\0', 'a': '\x07',
74
+ 'b': '\x08','v': '\x0B','f': '\x0C','e': '\x1b',
75
+ // ANSI reset / clear
76
+ 'reset': '\x1b[0m',
77
+ 'clr': '\x1b[0m',
78
+ // text styles (no-arg shortcuts)
79
+ 'bold': '\x1b[1m',
80
+ 'dim': '\x1b[2m',
81
+ 'italic': '\x1b[3m',
82
+ 'underline': '\x1b[4m',
83
+ 'blink': '\x1b[5m',
84
+ 'rapidblink': '\x1b[6m',
85
+ 'reverse': '\x1b[7m',
86
+ 'hidden': '\x1b[8m',
87
+ 'strike': '\x1b[9m',
88
+ 'overline': '\x1b[53m',
89
+ 'resetbold': '\x1b[22m',
90
+ 'resetdim': '\x1b[22m',
91
+ 'resetitalic':'\x1b[23m',
92
+ 'resetul': '\x1b[24m',
93
+ 'resetblink': '\x1b[25m',
94
+ 'resetrev': '\x1b[27m',
95
+ // cursor movement (fixed)
96
+ 'home': '\x1b[H',
97
+ 'cls': '\x1b[2J', // clear screen
98
+ 'clearline': '\x1b[2K', // clear current line
99
+ 'clearright': '\x1b[0K', // clear line right of cursor
100
+ 'clearleft': '\x1b[1K', // clear line left of cursor
101
+ 'cleardown': '\x1b[0J', // clear screen below cursor
102
+ 'clearup': '\x1b[1J', // clear screen above cursor
103
+ 'savecursor': '\x1b[s', // save cursor position
104
+ 'restorecursor':'\x1b[u', // restore cursor position
105
+ 'hidecursor': '\x1b[?25l', // hide cursor
106
+ 'showcursor': '\x1b[?25h', // show cursor
107
+ 'cursorup': '\x1b[1A',
108
+ 'cursordown': '\x1b[1B',
109
+ 'cursorright':'\x1b[1C',
110
+ 'cursorleft': '\x1b[1D',
111
+ 'cursorbol': '\x1b[0G', // cursor to beginning of line
112
+ // terminal modes
113
+ 'altscreen': '\x1b[?1049h', // enter alternate screen buffer
114
+ 'mainscreen': '\x1b[?1049l', // exit alternate screen buffer
115
+ 'wrap': '\x1b[?7h', // enable line wrap
116
+ 'nowrap': '\x1b[?7l', // disable line wrap
117
+ // foreground color shortcuts
118
+ 'black': '\x1b[30m', 'red': '\x1b[31m', 'green': '\x1b[32m',
119
+ 'yellow': '\x1b[33m', 'blue': '\x1b[34m', 'magenta': '\x1b[35m',
120
+ 'cyan': '\x1b[36m', 'white': '\x1b[37m',
121
+ // background shortcuts
122
+ 'bgblack': '\x1b[40m', 'bgred': '\x1b[41m', 'bggreen': '\x1b[42m',
123
+ 'bgyellow': '\x1b[43m', 'bgblue': '\x1b[44m', 'bgmagenta':'\x1b[45m',
124
+ 'bgcyan': '\x1b[46m', 'bgwhite': '\x1b[47m',
125
+ // misc chars
126
+ 'bell': '\x07',
127
+ 'tab': '\t',
128
+ 'cr': '\r',
129
+ 'lf': '\n',
130
+ 'crlf': '\r\n',
131
+ 'null': '\0',
132
+ 'esc': '\x1b',
133
+ 'space': ' ',
134
+ 'nbsp': '\u00A0',
135
+ 'zwsp': '\u200B', // zero-width space
136
+ 'zwnj': '\u200C',
137
+ 'zwj': '\u200D',
138
+ // box-drawing
139
+ 'hline': '\u2500', // ─
140
+ 'vline': '\u2502', // │
141
+ 'tlcorner': '\u250C', // ┌
142
+ 'trcorner': '\u2510', // ┐
143
+ 'blcorner': '\u2514', // └
144
+ 'brcorner': '\u2518', // ┘
145
+ 'ttee': '\u252C', // ┬
146
+ 'btee': '\u2534', // ┴
147
+ 'ltee': '\u251C', // ├
148
+ 'rtee': '\u2524', // ┤
149
+ 'cross': '\u253C', // ┼
150
+ // double box
151
+ 'dhline': '\u2550', // ═
152
+ 'dvline': '\u2551', // ║
153
+ 'dtlcorner': '\u2554', // ╔
154
+ 'dtrcorner': '\u2557', // ╗
155
+ 'dblcorner': '\u255A', // ╚
156
+ 'dbrcorner': '\u255D', // ╝
157
+ // arrows
158
+ 'uarr': '\u2191', // ↑
159
+ 'darr': '\u2193', // ↓
160
+ 'larr': '\u2190', // ←
161
+ 'rarr': '\u2192', // →
162
+ 'udarr': '\u2195', // ↕
163
+ 'lrarr': '\u2194', // ↔
164
+ // misc unicode
165
+ 'check': '\u2713', // ✓
166
+ 'cross2': '\u2717', // ✗
167
+ 'bullet': '\u2022', // •
168
+ 'ellipsis': '\u2026', // …
169
+ 'degree': '\u00B0', // °
170
+ 'copyright': '\u00A9', // ©
171
+ 'registered': '\u00AE', // ®
172
+ 'trademark': '\u2122', // ™
173
+ 'section': '\u00A7', // §
174
+ 'dagger': '\u2020', // †
175
+ 'middot': '\u00B7', // ·
176
+ 'endash': '\u2013', // –
177
+ 'emdash': '\u2014', // —
178
+ };
179
+
180
+ // Argument-taking built-in escape handlers: \NAME{arg1, arg2, ...} → string
181
+ // Each is a function (args: string[]) → string
182
+ const ESCAPE_HANDLERS = {
183
+ // \color{name} — set foreground color by name
184
+ color: (args) => {
185
+ const raw = (args[0] || '').trim();
186
+ // reassemble if split on inner comma: rgb:255, 100, 0 → rgb:255,100,0
187
+ const name = (args.length > 1 && (raw.startsWith('rgb:') || raw.startsWith('256:')))
188
+ ? args.join(',').toLowerCase().replace(/\s/g,'')
189
+ : raw.toLowerCase();
190
+ if (FG[name]) return FG[name];
191
+ // 256-color: \color{256:N}
192
+ if (name.startsWith('256:')) return `\x1b[38;5;${name.slice(4)}m`;
193
+ // RGB: \color{rgb:R,G,B}
194
+ if (name.startsWith('rgb:')) { const [r,g,b] = name.slice(4).split(','); return `\x1b[38;2;${r};${g};${b}m`; }
195
+ // hex: \color{#RRGGBB}
196
+ if (name.startsWith('#') && name.length === 7) {
197
+ const r = parseInt(name.slice(1,3),16), g = parseInt(name.slice(3,5),16), b = parseInt(name.slice(5,7),16);
198
+ return `\x1b[38;2;${r};${g};${b}m`;
199
+ }
200
+ return FG.default;
201
+ },
202
+ // \bg{name} — set background color by name
203
+ bg: (args) => {
204
+ const raw = (args[0] || '').trim();
205
+ const name = (args.length > 1 && (raw.startsWith('rgb:') || raw.startsWith('256:')))
206
+ ? args.join(',').toLowerCase().replace(/\s/g,'')
207
+ : raw.toLowerCase();
208
+ if (BG[name]) return BG[name];
209
+ if (name.startsWith('256:')) return `\x1b[48;5;${name.slice(4)}m`;
210
+ if (name.startsWith('rgb:')) { const [r,g,b] = name.slice(4).split(','); return `\x1b[48;2;${r};${g};${b}m`; }
211
+ if (name.startsWith('#') && name.length === 7) {
212
+ const r = parseInt(name.slice(1,3),16), g = parseInt(name.slice(3,5),16), b = parseInt(name.slice(5,7),16);
213
+ return `\x1b[48;2;${r};${g};${b}m`;
214
+ }
215
+ return BG.default;
216
+ },
217
+ // \textType{type} — set text style / SGR attribute
218
+ textType: (args) => {
219
+ const t = (args[0] || '').toLowerCase().trim();
220
+ if (TEXT_TYPES[t]) return TEXT_TYPES[t];
221
+ // raw SGR code: \textType{33}
222
+ if (/^\d+$/.test(t)) return `\x1b[${t}m`;
223
+ return '';
224
+ },
225
+ // \sgr{n} — raw SGR escape: \x1b[Nm
226
+ sgr: (args) => args.map(a => `\x1b[${a.trim()}m`).join(''),
227
+ // \cursor{up|down|left|right, N} — move cursor N cells
228
+ cursor: (args) => {
229
+ const dir = (args[0] || '').toLowerCase().trim();
230
+ const n = parseInt(args[1]) || 1;
231
+ const codes = { up:'A', down:'B', right:'C', left:'D', nextline:'E', prevline:'F', col:'G', row:'d' };
232
+ return codes[dir] ? `\x1b[${n}${codes[dir]}` : '';
233
+ },
234
+ // \pos{row,col} — move cursor to absolute position
235
+ pos: (args) => {
236
+ const row = parseInt(args[0]) || 1, col = parseInt(args[1]) || 1;
237
+ return `\x1b[${row};${col}H`;
238
+ },
239
+ // \erase{mode} — erase display or line
240
+ erase: (args) => {
241
+ const mode = (args[0] || 'screen').toLowerCase().trim();
242
+ const m = { screen:'2J', 'screen-down':'0J', 'screen-up':'1J', line:'2K', 'line-right':'0K', 'line-left':'1K' };
243
+ return m[mode] ? `\x1b[${m[mode]}` : '';
244
+ },
245
+ // \scroll{up|down, N} — scroll terminal
246
+ scroll: (args) => {
247
+ const dir = (args[0] || 'up').toLowerCase().trim();
248
+ const n = parseInt(args[1]) || 1;
249
+ return dir === 'down' ? `\x1b[${n}T` : `\x1b[${n}S`;
250
+ },
251
+ // \unicode{codepoint} — literal unicode char: \unicode{U+1F600} or \unicode{128512}
252
+ unicode: (args) => {
253
+ const raw = (args[0] || '').trim().replace(/^U\+/i,'');
254
+ const cp = parseInt(raw, 16);
255
+ return isNaN(cp) ? '' : String.fromCodePoint(cp);
256
+ },
257
+ // \hex{HH HH ...} — raw hex bytes
258
+ hex: (args) => args.map(a => String.fromCharCode(parseInt(a.trim(),16))).join(''),
259
+ // \repeat{char, N} — repeat a character N times
260
+ repeat: (args) => {
261
+ const ch = args[0] || ' ', n = parseInt(args[1]) || 1;
262
+ return ch.repeat(n);
263
+ },
264
+ // \pad{text, width, char} — pad text to width
265
+ pad: (args) => {
266
+ const text = args[0] || '', width = parseInt(args[1]) || 0, ch = args[2] || ' ';
267
+ return text.padEnd(width, ch);
268
+ },
269
+ // \padl{text, width, char} — left-pad
270
+ padl: (args) => {
271
+ const text = args[0] || '', width = parseInt(args[1]) || 0, ch = args[2] || ' ';
272
+ return text.padStart(width, ch);
273
+ },
274
+ // \upper{text} — uppercase at lex time
275
+ upper: (args) => (args[0] || '').toUpperCase(),
276
+ // \lower{text} — lowercase at lex time
277
+ lower: (args) => (args[0] || '').toLowerCase(),
278
+ // \link{url, text} — OSC 8 hyperlink (supported in iTerm2, Kitty, WezTerm)
279
+ link: (args) => `\x1b]8;;${args[0] || ''}\x1b\\${args[1] || args[0] || ''}\x1b]8;;\x1b\\`,
280
+ // \title{text} — set terminal window title
281
+ title: (args) => `\x1b]0;${args[0] || ''}\x07`,
282
+ // \notify{title, body} — terminal desktop notification (iTerm2/Kitty)
283
+ notify: (args) => `\x1b]9;${args[0] || ''}${args[1] ? ' ' + args[1] : ''}\x07`,
284
+ // \img{path, width, height} — Kitty/iTerm2 inline image protocol stub
285
+ img: (args) => `\x1b]1337;File=inline=1;width=${args[1]||'auto'};height=${args[2]||'auto'}:${args[0]||''}\x07`,
286
+ // \256fg{N} — 256-color foreground by number
287
+ '256fg': (args) => `\x1b[38;5;${parseInt(args[0])||0}m`,
288
+ // \256bg{N} — 256-color background by number
289
+ '256bg': (args) => `\x1b[48;5;${parseInt(args[0])||0}m`,
290
+ // \rgbfg{R,G,B} — true-color foreground
291
+ rgbfg: (args) => `\x1b[38;2;${parseInt(args[0])||0};${parseInt(args[1])||0};${parseInt(args[2])||0}m`,
292
+ // \rgbbg{R,G,B} — true-color background
293
+ rgbbg: (args) => `\x1b[48;2;${parseInt(args[0])||0};${parseInt(args[1])||0};${parseInt(args[2])||0}m`,
294
+ };
295
+
296
+ // Global custom escape registry — populated by #register escape or register_escape statement
297
+ const CUSTOM_ESCAPES = new Map();
298
+
299
+ /**
300
+ * processEscape(name, args, source, line, col)
301
+ * Core escape resolver used by readString() and fstring().
302
+ * name - escape name (multi-char allowed)
303
+ * args - array of string arguments (empty if no {})
304
+ * Returns the resolved string value.
305
+ */
306
+ function processEscape(name, args, line, col) {
307
+ // 1. custom registry first (allows overriding builtins)
308
+ if (CUSTOM_ESCAPES.has(name)) {
309
+ const handler = CUSTOM_ESCAPES.get(name);
310
+ if (typeof handler === 'function') return handler(args);
311
+ if (typeof handler === 'string') return handler;
312
+ }
313
+ // 2. argument-taking built-ins
314
+ if (ESCAPE_HANDLERS[name]) return ESCAPE_HANDLERS[name](args);
315
+ // 3. no-arg built-ins
316
+ if (ESCAPE_SEQUENCES[name] !== undefined) return ESCAPE_SEQUENCES[name];
317
+ // 4. raw octal: \123
318
+ if (/^[0-7]+$/.test(name)) return String.fromCharCode(parseInt(name, 8));
319
+ // 5. raw hex: \xFF or \x1b
320
+ if (/^x[0-9a-fA-F]{1,4}$/.test(name)) return String.fromCharCode(parseInt(name.slice(1), 16));
321
+ // 6. unicode codepoint: \uXXXX \UXXXXXXXX
322
+ if (/^u[0-9a-fA-F]{4}$/.test(name)) return String.fromCodePoint(parseInt(name.slice(1), 16));
323
+ if (/^U[0-9a-fA-F]{8}$/.test(name)) return String.fromCodePoint(parseInt(name.slice(1), 16));
324
+ // 7. unknown — pass through literally (backslash + name)
325
+ return '\\' + name;
326
+ }
327
+
328
+ const HOLLAND = {
329
+ million: 1e6,
330
+ billion: 1e9,
331
+ trillion: 1e12,
332
+ quadrillion: 1e15,
333
+ quintillion: 1e18,
334
+ sextillion: 1e21,
335
+ septillion: 1e24,
336
+ octillion: 1e27,
337
+ nonillion: 1e30,
338
+ decillion: 1e33,
339
+ googol: 1e100,
340
+ yotta: 1e24,
341
+ zetta: 1e21,
342
+ exa: 1e18,
343
+ peta: 1e15,
344
+ tera: 1e12,
345
+ giga: 1e9,
346
+ mega: 1e6,
347
+ kilo: 1e3,
348
+ hecto: 1e2,
349
+ deca: 1e1,
350
+ deci: 1e-1,
351
+ centi: 1e-2,
352
+ milli: 1e-3,
353
+ micro: 1e-6,
354
+ nano: 1e-9,
355
+ pico: 1e-12,
356
+ femto: 1e-15,
357
+ atto: 1e-18,
358
+ zepto: 1e-21,
359
+ yocto: 1e-24,
360
+ };
361
+ const LITERALS = {
362
+ true: Symbol('NOVA_TRUE'),
363
+ false: Symbol('NOVA_FALSE'),
364
+ null: Symbol('NOVA_NULL'),
365
+ nstr: '',
366
+ nfunc: () => Symbol('NOVA_NULL'),
367
+ };
368
+
369
+ // ── Dynamic / Meta Variables ────────────────────────────────────────────────
370
+ // These identifiers are intercepted at lex time and emitted as DVAR tokens.
371
+ // The executor resolves them to their runtime values.
372
+ // `default` is a special sentinel: when passed as a function argument, the
373
+ // parameter's declared default value is used instead.
374
+ const DYNAMIC_VARS = new Set([
375
+ 'default', // use parameter's default value when passed as arg
376
+ '__line__', // current source line number
377
+ '__col__', // current source column number
378
+ '__file__', // absolute path of current file (or "<repl>")
379
+ '__dirname__', // directory of current file
380
+ '__func__', // name of the enclosing function (or "<top>")
381
+ '__date__', // ISO date string (YYYY-MM-DD) at runtime
382
+ '__time__', // ISO time string (HH:MM:SS) at runtime
383
+ '__timestamp__', // Unix timestamp in ms at runtime
384
+ '__datetime__', // Full ISO datetime string at runtime
385
+ '__version__', // Nova runtime version
386
+ '__pid__', // process.pid
387
+ '__platform__', // process.platform
388
+ '__arch__', // os.arch()
389
+ '__argv__', // process.argv
390
+ '__env__', // full process.env object
391
+ '__scope__', // reference to current scope
392
+ '__caller__', // name of calling function (one level up)
393
+ '__module__', // current module name (file stem or "<repl>")
394
+ '__namespace__', // active namespace name or null
395
+ '__random__', // random float 0..1 (regenerated each read)
396
+ '__uuid__', // a fresh UUID each read
397
+ '__iter__', // current loop iteration index (-1 outside loops)
398
+ '__stack__', // simplified call stack as array
399
+ ]);
400
+
401
+ // Keywords that are ONLY valid inside an @classic { } block.
402
+ // Outside @classic these identifiers are treated as plain IDENTIFIER tokens.
403
+ const CLASSIC_KEYWORDS = new Set([
404
+ 'foreach','temp','keep','echo','gear','engage','sandbox','infer','addto',
405
+ 'macro','snippet','defunc','lambda','compose','partial',
406
+ 'backup','retrieve','describe','classify','rate',
407
+ 'using','unuse',
408
+ 'lend','sstream','declare','session','enter','resu',
409
+ 'warn','info','skip','end','clear','time','keyfunc',
410
+ 'print','log','println','logln','expt','option','env','hello','eval',
411
+ 'register_escape',
412
+ ]);
413
+
414
+ const KEYWORDS = new Set([
415
+ // core
416
+ 'var','let','const','class','if','else','for','repeat','unless','until',
417
+ 'throw','try','catch','finally','func','function','return','give', 'yield', 'goback',
418
+ 'async','await','do','while', 'block',
419
+ // control
420
+ 'switch','case','default','break','continue',
421
+ 'of','each','match','when','where','as',
422
+ // meta
423
+ 'assert','emit','on','new','extends', 'link',
424
+ // types
425
+ 'type','struct','interface','enum','trait','impl',
426
+ // accessors
427
+ 'get','set',
428
+ // smart variable modifiers
429
+ 'setter','getter','frozen','lazy','tracked','nonull','once',
430
+ // numeric constraint types
431
+ 'fnum','fint',
432
+ // classic compatibility block
433
+ 'classic',
434
+ // misc
435
+ 'import', 'import_builtin', 'from', 'export', 'default', 'namespace',
436
+ 'guard','when','with','loop','wait','run',
437
+ // http / server
438
+ 'server', 'fetch',
439
+ 'post', 'put', 'delete', 'patch', 'head', 'options',
440
+ // nvk — platform/system namespace (NOT keywords; listed here as reference only)
441
+ // nvk.notify, nvk.toast, nvk.vibrate, nvk.clipboard, nvk.camera,
442
+ // nvk.share, nvk.open, nvk.cpu, nvk.mem, nvk.hostname, nvk.uptime,
443
+ // nvk.pid, nvk.arch, nvk.platform, nvk.osPlatform, nvk.userInfo,
444
+ // nvk.network, nvk.load, nvk.tmpDir, nvk.pathDir, nvk.pathBase,
445
+ // nvk.pathExt, nvk.sha256, nvk.randomBytes, nvk.parseURL, nvk.uuid,
446
+ // nvk.readFile, nvk.writeFile, nvk.createFile, nvk.deleteFile,
447
+ // nvk.listFiles, nvk.execFile, nvk.exists, nvk.sh, nvk.exec,
448
+ // nvk.term, nvk.banner, nvk.warn, nvk.info
449
+ ]);
450
+
451
+ const { CustomError, formatError } = require('./error');
452
+
453
+ // Helper: split a comma-separated arg string respecting nested {}, (), []
454
+ function splitArgs(str) {
455
+ const args = [];
456
+ let cur = '', depth = 0;
457
+ for (const ch of str) {
458
+ if (ch === '{' || ch === '(' || ch === '[') { depth++; cur += ch; }
459
+ else if (ch === '}' || ch === ')' || ch === ']') { depth--; cur += ch; }
460
+ else if (ch === ',' && depth === 0) { args.push(cur.trim()); cur = ''; }
461
+ else cur += ch;
462
+ }
463
+ if (cur.trim()) args.push(cur.trim());
464
+ return args;
465
+ }
466
+
467
+
468
+ // Higher precedence = binds tighter.
469
+ const OPERATORS = new Map([
470
+ // assignment (0)
471
+ ['$', { precedence: 11, isUnary: true }],
472
+ ['=', { precedence: 0 }],
473
+ ['=>', { precedence: 0 }],
474
+ ['|>', { precedence: 0 }],
475
+ ['+=', { precedence: 0 }],
476
+ ['-=', { precedence: 0 }],
477
+ ['*=', { precedence: 0 }],
478
+ ['/=', { precedence: 0 }],
479
+ ['%=', { precedence: 0 }],
480
+ ['**=', { precedence: 0 }],
481
+ ['&&=', { precedence: 0 }],
482
+ ['||=', { precedence: 0 }],
483
+ ['??=', { precedence: 0 }],
484
+ // logical (2-4)
485
+ ['||', { precedence: 2 }],
486
+ ['??', { precedence: 3 }],
487
+ ['&&', { precedence: 4 }],
488
+ // bitwise (5-7)
489
+ ['|', { precedence: 5 }],
490
+ ['^', { precedence: 6 }],
491
+ ['&', { precedence: 7 }],
492
+ // equality (8)
493
+ ['===', { precedence: 8 }],
494
+ ['!==', { precedence: 8 }],
495
+ ['==', { precedence: 8 }],
496
+ ['!=', { precedence: 8 }],
497
+ // relational (9)
498
+ ['<=', { precedence: 9 }],
499
+ ['>=', { precedence: 9 }],
500
+ ['<', { precedence: 9 }],
501
+ ['>', { precedence: 9 }],
502
+ // shift / range (10)
503
+ ['>>>', { precedence: 10 }],
504
+ ['>>', { precedence: 10 }],
505
+ ['<<', { precedence: 10 }],
506
+ ['..', { precedence: 10 }],
507
+ // additive (11)
508
+ ['+', { precedence: 11, isUnary: true }],
509
+ ['-', { precedence: 11, isUnary: true }],
510
+ // multiplicative (12)
511
+ ['*', { precedence: 12, isUnary: true }],
512
+ ['/', { precedence: 12 }],
513
+ ['%', { precedence: 12 }],
514
+ // exponent right-assoc (13)
515
+ ['**', { precedence: 13, rightAssoc: true }],
516
+ // unary (14)
517
+ ['!', { precedence: 14, isUnary: true }],
518
+ ['~', { precedence: 14, isUnary: true }],
519
+ ['delete',{ precedence: 14, isUnary: true }],
520
+ // postfix (15)
521
+ ['++', { precedence: 15, isPostfix: true, isUnary: true }],
522
+ ['--', { precedence: 15, isPostfix: true, isUnary: true }],
523
+ ['?.', { precedence: 15 }],
524
+ // spread / chain / namespace
525
+ ['...', { precedence: 0, isUnary: true }],
526
+ ['::', { precedence: 15 }], // namespace access
527
+ ['#:', { precedence: 15 }], // map inspect
528
+ ['$:', { precedence: 15 }], // stream access
529
+ // keyword operators — logical
530
+ ['and', { precedence: 4 }],
531
+ ['or', { precedence: 2 }],
532
+ ['not', { precedence: 14, isUnary: true }],
533
+ ['typeof', { precedence: 14, isUnary: true }],
534
+ ['void', { precedence: 14, isUnary: true }],
535
+ ['instanceof', { precedence: 8 }],
536
+ ['in', { precedence: 8 }],
537
+ // Nova Classic infix operators — extra
538
+ ['pow', { precedence: 13, rightAssoc: true }], // like **
539
+ ['bigger', { precedence: 9 }],
540
+ ['smaller', { precedence: 9 }],
541
+ ['equals', { precedence: 8 }],
542
+ // Nova Classic infix operators
543
+ ['xor', { precedence: 3 }],
544
+ ['nand', { precedence: 4 }],
545
+ ['nor', { precedence: 2 }],
546
+ ['xnor', { precedence: 3 }],
547
+ ['is', { precedence: 8 }],
548
+ ['isnt', { precedence: 8 }],
549
+ ['istypeof', { precedence: 8 }],
550
+ ['matches', { precedence: 8 }],
551
+ ['between', { precedence: 8 }],
552
+ ['step', { precedence: 10 }],
553
+ ['zip', { precedence: 10 }],
554
+ ['intersect', { precedence: 7 }],
555
+ ['diff_arr', { precedence: 7 }],
556
+ ['union', { precedence: 5 }],
557
+ ['avg', { precedence: 11 }],
558
+ ['diff', { precedence: 11 }],
559
+ ['ratio', { precedence: 12 }],
560
+ ['mult_of', { precedence: 12 }],
561
+ ['gcd', { precedence: 12 }],
562
+ ['lcm', { precedence: 12 }],
563
+ ['pad_start', { precedence: 11 }],
564
+ ['pad_end', { precedence: 11 }],
565
+ ['cmp', { precedence: 8 }],
566
+ ['equals_ignore',{ precedence: 8 }],
567
+ ['extend', { precedence: 5 }],
568
+ ['concat', { precedence: 11 }],
569
+ ['index', { precedence: 14 }],
570
+ ]);
571
+
572
+ const PUNCTUATION = new Set(['(',')','{','}','[',']',',','.',':', ';','?','@','#', '\\']);
573
+ // Note: '...' is already in OPERATORS; '::' as two-char operator below
574
+
575
+ class Lexer {
576
+ constructor(source, classicMode = false) {
577
+ this.source = source;
578
+ this.position = 0;
579
+ this.line = 1;
580
+ this.column = 1;
581
+ this.tokens = [];
582
+ this.definitions = new Map();
583
+ this.isSkipping = false;
584
+ this.classicMode = classicMode; // true when tokenizing inside @classic { }
585
+ }
586
+
587
+ // ── preprocessor ──
588
+ readIdentifier() {
589
+ let v = '';
590
+ while (!this.isAtEnd() && this.isAlphaNumeric(this.peek())) v += this.advance();
591
+ return v;
592
+ }
593
+ readRestOfLine() {
594
+ let v = '';
595
+ while (!this.isAtEnd() && this.peek() !== '\n') v += this.advance();
596
+ return v.trim();
597
+ }
598
+ skipLine() {
599
+ while (!this.isAtEnd() && this.peek() !== '\n') this.advance();
600
+ if (this.peek() === '\n') { this.line++; this.column = 1; this.position++; }
601
+ }
602
+ handleDirective() {
603
+ this.advance();
604
+ while (this.isWhitespace(this.peek())) this.advance();
605
+ let shouldSkip = true;
606
+ const dirName = this.readIdentifier();
607
+ while (this.isWhitespace(this.peek())) this.advance();
608
+ const dirArg = this.readIdentifier();
609
+ switch (dirName) {
610
+ case 'define': {
611
+ if (dirArg) {
612
+ let defVal = this.readRestOfLine() ?? true;
613
+ if (defVal !== true && typeof defVal === 'string') {
614
+ const trimmed = defVal.trim();
615
+ // Bare word (no spaces, quotes, operators) -> wrap as quoted string for injection
616
+ if (/^[A-Za-z_][A-Za-z0-9_]*$/.test(trimmed)) {
617
+ defVal = '"' + trimmed + '"';
618
+ }
619
+ }
620
+ this.definitions.set(dirArg, defVal);
621
+ }
622
+ break;
623
+ }
624
+ case 'undef': if (dirArg) this.definitions.delete(dirArg); break;
625
+ case 'register': {
626
+ if (dirArg === 'operator') {
627
+ const name = this.readIdentifier();
628
+ OPERATORS.set(name, JSON.parse(this.readRestOfLine()));
629
+ OPERATORS.get(name).custom = true;
630
+ } else if (dirArg === 'escape') {
631
+ // #register escape NAME replacement_string_or_function
632
+ const escapeName = this.readIdentifier();
633
+ const rest = this.readRestOfLine().trim();
634
+ // If rest is a quoted string, use as literal replacement
635
+ if ((rest.startsWith('"') && rest.endsWith('"')) ||
636
+ (rest.startsWith("'") && rest.endsWith("'"))) {
637
+ CUSTOM_ESCAPES.set(escapeName, rest.slice(1, -1)
638
+ .replace(/\\n/g, '\n').replace(/\\t/g, '\t')
639
+ .replace(/\\r/g, '\r').replace(/\\x1b/g, '\x1b'));
640
+ } else if (rest.startsWith('\\x') || rest.startsWith('\\u') || rest.startsWith('\\e')) {
641
+ // raw escape literal in the directive value
642
+ CUSTOM_ESCAPES.set(escapeName, rest.replace(/\\x([0-9a-fA-F]{2})/g, (_,h) => String.fromCharCode(parseInt(h,16)))
643
+ .replace(/\\u([0-9a-fA-F]{4})/g, (_,h) => String.fromCodePoint(parseInt(h,16)))
644
+ .replace(/\\e/g, '\x1b'));
645
+ } else {
646
+ // treat as a literal string value
647
+ CUSTOM_ESCAPES.set(escapeName, rest);
648
+ }
649
+ shouldSkip = false; // readRestOfLine already consumed the line
650
+ }
651
+ break;
652
+ }
653
+ case 'inject': this.tokens.push(JSON.parse(this.readRestOfLine())); break;
654
+ case 'ifdef': if (!this.isSkipping) this.isSkipping = !this.definitions.has(dirArg); break;
655
+ case 'ifndef': if (!this.isSkipping) this.isSkipping = this.definitions.has(dirArg); break;
656
+ case 'endif': this.isSkipping = false; break;
657
+ default: this.addToken('LITERAL', dirArg); shouldSkip = false; break;
658
+ }
659
+ if (shouldSkip) this.skipLine();
660
+ }
661
+
662
+ // ── main tokenize ──
663
+ tokenize() {
664
+ while (!this.isAtEnd()) {
665
+ const char = this.peek();
666
+ if (char === '#') { this.handleDirective(); continue; }
667
+ if (this.isSkipping) {
668
+ if (char === '\n') { this.line++; this.column = 1; }
669
+ else if (char === '\t') { this.column += 3; }
670
+ else { this.column++; }
671
+ this.position++;
672
+ continue;
673
+ }
674
+ this.advance();
675
+ if (this.isWhitespace(char)) { if (char === '\t') this.column += 3; continue; }
676
+ if (char === '\n') { this.line++; this.column = 1; continue; }
677
+ // comments
678
+ if (char === '/') {
679
+ const next = this.peek();
680
+ // /?/ executable comment: runs Nova code inline
681
+ if (next === '?') {
682
+ this.advance();
683
+ if (this.peek() === '/') {
684
+ this.advance();
685
+ let expr = '';
686
+ while (!this.isAtEnd() && this.peek() !== '\n') expr += this.advance();
687
+ // Inject as a special token for the parser to eval
688
+ this.addToken('EXEC_COMMENT', expr.trim(), this.column);
689
+ continue;
690
+ }
691
+ }
692
+ if (next === '!' || next === '/') { this.advance(); this.skipComment(); continue; }
693
+ if (next === '*') { this.advance(); this.skipBlockComment(); continue; }
694
+ }
695
+ // strings
696
+ if (char === '"' || char === "'") { this.readString(char); continue; }
697
+ // __ident__
698
+ if (char === '_' && this.peek() === '_') { this.advance(); this.getDoubleString('_','_'); continue; }
699
+ // numbers
700
+ if (this.isDigit(char)) { this.readNumber(char); continue; }
701
+ // identifiers / keywords / f-strings
702
+ if (this.isAlpha(char)) {
703
+ let ident = char;
704
+ while (this.isAlphaNumeric(this.peek())) ident += this.advance();
705
+ // f-string prefix
706
+ if (ident === 'f' && (this.peek() === '"' || this.peek() === "'")) {
707
+ this.fstring(this.advance()); continue;
708
+ }
709
+ // raw string prefix
710
+ if (ident === 'r' && (this.peek() === '"' || this.peek() === "'")) {
711
+ this.rstring(this.advance()); continue;
712
+ }
713
+ // URL literal: http(s)://, ftp://, ws(s)://, content://, localhost(:port)?
714
+ const col2 = this.column - ident.length - 1;
715
+ const isScheme = ['http','https','ftp','ws','wss','content'].includes(ident);
716
+ const isLocalhost = ident === 'localhost';
717
+ if (isScheme && this.peek() === ':' && this.source[this.position+1] === '/' && this.source[this.position+2] === '/') {
718
+ let url = ident;
719
+ while (!this.isAtEnd()) {
720
+ const ch = this.peek();
721
+ // stop at whitespace, newline, or unambiguous delimiters (but keep ( for query-style calls)
722
+ if (ch === ' ' || ch === '\t' || ch === '\r' || ch === '\n' || ch === ';' || ch === ',' || ch === ')' || ch === '}' || ch === ']' || ch === '"' || ch === "'") break;
723
+ // stop before ( so caller can use url(args) syntax
724
+ if (ch === '(') break;
725
+ url += this.advance();
726
+ }
727
+ this.addToken('URL', url, col2); continue;
728
+ }
729
+ if (isLocalhost && this.peek() === ':' && /[0-9]/.test(this.source[this.position+1] ?? '')) {
730
+ let url = ident;
731
+ while (!this.isAtEnd()) {
732
+ const ch = this.peek();
733
+ if (ch === ' ' || ch === '\t' || ch === '\r' || ch === '\n' || ch === ';' || ch === ',' || ch === ')' || ch === '}' || ch === ']' || ch === '"' || ch === "'" || ch === '(') break;
734
+ url += this.advance();
735
+ }
736
+ this.addToken('URL', url, col2); continue;
737
+ }
738
+ // preprocessor replacement
739
+ if (this.definitions.has(ident)) {
740
+ const r = this.definitions.get(ident);
741
+ if (r !== true) { this.injectReplacement(r); continue; }
742
+ }
743
+ this.identifier(ident); continue;
744
+ }
745
+ this.symbol(char);
746
+ }
747
+ this.addToken('EOF', null, this.column);
748
+ return this.tokens;
749
+ }
750
+
751
+ identifier(value) {
752
+ const col = this.column - 1;
753
+ if (Object.prototype.hasOwnProperty.call(LITERALS, value)) {
754
+ this.addToken('LITERAL', LITERALS[value], col);
755
+ } else if (DYNAMIC_VARS.has(value)) {
756
+ // Dynamic/meta variable — bake source location into the token so the
757
+ // executor can report exact __line__/__col__ without re-scanning.
758
+ this.tokens.push({ type: 'DVAR', value, line: this.line, column: col, srcLine: this.line, srcCol: col });
759
+ } else if (OPERATORS.has(value)) {
760
+ this.addOperatorToken(value, col);
761
+ } else if (CLASSIC_KEYWORDS.has(value)) {
762
+ // Classic keywords only become KEYWORD tokens inside @classic { } blocks.
763
+ // Outside @classic they are plain identifiers so they don't pollute the core namespace.
764
+ this.addToken(this.classicMode ? 'KEYWORD' : 'IDENTIFIER', value, col);
765
+ } else {
766
+ this.addToken(KEYWORDS.has(value) ? 'KEYWORD' : 'IDENTIFIER', value, col);
767
+ }
768
+ }
769
+
770
+ injectReplacement(str) {
771
+ this.source = str + this.source.slice(this.position);
772
+ this.position = 0; this.column = 1;
773
+ }
774
+
775
+ readNumber(firstChar) {
776
+ const col = this.column - 1;
777
+ let value = firstChar;
778
+ // hex 0x
779
+ if (firstChar === '0' && (this.peek() === 'x' || this.peek() === 'X')) {
780
+ value += this.advance();
781
+ while (/[0-9a-fA-F_]/.test(this.peek())) { const c = this.advance(); if (c !== '_') value += c; }
782
+ this.addToken('NUMBER', parseInt(value, 16), col); return;
783
+ }
784
+ // binary 0b
785
+ if (firstChar === '0' && (this.peek() === 'b' || this.peek() === 'B')) {
786
+ value += this.advance();
787
+ while (this.peek() === '0' || this.peek() === '1' || this.peek() === '_') {
788
+ const c = this.advance(); if (c !== '_') value += c;
789
+ }
790
+ this.addToken('NUMBER', parseInt(value, 2), col); return;
791
+ }
792
+ // custom base 0rBASE_NUMBER, e.g. 0r36 for base36
793
+ if (firstChar === '0' && (this.peek() === 'r' || this.peek() === 'R')) {
794
+ value += this.advance();
795
+ let baseStr = '';
796
+ while (this.isDigit(this.peek())) baseStr += this.advance();
797
+ const base = parseInt(baseStr, 10);
798
+ if (isNaN(base) || base < 2 || base > 36) throw this.error(`Invalid numeric base '${baseStr}'`);
799
+ while (true) {
800
+ const c = this.peek();
801
+ if (c === '_') { this.advance(); continue; }
802
+ const digit = parseInt(c, base);
803
+ if (isNaN(digit)) break;
804
+ value += this.advance();
805
+ }
806
+ this.addToken('NUMBER', parseInt(value.slice(2), base), col); return;
807
+ }
808
+ // nv holland 0h_NAME, name is taken its value from HOLLANDS array, like: billion, trillion, etc.
809
+ if (firstChar === '0' && (this.peek() === 'h' || this.peek() === 'H')) {
810
+ value += this.advance();
811
+ while (this.isAlpha(this.peek()) || this.peek() === '_') value += this.advance();
812
+ const name = value.slice(2).replace(/_/g, '');
813
+ if (!HOLLAND.hasOwnProperty(name)) throw this.error(`Unknown holland literal '${name}'`);
814
+ this.addToken('NUMBER', HOLLAND[name], col); return;
815
+ }
816
+ // decimal (with _ separators)
817
+ while (this.isDigit(this.peek()) || this.peek() === '_') {
818
+ const c = this.advance(); if (c !== '_') value += c;
819
+ }
820
+ if (this.peek() === '.' && this.isDigit(this.peekNext())) {
821
+ value += this.advance();
822
+ while (this.isDigit(this.peek()) || this.peek() === '_') {
823
+ const c = this.advance(); if (c !== '_') value += c;
824
+ }
825
+ }
826
+ if (this.peek() === 'e' || this.peek() === 'E') {
827
+ value += this.advance();
828
+ if (this.peek() === '+' || this.peek() === '-') value += this.advance();
829
+ while (this.isDigit(this.peek())) value += this.advance();
830
+ }
831
+ if (this.peek() === 'n') {
832
+ this.advance();
833
+ this.addToken('NUMBER', BigInt(value), col);
834
+ return;
835
+ }
836
+ // misc: support for k/m... suffixes (e.g. 10k = 10000, 2.5m = 2500000)
837
+ const suffixes = { k: 1e3, m: 1e6, b: 1e9, t: 1e12 };
838
+ const suffix = this.peek().toLowerCase();
839
+ if (suffixes.hasOwnProperty(suffix)) {
840
+ this.advance();
841
+ this.addToken('NUMBER', parseFloat(value) * suffixes[suffix], col);
842
+ return;
843
+ }
844
+ this.addToken('NUMBER', parseFloat(value), col);
845
+ }
846
+
847
+ skipComment() { while (!this.isAtEnd() && this.peek() !== '\n') this.advance(); }
848
+ skipBlockComment() {
849
+ while (!this.isAtEnd()) {
850
+ const ch = this.advance();
851
+ if (ch === '*' && this.peek() === '/') { this.advance(); return; }
852
+ if (ch === '\n') { this.line++; this.column = 1; }
853
+ }
854
+ throw this.error('Unterminated block comment');
855
+ }
856
+
857
+ // ── escape sequence reader ────────────────────────────────────────────
858
+ // Called after consuming the leading backslash inside any string context.
859
+ // Reads the escape name (one or more chars) and optional {arg1, arg2, ...} block.
860
+ // Returns the resolved string value.
861
+ readEscape() {
862
+ // Read escape name: one or more alphanumeric/underscore/dash chars OR a single symbol char
863
+ let name = '';
864
+ const first = this.peek();
865
+ if (!first || first === '\0') return '\\';
866
+
867
+ // Single-symbol escapes: \\ \' \" \{ \} \`
868
+ if (/[\\'"{}` ]/.test(first)) {
869
+ this.advance();
870
+ // Let processEscape handle single-char symbol escapes
871
+ return processEscape(first, [], this.line, this.column);
872
+ }
873
+
874
+ // Read multi-char name
875
+ while (!this.isAtEnd() && /[A-Za-z0-9_\-]/.test(this.peek())) name += this.advance();
876
+
877
+ // No name at all — treat as literal backslash
878
+ if (!name) {
879
+ const ch = this.advance();
880
+ return processEscape(ch, [], this.line, this.column);
881
+ }
882
+
883
+ // Read optional argument block: {arg1, arg2, ...}
884
+ let args = [];
885
+ if (this.peek() === '{') {
886
+ this.advance(); // consume {
887
+ let argStr = '', depth = 1;
888
+ while (!this.isAtEnd() && depth > 0) {
889
+ const ch = this.peek();
890
+ if (ch === '{') { depth++; argStr += this.advance(); }
891
+ else if (ch === '}') { depth--; if (depth > 0) argStr += this.advance(); else this.advance(); }
892
+ else if (ch === '\n') { this.line++; this.column = 1; this.position++; argStr += '\n'; }
893
+ else argStr += this.advance();
894
+ }
895
+ // Split on top-level commas (not nested in brackets/parens)
896
+ args = splitArgs(argStr);
897
+ }
898
+
899
+ return processEscape(name, args, this.line, this.column);
900
+ }
901
+
902
+ readString(quote) {
903
+ const col = this.column;
904
+ let value = '';
905
+ while (!this.isAtEnd() && this.peek() !== quote) {
906
+ const ch = this.advance();
907
+ if (ch === '\n') { this.line++; this.column = 1; value += ch; continue; }
908
+ if (ch === '\\') { value += this.readEscape(); continue; }
909
+ value += ch;
910
+ }
911
+ if (this.isAtEnd()) throw this.error('Unterminated string literal');
912
+ this.advance(); // consume closing quote
913
+ this.addToken('STRING', value, col);
914
+ }
915
+
916
+ rstring(quote) {
917
+ const col = this.column;
918
+ let value = '';
919
+ while (!this.isAtEnd() && this.peek() !== quote) {
920
+ const ch = this.advance();
921
+ if (ch === '\n') { this.line++; this.column = 1; value += ch; continue; }
922
+ value += ch;
923
+ }
924
+ if (this.isAtEnd()) throw this.error('Unterminated raw string literal');
925
+ this.advance(); // consume closing quote
926
+ this.addToken('STRING', value, col);
927
+ }
928
+
929
+ getDoubleString(q, next) {
930
+ const col = this.column;
931
+ let value = '';
932
+ while (!this.isAtEnd()) {
933
+ const ch = this.advance();
934
+ if (ch === q && this.peek() === next) {
935
+ this.advance();
936
+ const fullName = '__' + value.trim() + '__';
937
+ if (DYNAMIC_VARS.has(fullName)) {
938
+ // Emit as DVAR token with baked-in source location
939
+ this.tokens.push({ type: 'DVAR', value: fullName, line: this.line, column: col, srcLine: this.line, srcCol: col });
940
+ } else {
941
+ this.addToken('IDENTIFIER', fullName, col);
942
+ }
943
+ return;
944
+ }
945
+ value += ch;
946
+ }
947
+ throw this.error('Unterminated __identifier__');
948
+ }
949
+
950
+ fstring(quote) {
951
+ const col = this.column - 2;
952
+ this.addToken('FSTRING_START', 'f"', col);
953
+ let value = '';
954
+ const flush = () => {
955
+ if (value.length > 0) { this.addToken('STRING_PART', value, this.column - value.length - 1); value = ''; }
956
+ };
957
+ while (!this.isAtEnd()) {
958
+ const ch = this.advance();
959
+ if (ch === '{') {
960
+ flush();
961
+ this.addToken('INTERPOLATION_START', '{', this.column - 2);
962
+ let depth = 1, expr = '';
963
+ while (!this.isAtEnd() && depth > 0) {
964
+ const c = this.advance();
965
+ if (c === '{') depth++; else if (c === '}') depth--;
966
+ if (depth > 0) expr += c;
967
+ }
968
+ const innerLexer = new Lexer(expr, this.classicMode);
969
+ innerLexer.definitions = new Map(this.definitions); // inherit macros
970
+ const innerTokens = innerLexer.tokenize();
971
+ innerTokens.pop();
972
+ this.tokens.push(...innerTokens);
973
+ this.addToken('INTERPOLATION_END', '}', this.column - 1);
974
+ continue;
975
+ }
976
+ if (ch === '\\') { value += this.readEscape(); continue; }
977
+ if (ch === quote) { flush(); this.addToken('FSTRING_END', 'f"', this.column - 1); return; }
978
+ if (ch === '\n') { this.line++; this.column = 1; value += ch; continue; }
979
+ value += ch;
980
+ }
981
+ throw this.error('Unterminated f-string');
982
+ }
983
+
984
+ symbol(ch) {
985
+ const col = this.column - 1;
986
+ const n1 = this.peek();
987
+ const n2 = this.source[this.position + 1] ?? '\0';
988
+ const s3 = ch + n1 + n2;
989
+ const s2 = ch + n1;
990
+ if (OPERATORS.has(s3)) { this.advance(); this.advance(); this.addOperatorToken(s3, col); return; }
991
+ if (OPERATORS.has(s2)) { this.advance(); this.addOperatorToken(s2, col); return; }
992
+ if (OPERATORS.has(ch)) { this.addOperatorToken(ch, col); return; }
993
+ // '#' followed by identifier or standalone: size/length operator
994
+ if (ch === '#') {
995
+ // if followed by identifier, treat as unary size prefix
996
+ this.addToken('PUNCTUATION', '#', col); return;
997
+ }
998
+ if (PUNCTUATION.has(ch)) { this.addToken('PUNCTUATION', ch, col); return; }
999
+ throw this.error("Unexpected character '" + ch + "'");
1000
+ }
1001
+
1002
+ addOperatorToken(op, col) {
1003
+ const info = OPERATORS.get(op) || {};
1004
+ this.tokens.push({ type:'OPERATOR', value:op, line:this.line, column:col,
1005
+ precedence: info.precedence ?? 0, isUnary: !!info.isUnary,
1006
+ isPostfix: !!info.isPostfix, rightAssoc: !!info.rightAssoc });
1007
+ }
1008
+
1009
+ peek() { return this.source[this.position] ?? '\0'; }
1010
+ peekNext() { return this.source[this.position + 1] ?? '\0'; }
1011
+ advance() { const ch = this.source[this.position++] ?? '\0'; this.column++; return ch; }
1012
+ isAtEnd() { return this.position >= this.source.length; }
1013
+ isWhitespace(ch) { return ch === ' ' || ch === '\t' || ch === '\r'; }
1014
+ isDigit(ch) { return ch >= '0' && ch <= '9'; }
1015
+ isAlpha(ch) { return /[A-Za-z_]/.test(ch); }
1016
+ isAlphaNumeric(ch) { return /[A-Za-z0-9_]/.test(ch); }
1017
+ addToken(type, value, col) {
1018
+ this.tokens.push({ type, value, line: this.line, column: col ?? this.column });
1019
+ }
1020
+ error(msg) {
1021
+ const fmt = formatError('Lexer', msg, this.line, this.column, this.source);
1022
+ throw new (CustomError('LexError'))(fmt);
1023
+ }
1024
+ }
1025
+
1026
+ module.exports = { Lexer, KEYWORDS, CLASSIC_KEYWORDS, OPERATORS, PUNCTUATION, CUSTOM_ESCAPES, processEscape, ESCAPE_SEQUENCES, ESCAPE_HANDLERS, DYNAMIC_VARS };