gofront 0.0.7 → 0.0.9

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.
@@ -0,0 +1,235 @@
1
+ // Templ parser: extends the Go Parser to handle TemplDecl nodes.
2
+ //
3
+ // A .templ file is tokenised by TemplLexer into a mixed stream of standard Go
4
+ // tokens (T.*) and HTML/templ tokens (TT.*). This parser handles TT.TEMPL_KW
5
+ // in parseTopDecl; everything else is delegated to the base Parser.
6
+ //
7
+ // Produced AST additions:
8
+ // TemplDecl { kind, name, params, body: TemplNode[] }
9
+ // TemplElement { kind, tag, attrs, children }
10
+ // TemplText { kind, value }
11
+ // TemplExpr { kind, tokens } ← raw Go token array
12
+ // TemplComponent { kind, tokens } ← raw Go token array for call
13
+ // TemplChildren { kind }
14
+ // TemplIf { kind, condTokens, then: TemplNode[], else_: TemplNode[] | null }
15
+ // TemplFor { kind, stmtTokens, body: TemplNode[] }
16
+ // TemplSwitch { kind, exprTokens, cases: TemplCase[] }
17
+ // TemplCase { kind, caseTokens: tokens | null (null = default), body: TemplNode[] }
18
+
19
+ import { T } from "./lexer.js";
20
+ import { Parser } from "./parser.js";
21
+ import { TT } from "./templ-lexer.js";
22
+
23
+ const VOID_ELEMENTS = new Set([
24
+ "area",
25
+ "base",
26
+ "br",
27
+ "col",
28
+ "embed",
29
+ "hr",
30
+ "img",
31
+ "input",
32
+ "link",
33
+ "meta",
34
+ "param",
35
+ "source",
36
+ "track",
37
+ "wbr",
38
+ ]);
39
+
40
+ export class TemplParser extends Parser {
41
+ // ── Top-level declaration override ───────────────────────────
42
+
43
+ parseTopDecl() {
44
+ if (this.check(TT.TEMPL_KW)) return this.parseTemplDecl();
45
+ return super.parseTopDecl();
46
+ }
47
+
48
+ // ── TemplDecl ─────────────────────────────────────────────────
49
+
50
+ parseTemplDecl() {
51
+ const _line = this.peek().line;
52
+ this.advance(); // consume TEMPL_KW
53
+
54
+ const name = this.expect(T.IDENT).value;
55
+
56
+ // Parse parameter list (standard Go params: LPAREN...RPAREN)
57
+ const params = this.parseParamList();
58
+
59
+ // Optional semicolon between ) and {
60
+ this.semi();
61
+
62
+ // Consume the opening { of the HTML body
63
+ this.expect(T.LBRACE);
64
+
65
+ // Parse the HTML body; parseHtmlChildren consumes the closing RBRACE
66
+ const body = this.parseHtmlChildren();
67
+
68
+ this.semi();
69
+
70
+ return { kind: "TemplDecl", name, params, body, _line };
71
+ }
72
+
73
+ // ── HTML node parsing ─────────────────────────────────────────
74
+
75
+ // Parse zero or more HTML nodes until RBRACE (end of body) or EOF.
76
+ // Consumes the RBRACE.
77
+ parseHtmlChildren() {
78
+ const nodes = [];
79
+ while (!this.check(T.RBRACE) && !this.check(T.EOF)) {
80
+ if (this.check(TT.TEMPL_ELSE)) break; // handled by caller (parseTemplIf)
81
+ const node = this.parseHtmlNode();
82
+ if (node) nodes.push(node);
83
+ }
84
+ if (this.check(T.RBRACE)) this.advance(); // consume }
85
+ return nodes;
86
+ }
87
+
88
+ // Parse a single HTML node from the current token.
89
+ parseHtmlNode() {
90
+ const t = this.peek();
91
+ switch (t.type) {
92
+ case TT.HTML_OPEN:
93
+ return this.parseHtmlElement();
94
+ case TT.HTML_SELF: {
95
+ this.advance();
96
+ return {
97
+ kind: "TemplElement",
98
+ tag: t.value.tag,
99
+ attrs: t.value.attrs,
100
+ children: [],
101
+ };
102
+ }
103
+ case TT.HTML_TEXT:
104
+ this.advance();
105
+ return { kind: "TemplText", value: t.value };
106
+ case TT.TEMPL_EXPR:
107
+ this.advance();
108
+ return { kind: "TemplExpr", tokens: t.value };
109
+ case TT.TEMPL_COMP:
110
+ this.advance();
111
+ return { kind: "TemplComponent", tokens: t.value };
112
+ case TT.TEMPL_CHILDREN:
113
+ this.advance();
114
+ return { kind: "TemplChildren" };
115
+ case TT.TEMPL_IF: {
116
+ this.advance();
117
+ return this.parseTemplIf(t);
118
+ }
119
+ case TT.TEMPL_FOR: {
120
+ this.advance();
121
+ return this.parseTemplFor(t);
122
+ }
123
+ case TT.TEMPL_SWITCH: {
124
+ this.advance();
125
+ return this.parseTemplSwitch(t);
126
+ }
127
+ default:
128
+ // Unknown token inside HTML body — skip it to avoid infinite loops
129
+ this.advance();
130
+ return null;
131
+ }
132
+ }
133
+
134
+ // Parse children of an element until the matching close tag.
135
+ parseHtmlElement() {
136
+ const openTok = this.advance(); // HTML_OPEN
137
+ const { tag, attrs } = openTok.value;
138
+
139
+ // Void elements have no children and no close tag even without />
140
+ if (VOID_ELEMENTS.has(tag)) {
141
+ return { kind: "TemplElement", tag, attrs, children: [] };
142
+ }
143
+
144
+ const children = [];
145
+ while (!this.check(T.EOF) && !this.check(T.RBRACE)) {
146
+ if (this.check(TT.HTML_CLOSE)) break;
147
+ // Also break on a close tag for a different ancestor (shouldn't happen in
148
+ // well-formed HTML, but prevents infinite loops on malformed input)
149
+ if (this.check(TT.TEMPL_ELSE)) break;
150
+ const node = this.parseHtmlNode();
151
+ if (node) children.push(node);
152
+ }
153
+ if (this.check(TT.HTML_CLOSE)) {
154
+ const closeTag = this.peek().value;
155
+ if (closeTag !== tag) {
156
+ throw new Error(
157
+ `${this.filename ?? "<templ>"}:${this.peek().line}: unclosed tag <${tag}>, got </${closeTag}>`,
158
+ );
159
+ }
160
+ this.advance(); // consume HTML_CLOSE
161
+ } else if (!VOID_ELEMENTS.has(tag)) {
162
+ // Non-void element with no close tag — parse error
163
+ throw new Error(
164
+ `${this.filename ?? "<templ>"}:${openTok.line}: unclosed tag <${tag}>`,
165
+ );
166
+ }
167
+ return { kind: "TemplElement", tag, attrs, children };
168
+ }
169
+
170
+ // Parse a TemplIf node. `tok` is the TEMPL_IF token (already advanced).
171
+ parseTemplIf(tok) {
172
+ const condTokens = tok.value; // array of Go tokens
173
+ const then = this.parseHtmlChildren(); // reads body until RBRACE
174
+ let else_ = null;
175
+ if (this.check(TT.TEMPL_ELSE)) {
176
+ this.advance(); // consume TEMPL_ELSE
177
+ if (this.check(TT.TEMPL_IF)) {
178
+ const t2 = this.advance();
179
+ else_ = [this.parseTemplIf(t2)]; // else if → wrap in array
180
+ } else {
181
+ else_ = this.parseHtmlChildren(); // else body until RBRACE
182
+ }
183
+ }
184
+ return { kind: "TemplIf", condTokens, then, else_ };
185
+ }
186
+
187
+ // Parse a TemplFor node. `tok` is the TEMPL_FOR token (already advanced).
188
+ parseTemplFor(tok) {
189
+ const stmtTokens = tok.value;
190
+ const body = this.parseHtmlChildren();
191
+ return { kind: "TemplFor", stmtTokens, body };
192
+ }
193
+
194
+ // Parse a TemplSwitch node. `tok` is the TEMPL_SWITCH token (already advanced).
195
+ parseTemplSwitch(tok) {
196
+ const exprTokens = tok.value;
197
+ const cases = [];
198
+ while (
199
+ !this.check(TT.TEMPL_END) &&
200
+ !this.check(T.RBRACE) &&
201
+ !this.check(T.EOF)
202
+ ) {
203
+ if (this.check(TT.TEMPL_CASE)) {
204
+ const caseTok = this.advance();
205
+ const body = this.parseTemplCaseBody();
206
+ cases.push({ kind: "TemplCase", caseTokens: caseTok.value, body });
207
+ } else if (this.check(TT.TEMPL_DEFAULT)) {
208
+ this.advance();
209
+ const body = this.parseTemplCaseBody();
210
+ cases.push({ kind: "TemplCase", caseTokens: null, body });
211
+ } else {
212
+ this.advance(); // skip unexpected
213
+ }
214
+ }
215
+ if (this.check(TT.TEMPL_END)) this.advance(); // consume closing }
216
+ return { kind: "TemplSwitch", exprTokens, cases };
217
+ }
218
+
219
+ // Parse HTML nodes for a switch case body — stops at CASE/DEFAULT/END/RBRACE.
220
+ parseTemplCaseBody() {
221
+ const nodes = [];
222
+ while (
223
+ !this.check(TT.TEMPL_CASE) &&
224
+ !this.check(TT.TEMPL_DEFAULT) &&
225
+ !this.check(TT.TEMPL_END) &&
226
+ !this.check(T.RBRACE) &&
227
+ !this.check(T.EOF)
228
+ ) {
229
+ if (this.check(TT.TEMPL_ELSE)) break;
230
+ const node = this.parseHtmlNode();
231
+ if (node) nodes.push(node);
232
+ }
233
+ return nodes;
234
+ }
235
+ }
@@ -8,6 +8,9 @@
8
8
 
