lex.nx source
↩ module page · 590 lines · 19362 B
1// lex.nx -- NishiLang source tokenizer, ported from lex.c.
2//
3// Accepts a null-terminated source buffer and produces a stream of
4// Tok records. Mirrors lex.c's token layout (kind + line/col +
5// either int_val or text[]). Same keyword set.
6//
7// Single-pass; zero allocations per token beyond the growing token
8// array. Extends at 2x when full. Whitespace + // comments are
9// skipped. Numbers support 0x/0b/0o prefixes with underscore
10// separators. String literals read bytes until closing `"`.
11
12// ---- syscalls ----
13
14// ---- token kinds (subset of lex.h, integer constants) ----
15//
16// 0 EOF
17// 1 INT
18// 2 IDENT
19// 3 STRING
20// 10 FUNC 11 LET 12 VAR 13 CONST
21// 14 STATIC 15 IF 16 ELSE 17 WHILE
22// 18 LOOP 19 FOR 20 IN 21 BREAK
23// 22 CONTINUE 23 RETURN 24 TRUE 25 FALSE
24// 26 STRUCT 27 ENUM 28 MATCH 29 COMPTIME
25// 30 EXTERN 31 AS
26// 40 PLUS 41 MINUS 42 STAR 43 SLASH
27// 44 PERCENT 45 ASSIGN 46 EQ 47 NE
28// 48 LT 49 GT 50 LE 51 GE
29// 52 AND_AND 53 OR_OR 54 BANG
30// 55 AMP 56 PIPE 57 CARET 58 TILDE
31// 59 SHL 60 SHR
32// 61 ARROW 62 DOT_DOT 63 DOT 64 COLON
33// 65 SEMI 66 COMMA
34// 67 LPAREN 68 RPAREN 69 LBRACE 70 RBRACE
35// 71 LBRACKET 72 RBRACKET 73 AT 74 HASH
36
37// ---- Tok struct ----
38//
39// Laid out to match lex.h approximately. text[] is fixed-size
40// 64-byte for identifiers and string literals; that's the same
41// MAX_IDENT the C lexer uses.
42
43import "syscalls.nx"
44import "types.nx"
45import "lex_kinds.nx"
46// Tok struct + TOK_BYTES const live in lex_kinds.nx (canonical
47// home). Removed duplicate definition here 2026-04-26 per
48// nx_type_identity_check.sh -- duplicate Type instances at
49// compile-time create T#selfhost-006-class bugs even when
50// structurally identical.
51
52// ---- character classes ----
53
54func is_alpha(c: i64) -> i64 {
55 if c >= 0x41 { if c <= 0x5A { return 1 } }
56 if c >= 0x61 { if c <= 0x7A { return 1 } }
57 if c == 0x5F { return 1 }
58 return 0
59}
60func is_digit(c: i64) -> i64 {
61 if c >= 0x30 { if c <= 0x39 { return 1 } }
62 return 0
63}
64func is_hexdigit(c: i64) -> i64 {
65 if is_digit(c) { return 1 }
66 if c >= 0x41 { if c <= 0x46 { return 1 } }
67 if c >= 0x61 { if c <= 0x66 { return 1 } }
68 return 0
69}
70func hex_val(c: i64) -> i64 {
71 if c >= 0x30 { if c <= 0x39 { return c - 0x30 } }
72 if c >= 0x41 { if c <= 0x46 { return c - 0x37 } }
73 if c >= 0x61 { if c <= 0x66 { return c - 0x57 } }
74 return 0
75}
76func is_alnum(c: i64) -> i64 {
77 if is_alpha(c) { return 1 }
78 if is_digit(c) { return 1 }
79 return 0
80}
81
82// ---- keyword table ----
83//
84// Linear scan; we have ~20 keywords. Returns token kind or -1.
85
86func keyword_lookup(t: *u8, len: i64) -> i64 {
87 if streq_n(t, "func", len) { return 10 }
88 if streq_n(t, "let", len) { return 11 }
89 if streq_n(t, "var", len) { return 12 }
90 if streq_n(t, "const", len) { return 13 }
91 if streq_n(t, "static", len) { return 14 }
92 if streq_n(t, "if", len) { return 15 }
93 if streq_n(t, "else", len) { return 16 }
94 if streq_n(t, "while", len) { return 17 }
95 if streq_n(t, "loop", len) { return 18 }
96 if streq_n(t, "for", len) { return 19 }
97 if streq_n(t, "in", len) { return 20 }
98 if streq_n(t, "break", len) { return 21 }
99 if streq_n(t, "continue", len) { return 22 }
100 if streq_n(t, "return", len) { return 23 }
101 if streq_n(t, "true", len) { return 24 }
102 if streq_n(t, "false", len) { return 25 }
103 if streq_n(t, "struct", len) { return 26 }
104 if streq_n(t, "enum", len) { return 27 }
105 if streq_n(t, "match", len) { return 28 }
106 if streq_n(t, "comptime", len) { return 29 }
107 if streq_n(t, "extern", len) { return 30 }
108 if streq_n(t, "as", len) { return 31 }
109 return 2 // generic IDENT
110}
111
112// ---- Lex state ----
113
114struct Lex {
115 src: *u8,
116 pos: i64,
117 line: i64,
118 col: i64,
119 tokens: *Tok,
120 n_tokens: i64,
121 cap: i64,
122}
123
124// Push a token into the growing array. Simple, no geometric
125// growth for now: caller pre-allocates enough.
126
127func push_tok(L: *Lex, t: *Tok) -> i64 {
128 let base: i64 = L.tokens as i64
129 let slot: *Tok = (base + L.n_tokens * TOK_BYTES) as *Tok
130 slot.kind = t.kind
131 slot.line = t.line
132 slot.col = t.col
133 slot.int_val = t.int_val
134 slot.text0 = t.text0
135 slot.text1 = t.text1
136 slot.text2 = t.text2
137 slot.text3 = t.text3
138 slot.text4 = t.text4
139 slot.text5 = t.text5
140 slot.text6 = t.text6
141 slot.text7 = t.text7
142 slot.str_data = t.str_data
143 slot.str_len = t.str_len
144 L.n_tokens = L.n_tokens + 1
145 return 0
146}
147
148func advance(L: *Lex) -> i64 {
149 let s: *u8 = L.src
150 let c: i64 = s[L.pos]
151 if c == 0x0A {
152 L.line = L.line + 1
153 L.col = 1
154 }
155 if c != 0x0A {
156 if c != 0 { L.col = L.col + 1 }
157 }
158 L.pos = L.pos + 1
159 return c
160}
161func peek(L: *Lex) -> i64 {
162 let s: *u8 = L.src
163 return s[L.pos]
164}
165func peek2(L: *Lex) -> i64 {
166 let s: *u8 = L.src
167 if s[L.pos] == 0 { return 0 }
168 return s[L.pos + 1]
169}
170
171// ---- sub-lexers ----
172
173func skip_ws_comments(L: *Lex) -> i64 {
174 var go: i64 = 1
175 while go {
176 let c: i64 = peek(L)
177 if c == 0x20 { advance(L) }
178 if c == 0x09 { advance(L) }
179 if c == 0x0A { advance(L) }
180 if c == 0x0D { advance(L) }
181 if c == 0x2F {
182 if peek2(L) == 0x2F {
183 // Line comment
184 while peek(L) != 0 {
185 if peek(L) == 0x0A { break }
186 advance(L)
187 }
188 continue
189 }
190 // '/' not followed by another '/' -- division operator.
191 // Hand it back to the outer lexer to tokenise. Setting
192 // go=0 directly here avoids an infinite loop through the
193 // cascade below (which treats '/' as whitespace-like and
194 // never sets go=0, looping forever on a bare slash).
195 go = 0
196 continue
197 }
198 if c != 0x20 {
199 if c != 0x09 {
200 if c != 0x0A {
201 if c != 0x0D {
202 go = 0
203 }
204 }
205 }
206 }
207 }
208 return 0
209}
210
211func lex_ident_or_kw(L: *Lex) -> i64 {
212 let tr: *u8 = sys_mmap(TOK_BYTES)
213 let t: *Tok = tr as *Tok
214 t.kind = 2
215 t.line = L.line
216 t.col = L.col
217 t.int_val = 0
218 t.text0 = 0; t.text1 = 0; t.text2 = 0; t.text3 = 0
219 t.text4 = 0; t.text5 = 0; t.text6 = 0; t.text7 = 0
220 let text: *u8 = tok_text_ptr(t)
221 var i: i64 = 0
222 while i < 63 {
223 let c: i64 = peek(L)
224 if is_alnum(c) {
225 text[i] = c & 0xFF
226 advance(L)
227 i = i + 1
228 }
229 if is_alnum(c) == 0 { break }
230 }
231 text[i] = 0
232 t.kind = keyword_lookup(text, i)
233 push_tok(L, t)
234 return 0
235}
236
237func lex_number(L: *Lex) -> i64 {
238 let tr: *u8 = sys_mmap(TOK_BYTES)
239 let t: *Tok = tr as *Tok
240 t.kind = 1
241 t.line = L.line
242 t.col = L.col
243 t.int_val = 0
244 t.text0 = 0; t.text1 = 0; t.text2 = 0; t.text3 = 0
245 t.text4 = 0; t.text5 = 0; t.text6 = 0; t.text7 = 0
246
247 var n: i64 = 0
248 let c0: i64 = peek(L)
249 if c0 == 0x30 {
250 let c1: i64 = peek2(L)
251 if c1 == 0x78 { // 0x hex
252 advance(L); advance(L)
253 var go: i64 = 1
254 while go {
255 let c: i64 = peek(L)
256 if c == 0x5F { advance(L); continue }
257 if is_hexdigit(c) {
258 n = (n << 4) | hex_val(c)
259 advance(L)
260 }
261 if is_hexdigit(c) == 0 { if c != 0x5F { go = 0 } }
262 }
263 t.int_val = n
264 push_tok(L, t)
265 return 0
266 }
267 if c1 == 0x62 { // 0b binary
268 advance(L); advance(L)
269 var go: i64 = 1
270 while go {
271 let c: i64 = peek(L)
272 if c == 0x5F { advance(L); continue }
273 if c == 0x30 { n = (n << 1); advance(L); continue }
274 if c == 0x31 { n = (n << 1) | 1; advance(L); continue }
275 go = 0
276 }
277 t.int_val = n
278 push_tok(L, t)
279 return 0
280 }
281 if c1 == 0x6F { // 0o octal
282 advance(L); advance(L)
283 var go: i64 = 1
284 while go {
285 let c: i64 = peek(L)
286 if c == 0x5F { advance(L); continue }
287 if c >= 0x30 {
288 if c <= 0x37 {
289 n = (n << 3) | (c - 0x30)
290 advance(L)
291 continue
292 }
293 }
294 go = 0
295 }
296 t.int_val = n
297 push_tok(L, t)
298 return 0
299 }
300 }
301 // Decimal. Consume whole-part digits.
302 var go: i64 = 1
303 while go {
304 let c: i64 = peek(L)
305 if c == 0x5F { advance(L); continue }
306 if is_digit(c) {
307 n = n * 10 + (c - 0x30)
308 advance(L)
309 continue
310 }
311 go = 0
312 }
313
314 // Float split: if the current char is '.' AND the next char is a
315 // digit (not a method call like `x.foo` or range like `0..10`),
316 // switch to TK_FLOAT and consume the fractional part.
317 let c_after: i64 = peek(L)
318 if c_after == 0x2E {
319 let c_next: i64 = peek2(L)
320 if is_digit(c_next) {
321 advance(L) // consume '.'
322 var frac: i64 = 0
323 var frac_digits: i64 = 0
324 var fgo: i64 = 1
325 while fgo {
326 let c: i64 = peek(L)
327 if c == 0x5F { advance(L); continue }
328 if is_digit(c) {
329 frac = frac * 10 + (c - 0x30)
330 frac_digits = frac_digits + 1
331 advance(L)
332 continue
333 }
334 fgo = 0
335 }
336 // Optional 'f' / 'F' suffix -- now distinguished:
337 // `1.5f32` / `1.5F32` -> TK_FLOAT_F32 (explicit single)
338 // `1.5f64` / `1.5F64` -> TK_FLOAT (explicit default)
339 // `1.5f` / `1.5` -> TK_FLOAT (default = f64)
340 // Lexer consumes the suffix bytes; parse.nx dispatches on
341 // the resulting kind.
342 var sfx_kind: i64 = 4 // TK_FLOAT (default f64)
343 let c_sfx: i64 = peek(L)
344 if c_sfx == 0x66 {
345 advance(L)
346 let n1: i64 = L.src[L.pos]
347 let n2: i64 = L.src[L.pos + 1]
348 if n1 == 0x33 {
349 if n2 == 0x32 {
350 advance(L); advance(L)
351 sfx_kind = 5 // TK_FLOAT_F32
352 }
353 }
354 if n1 == 0x36 {
355 if n2 == 0x34 {
356 advance(L); advance(L)
357 // sfx_kind stays at TK_FLOAT (default)
358 }
359 }
360 }
361 if c_sfx == 0x46 {
362 advance(L)
363 let n1: i64 = L.src[L.pos]
364 let n2: i64 = L.src[L.pos + 1]
365 if n1 == 0x33 {
366 if n2 == 0x32 {
367 advance(L); advance(L)
368 sfx_kind = 5
369 }
370 }
371 if n1 == 0x36 {
372 if n2 == 0x34 {
373 advance(L); advance(L)
374 }
375 }
376 }
377 t.kind = sfx_kind
378 t.int_val = n
379 t.text0 = frac
380 t.text1 = frac_digits
381 push_tok(L, t)
382 return 0
383 }
384 }
385
386 t.int_val = n
387 push_tok(L, t)
388 return 0
389}
390
391func lex_string(L: *Lex) -> i64 {
392 let tr: *u8 = sys_mmap(TOK_BYTES)
393 let t: *Tok = tr as *Tok
394 t.kind = 3
395 t.line = L.line
396 t.col = L.col
397 t.int_val = 0
398 t.text0 = 0; t.text1 = 0; t.text2 = 0; t.text3 = 0
399 t.text4 = 0; t.text5 = 0; t.text6 = 0; t.text7 = 0
400 t.str_data = 0
401 t.str_len = 0
402 advance(L) // opening "
403
404 // Pre-scan to determine the raw length (upper bound; escapes
405 // shrink it). Self-host pipeline NEEDS escape processing here:
406 // string literals like "nxc: expand start\n" arrive with the
407 // backslash-n two-byte sequence; without processing, sys_write
408 // emits the literal `\` + `n` instead of a newline, so anything
409 // that uses a string literal at runtime miscompiles.
410 let scan_start: i64 = L.pos
411 var scan: i64 = L.pos
412 let src_chk: *u8 = L.src
413 while src_chk[scan] != 0 {
414 if src_chk[scan] == 0x22 { break }
415 scan = scan + 1
416 }
417 let raw_len: i64 = scan - scan_start
418
419 let buf: *u8 = sys_mmap(raw_len + 16)
420
421 // Inline copy + populate text[] prefix for legacy consumers.
422 let text: *u8 = tok_text_ptr(t)
423 var i: i64 = 0
424 while peek(L) != 0 {
425 if peek(L) == 0x22 { break }
426 var c: i64 = peek(L) & 0xFF
427 if c == 0x5C {
428 // Escape sequence: peek next char, advance past both.
429 advance(L)
430 let nx: i64 = peek(L) & 0xFF
431 if nx == 0x6E { c = 0x0A } // \n
432 if nx == 0x74 { c = 0x09 } // \t
433 if nx == 0x72 { c = 0x0D } // \r
434 if nx == 0x5C { c = 0x5C } // \\
435 if nx == 0x22 { c = 0x22 } // \"
436 if nx == 0x30 { c = 0x00 } // \0 (single, no octal)
437 // Other escapes: drop the backslash + use the literal char
438 // (matches gas-style "unknown escape passes literal").
439 if nx != 0x6E { if nx != 0x74 { if nx != 0x72 {
440 if nx != 0x5C { if nx != 0x22 { if nx != 0x30 {
441 c = nx
442 } } } } } }
443 }
444 buf[i] = c
445 if i < 63 { text[i] = c }
446 i = i + 1
447 advance(L)
448 }
449 buf[i] = 0
450 if i < 63 { text[i] = 0 }
451
452 t.str_data = buf as i64
453 t.str_len = i
454
455 if peek(L) == 0x22 { advance(L) }
456 push_tok(L, t)
457 return 0
458}
459
460func emit_punct(L: *Lex, kind: i64, len: i64) -> i64 {
461 let tr: *u8 = sys_mmap(TOK_BYTES)
462 let t: *Tok = tr as *Tok
463 t.kind = kind
464 t.line = L.line
465 t.col = L.col
466 t.int_val = 0
467 t.text0 = 0; t.text1 = 0; t.text2 = 0; t.text3 = 0
468 t.text4 = 0; t.text5 = 0; t.text6 = 0; t.text7 = 0
469 var k: i64 = 0
470 while k < len {
471 advance(L)
472 k = k + 1
473 }
474 push_tok(L, t)
475 return 0
476}
477
478// ---- public entry ----
479//
480// Tokenise the entire source, terminate with an EOF token, return
481// the tokens array.
482
483func lex_source(src: *u8, cap: i64) -> *Tok {
484 let toks_raw: *u8 = sys_mmap(cap * TOK_BYTES + TOK_BYTES)
485 let toks: *Tok = toks_raw as *Tok
486
487 let L_raw: *u8 = sys_mmap(64)
488 let L: *Lex = L_raw as *Lex
489 L.src = src
490 L.pos = 0
491 L.line = 1
492 L.col = 1
493 L.tokens = toks
494 L.n_tokens = 0
495 L.cap = cap
496
497 var go: i64 = 1
498 while go {
499 skip_ws_comments(L)
500 let c: i64 = peek(L)
501 if c == 0 {
502 go = 0
503 continue
504 }
505 if is_alpha(c) { lex_ident_or_kw(L); continue }
506 if c == 0x5F { lex_ident_or_kw(L); continue }
507 if is_digit(c) { lex_number(L); continue }
508 if c == 0x22 { lex_string(L); continue }
509
510 let c2: i64 = peek2(L)
511 // Multi-char first
512 if c == 0x2D {
513 if c2 == 0x3E { emit_punct(L, 61, 2); continue } // ->
514 }
515 if c == 0x3D {
516 if c2 == 0x3D { emit_punct(L, 46, 2); continue } // ==
517 }
518 if c == 0x21 {
519 if c2 == 0x3D { emit_punct(L, 47, 2); continue } // !=
520 }
521 if c == 0x3C {
522 if c2 == 0x3D { emit_punct(L, 50, 2); continue } // <=
523 if c2 == 0x3C { emit_punct(L, 59, 2); continue } // <<
524 }
525 if c == 0x3E {
526 if c2 == 0x3D { emit_punct(L, 51, 2); continue } // >=
527 if c2 == 0x3E { emit_punct(L, 60, 2); continue } // >>
528 }
529 if c == 0x26 {
530 if c2 == 0x26 { emit_punct(L, 52, 2); continue } // &&
531 }
532 if c == 0x7C {
533 if c2 == 0x7C { emit_punct(L, 53, 2); continue } // ||
534 }
535 if c == 0x2E {
536 if c2 == 0x2E { emit_punct(L, 62, 2); continue } // ..
537 }
538 if c == 0x3A {
539 if c2 == 0x3A { emit_punct(L, 75, 2); continue } // ::
540 }
541 if c == 0x3D {
542 if c2 == 0x3E { emit_punct(L, 76, 2); continue } // =>
543 }
544
545 if c == 0x2B { emit_punct(L, 40, 1); continue }
546 if c == 0x2D { emit_punct(L, 41, 1); continue }
547 if c == 0x2A { emit_punct(L, 42, 1); continue }
548 if c == 0x2F { emit_punct(L, 43, 1); continue }
549 if c == 0x25 { emit_punct(L, 44, 1); continue }
550 if c == 0x3D { emit_punct(L, 45, 1); continue }
551 if c == 0x3C { emit_punct(L, 48, 1); continue }
552 if c == 0x3E { emit_punct(L, 49, 1); continue }
553 if c == 0x21 { emit_punct(L, 54, 1); continue }
554 if c == 0x26 { emit_punct(L, 55, 1); continue }
555 if c == 0x7C { emit_punct(L, 56, 1); continue }
556 if c == 0x5E { emit_punct(L, 57, 1); continue }
557 if c == 0x7E { emit_punct(L, 58, 1); continue }
558 if c == 0x2E { emit_punct(L, 63, 1); continue }
559 if c == 0x3A { emit_punct(L, 64, 1); continue }
560 if c == 0x3B { emit_punct(L, 65, 1); continue }
561 if c == 0x2C { emit_punct(L, 66, 1); continue }
562 if c == 0x28 { emit_punct(L, 67, 1); continue }
563 if c == 0x29 { emit_punct(L, 68, 1); continue }
564 if c == 0x7B { emit_punct(L, 69, 1); continue }
565 if c == 0x7D { emit_punct(L, 70, 1); continue }
566 if c == 0x5B { emit_punct(L, 71, 1); continue }
567 if c == 0x5D { emit_punct(L, 72, 1); continue }
568 if c == 0x40 { emit_punct(L, 73, 1); continue }
569 if c == 0x23 { emit_punct(L, 74, 1); continue }
570 if c == 0x3F { emit_punct(L, 77, 1); continue } // ? TK_QUESTION (ternary)
571
572 // Unknown: skip to prevent infinite loop.
573 advance(L)
574 }
575
576 // Terminator EOF
577 let last_raw: *u8 = sys_mmap(TOK_BYTES)
578 let last: *Tok = last_raw as *Tok
579 last.kind = 0
580 last.line = L.line
581 last.col = L.col
582 last.int_val = 0
583 last.text0 = 0; last.text1 = 0; last.text2 = 0; last.text3 = 0
584 last.text4 = 0; last.text5 = 0; last.text6 = 0; last.text7 = 0
585 push_tok(L, last)
586
587 return toks
588}
589
590// Library only; self-test lives in lex_test.nx.