gofront 0.0.4 → 0.0.6

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.
@@ -3,10 +3,15 @@
3
3
  import {
4
4
  ANY,
5
5
  BOOL,
6
+ COMPLEX128,
6
7
  ERROR,
8
+ FLOAT64,
7
9
  INT,
8
10
  isAny,
11
+ isArray,
9
12
  isBool,
13
+ isComplex,
14
+ isComplexOrNumeric,
10
15
  isNumeric,
11
16
  isString,
12
17
  NIL,
@@ -14,6 +19,7 @@ import {
14
19
  STRING,
15
20
  typeStr,
16
21
  UNTYPED_BOOL,
22
+ UNTYPED_COMPLEX,
17
23
  UNTYPED_FLOAT,
18
24
  UNTYPED_INT,
19
25
  UNTYPED_STRING,
@@ -45,6 +51,9 @@ export const expressionCheckMethods = {
45
51
  break;
46
52
  }
47
53
 
54
+ case "ImagLit":
55
+ return UNTYPED_COMPLEX;
56
+
48
57
  case "Ident": {
49
58
  const t = scope.lookup(expr.name);
50
59
  if (!t) return this.err(`Undefined: '${expr.name}'`, expr);
@@ -60,10 +69,27 @@ export const expressionCheckMethods = {
60
69
  return BOOL;
61
70
  }
62
71
  if (expr.op === "-" || expr.op === "+") {
63
- if (!isNumeric(ot) && !isAny(ot))
72
+ if (!isNumeric(ot) && !isComplex(ot) && !isAny(ot))
64
73
  this.err(`${expr.op} requires numeric`, expr);
65
74
  return ot;
66
75
  }
76
+ if (expr.op === "&") {
77
+ // Mark the operand as address-taken for codegen
78
+ if (expr.operand.kind === "Ident") {
79
+ expr.operand._addressTaken = true;
80
+ const sym = scope.lookup(expr.operand.name);
81
+ if (sym) sym._addressTaken = true;
82
+ }
83
+ return { kind: "pointer", base: ot };
84
+ }
85
+ if (expr.op === "*") {
86
+ if (ot === ANY) return ANY;
87
+ if (ot.kind === "pointer") return ot.base;
88
+ if (ot.kind === "named" && ot.underlying?.kind === "pointer")
89
+ return ot.underlying.base;
90
+ this.err(`cannot dereference non-pointer type ${typeStr(ot)}`, expr);
91
+ return ANY;
92
+ }
67
93
  return ot;
68
94
  }
69
95
 
@@ -114,7 +140,26 @@ export const expressionCheckMethods = {
114
140
  const bt = this.checkExpr(expr.expr, scope);
115
141
  this.checkExpr(expr.index, scope);
116
142
  if (isAny(bt)) return ANY;
117
- if (bt.kind === "slice" || bt.kind === "array") return bt.elem;
143
+ if (bt.kind === "slice" || bt.kind === "array") {
144
+ // Compile-time bounds checking for arrays
145
+ if (bt.kind === "array" && bt.size != null) {
146
+ const idx = this._constIntValue(expr.index);
147
+ if (idx !== null) {
148
+ if (idx < 0) {
149
+ this.err(
150
+ `invalid array index ${idx} (index must not be negative)`,
151
+ expr,
152
+ );
153
+ } else if (idx >= bt.size) {
154
+ this.err(
155
+ `invalid array index ${idx} (out of bounds for ${typeStr(bt)})`,
156
+ expr,
157
+ );
158
+ }
159
+ }
160
+ }
161
+ return bt.elem;
162
+ }
118
163
  if (bt.kind === "map") {
119
164
  expr._mapValueType = bt.value; // for codegen zero-value fallback
120
165
  return bt.value;
@@ -172,8 +217,74 @@ export const expressionCheckMethods = {
172
217
  }
173
218
 
174
219
  case "TypeConversion": {
175
- this.checkExpr(expr.expr, scope);
176
- return this.resolveTypeNode(expr.targetType, scope);
220
+ const srcType = this.checkExpr(expr.expr, scope);
221
+ const targetType = this.resolveTypeNode(expr.targetType, scope);
222
+ const srcResolved =
223
+ srcType?.kind === "named" ? srcType.underlying : srcType;
224
+ const tgtResolved =
225
+ targetType?.kind === "named" ? targetType.underlying : targetType;
226
+ // Reject float64(complex) — must use real()/imag()
227
+ if (isNumeric(targetType) && isComplex(srcType)) {
228
+ this.err(
229
+ `cannot convert ${typeStr(srcType)} to ${typeStr(targetType)} (use real() or imag())`,
230
+ expr,
231
+ );
232
+ }
233
+ // Allow complex128/64(numeric) and complex128/64(complex)
234
+ if (isComplex(targetType)) {
235
+ if (!isComplexOrNumeric(srcType) && !isAny(srcType)) {
236
+ this.err(
237
+ `cannot convert ${typeStr(srcType)} to ${typeStr(targetType)}`,
238
+ expr,
239
+ );
240
+ }
241
+ }
242
+ // Slice → array conversion: [N]T(slice)
243
+ if (tgtResolved?.kind === "array" && srcResolved?.kind === "slice") {
244
+ const tgtElem = tgtResolved.elem;
245
+ const srcElem = srcResolved.elem;
246
+ if (
247
+ tgtElem &&
248
+ srcElem &&
249
+ !isAny(tgtElem) &&
250
+ !isAny(srcElem) &&
251
+ typeStr(tgtElem) !== typeStr(srcElem)
252
+ ) {
253
+ this.err(
254
+ `cannot convert ${typeStr(srcType)} to ${typeStr(targetType)}`,
255
+ expr,
256
+ );
257
+ }
258
+ return targetType;
259
+ }
260
+ // Array → slice conversion: []T(array)
261
+ if (tgtResolved?.kind === "slice" && srcResolved?.kind === "array") {
262
+ const tgtElem = tgtResolved.elem;
263
+ const srcElem = srcResolved.elem;
264
+ if (
265
+ tgtElem &&
266
+ srcElem &&
267
+ !isAny(tgtElem) &&
268
+ !isAny(srcElem) &&
269
+ typeStr(tgtElem) !== typeStr(srcElem)
270
+ ) {
271
+ this.err(
272
+ `cannot convert ${typeStr(srcType)} to ${typeStr(targetType)}`,
273
+ expr,
274
+ );
275
+ }
276
+ return targetType;
277
+ }
278
+ // Reject conversions between array and incompatible types
279
+ if (tgtResolved?.kind === "array" && srcResolved?.kind !== "array") {
280
+ if (!isAny(srcType)) {
281
+ this.err(
282
+ `cannot convert ${typeStr(srcType)} to ${typeStr(targetType)}`,
283
+ expr,
284
+ );
285
+ }
286
+ }
287
+ return targetType;
177
288
  }
178
289
 
179
290
  case "TypeAssertExpr": {
@@ -211,7 +322,7 @@ export const expressionCheckMethods = {
211
322
  // string range: (int index, rune value) — rune is an alias for int
212
323
  return { kind: "tuple", types: [INT, INT] };
213
324
  }
214
- if (resolved?.kind === "slice") {
325
+ if (resolved?.kind === "slice" || resolved?.kind === "array") {
215
326
  return { kind: "tuple", types: [INT, resolved.elem ?? ANY] };
216
327
  }
217
328
  if (resolved?.kind === "map") {
@@ -224,14 +335,52 @@ export const expressionCheckMethods = {
224
335
  return { kind: "tuple", types: [collType] };
225
336
  }
226
337
 
338
+ case "InstantiationExpr": {
339
+ // Generic instantiation: Foo[int] or Foo[int, string]
340
+ const baseType = this.checkExpr(expr.expr, scope);
341
+ if (baseType?.kind === "generic") {
342
+ const typeArgs = expr.typeArgs.map((ta) =>
343
+ this.resolveTypeNode(ta, scope),
344
+ );
345
+ // Check constraints
346
+ for (
347
+ let i = 0;
348
+ i < baseType.typeParams.length && i < typeArgs.length;
349
+ i++
350
+ ) {
351
+ const tp = baseType.typeParams[i];
352
+ const constraint = tp.constraint
353
+ ? this.resolveTypeNode(tp.constraint, scope)
354
+ : null;
355
+ if (constraint) this.checkConstraint(typeArgs[i], constraint, expr);
356
+ }
357
+ return this.instantiateGenericFunc(baseType, typeArgs);
358
+ }
359
+ return baseType;
360
+ }
361
+
227
362
  default:
228
363
  return ANY;
229
364
  }
230
365
  return ANY;
231
366
  },
232
367
 
368
+ _constIntValue(expr) {
369
+ if (expr.kind === "BasicLit" && expr.litKind === "INT")
370
+ return Number(expr.value);
371
+ if (
372
+ expr.kind === "UnaryExpr" &&
373
+ expr.op === "-" &&
374
+ expr.operand?.kind === "BasicLit" &&
375
+ expr.operand.litKind === "INT"
376
+ ) {
377
+ return -Number(expr.operand.value);
378
+ }
379
+ return null;
380
+ },
381
+
233
382
  checkCall(expr, scope) {
234
- const fnType = this.checkExpr(expr.func, scope);
383
+ let fnType = this.checkExpr(expr.func, scope);
235
384
 
236
385
  // Handle built-ins before evaluating args; some (e.g. new) take type names as
237
386
  // arguments, which would otherwise produce false "Undefined" errors.
@@ -261,6 +410,30 @@ export const expressionCheckMethods = {
261
410
 
262
411
  if (isAny(fnType)) return ANY;
263
412
 
413
+ // Generic function inference
414
+ if (fnType.kind === "generic") {
415
+ const typeArgs = this.inferTypeArgs(fnType, argTypes);
416
+ if (!typeArgs) {
417
+ return this.err(
418
+ `Cannot infer type arguments for generic function '${fnType.name}'`,
419
+ expr,
420
+ );
421
+ }
422
+ // Check constraints
423
+ for (
424
+ let i = 0;
425
+ i < fnType.typeParams.length && i < typeArgs.length;
426
+ i++
427
+ ) {
428
+ const tp = fnType.typeParams[i];
429
+ const constraint = tp.constraint
430
+ ? this.resolveTypeNode(tp.constraint, scope)
431
+ : null;
432
+ if (constraint) this.checkConstraint(typeArgs[i], constraint, expr);
433
+ }
434
+ fnType = this.instantiateGenericFunc(fnType, typeArgs);
435
+ }
436
+
264
437
  if (fnType.kind !== "func") {
265
438
  return this.err(`Cannot call non-function type ${typeStr(fnType)}`, expr);
266
439
  }
@@ -302,10 +475,32 @@ export const expressionCheckMethods = {
302
475
  checkBuiltin(name, expr, argTypes, scope) {
303
476
  switch (name) {
304
477
  case "len":
305
- case "cap":
478
+ case "cap": {
479
+ // Compile-time len() for fixed arrays
480
+ if (
481
+ name === "len" &&
482
+ argTypes[0]?.kind === "array" &&
483
+ argTypes[0].size != null
484
+ ) {
485
+ expr._constLen = argTypes[0].size;
486
+ } else if (
487
+ name === "len" &&
488
+ argTypes[0]?.kind === "named" &&
489
+ argTypes[0].underlying?.kind === "array" &&
490
+ argTypes[0].underlying.size != null
491
+ ) {
492
+ expr._constLen = argTypes[0].underlying.size;
493
+ }
306
494
  return INT;
307
- case "append":
308
- return argTypes[0] ?? ANY;
495
+ }
496
+ case "append": {
497
+ const sliceType = argTypes[0] ?? ANY;
498
+ if (isArray(sliceType)) {
499
+ this.err(`cannot append to array (type ${typeStr(sliceType)})`, expr);
500
+ return sliceType;
501
+ }
502
+ return sliceType;
503
+ }
309
504
  case "copy":
310
505
  return INT;
311
506
  case "delete":
@@ -334,6 +529,39 @@ export const expressionCheckMethods = {
334
529
  return argTypes[0] ?? ANY;
335
530
  case "clear":
336
531
  return VOID;
532
+ case "complex": {
533
+ if (expr.args.length !== 2) {
534
+ this.err("complex() requires exactly 2 arguments", expr);
535
+ return ANY;
536
+ }
537
+ const crt = argTypes[0];
538
+ const cit = argTypes[1];
539
+ if (!isNumeric(crt)) {
540
+ this.err(`cannot use ${typeStr(crt)} as float in complex()`, expr);
541
+ return ANY;
542
+ }
543
+ if (!isNumeric(cit)) {
544
+ this.err(`cannot use ${typeStr(cit)} as float in complex()`, expr);
545
+ return ANY;
546
+ }
547
+ if (crt.kind === "untyped" && cit.kind === "untyped")
548
+ return UNTYPED_COMPLEX;
549
+ return COMPLEX128;
550
+ }
551
+ case "real":
552
+ case "imag": {
553
+ if (expr.args.length !== 1) {
554
+ this.err(`${name}() requires exactly 1 argument`, expr);
555
+ return ANY;
556
+ }
557
+ const zt = argTypes[0];
558
+ if (!isComplex(zt)) {
559
+ this.err(`cannot use ${typeStr(zt)} as complex in ${name}()`, expr);
560
+ return ANY;
561
+ }
562
+ if (zt.kind === "untyped") return UNTYPED_FLOAT;
563
+ return FLOAT64;
564
+ }
337
565
  default:
338
566
  return ANY;
339
567
  }
@@ -347,6 +575,19 @@ export const expressionCheckMethods = {
347
575
  : this.resolveTypeNode(expr.typeExpr, scope);
348
576
  const base = t.kind === "named" ? t.underlying : t;
349
577
 
578
+ // [...]T size inference: count elements and set size
579
+ const typeNode = expr.typeExpr;
580
+ if (
581
+ typeNode?.kind === "ArrayType" &&
582
+ typeNode.inferLen &&
583
+ base?.kind === "array"
584
+ ) {
585
+ const maxIndex = computeMaxIndex(expr.elems);
586
+ base.size = maxIndex + 1;
587
+ // If t is a named type wrapping the array, the underlying is already base
588
+ // If t === base, it's the same object
589
+ }
590
+
350
591
  if (base?.kind === "struct") {
351
592
  // Detect positional (unkeyed) literal: all elements must be non-KeyValueExpr
352
593
  const hasPositional = expr.elems.some((e) => e.kind !== "KeyValueExpr");
@@ -404,11 +645,26 @@ export const expressionCheckMethods = {
404
645
  if (elem.kind === "CompositeLit" && elem.typeExpr === null) {
405
646
  const et = this.checkCompositeLit(elem, scope, base.elem);
406
647
  elem._type = et;
407
- } else if (elem.kind !== "KeyValueExpr") {
648
+ } else if (elem.kind === "KeyValueExpr") {
649
+ const vt = this.checkExpr(elem.value, scope);
650
+ this.assertAssignable(base.elem, vt, elem.value);
651
+ } else {
408
652
  const et = this.checkExpr(elem, scope);
409
653
  this.assertAssignable(base.elem, et, elem);
410
654
  }
411
655
  }
656
+
657
+ // Composite literal element count validation for explicit-size arrays
658
+ if (base.kind === "array" && base.size != null && !typeNode?.inferLen) {
659
+ const maxIndex = computeMaxIndex(expr.elems);
660
+ if (maxIndex >= base.size) {
661
+ this.err(
662
+ `array index ${maxIndex} out of bounds [0:${base.size}]`,
663
+ expr,
664
+ );
665
+ }
666
+ }
667
+
412
668
  return t;
413
669
  }
414
670
 
@@ -445,3 +701,16 @@ export const expressionCheckMethods = {
445
701
  return t ?? ANY;
446
702
  },
447
703
  };
704
+
705
+ function computeMaxIndex(elems) {
706
+ let sequential = 0;
707
+ let maxKeyed = -1;
708
+ for (const e of elems) {
709
+ if (e.kind === "KeyValueExpr" && e.key?.value !== undefined) {
710
+ maxKeyed = Math.max(maxKeyed, Number(e.key.value));
711
+ } else {
712
+ sequential++;
713
+ }
714
+ }
715
+ return Math.max(sequential - 1, maxKeyed);
716
+ }
@@ -7,6 +7,7 @@ import {
7
7
  isAny,
8
8
  isBool,
9
9
  isVoid,
10
+ iteratorYieldParams,
10
11
  Scope,
11
12
  } from "./types.js";
12
13
 
@@ -182,15 +183,30 @@ export const statementCheckMethods = {
182
183
 
183
184
  case "ForStmt": {
184
185
  const inner = new Scope(scope);
185
- if (stmt.init) this.checkStmt(stmt.init, inner, returnType);
186
+ let iterInfo = null;
187
+ if (stmt.init?.rhs?.[0]?.kind === "RangeExpr") {
188
+ iterInfo = this._checkRangeIterStmt(stmt.init, inner, returnType);
189
+ }
190
+ if (!iterInfo && stmt.init)
191
+ this.checkStmt(stmt.init, inner, returnType);
186
192
  if (stmt.cond) {
187
- // for range N { } — RangeExpr as condition is valid (Go 1.22 int range)
188
193
  if (stmt.cond.kind !== "RangeExpr") {
189
194
  const ct = this.checkExpr(stmt.cond, inner);
190
195
  if (!isBool(ct) && !isAny(ct))
191
196
  this.err("For condition must be bool", stmt);
192
197
  } else {
193
- this.checkExpr(stmt.cond.expr, inner);
198
+ // for range expr — could be int range or 0-param iterator
199
+ const ct = this.checkExpr(stmt.cond.expr, inner);
200
+ const info = iteratorYieldParams(ct);
201
+ if (info) {
202
+ stmt.cond._isIterator = true;
203
+ stmt.cond._yieldParams = info.yieldParams;
204
+ } else if (ct?.kind === "func") {
205
+ this.err(
206
+ "cannot range over func: not an iterator function",
207
+ stmt,
208
+ );
209
+ }
194
210
  }
195
211
  }
196
212
  if (stmt.post) this.checkStmt(stmt.post, inner, returnType);
@@ -284,4 +300,49 @@ export const statementCheckMethods = {
284
300
  break;
285
301
  }
286
302
  },
303
+
304
+ _checkRangeIterStmt(initStmt, scope, _returnType) {
305
+ const rangeExpr = initStmt.rhs[0];
306
+ const iterType = this.checkExpr(rangeExpr.expr, scope);
307
+ const info = iteratorYieldParams(iterType);
308
+ if (!info) {
309
+ // If it's a func but not a valid iterator shape, reject it
310
+ const underlying =
311
+ iterType?.kind === "named" ? iterType.underlying : iterType;
312
+ if (underlying?.kind === "func") {
313
+ this.err("cannot range over func: not an iterator function", initStmt);
314
+ }
315
+ return null;
316
+ }
317
+
318
+ const lhs = initStmt.lhs;
319
+ if (lhs.length > info.yieldParams.length) {
320
+ this.err(
321
+ `range over iterator: too many loop variables (got ${lhs.length}, max ${info.yieldParams.length})`,
322
+ initStmt,
323
+ );
324
+ }
325
+
326
+ // Annotate for codegen
327
+ rangeExpr._isIterator = true;
328
+ rangeExpr._yieldParams = info.yieldParams;
329
+
330
+ // Bind loop variables to their yield param types
331
+ if (initStmt.kind === "DefineStmt") {
332
+ for (let i = 0; i < lhs.length; i++) {
333
+ const name = lhs[i].name ?? lhs[i];
334
+ if (name === "_") continue;
335
+ const varType = info.yieldParams[i] ?? ANY;
336
+ scope.defineLocal(name, varType);
337
+ }
338
+ } else {
339
+ // AssignStmt — just type-check the lhs expressions
340
+ for (const e of lhs) {
341
+ if (e.kind === "Ident" && e.name !== "_") {
342
+ this.checkExpr(e, scope);
343
+ }
344
+ }
345
+ }
346
+ return info;
347
+ },
287
348
  };
@@ -47,7 +47,13 @@ export const BOOL = { kind: "basic", name: "bool" };
47
47
  export const ANY = { kind: "basic", name: "any" };
48
48
  export const VOID = { kind: "basic", name: "void" };
49
49
  export const NIL = { kind: "basic", name: "nil" };
50
- export const ERROR = { kind: "basic", name: "error" };
50
+ export const ERROR = {
51
+ kind: "interface",
52
+ name: "error",
53
+ methods: new Map([
54
+ ["Error", { kind: "func", params: [], returns: [STRING], async: false }],
55
+ ]),
56
+ };
51
57
 
52
58
  // ── Untyped constant types (Go spec §Constants) ─────────────
53
59
  // Untyped constants coerce to any compatible typed context.
@@ -56,6 +62,11 @@ export const UNTYPED_FLOAT = { kind: "untyped", base: "float64" };
56
62
  export const UNTYPED_STRING = { kind: "untyped", base: "string" };
57
63
  export const UNTYPED_BOOL = { kind: "untyped", base: "bool" };
58
64
 
65
+ // ── Complex types ────────────────────────────────────────────
66
+ export const COMPLEX128 = { kind: "basic", name: "complex128" };
67
+ export const COMPLEX64 = { kind: "basic", name: "complex64" };
68
+ export const UNTYPED_COMPLEX = { kind: "untyped", base: "complex128" };
69
+
59
70
  export const BASIC_TYPES = {
60
71
  int: INT,
61
72
  float64: FLOAT64,
@@ -64,7 +75,6 @@ export const BASIC_TYPES = {
64
75
  any: ANY,
65
76
  byte: INT,
66
77
  rune: INT,
67
- error: ERROR,
68
78
  // Sized integer / float aliases — all map to int or float64 at runtime
69
79
  uint: INT,
70
80
  int8: INT,
@@ -77,19 +87,51 @@ export const BASIC_TYPES = {
77
87
  uint64: INT,
78
88
  uintptr: INT,
79
89
  float32: FLOAT64,
90
+ complex64: COMPLEX64,
91
+ complex128: COMPLEX128,
80
92
  };
81
93
 
94
+ export const COMPARABLE = { kind: "basic", name: "comparable" };
95
+
96
+ export function isTypeParam(t) {
97
+ return t?.kind === "typeParam";
98
+ }
99
+
100
+ export function isGeneric(t) {
101
+ return t?.kind === "generic";
102
+ }
103
+
82
104
  export function isNumeric(t) {
83
- if (t?.kind === "untyped") return t.base === "int" || t.base === "float64";
84
- return t?.kind === "basic" && (t.name === "int" || t.name === "float64");
105
+ if (!t) return false;
106
+ if (t.kind === "untyped") return t.base === "int" || t.base === "float64";
107
+ if (t.kind === "basic") return t.name === "int" || t.name === "float64";
108
+ if (t.kind === "named") return isNumeric(t.underlying);
109
+ return false;
110
+ }
111
+ export function isComplex(t) {
112
+ if (!t) return false;
113
+ if (t.kind === "basic" && (t.name === "complex128" || t.name === "complex64"))
114
+ return true;
115
+ if (t.kind === "untyped" && t.base === "complex128") return true;
116
+ if (t.kind === "named") return isComplex(t.underlying);
117
+ return false;
118
+ }
119
+ export function isComplexOrNumeric(t) {
120
+ return isNumeric(t) || isComplex(t);
85
121
  }
86
122
  export function isString(t) {
87
- if (t?.kind === "untyped") return t.base === "string";
88
- return t?.kind === "basic" && t.name === "string";
123
+ if (!t) return false;
124
+ if (t.kind === "untyped") return t.base === "string";
125
+ if (t.kind === "basic") return t.name === "string";
126
+ if (t.kind === "named") return isString(t.underlying);
127
+ return false;
89
128
  }
90
129
  export function isBool(t) {
91
- if (t?.kind === "untyped") return t.base === "bool";
92
- return t?.kind === "basic" && t.name === "bool";
130
+ if (!t) return false;
131
+ if (t.kind === "untyped") return t.base === "bool";
132
+ if (t.kind === "basic") return t.name === "bool";
133
+ if (t.kind === "named") return isBool(t.underlying);
134
+ return false;
93
135
  }
94
136
  export function isAny(t) {
95
137
  return t?.kind === "basic" && t.name === "any";
@@ -101,7 +143,23 @@ export function isVoid(t) {
101
143
  return t?.kind === "basic" && t.name === "void";
102
144
  }
103
145
  export function isError(t) {
104
- return t?.kind === "basic" && t.name === "error";
146
+ if (!t) return false;
147
+ if (t === ERROR) return true;
148
+ if (t.kind === "interface" && t.name === "error") return true;
149
+ if (t.kind === "named" && isError(t.underlying)) return true;
150
+ return false;
151
+ }
152
+ export function isPointer(t) {
153
+ if (!t) return false;
154
+ if (t.kind === "pointer") return true;
155
+ if (t.kind === "named") return t.underlying?.kind === "pointer";
156
+ return false;
157
+ }
158
+ export function isArray(t) {
159
+ if (!t) return false;
160
+ if (t.kind === "array") return true;
161
+ if (t.kind === "named") return t.underlying?.kind === "array";
162
+ return false;
105
163
  }
106
164
  export function isUntyped(t) {
107
165
  return t?.kind === "untyped";
@@ -119,6 +177,8 @@ export function defaultType(t) {
119
177
  return STRING;
120
178
  case "bool":
121
179
  return BOOL;
180
+ case "complex128":
181
+ return COMPLEX128;
122
182
  default:
123
183
  return ANY;
124
184
  }
@@ -135,7 +195,7 @@ export function typeStr(t) {
135
195
  case "slice":
136
196
  return `[]${typeStr(t.elem)}`;
137
197
  case "array":
138
- return `[${t.size}]${typeStr(t.elem)}`;
198
+ return `[${t.size ?? "..."}]${typeStr(t.elem)}`;
139
199
  case "map":
140
200
  return `map[${typeStr(t.key)}]${typeStr(t.value)}`;
141
201
  case "struct":
@@ -152,11 +212,37 @@ export function typeStr(t) {
152
212
  return t.name;
153
213
  case "pointer":
154
214
  return `*${typeStr(t.base)}`;
215
+ case "typeParam":
216
+ return t.name;
217
+ case "generic":
218
+ return t.name || `generic(${typeStr(t.underlying)})`;
155
219
  default:
156
220
  return "?";
157
221
  }
158
222
  }
159
223
 
224
+ // ── Iterator function detection (Go 1.23 range-over-func) ───
225
+
226
+ /**
227
+ * Returns null if t is not an iterator function.
228
+ * Returns { yieldParams: Type[] } if it is, where yieldParams are the
229
+ * types the range variables will be bound to (0, 1, or 2 elements).
230
+ *
231
+ * An iterator is func(yield func(...) bool) — single param that is a
232
+ * func returning bool with 0-2 params.
233
+ */
234
+ export function iteratorYieldParams(t) {
235
+ const fn = t?.kind === "named" ? t.underlying : t;
236
+ if (fn?.kind !== "func") return null;
237
+ if (fn.params.length !== 1) return null;
238
+ const yieldFn =
239
+ fn.params[0]?.kind === "named" ? fn.params[0].underlying : fn.params[0];
240
+ if (yieldFn?.kind !== "func") return null;
241
+ if (yieldFn.returns.length !== 1 || !isBool(yieldFn.returns[0])) return null;
242
+ if (yieldFn.params.length > 2) return null;
243
+ return { yieldParams: yieldFn.params };
244
+ }
245
+
160
246
  // ── Scope / environment ──────────────────────────────────────
161
247
 
162
248
  export class Scope {