9
9
  import { ANY, BOOL, ERROR, FLOAT64, INT, STRING, VOID } from "./types.js";
10
10
 
11
+ // time.Time named type — used by the time package
12
+ const TIME_T = { kind: "named", name: "time.Time", underlying: ANY };
13
+
11
14
  export function setupGlobals(globals, types) {
12
15
  // Browser globals — typed as 'any' so any access/call is permitted
13
16
  const browserGlobals = [
@@ -113,6 +116,24 @@ export function setupGlobals(globals, types) {
113
116
  returns: [INT, ERROR],
114
117
  variadic: true,
115
118
  },
119
+ Sscan: {
120
+ kind: "func",
121
+ params: [STRING, ANY],
122
+ returns: [INT, ERROR],
123
+ variadic: true,
124
+ },
125
+ Sscanln: {
126
+ kind: "func",
127
+ params: [STRING, ANY],
128
+ returns: [INT, ERROR],
129
+ variadic: true,
130
+ },
131
+ Sscanf: {
132
+ kind: "func",
133
+ params: [STRING, STRING, ANY],
134
+ returns: [INT, ERROR],
135
+ variadic: true,
136
+ },
116
137
  },
117
138
  });
118
139
 
@@ -164,6 +185,71 @@ export function setupGlobals(globals, types) {
164
185
  returns: [STRING],
165
186
  },
