gofront 1.6.1 → 1.7.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.
@@ -57,6 +57,7 @@ export class TypeChecker {
57
57
  this._typeSwitchDepth = 0; // for rejecting fallthrough in type switch
58
58
  this._deferCount = 0; // tracks defer usage in current function body
59
59
  this._imports = []; // tracked imports for unused-import detection
60
+ this._constExprs = new Map(); // name -> AST initializer expr
60
61
  this._setupGlobals();
61
62
  // Register error as a named type (interface)
62
63
  this.types.set("error", ERROR);
@@ -220,7 +221,11 @@ export class TypeChecker {
220
221
  const type = spec.type
221
222
  ? this.resolveTypeNode(spec.type, this.globals)
222
223
  : ANY;
223
- for (const name of spec.names) this.globals.defineConst(name, type);
224
+ for (let i = 0; i < spec.names.length; i++) {
225
+ const name = spec.names[i];
226
+ const expr = spec.value?.[i] ?? spec.value?.[0] ?? null;
227
+ this.globals.defineConst(name, type, expr);
228
+ }
224
229
  }
225
230
  }
226
231
 
@@ -510,7 +515,8 @@ export class TypeChecker {
510
515
  const struct = type.underlying;
511
516
  if (struct?.kind === "struct" && struct._embeds) {
512
517
  for (const embed of struct._embeds) {
513
- const base = embed.kind === "named" ? embed.underlying : embed;
518
+ const inner = embed.kind === "pointer" ? embed.base : embed;
519
+ const base = inner.kind === "named" ? inner.underlying : inner;
514
520
  if (base?.kind !== "struct" || !base.methods) continue;
515
521
  for (const [mName, mType] of base.methods.entries()) {
516
522
  if (!struct.methods.has(mName)) struct.methods.set(mName, mType);
@@ -899,14 +905,31 @@ export class TypeChecker {
899
905
  let type = spec.type ? this.resolveTypeNode(spec.type, scope) : null;
900
906
  if (spec.value) {
901
907
  const valTypes = spec.value.map((v) => this.checkExpr(v, scope));
902
- if (!type) type = defaultType(valTypes[0]) ?? ANY;
908
+ const isTuple = valTypes.length === 1 && valTypes[0]?.kind === "tuple";
909
+ const valTypesFlat = isTuple ? valTypes[0].types : valTypes;
910
+ if (spec.names.length !== valTypesFlat.length) {
911
+ const vars =
912
+ spec.names.length === 1
913
+ ? "1 variable"
914
+ : `${spec.names.length} variables`;
915
+ const vals = isTuple
916
+ ? `returns ${valTypesFlat.length} values`
917
+ : valTypesFlat.length === 1
918
+ ? "1 value"
919
+ : `${valTypesFlat.length} values`;
920
+ this.err(`assignment mismatch: ${vars} but ${vals}`, spec);
921
+ }
903
922
  for (let i = 0; i < spec.names.length; i++) {
904
- const vt = valTypes[i] ?? ANY;
905
- this.assertAssignable(type, vt, spec.value[i]);
923
+ const vt = valTypesFlat[i] ?? ANY;
924
+ const varType = type ?? defaultType(vt) ?? ANY;
925
+ if (type)
926
+ this.assertAssignable(type, vt, spec.value[i] ?? spec.value[0]);
927
+ scope.defineLocal(spec.names[i], varType);
906
928
  }
929
+ } else {
930
+ if (!type) type = ANY;
931
+ for (const name of spec.names) scope.defineLocal(name, type);
907
932
  }
908
- if (!type) type = ANY;
909
- for (const name of spec.names) scope.defineLocal(name, type);
910
933
  }
911
934
  this._sharedAllocOk = savedSharedAlloc;
912
935
  }
@@ -918,7 +941,11 @@ export class TypeChecker {
918
941
  const type = spec.type
919
942
  ? this.resolveTypeNode(spec.type, scope)
920
943
  : (valTypes[0] ?? ANY);
921
- for (const name of spec.names) scope.defineConst(name, type);
944
+ for (let i = 0; i < spec.names.length; i++) {
945
+ const name = spec.names[i];
946
+ const expr = spec.value?.[i] ?? spec.value?.[0] ?? null;
947
+ scope.defineConst(name, type, expr);
948
+ }
922
949
  }
923
950
  }
924
951
 
@@ -1,6 +1,7 @@
1
1
  // Type resolution methods — resolveTypeNode, resolveType, fieldType, generics.
2
2
  // Installed as a mixin on TypeChecker.prototype.
3
3
 
4
+ import { parseJsonTag } from "../parser/struct-tag.js";
4
5
  import {
5
6
  ANY,
6
7
  BASIC_TYPES,
@@ -209,10 +210,28 @@ export const resolveMethods = {
209
210
  _resolveStructType(node, scope) {
210
211
  const fields = new Map();
211
212
  const embeds = [];
213
+ const structFields = [];
212
214
  for (const f of node.fields) {
213
215
  const ft = this.resolveTypeNode(f.type, scope);
216
+ structFields.push({
217
+ names: f.names,
218
+ type: ft,
219
+ tag: f.tag ?? null,
220
+ embedded: Boolean(f.embedded),
221
+ astType: f.type,
222
+ });
214
223
  if (f.embedded) {
215
- const base = ft.kind === "named" ? ft.underlying : ft;
224
+ const inner = ft.kind === "pointer" ? ft.base : ft;
225
+ const embedName = (
226
+ f.astType?.name ??
227
+ f.type?.name ??
228
+ f.type?.base?.name ??
229
+ (inner.kind === "named" ? inner.name : "")
230
+ )
231
+ .split(".")
232
+ .pop();
233
+ if (embedName) fields.set(embedName, ft);
234
+ const base = inner.kind === "named" ? inner.underlying : inner;
216
235
  if (base?.kind === "struct") {
217
236
  for (const [k, v] of base.fields.entries()) fields.set(k, v);
218
237
  }
@@ -221,7 +240,13 @@ export const resolveMethods = {
221
240
  for (const n of f.names) fields.set(n, ft);
222
241
  }
223
242
  }
224
- return { kind: "struct", fields, methods: new Map(), _embeds: embeds };
243
+ return {
244
+ kind: "struct",
245
+ fields,
246
+ methods: new Map(),
247
+ _embeds: embeds,
248
+ _structFields: structFields,
249
+ };
225
250
  },
226
251
 
227
252
  _resolveInterfaceType(node, scope) {
@@ -434,6 +459,7 @@ export const resolveMethods = {
434
459
  case "namespace":
435
460
  return this._fieldTypeNamespace(base, field, node);
436
461
  case "shared":
462
+ case "simd":
437
463
  if (base.methods?.has(field)) return base.methods.get(field);
438
464
  return this.err(`No method '${field}' on ${typeStr(baseType)}`, node);
439
465
  case "slice":
@@ -537,4 +563,127 @@ export const resolveMethods = {
537
563
  }
538
564
  return this.err(`No member '${field}' in namespace ${base.name}`, node);
539
565
  },
566
+
567
+ resolveJsonFields(structType) {
568
+ return resolveJsonFields(structType);
569
+ },
540
570
  };
571
+
572
+ function isExported(name) {
573
+ if (!name || typeof name !== "string") return false;
574
+ const code = name.charCodeAt(0);
575
+ return code >= 65 && code <= 90;
576
+ }
577
+
578
+ function _processJsonField(f, depth, path, visited, queue, candidates) {
579
+ const tagInfo = parseJsonTag(f.tag);
580
+ if (tagInfo.ignored) return;
581
+
582
+ if (f.embedded) {
583
+ const isPtr = f.type.kind === "pointer" || f.type.kind === "PointerType";
584
+ const inner = isPtr ? f.type.base : f.type;
585
+ const embedStruct = inner.kind === "named" ? inner.underlying : inner;
586
+ const embedName = (
587
+ f.astType?.name ??
588
+ f.astType?.base?.name ??
589
+ inner.name ??
590
+ ""
591
+ )
592
+ .split(".")
593
+ .pop();
594
+
595
+ if (tagInfo.name !== null) {
596
+ if (isExported(embedName)) {
597
+ candidates.push({
598
+ jsonKey: tagInfo.name,
599
+ goName: embedName,
600
+ type: f.type,
601
+ omitempty: tagInfo.omitempty,
602
+ depth,
603
+ tagged: true,
604
+ isEmbedded: true,
605
+ path: [...path, { name: embedName, isPtr, typeName: embedName }],
606
+ });
607
+ }
608
+ } else if (embedStruct?.kind === "struct") {
609
+ if (!visited.has(embedStruct)) {
610
+ const nextVisited = new Set(visited);
611
+ nextVisited.add(embedStruct);
612
+ queue.push({
613
+ struct: embedStruct,
614
+ depth: depth + 1,
615
+ path: [...path, { name: embedName, isPtr, typeName: embedName }],
616
+ visited: nextVisited,
617
+ });
618
+ }
619
+ }
620
+ return;
621
+ }
622
+
623
+ for (const name of f.names) {
624
+ if (!isExported(name)) continue;
625
+ candidates.push({
626
+ jsonKey: tagInfo.name !== null ? tagInfo.name : name,
627
+ goName: name,
628
+ type: f.type,
629
+ omitempty: tagInfo.omitempty,
630
+ quoted: tagInfo.options.includes("string"),
631
+ depth,
632
+ tagged: tagInfo.name !== null,
633
+ path: [...path, { name, isPtr: false }],
634
+ });
635
+ }
636
+ }
637
+
638
+ function _resolveCandidateCollisions(candidates) {
639
+ const byKey = new Map();
640
+ for (const cand of candidates) {
641
+ if (!byKey.has(cand.jsonKey)) byKey.set(cand.jsonKey, []);
642
+ byKey.get(cand.jsonKey).push(cand);
643
+ }
644
+
645
+ const winningFields = [];
646
+ for (const [, group] of byKey.entries()) {
647
+ let minDepth = Infinity;
648
+ for (const c of group) {
649
+ if (c.depth < minDepth) minDepth = c.depth;
650
+ }
651
+
652
+ const atMin = group.filter((c) => c.depth === minDepth);
653
+ if (atMin.length === 1) {
654
+ winningFields.push(atMin[0]);
655
+ } else {
656
+ const tagged = atMin.filter((c) => c.tagged);
657
+ if (tagged.length === 1) winningFields.push(tagged[0]);
658
+ }
659
+ }
660
+
661
+ winningFields.sort((a, b) => candidates.indexOf(a) - candidates.indexOf(b));
662
+ return winningFields;
663
+ }
664
+
665
+ export function resolveJsonFields(structType) {
666
+ if (!structType) return [];
667
+ const underlying =
668
+ structType.kind === "named" ? structType.underlying : structType;
669
+ if (underlying?.kind !== "struct") return [];
670
+ if (underlying._jsonFields) return underlying._jsonFields;
671
+
672
+ const queue = [
673
+ { struct: underlying, depth: 0, path: [], visited: new Set([underlying]) },
674
+ ];
675
+ const candidates = [];
676
+
677
+ while (queue.length > 0) {
678
+ const { struct: curr, depth, path, visited } = queue.shift();
679
+ if (curr?.kind !== "struct") continue;
680
+
681
+ for (const f of curr._structFields || []) {
682
+ _processJsonField(f, depth, path, visited, queue, candidates);
683
+ }
684
+ }
685
+
686
+ const winning = _resolveCandidateCollisions(candidates);
687
+ underlying._jsonFields = winning;
688
+ return winning;
689
+ }
@@ -83,6 +83,29 @@ export const statementCheckMethods = {
83
83
  }
84
84
  },
85
85
 
86
+ _checkDefineCommaOkMap(stmt, scope) {
87
+ if (
88
+ stmt.lhs.length !== 2 ||
89
+ stmt.rhs.length !== 1 ||
90
+ stmt.rhs[0].kind !== "IndexExpr"
91
+ )
92
+ return false;
93
+ const mapBaseType = this.checkExpr(stmt.rhs[0].expr, scope);
94
+ const resolvedBase =
95
+ mapBaseType?.kind === "named" ? mapBaseType.underlying : mapBaseType;
96
+ if (resolvedBase?.kind !== "map") return false;
97
+ this.checkExpr(stmt.rhs[0].index, scope);
98
+ stmt._commaOkMap = true;
99
+ stmt.rhs[0]._mapValueType = resolvedBase.value;
100
+ this._declareDefineVars(stmt, [resolvedBase.value, BOOL], scope);
101
+ const allRedecl = stmt.lhs.every((e) => e._redecl || (e.name ?? e) === "_");
102
+ if (allRedecl && stmt.lhs.length > 0) {
103
+ this.err("no new variables on left side of :=", stmt.lhs[0]);
104
+ }
105
+ stmt._rhsTypes = [resolvedBase.value, BOOL];
106
+ return true;
107
+ },
108
+
86
109
  _checkDefineStmt(stmt, scope) {
87
110
  // comma-ok type assertion: n, ok := x.(T)
88
111
  if (
@@ -92,10 +115,24 @@ export const statementCheckMethods = {
92
115
  ) {
93
116
  stmt.rhs[0]._commaOk = true;
94
117
  }
118
+ if (this._checkDefineCommaOkMap(stmt, scope)) return;
95
119
  const rhs = stmt.rhs.map((e) => this.checkExpr(e, scope));
96
- const rhsFlat =
97
- rhs.length === 1 && rhs[0].kind === "tuple" ? rhs[0].types : rhs;
120
+ const isTuple = rhs.length === 1 && rhs[0]?.kind === "tuple";
121
+ const rhsFlat = isTuple ? rhs[0].types : rhs;
98
122
  if (this._checkDefineCommaOkAssert(stmt, rhs, scope)) return;
123
+ if (
124
+ stmt.rhs[0]?.kind !== "RangeExpr" &&
125
+ stmt.lhs.length !== rhsFlat.length
126
+ ) {
127
+ const vars =
128
+ stmt.lhs.length === 1 ? "1 variable" : `${stmt.lhs.length} variables`;
129
+ const vals = isTuple
130
+ ? `returns ${rhsFlat.length} values`
131
+ : rhsFlat.length === 1
132
+ ? "1 value"
133
+ : `${rhsFlat.length} values`;
134
+ this.err(`assignment mismatch: ${vars} but ${vals}`, stmt);
135
+ }
99
136
  this._declareDefineVars(stmt, rhsFlat, scope);
100
137
  const allRedecl = stmt.lhs.every((e) => e._redecl || (e.name ?? e) === "_");
101
138
  if (allRedecl && stmt.lhs.length > 0) {
@@ -110,15 +147,15 @@ export const statementCheckMethods = {
110
147
  stmt.rhs.length !== 1 ||
111
148
  stmt.rhs[0].kind !== "IndexExpr"
112
149
  )
113
- return false;
150
+ return null;
114
151
  const mapBaseType = this.checkExpr(stmt.rhs[0].expr, scope);
115
152
  const resolvedBase =
116
153
  mapBaseType?.kind === "named" ? mapBaseType.underlying : mapBaseType;
117
- if (resolvedBase?.kind !== "map") return false;
154
+ if (resolvedBase?.kind !== "map") return null;
118
155
  this.checkExpr(stmt.rhs[0].index, scope);
119
156
  stmt._commaOkMap = true;
120
157
  stmt.rhs[0]._mapValueType = resolvedBase.value;
121
- return true;
158
+ return [resolvedBase.value, BOOL];
122
159
  },
123
160
 
124
161
  _checkConstAssignment(stmt, scope) {
@@ -163,12 +200,34 @@ export const statementCheckMethods = {
163
200
  },
164
201
 
165
202
  _checkAssignStmt(stmt, scope) {
166
- if (this._checkAssignCommaOkMap(stmt, scope)) return;
203
+ const commaOkMapTypes = this._checkAssignCommaOkMap(stmt, scope);
167
204
  if (this._isCommaOkTypeAssertAssign(stmt)) stmt.rhs[0]._commaOk = true;
168
205
  this._checkConstAssignment(stmt, scope);
169
- const rhs = stmt.rhs.map((e) => this.checkExpr(e, scope));
170
- const rhsFlat =
171
- rhs.length === 1 && rhs[0]?.kind === "tuple" ? rhs[0].types : rhs;
206
+ let rhsFlat;
207
+ if (commaOkMapTypes) {
208
+ rhsFlat = commaOkMapTypes;
209
+ } else {
210
+ const rhs = stmt.rhs.map((e) => this.checkExpr(e, scope));
211
+ const isTuple = rhs.length === 1 && rhs[0]?.kind === "tuple";
212
+ rhsFlat = isTuple ? rhs[0].types : rhs;
213
+ if (
214
+ !stmt.rhs[0]?._commaOk &&
215
+ stmt.rhs[0]?.kind !== "RangeExpr" &&
216
+ stmt.lhs.length !== rhsFlat.length
217
+ ) {
218
+ const vars =
219
+ stmt.lhs.length === 1 ? "1 variable" : `${stmt.lhs.length} variables`;
220
+ const vals = isTuple
221
+ ? `returns ${rhsFlat.length} values`
222
+ : rhsFlat.length === 1
223
+ ? "1 value"
224
+ : `${rhsFlat.length} values`;
225
+ this.err(`assignment mismatch: ${vars} but ${vals}`, stmt);
226
+ }
227
+ if (stmt.rhs[0]?._commaOk) {
228
+ rhsFlat = [rhs[0], BOOL];
229
+ }
230
+ }
172
231
  const lhsTypes = stmt.lhs.map((e) => this._checkAssignLhsType(e, scope));
173
232
  this._checkAssignTypes(stmt, rhsFlat, lhsTypes);
174
233
  },
@@ -853,6 +853,43 @@ export function setupExtendedGlobals(globals, types) {
853
853
  types.set("encoding/base64.Encoding", BASE64_ENCODING_T);
854
854
  types.set("base64.Encoding", BASE64_ENCODING_T);
855
855
 
856
+ // ── encoding/json ─────────────────────────────────────────────
857
+ const RAW_MESSAGE_T = {
858
+ kind: "named",
859
+ name: "json.RawMessage",
860
+ underlying: BYTE_SLICE,
861
+ methods: new Map(),
862
+ };
863
+ globals.define("json", {
864
+ kind: "namespace",
865
+ name: "json",
866
+ members: {
867
+ Marshal: {
868
+ kind: "func",
869
+ params: [ANY],
870
+ returns: [BYTE_SLICE, ERROR],
871
+ },
872
+ MarshalIndent: {
873
+ kind: "func",
874
+ params: [ANY, STRING, STRING],
875
+ returns: [BYTE_SLICE, ERROR],
876
+ },
877
+ Unmarshal: {
878
+ kind: "func",
879
+ params: [BYTE_SLICE, ANY],
880
+ returns: [ERROR],
881
+ },
882
+ Valid: {
883
+ kind: "func",
884
+ params: [BYTE_SLICE],
885
+ returns: [BOOL],
886
+ },
887
+ RawMessage: RAW_MESSAGE_T,
888
+ },
889
+ });
890
+ types.set("encoding/json.RawMessage", RAW_MESSAGE_T);
891
+ types.set("json.RawMessage", RAW_MESSAGE_T);
892
+
856
893
  for (const name of [
857
894
  "append",
858
895
  "len",
@@ -0,0 +1,73 @@
1
+ // src/typechecker/stdlib/simd.js
2
+ // `gofront/simd` — WebAssembly SIMD types and vector operations.
3
+ //
4
+ // F32x4 is a 128-bit 4-wide float32 vector with Go value semantics:
5
+ // - Lowered to WASM v128 in wasm packages.
6
+ // - Lowered to a frozen 4-number array with Math.fround in js packages.
7
+
8
+ import { FLOAT32, INT } from "../types.js";
9
+
10
+ export function setupSimdGlobals(globals, types) {
11
+ const f32x4Type = {
12
+ kind: "named",
13
+ name: "simd.F32x4",
14
+ underlying: {
15
+ kind: "simd",
16
+ simd: "F32x4",
17
+ elem: FLOAT32,
18
+ lanes: 4,
19
+ },
20
+ };
21
+
22
+ f32x4Type.underlying.methods = new Map([
23
+ ["Add", { kind: "func", params: [f32x4Type], returns: [f32x4Type] }],
24
+ ["Sub", { kind: "func", params: [f32x4Type], returns: [f32x4Type] }],
25
+ ["Mul", { kind: "func", params: [f32x4Type], returns: [f32x4Type] }],
26
+ ["Div", { kind: "func", params: [f32x4Type], returns: [f32x4Type] }],
27
+ ["Min", { kind: "func", params: [f32x4Type], returns: [f32x4Type] }],
28
+ ["Max", { kind: "func", params: [f32x4Type], returns: [f32x4Type] }],
29
+ ["Dot3", { kind: "func", params: [f32x4Type], returns: [FLOAT32] }],
30
+ ["Dot4", { kind: "func", params: [f32x4Type], returns: [FLOAT32] }],
31
+ ["Lane", { kind: "func", params: [INT], returns: [FLOAT32] }],
32
+ [
33
+ "WithLane",
34
+ { kind: "func", params: [INT, FLOAT32], returns: [f32x4Type] },
35
+ ],
36
+ ["Splat", { kind: "func", params: [FLOAT32], returns: [f32x4Type] }],
37
+ ]);
38
+
39
+ types.set("simd.F32x4", f32x4Type);
40
+ types.set("F32x4", f32x4Type);
41
+
42
+ const sharedFloat32 = types.get("shared.Float32") ?? {
43
+ kind: "named",
44
+ name: "shared.Float32",
45
+ underlying: { kind: "shared", elem: FLOAT32, shared: "Float32" },
46
+ };
47
+
48
+ const members = {
49
+ F32x4: f32x4Type,
50
+ NewF32x4: {
51
+ kind: "func",
52
+ params: [FLOAT32, FLOAT32, FLOAT32, FLOAT32],
53
+ returns: [f32x4Type],
54
+ },
55
+ Splat: {
56
+ kind: "func",
57
+ params: [FLOAT32],
58
+ returns: [f32x4Type],
59
+ },
60
+ LoadF32x4: {
61
+ kind: "func",
62
+ params: [sharedFloat32, INT],
63
+ returns: [f32x4Type],
64
+ },
65
+ StoreF32x4: {
66
+ kind: "func",
67
+ params: [sharedFloat32, INT, f32x4Type],
68
+ returns: [],
69
+ },
70
+ };
71
+
72
+ globals.define("simd", { kind: "namespace", name: "simd", members });
73
+ }
@@ -10,11 +10,13 @@
10
10
  import { setupCoreGlobals } from "./stdlib/core.js";
11
11
  import { setupExtendedGlobals } from "./stdlib/extended.js";
12
12
  import { setupSharedGlobals } from "./stdlib/shared.js";
13
+ import { setupSimdGlobals } from "./stdlib/simd.js";
13
14
  import { setupWebGlobals } from "./stdlib/web.js";
14
15
 
15
16
  export function setupGlobals(globals, types) {
16
17
  setupCoreGlobals(globals, types);
17
18
  setupExtendedGlobals(globals, types);
18
19
  setupSharedGlobals(globals, types);
20
+ setupSimdGlobals(globals, types);
19
21
  setupWebGlobals(globals, types);
20
22
  }
@@ -126,6 +126,7 @@ export const WASM_SUPPORTED_STDLIB = new Set([
126
126
  "time",
127
127
  "testing",
128
128
  "gofront/shared",
129
+ "gofront/simd",
129
130
  "sync",
130
131
  "crypto/rand",
131
132
  "crypto/sha256",
@@ -139,12 +140,14 @@ export const STDLIB_PACKAGES = new Set([
139
140
  "os",
140
141
  "gom",
141
142
  "regexp",
143
+ "encoding/json",
142
144
  ]);
143
145
 
144
146
  // Per-namespace member subset the WASM backend implements. `null` means every
145
147
  // member the checker knows about. Members outside the set are rejected in
146
148
  // wasm/both packages with "'strings.Split' is not yet available in wasm packages".
147
149
  export const WASM_STDLIB_MEMBERS = new Map([
150
+ ["json", new Set()],
148
151
  [
149
152
  "math",
150
153
  new Set([
@@ -839,8 +842,13 @@ export function typeStr(t) {
839
842
  return t.name || "interface{...}";
840
843
  case "namespace":
841
844
  return t.name || "namespace{...}";
842
- case "func":
843
- return `func(${t.params.map(typeStr).join(", ")}) ${typeStr(t.returns[0] ?? VOID)}`;
845
+ case "func": {
846
+ const retStr =
847
+ t.returns.length > 1
848
+ ? `(${t.returns.map(typeStr).join(", ")})`
849
+ : typeStr(t.returns[0] ?? VOID);
850
+ return `func(${t.params.map(typeStr).join(", ")}) ${retStr}`;
851
+ }
844
852
  case "tuple":
845
853
  return `(${t.types.map(typeStr).join(", ")})`;
846
854
  case "named":
@@ -887,6 +895,7 @@ export class Scope {
887
895
  this.parent = parent;
888
896
  this.symbols = new Map();
889
897
  this._consts = new Set(); // names declared as const in this scope
898
+ this._constExprs = new Map(); // name -> AST initializer expr
890
899
  this._locals = new Set(); // names declared as local variables (var / :=)
891
900
  this._used = new Set(); // names referenced in this scope
892
901
  }
@@ -897,9 +906,15 @@ export class Scope {
897
906
  this.symbols.set(name, type);
898
907
  if (name !== "_") this._locals.add(name);
899
908
  }
900
- defineConst(name, type) {
909
+ defineConst(name, type, expr = null) {
901
910
  this.symbols.set(name, type);
902
911
  this._consts.add(name);
912
+ if (expr) this._constExprs.set(name, expr);
913
+ }
914
+ lookupConstExpr(name) {
915
+ if (this._constExprs.has(name)) return this._constExprs.get(name);
916
+ if (this.parent) return this.parent.lookupConstExpr(name);
917
+ return null;
903
918
  }
904
919
  isConst(name) {
905
920
  if (this._consts.has(name)) return true;