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.
- package/LICENSE +1 -1
- package/README.md +1574 -597
- package/bin/novac +468 -171
- package/bin/nvc +522 -0
- package/bin/nvml +78 -17
- package/demo.nv +0 -0
- package/demo_builtins.nv +0 -0
- package/demo_http.nv +0 -0
- package/examples/bf.nv +69 -0
- package/examples/math.nv +21 -0
- package/kits/birdAPI/kitdef.js +954 -0
- package/kits/kitRNG/kitdef.js +740 -0
- package/kits/kitSSH/kitdef.js +1272 -0
- package/kits/kitadb/kitdef.js +606 -0
- package/kits/kitai/kitdef.js +2185 -0
- package/kits/kitansi/kitdef.js +1402 -0
- package/kits/kitcanvas/kitdef.js +914 -0
- package/kits/kitclippy/kitdef.js +925 -0
- package/kits/kitformat/kitdef.js +1485 -0
- package/kits/kitgps/kitdef.js +1862 -0
- package/kits/kitlibproc/kitdef.js +3 -2
- package/kits/kitmatrix/ex.js +19 -0
- package/kits/kitmatrix/kitdef.js +960 -0
- package/kits/kitmorse/kitdef.js +229 -0
- package/kits/kitmpatch/kitdef.js +906 -0
- package/kits/kitnet/kitdef.js +1401 -0
- package/kits/kitnovacweb/README.md +1416 -143
- package/kits/kitnovacweb/kitdef.js +92 -2
- package/kits/kitnovacweb/nvml/executor.js +578 -176
- package/kits/kitnovacweb/nvml/index.js +2 -2
- package/kits/kitnovacweb/nvml/lexer.js +72 -69
- package/kits/kitnovacweb/nvml/parser.js +328 -159
- package/kits/kitnovacweb/nvml/renderer.js +770 -270
- package/kits/kitparse/kitdef.js +1688 -0
- package/kits/kitproto/kitdef.js +613 -0
- package/kits/kitqr/kitdef.js +637 -0
- package/kits/kitregex++/kitdef.js +1353 -0
- package/kits/kitrequire/kitdef.js +1599 -0
- package/kits/kitx11/kitdef.js +1 -0
- package/kits/kitx11/kitx11.js +2472 -0
- package/kits/kitx11/kitx11_conn.js +948 -0
- package/kits/kitx11/kitx11_worker.js +121 -0
- package/kits/libtea/kitdef.js +2691 -0
- package/kits/libterm/ex.js +285 -0
- package/kits/libterm/kitdef.js +1927 -0
- package/novac/LICENSE +21 -0
- package/novac/README.md +1823 -0
- package/novac/bin/novac +950 -0
- package/novac/bin/nvc +522 -0
- package/novac/bin/nvml +542 -0
- package/novac/demo.nv +245 -0
- package/novac/demo_builtins.nv +209 -0
- package/novac/demo_http.nv +62 -0
- package/novac/examples/bf.nv +69 -0
- package/novac/examples/math.nv +21 -0
- package/novac/kits/kitai/kitdef.js +2185 -0
- package/novac/kits/kitansi/kitdef.js +1402 -0
- package/novac/kits/kitformat/kitdef.js +1485 -0
- package/novac/kits/kitgps/kitdef.js +1862 -0
- package/novac/kits/kitlibfs/kitdef.js +231 -0
- package/{examples/example-project/nova_modules → novac/kits}/kitlibproc/kitdef.js +3 -2
- package/novac/kits/kitmatrix/ex.js +19 -0
- package/novac/kits/kitmatrix/kitdef.js +960 -0
- package/novac/kits/kitmpatch/kitdef.js +906 -0
- package/novac/kits/kitnovacweb/README.md +1572 -0
- package/novac/kits/kitnovacweb/demo.nv +12 -0
- package/novac/kits/kitnovacweb/demo.nvml +71 -0
- package/novac/kits/kitnovacweb/index.nova +12 -0
- package/novac/kits/kitnovacweb/kitdef.js +692 -0
- package/novac/kits/kitnovacweb/nova.kit.json +8 -0
- package/novac/kits/kitnovacweb/nvml/executor.js +739 -0
- package/novac/kits/kitnovacweb/nvml/index.js +67 -0
- package/novac/kits/kitnovacweb/nvml/lexer.js +263 -0
- package/novac/kits/kitnovacweb/nvml/parser.js +508 -0
- package/novac/kits/kitnovacweb/nvml/renderer.js +924 -0
- package/novac/kits/kitparse/kitdef.js +1688 -0
- package/novac/kits/kitregex++/kitdef.js +1353 -0
- package/novac/kits/kitrequire/kitdef.js +1599 -0
- package/novac/kits/kitx11/kitdef.js +1 -0
- package/novac/kits/kitx11/kitx11.js +2472 -0
- package/novac/kits/kitx11/kitx11_conn.js +948 -0
- package/novac/kits/kitx11/kitx11_worker.js +121 -0
- package/novac/kits/libtea/tf.js +2691 -0
- package/novac/kits/libterm/ex.js +285 -0
- package/novac/kits/libterm/kitdef.js +1927 -0
- package/novac/node_modules/chalk/license +9 -0
- package/novac/node_modules/chalk/package.json +83 -0
- package/novac/node_modules/chalk/readme.md +297 -0
- package/novac/node_modules/chalk/source/index.d.ts +325 -0
- package/novac/node_modules/chalk/source/index.js +225 -0
- package/novac/node_modules/chalk/source/utilities.js +33 -0
- package/novac/node_modules/chalk/source/vendor/ansi-styles/index.d.ts +236 -0
- package/novac/node_modules/chalk/source/vendor/ansi-styles/index.js +223 -0
- package/novac/node_modules/chalk/source/vendor/supports-color/browser.d.ts +1 -0
- package/novac/node_modules/chalk/source/vendor/supports-color/browser.js +34 -0
- package/novac/node_modules/chalk/source/vendor/supports-color/index.d.ts +55 -0
- package/novac/node_modules/chalk/source/vendor/supports-color/index.js +190 -0
- package/novac/node_modules/commander/LICENSE +22 -0
- package/novac/node_modules/commander/Readme.md +1176 -0
- package/novac/node_modules/commander/esm.mjs +16 -0
- package/novac/node_modules/commander/index.js +24 -0
- package/novac/node_modules/commander/lib/argument.js +150 -0
- package/novac/node_modules/commander/lib/command.js +2777 -0
- package/novac/node_modules/commander/lib/error.js +39 -0
- package/novac/node_modules/commander/lib/help.js +747 -0
- package/novac/node_modules/commander/lib/option.js +380 -0
- package/novac/node_modules/commander/lib/suggestSimilar.js +101 -0
- package/novac/node_modules/commander/package-support.json +19 -0
- package/novac/node_modules/commander/package.json +82 -0
- package/novac/node_modules/commander/typings/esm.d.mts +3 -0
- package/novac/node_modules/commander/typings/index.d.ts +1113 -0
- package/novac/node_modules/node-addon-api/LICENSE.md +9 -0
- package/novac/node_modules/node-addon-api/README.md +95 -0
- package/novac/node_modules/node-addon-api/common.gypi +21 -0
- package/novac/node_modules/node-addon-api/except.gypi +25 -0
- package/novac/node_modules/node-addon-api/index.js +14 -0
- package/novac/node_modules/node-addon-api/napi-inl.deprecated.h +186 -0
- package/novac/node_modules/node-addon-api/napi-inl.h +7165 -0
- package/novac/node_modules/node-addon-api/napi.h +3364 -0
- package/novac/node_modules/node-addon-api/node_addon_api.gyp +42 -0
- package/novac/node_modules/node-addon-api/node_api.gyp +9 -0
- package/novac/node_modules/node-addon-api/noexcept.gypi +26 -0
- package/novac/node_modules/node-addon-api/package-support.json +21 -0
- package/novac/node_modules/node-addon-api/package.json +480 -0
- package/novac/node_modules/node-addon-api/tools/README.md +73 -0
- package/novac/node_modules/node-addon-api/tools/check-napi.js +99 -0
- package/novac/node_modules/node-addon-api/tools/clang-format.js +71 -0
- package/novac/node_modules/node-addon-api/tools/conversion.js +301 -0
- package/novac/node_modules/serialize-javascript/LICENSE +27 -0
- package/novac/node_modules/serialize-javascript/README.md +149 -0
- package/novac/node_modules/serialize-javascript/index.js +297 -0
- package/novac/node_modules/serialize-javascript/package.json +33 -0
- package/novac/package.json +27 -0
- package/novac/scripts/update-bin.js +24 -0
- package/novac/src/core/bstd.js +1035 -0
- package/novac/src/core/config.js +155 -0
- package/novac/src/core/describe.js +187 -0
- package/novac/src/core/emitter.js +499 -0
- package/novac/src/core/error.js +86 -0
- package/novac/src/core/executor.js +5606 -0
- package/novac/src/core/formatter.js +686 -0
- package/novac/src/core/lexer.js +1026 -0
- package/novac/src/core/nova_builtins.js +717 -0
- package/novac/src/core/nova_thread_worker.js +166 -0
- package/novac/src/core/parser.js +2181 -0
- package/novac/src/core/types.js +112 -0
- package/novac/src/index.js +28 -0
- package/novac/src/runtime/stdlib.js +244 -0
- package/package.json +6 -3
- package/scripts/update-bin.js +0 -0
- package/src/core/bstd.js +838 -362
- package/src/core/executor.js +2578 -170
- package/src/core/lexer.js +502 -54
- package/src/core/nova_builtins.js +21 -3
- package/src/core/parser.js +413 -72
- package/src/core/types.js +30 -2
- package/src/index.js +0 -0
- package/examples/example-project/README.md +0 -3
- package/examples/example-project/src/main.nova +0 -3
- package/src/core/environment.js +0 -0
- /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 };
|