166
187
  EqualFold: strFn2(STRING, BOOL),
188
+ Fields: strFn1({ kind: "slice", elem: STRING }),
189
+ Cut: {
190
+ kind: "func",
191
+ params: [STRING, STRING],
192
+ returns: [STRING, STRING, BOOL],
193
+ },
194
+ CutPrefix: {
195
+ kind: "func",
196
+ params: [STRING, STRING],
197
+ returns: [STRING, BOOL],
198
+ },
199
+ CutSuffix: {
200
+ kind: "func",
201
+ params: [STRING, STRING],
202
+ returns: [STRING, BOOL],
203
+ },
204
+ SplitN: strFn3(STRING, INT, { kind: "slice", elem: STRING }),
205
+ SplitAfter: strFn2(STRING, { kind: "slice", elem: STRING }),
206
+ SplitAfterN: strFn3(STRING, INT, { kind: "slice", elem: STRING }),
207
+ IndexAny: strFn2(STRING, INT),
208
+ LastIndexAny: strFn2(STRING, INT),
209
+ ContainsAny: strFn2(STRING, BOOL),
210
+ ContainsRune: strFn2(INT, BOOL),
211
+ IndexRune: strFn2(INT, INT),
212
+ IndexByte: strFn2(INT, INT),
213
+ LastIndexByte: strFn2(INT, INT),
214
+ Map: {
215
+ kind: "func",
216
+ params: [ANY, STRING],
217
+ returns: [STRING],
218
+ },
219
+ Title: strFn1(STRING),
220
+ ToTitle: strFn1(STRING),
221
+ TrimFunc: {
222
+ kind: "func",
223
+ params: [STRING, ANY],
224
+ returns: [STRING],
225
+ },
226
+ TrimLeftFunc: {
227
+ kind: "func",
228
+ params: [STRING, ANY],
229
+ returns: [STRING],
230
+ },
231
+ TrimRightFunc: {
232
+ kind: "func",
233
+ params: [STRING, ANY],
234
+ returns: [STRING],
235
+ },
236
+ IndexFunc: {
237
+ kind: "func",
238
+ params: [STRING, ANY],
239
+ returns: [INT],
240
+ },
241
+ LastIndexFunc: {
242
+ kind: "func",
243
+ params: [STRING, ANY],
244
+ returns: [INT],
245
+ },
246
+ NewReplacer: {
247
+ kind: "func",
248
+ params: [STRING],
249
+ returns: [ANY],
250
+ variadic: true,
251
+ },
252
+ NewReader: { kind: "func", params: [STRING], returns: [ANY] },
167
253
  },
168
254
  });
169
255
 
@@ -205,6 +291,46 @@ export function setupGlobals(globals, types) {
205
291
  params: [{ kind: "slice", elem: BYTE_SLICE }, BYTE_SLICE],
206
292
  returns: [BYTE_SLICE],
207
293
  },
294
+ ReplaceAll: {
295
+ kind: "func",
296
+ params: [BYTE_SLICE, BYTE_SLICE, BYTE_SLICE],
297
+ returns: [BYTE_SLICE],
298
+ },
299
+ TrimPrefix: byFn2(BYTE_SLICE, BYTE_SLICE),
300
+ TrimSuffix: byFn2(BYTE_SLICE, BYTE_SLICE),
301
+ TrimLeft: byFn2(STRING, BYTE_SLICE),
302
+ TrimRight: byFn2(STRING, BYTE_SLICE),
303
+ TrimFunc: {
304
+ kind: "func",
305
+ params: [BYTE_SLICE, ANY],
306
+ returns: [BYTE_SLICE],
307
+ },
308
+ IndexByte: byFn2(INT, INT),
309
+ LastIndex: byFn2(BYTE_SLICE, INT),
310
+ LastIndexByte: byFn2(INT, INT),
311
+ Fields: byFn1({ kind: "slice", elem: BYTE_SLICE }),
312
+ Cut: {
313
+ kind: "func",
314
+ params: [BYTE_SLICE, BYTE_SLICE],
315
+ returns: [BYTE_SLICE, BYTE_SLICE, BOOL],
316
+ },
317
+ ContainsAny: byFn2(STRING, BOOL),
318
+ ContainsRune: byFn2(INT, BOOL),
319
+ Map: {
320
+ kind: "func",
321
+ params: [ANY, BYTE_SLICE],
322
+ returns: [BYTE_SLICE],
323
+ },
324
+ SplitN: {
325
+ kind: "func",
326
+ params: [BYTE_SLICE, BYTE_SLICE, INT],
327
+ returns: [{ kind: "slice", elem: BYTE_SLICE }],
328
+ },
329
+ NewReader: {
330
+ kind: "func",
331
+ params: [BYTE_SLICE],
332
+ returns: [ANY],
333
+ },
208
334
  },
209
335
  });
210
336
 
@@ -241,6 +367,22 @@ export function setupGlobals(globals, types) {
241
367
  params: [STRING],
242
368
  returns: [BOOL, ERROR],
243
369
  },
370
+ Quote: { kind: "func", params: [STRING], returns: [STRING] },
371
+ Unquote: {
372
+ kind: "func",
373
+ params: [STRING],
374
+ returns: [STRING, ERROR],
375
+ },
376
+ AppendInt: {
377
+ kind: "func",
378
+ params: [{ kind: "slice", elem: INT }, INT, INT],
379
+ returns: [{ kind: "slice", elem: INT }],
380
+ },
381
+ AppendFloat: {
382
+ kind: "func",
383
+ params: [{ kind: "slice", elem: INT }, FLOAT64, INT, INT, INT],
384
+ returns: [{ kind: "slice", elem: INT }],
385
+ },
244
386
  },
245
387
  });
246
388
 
@@ -279,6 +421,26 @@ export function setupGlobals(globals, types) {
279
421
  params: [ANY, ANY],
280
422
  returns: [BOOL],
281
423
  },
424
+ Search: {
425
+ kind: "func",
426
+ params: [INT, ANY],
427
+ returns: [INT],
428
+ },
429
+ IntsAreSorted: {
430
+ kind: "func",
431
+ params: [{ kind: "slice", elem: INT }],
432
+ returns: [BOOL],
433
+ },
434
+ Float64sAreSorted: {
435
+ kind: "func",
436
+ params: [{ kind: "slice", elem: FLOAT64 }],
437
+ returns: [BOOL],
438
+ },
439
+ StringsAreSorted: {
440
+ kind: "func",
441
+ params: [{ kind: "slice", elem: STRING }],
442
+ returns: [BOOL],
443
+ },
282
444
  },
283
445
  });
284
446
 
@@ -327,6 +489,26 @@ export function setupGlobals(globals, types) {
327
489
  returns: [BOOL],
328
490
  },
329
491
  NaN: { kind: "func", params: [], returns: [FLOAT64] },
492
+ Atan: { kind: "func", params: [FLOAT64], returns: [FLOAT64] },
493
+ Atan2: { kind: "func", params: [FLOAT64, FLOAT64], returns: [FLOAT64] },
494
+ Asin: { kind: "func", params: [FLOAT64], returns: [FLOAT64] },
495
+ Acos: { kind: "func", params: [FLOAT64], returns: [FLOAT64] },
496
+ Exp: { kind: "func", params: [FLOAT64], returns: [FLOAT64] },
497
+ Exp2: { kind: "func", params: [FLOAT64], returns: [FLOAT64] },
498
+ Trunc: { kind: "func", params: [FLOAT64], returns: [FLOAT64] },
499
+ Hypot: { kind: "func", params: [FLOAT64, FLOAT64], returns: [FLOAT64] },
500
+ Signbit: { kind: "func", params: [FLOAT64], returns: [BOOL] },
501
+ Copysign: {
502
+ kind: "func",
503
+ params: [FLOAT64, FLOAT64],
504
+ returns: [FLOAT64],
505
+ },
506
+ Dim: { kind: "func", params: [FLOAT64, FLOAT64], returns: [FLOAT64] },
507
+ Remainder: {
508
+ kind: "func",
509
+ params: [FLOAT64, FLOAT64],
510
+ returns: [FLOAT64],
511
+ },
330
512
  // Constants
331
513
  Pi: FLOAT64,
332
514
  E: FLOAT64,
@@ -353,15 +535,57 @@ export function setupGlobals(globals, types) {
353
535
  kind: "namespace",
354
536
  name: "time",
355
537
  members: {
356
- Now: { kind: "func", params: [], returns: [ANY] },
357
- Since: { kind: "func", params: [ANY], returns: [ANY] },
538
+ Now: { kind: "func", params: [], returns: [TIME_T] },
539
+ Since: { kind: "func", params: [TIME_T], returns: [INT] },
358
540
  Sleep: { kind: "func", params: [ANY], returns: [VOID], async: true },
541
+ Parse: {
542
+ kind: "func",
543
+ params: [STRING, STRING],
544
+ returns: [TIME_T, ERROR],
545
+ },
546
+ Unix: { kind: "func", params: [INT, INT], returns: [TIME_T] },
547
+ Date: {
548
+ kind: "func",
549
+ params: [INT, INT, INT, INT, INT, INT, INT, ANY],
550
+ returns: [TIME_T],
551
+ },
359
552
  Millisecond: INT,
360
553
  Second: INT,
361
554
  Minute: INT,
362
555
  Hour: INT,
556
+ // Layout constants
557
+ RFC3339: STRING,
558
+ RFC3339Nano: STRING,
559
+ DateOnly: STRING,
560
+ TimeOnly: STRING,
561
+ DateTime: STRING,
562
+ // Location constants
563
+ UTC: ANY,
564
+ Local: ANY,
565
+ // Month constants
566
+ January: INT,
567
+ February: INT,
568
+ March: INT,
569
+ April: INT,
570
+ May: INT,
571
+ June: INT,
572
+ July: INT,
573
+ August: INT,
574
+ September: INT,
575
+ October: INT,
576
+ November: INT,
577
+ December: INT,
578
+ // Weekday constants
579
+ Sunday: INT,
580
+ Monday: INT,
581
+ Tuesday: INT,
582
+ Wednesday: INT,
583
+ Thursday: INT,
584
+ Friday: INT,
585
+ Saturday: INT,
363
586
  },
364
587
  });
588
+ types.set("time.Time", TIME_T);
365
589
 
366
590
  // unicode package
367
591
  const runeToStr = { kind: "func", params: [INT], returns: [BOOL] };
@@ -455,22 +679,30 @@ export function setupGlobals(globals, types) {
455
679
  // io package — Writer interface + WriteString
456
680
  // io.Writer is typed as ANY so GoFront's built-in writer types (strings.Builder,
457
681
  // bytes.Buffer) are accepted without explicit interface-satisfaction registration.
682
+ const BYTE_SLICE_IO = { kind: "slice", elem: INT };
458
683
  globals.define("io", {
459
684
  kind: "namespace",
460
685
  name: "io",
461
686
  members: {
462
687
  Writer: ANY,
688
+ Reader: ANY,
463
689
  EOF: ERROR,
464
690
  WriteString: {
465
691
  kind: "func",
466
692
  params: [ANY, STRING],
467
693
  returns: [INT, ERROR],
468
694
  },
695
+ ReadAll: {
696
+ kind: "func",
697
+ params: [ANY],
698
+ returns: [BYTE_SLICE_IO, ERROR],
699
+ },
469
700
  Discard: ANY,
470
701
  },
471
702
  });
472
703
  // Register io.Writer as a type alias so it resolves in parameter/field annotations.
473
704
  types.set("io.Writer", ANY);
705
+ types.set("io.Reader", ANY);
474
706
 
475
707
  // gom package — browser-native component built-in
476
708
  const GOM_NODE_T = {
@@ -742,6 +974,77 @@ export function setupGlobals(globals, types) {
742
974
  },
743
975
  });
744
976
 
977
+ // math/rand package — import "math/rand" → local name "rand"
978
+ globals.define("rand", {
979
+ kind: "namespace",
980
+ name: "rand",
981
+ members: {
982
+ Intn: { kind: "func", params: [INT], returns: [INT] },
983
+ Float64: { kind: "func", params: [], returns: [FLOAT64] },
984
+ Float32: { kind: "func", params: [], returns: [FLOAT64] },
985
+ Int: { kind: "func", params: [], returns: [INT] },
986
+ Int63: { kind: "func", params: [], returns: [INT] },
987
+ Int63n: { kind: "func", params: [INT], returns: [INT] },
988
+ Int31: { kind: "func", params: [], returns: [INT] },
989
+ Int31n: { kind: "func", params: [INT], returns: [INT] },
990
+ Seed: { kind: "func", params: [INT], returns: [VOID] },
991
+ Shuffle: { kind: "func", params: [INT, ANY], returns: [VOID] },
992
+ Perm: {
993
+ kind: "func",
994
+ params: [INT],
995
+ returns: [{ kind: "slice", elem: INT }],
996
+ },
997
+ },
998
+ });
999
+
1000
+ // unicode/utf8 package — import "unicode/utf8" → local name "utf8"
1001
+ globals.define("utf8", {
1002
+ kind: "namespace",
1003
+ name: "utf8",
1004
+ members: {
1005
+ RuneCountInString: { kind: "func", params: [STRING], returns: [INT] },
1006
+ RuneLen: { kind: "func", params: [INT], returns: [INT] },
1007
+ ValidString: { kind: "func", params: [STRING], returns: [BOOL] },
1008
+ ValidRune: { kind: "func", params: [INT], returns: [BOOL] },
1009
+ DecodeRuneInString: {
1010
+ kind: "func",
1011
+ params: [STRING],
1012
+ returns: [INT, INT],
1013
+ },
1014
+ DecodeLastRuneInString: {
1015
+ kind: "func",
1016
+ params: [STRING],
1017
+ returns: [INT, INT],
1018
+ },
1019
+ FullRuneInString: { kind: "func", params: [STRING], returns: [BOOL] },
1020
+ // Constants
1021
+ RuneError: INT,
1022
+ MaxRune: INT,
1023
+ UTFMax: INT,
1024
+ },
1025
+ });
1026
+
1027
+ // path package — import "path" or "path/filepath" → local name "path"
1028
+ globals.define("path", {
1029
+ kind: "namespace",
1030
+ name: "path",
1031
+ members: {
1032
+ Base: { kind: "func", params: [STRING], returns: [STRING] },
1033
+ Dir: { kind: "func", params: [STRING], returns: [STRING] },
1034
+ Ext: { kind: "func", params: [STRING], returns: [STRING] },
1035
+ Join: {
1036
+ kind: "func",
1037
+ params: [STRING],
1038
+ returns: [STRING],
1039
+ variadic: true,
1040
+ },
1041
+ Clean: { kind: "func", params: [STRING], returns: [STRING] },
1042
+ IsAbs: { kind: "func", params: [STRING], returns: [BOOL] },
1043
+ Split: { kind: "func", params: [STRING], returns: [STRING, STRING] },
1044
+ Match: { kind: "func", params: [STRING, STRING], returns: [BOOL, ERROR] },
1045
+ },
1046
+ });
1047
+
745
1048
  // Built-in functions
746
1049
  globals.define("append", { kind: "builtin", name: "append" });
747
1050
  globals.define("len", { kind: "builtin", name: "len" });
@@ -15,19 +15,22 @@
15
15
  export class TypeCheckError extends Error {
16
16
  constructor(msg, node, filename, sourceCode) {
17
17
  const lineNum = node?.line || node?._line;
18
+ const colNum = node?.col || node?._col;
18
19
  const loc = filename
19
20
  ? lineNum
20
- ? ` in ${filename} at line ${lineNum}`
21
+ ? ` in ${filename} at line ${lineNum}:${colNum ?? 1}`
21
22
  : ` in ${filename}`
22
23
  : lineNum
23
- ? ` at line ${lineNum}`
24
+ ? ` at line ${lineNum}:${colNum ?? 1}`
24
25
  : "";
25
26
  let lineContext = "";
26
27
  if (lineNum && sourceCode) {
27
28
  const lines = sourceCode.split("\n");
28
29
  const lineStr = lines[lineNum - 1];
29
30
  if (lineStr !== undefined) {
30
- lineContext = `\n ${lineNum} | ${lineStr}`;
31
+ const prefix = ` ${lineNum} | `;
32
+ const caretPad = " ".repeat((colNum ?? 1) - 1);
33
+ lineContext = `\n${prefix}${lineStr}\n${" ".repeat(prefix.length)}${caretPad}^`;
31
34
  }
32
35
  }
33
36
  super(`Type error${loc}: ${msg}${lineContext}`);
@@ -93,14 +96,6 @@ export const BASIC_TYPES = {
93
96
 
94
97
  export const COMPARABLE = { kind: "basic", name: "comparable" };
95
98
 
96
- export function isTypeParam(t) {
97
- return t?.kind === "typeParam";
98
- }
99
-
100
- export function isGeneric(t) {
101
- return t?.kind === "generic";
102
- }
103
-
104
99
  function makeBasicPredicate(...names) {
105
100
  const set = new Set(names);
106
101
  const pred = (t) => {
@@ -137,13 +132,6 @@ export function isNil(t) {
137
132
  export function isVoid(t) {
138
133
  return t?.kind === "basic" && t.name === "void";
139
134
  }
140
- export function isError(t) {
141
- if (!t) return false;
142
- if (t === ERROR) return true;
143
- if (t.kind === "interface" && t.name === "error") return true;
144
- if (t.kind === "named" && isError(t.underlying)) return true;
145
- return false;
146
- }
147
135
  export function isPointer(t) {
148
136
  if (!t) return false;
149
137
  if (t.kind === "pointer") return true;