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.
package/src/dts-parser.js CHANGED
@@ -203,9 +203,48 @@ export class DtsParser {
203
203
 
204
204
  // ── Type parsing ─────────────────────────────────────────────
205
205
 
206
+ _parseTupleElement() {
207
+ this.skip();
208
+ if (this.startsWith("...")) this.pos += 3;
209
+
210
+ const savedPos = this.pos;
211
+ const ident = this.readIdent();
212
+ if (ident) {
213
+ this.skip();
214
+ if (this.ch() === "?") {
215
+ this.pos++;
216
+ this.skip();
217
+ }
218
+ if (this.ch() === ":") {
219
+ this.pos++;
220
+ return this.parseType(false);
221
+ }
222
+ }
223
+ this.pos = savedPos;
224
+ return this.parseType(false);
225
+ }
226
+
227
+ _parseTupleType(inReturn) {
228
+ this.consume("[");
229
+ const elements = [];
230
+ while (!this.eof() && this.peek() !== "]") {
231
+ this.skip();
232
+ if (this.ch() === "]") break;
233
+ elements.push(this._parseTupleElement());
234
+ this.skip();
235
+ if (this.ch() !== ",") break;
236
+ this.pos++;
237
+ }
238
+ this.consume("]");
239
+ if (inReturn) {
240
+ return { kind: "tuple", types: elements };
241
+ }
242
+ return { kind: "slice", elem: ANY };
243
+ }
244
+
206
245
  // Parse a TypeScript type expression and return a GoFront type.
207
246
  // Always returns something; falls back to ANY for complex types.
208
- parseType() {
247
+ parseType(inReturn = false) {
209
248
  this.skip();
210
249
 
211
250
  // Skip modifiers
@@ -226,13 +265,16 @@ export class DtsParser {
226
265
  this.skip();
227
266
  if (this.ch() === "|" || this.ch() === "&") this.pos++;
228
267
 
229
- let base = this._parseBaseType();
268
+ let base = this._parseBaseType(inReturn);
230
269
 
231
270
  this.skip();
232
271
  // Array suffix: T[] or T[][]
233
272
  while (this.startsWith("[]")) {
234
273
  this.pos += 2;
235
- base = { kind: "slice", elem: base };
274
+ base = {
275
+ kind: "slice",
276
+ elem: base.kind === "tuple" ? { kind: "slice", elem: ANY } : base,
277
+ };
236
278
  this.skip();
237
279
  }
238
280
 
@@ -285,14 +327,11 @@ export class DtsParser {
285
327
  return { kind: "basic", name: "any", alias: name };
286
328
  }
287
329
 
288
- _parseBaseType() {
330
+ _parseBaseType(inReturn = false) {
289
331
  this.skip();
290
332
  const c = this.ch();
291
333
  if (c === "[") {
292
- this.pos++;
293
- this.skipTypeExpr("]");
294
- this.consume("]");
295
- return ANY;
334
+ return this._parseTupleType(inReturn);
296
335
  }
297
336
  if (c === "{") {
298
337
  this.skipBlock();
@@ -399,15 +438,24 @@ export class DtsParser {
399
438
  let ret = VOID;
400
439
  if (this.ch() === ":") {
401
440
  this.pos++;
402
- ret = this.parseType();
441
+ ret = this.parseType(true);
403
442
  }
404
443
  this.consume(";");
444
+ const isVariadic = params.some((p) => p.variadic);
405
445
  return {
406
446
  name,
407
447
  val: {
408
448
  kind: "func",
409
- params: params.map((p) => p.type),
410
- returns: [ret],
449
+ params: params.map((p) =>
450
+ p.variadic && p.type?.kind === "slice" ? p.type.elem : p.type,
451
+ ),
452
+ returns:
453
+ ret.kind === "tuple"
454
+ ? ret.types.length > 0
455
+ ? ret.types
456
+ : [VOID]
457
+ : [ret],
458
+ ...(isVariadic ? { variadic: true } : {}),
411
459
  },
412
460
  };
413
461
  }
@@ -545,13 +593,22 @@ export class DtsParser {
545
593
  let ret = VOID;
546
594
  if (this.ch() === ":") {
547
595
  this.pos++;
548
- ret = this.parseType();
596
+ ret = this.parseType(true);
549
597
  }
550
598
  this.consume(";");
599
+ const isVariadic = params.some((p) => p.variadic);
551
600
  out[name] = {
552
601
  kind: "func",
553
- params: params.map((p) => p.type),
554
- returns: [ret],
602
+ params: params.map((p) =>
603
+ p.variadic && p.type?.kind === "slice" ? p.type.elem : p.type,
604
+ ),
605
+ returns:
606
+ ret.kind === "tuple"
607
+ ? ret.types.length > 0
608
+ ? ret.types
609
+ : [VOID]
610
+ : [ret],
611
+ ...(isVariadic ? { variadic: true } : {}),
555
612
  };
556
613
  } else if (this.ch() === ":") {
557
614
  this.pos++;
@@ -6,12 +6,7 @@ import { forEachChild } from "./walk.js";
6
6
  export function isReferenceType(t) {
7
7
  if (!t) return false;
8
8
  const base = t.kind === "named" ? t.underlying : t;
9
- return (
10
- base?.kind === "struct" ||
11
- base?.kind === "slice" ||
12
- base?.kind === "map" ||
13
- base?.kind === "func"
14
- );
9
+ return base?.kind === "struct" || base?.kind === "func";
15
10
  }
16
11
 
17
12
  // Scan AST node for _addressTaken idents on scalars and populate a Set of boxed variable names.
@@ -9,7 +9,8 @@ export function computeEmbeddedStubs(name, methodDecls, checker) {
9
9
  const declared = new Set(methodDecls.map((m) => m.name));
10
10
  const stubs = [];
11
11
 
12
- for (const embed of resolvedType._embeds) {
12
+ for (const rawEmbed of resolvedType._embeds) {
13
+ const embed = rawEmbed.kind === "pointer" ? rawEmbed.base : rawEmbed;
13
14
  const embedName = embed.kind === "named" ? embed.name : null;
14
15
  if (!embedName) continue;
15
16
  const embedBase = embed.kind === "named" ? embed.underlying : embed;
@@ -0,0 +1,144 @@
1
+ // src/parser/struct-tag.js
2
+ // Parse Go struct field tags matching Go's reflect.StructTag specification.
3
+
4
+ function unquoteTagValue(s) {
5
+ if (s.length < 2 || s[0] !== '"' || s[s.length - 1] !== '"') return s;
6
+ const inner = s.slice(1, -1);
7
+ let res = "";
8
+ for (let i = 0; i < inner.length; i++) {
9
+ if (inner[i] === "\\" && i + 1 < inner.length) {
10
+ i++;
11
+ const esc = inner[i];
12
+ switch (esc) {
13
+ case "n":
14
+ res += "\n";
15
+ break;
16
+ case "t":
17
+ res += "\t";
18
+ break;
19
+ case "r":
20
+ res += "\r";
21
+ break;
22
+ case '"':
23
+ res += '"';
24
+ break;
25
+ case "\\":
26
+ res += "\\";
27
+ break;
28
+ case "a":
29
+ res += "\x07";
30
+ break;
31
+ case "b":
32
+ res += "\b";
33
+ break;
34
+ case "f":
35
+ res += "\f";
36
+ break;
37
+ case "v":
38
+ res += "\v";
39
+ break;
40
+ case "0":
41
+ res += "\0";
42
+ break;
43
+ default:
44
+ res += `\\${esc}`;
45
+ break;
46
+ }
47
+ } else {
48
+ res += inner[i];
49
+ }
50
+ }
51
+ return res;
52
+ }
53
+
54
+ /**
55
+ * Parses a raw struct tag string into a Map of key -> unquoted string value,
56
+ * adhering to Go reflect.StructTag convention.
57
+ *
58
+ * @param {string | null | undefined} tag
59
+ * @returns {Map<string, string>}
60
+ */
61
+ export function parseStructTag(tag) {
62
+ const map = new Map();
63
+ if (!tag || typeof tag !== "string") return map;
64
+
65
+ let str = tag;
66
+ while (str.length > 0) {
67
+ // Skip leading spaces
68
+ let i = 0;
69
+ while (i < str.length && str.charCodeAt(i) === 32) i++;
70
+ str = str.slice(i);
71
+ if (str.length === 0) break;
72
+
73
+ // Scan to colon: key must not contain space, quote, colon, or control characters
74
+ i = 0;
75
+ while (
76
+ i < str.length &&
77
+ str.charCodeAt(i) > 32 &&
78
+ str[i] !== ":" &&
79
+ str[i] !== '"' &&
80
+ str.charCodeAt(i) !== 127
81
+ ) {
82
+ i++;
83
+ }
84
+ if (
85
+ i === 0 ||
86
+ i + 1 >= str.length ||
87
+ str[i] !== ":" ||
88
+ str[i + 1] !== '"'
89
+ ) {
90
+ break;
91
+ }
92
+ const key = str.slice(0, i);
93
+ str = str.slice(i + 1); // skip colon, now points to opening '"'
94
+
95
+ // Scan quoted string to find closing '"'
96
+ i = 1;
97
+ while (i < str.length && str[i] !== '"') {
98
+ if (str[i] === "\\") i++; // skip escaped char
99
+ i++;
100
+ }
101
+ if (i >= str.length) break;
102
+
103
+ const qval = str.slice(0, i + 1);
104
+ str = str.slice(i + 1);
105
+
106
+ if (!map.has(key)) {
107
+ map.set(key, unquoteTagValue(qval));
108
+ }
109
+ }
110
+ return map;
111
+ }
112
+
113
+ /**
114
+ * Parses the "json" key from a struct tag string or parsed Map according to encoding/json rules.
115
+ *
116
+ * @param {string | Map<string, string> | null | undefined} tag
117
+ * @returns {{ name: string | null, omitempty: boolean, ignored: boolean, options: string[] }}
118
+ */
119
+ export function parseJsonTag(tag) {
120
+ if (!tag) {
121
+ return { name: null, omitempty: false, ignored: false, options: [] };
122
+ }
123
+ const map = typeof tag === "string" ? parseStructTag(tag) : tag;
124
+ if (!map.has("json")) {
125
+ return { name: null, omitempty: false, ignored: false, options: [] };
126
+ }
127
+ const raw = map.get("json");
128
+ const parts = raw.split(",");
129
+ const rawName = parts[0];
130
+ const options = parts.slice(1);
131
+ const omitempty = options.includes("omitempty");
132
+
133
+ if (rawName === "-" && parts.length === 1) {
134
+ return { name: "-", omitempty: false, ignored: true, options };
135
+ }
136
+
137
+ const name = rawName === "" ? null : rawName;
138
+ return {
139
+ name,
140
+ omitempty,
141
+ ignored: false,
142
+ options,
143
+ };
144
+ }
@@ -175,23 +175,80 @@ export const typeParserMethods = {
175
175
  this.expect(T.LBRACE);
176
176
  const fields = [];
177
177
  while (!this.check(T.RBRACE) && !this.check(T.EOF)) {
178
+ // Embedded pointer field: *Base or *pkg.Base
179
+ if (this.check(T.STAR)) {
180
+ this.advance();
181
+ let typeName = this.expect(T.IDENT).value;
182
+ if (this.check(T.DOT)) {
183
+ this.advance();
184
+ typeName += `.${this.expect(T.IDENT).value}`;
185
+ }
186
+ const type = {
187
+ kind: "PointerType",
188
+ base: { kind: "TypeName", name: typeName },
189
+ };
190
+ let tag = null;
191
+ if (this.check(T.STRING)) {
192
+ tag = this.advance().value;
193
+ }
194
+ fields.push({
195
+ names: [],
196
+ type,
197
+ embedded: true,
198
+ tag,
199
+ });
200
+ this.semi();
201
+ continue;
202
+ }
203
+
178
204
  const names = this.parseIdentList();
179
- // Check if this is an embedded field (just a type name)
180
- // e.g., type T struct { Base }
181
- if (this.check(T.SEMICOLON) || this.check(T.RBRACE)) {
182
- const typeName = names[0]; // assuming no comma list for embedded
205
+ // Check if this is a qualified embedded field: pkg.Base
206
+ if (names.length === 1 && this.check(T.DOT)) {
207
+ this.advance();
208
+ const typeName = `${names[0]}.${this.expect(T.IDENT).value}`;
209
+ let tag = null;
210
+ if (this.check(T.STRING)) {
211
+ tag = this.advance().value;
212
+ }
183
213
  fields.push({
184
214
  names: [],
185
215
  type: { kind: "TypeName", name: typeName },
186
216
  embedded: true,
217
+ tag,
187
218
  });
188
219
  this.semi();
189
220
  continue;
190
221
  }
222
+
223
+ // Embedded field (just a type name, e.g. Base), optionally tagged
224
+ if (
225
+ names.length === 1 &&
226
+ (this.check(T.SEMICOLON) ||
227
+ this.check(T.RBRACE) ||
228
+ this.check(T.STRING))
229
+ ) {
230
+ const typeName = names[0];
231
+ let tag = null;
232
+ if (this.check(T.STRING)) {
233
+ tag = this.advance().value;
234
+ }
235
+ fields.push({
236
+ names: [],
237
+ type: { kind: "TypeName", name: typeName },
238
+ embedded: true,
239
+ tag,
240
+ });
241
+ this.semi();
242
+ continue;
243
+ }
244
+
191
245
  const type = this.parseType();
192
246
  // Consume optional struct tag (backtick string), e.g. `json:"name"`
193
- if (this.check(T.STRING)) this.advance();
194
- fields.push({ names, type });
247
+ let tag = null;
248
+ if (this.check(T.STRING)) {
249
+ tag = this.advance().value;
250
+ }
251
+ fields.push({ names, type, tag });
195
252
  this.semi();
196
253
  }
197
254
  this.expect(T.RBRACE);
@@ -27,6 +27,16 @@ import {
27
27
  UNTYPED_INT,
28
28
  } from "./types.js";
29
29
 
30
+ // Unnamed type literal kinds for Go's "identical underlying types" assignability rule.
31
+ const TYPE_LITERAL_KINDS = new Set([
32
+ "slice",
33
+ "map",
34
+ "func",
35
+ "pointer",
36
+ "array",
37
+ "struct",
38
+ ]);
39
+
30
40
  /** @typedef {import('./index.js').TypeChecker} TypeChecker */
31
41
 
32
42
  // Untyped constant assignability: maps source.base → Set of compatible target.name values
@@ -195,6 +205,12 @@ export const assignabilityMethods = {
195
205
  target.kind === "named" ? target.underlying : target,
196
206
  );
197
207
  if (!isAny(target) && tBase?.kind !== "interface") return;
208
+ const named = this._namedMethodBoxSource(source);
209
+ if (named) {
210
+ node._ifaceBox = named.ptr ? "named_ptr" : "named";
211
+ node._ifaceNamedType = named.type;
212
+ return;
213
+ }
198
214
  const sBase = this.resolveType(
199
215
  source.kind === "named" ? source.underlying : source,
200
216
  );
@@ -204,6 +220,24 @@ export const assignabilityMethods = {
204
220
  if (info.ptrFields) node._ifacePtrFields = info.ptrFields;
205
221
  },
206
222
 
223
+ // A named non-struct type with methods (or a pointer to one) entering an
224
+ // interface: the JS backend keeps such values raw and boxes them here so
225
+ // dynamic method dispatch works.
226
+ _namedMethodBoxSource(source) {
227
+ const isPtr = source?.kind === "pointer";
228
+ const t = isPtr ? source.base : source;
229
+ if (t?.kind !== "named") return null;
230
+ const u = this.resolveType(t.underlying);
231
+ if (!u || u.kind === "struct" || u.kind === "interface") return null;
232
+ const methods = t.methods ?? t._generic?.methods;
233
+ if (!methods || methods.size === 0) return null;
234
+ // Only user-declared types (methods carry _ptrRecv); stdlib named types
235
+ // such as time.Duration have no box class.
236
+ if (![...methods.values()].some((m) => Object.hasOwn(m ?? {}, "_ptrRecv")))
237
+ return null;
238
+ return { type: t, ptr: isPtr };
239
+ },
240
+
207
241
  _getStructPtrFields(st, prefix = "", seen = new Set()) {
208
242
  if (st?.kind !== "struct" || !st?.fields) return [];
209
243
  if (seen.has(st)) return [];
@@ -316,9 +350,20 @@ export const assignabilityMethods = {
316
350
  );
317
351
  return;
318
352
  }
353
+ if (this._identicalUnderlyingLiteral(target, source, tBase, sBase)) return;
319
354
  this.err(`Cannot assign ${typeStr(source)} to ${typeStr(target)}`, node);
320
355
  },
321
356
 
357
+ // Identical underlying types are assignable when one side is an unnamed
358
+ // type literal (`var n Names = []string{...}`), as in Go.
359
+ _identicalUnderlyingLiteral(target, source, tBase, sBase) {
360
+ if ((target.kind === "named") === (source.kind === "named")) return false;
361
+ const unnamed = target.kind === "named" ? source : target;
362
+ return (
363
+ TYPE_LITERAL_KINDS.has(unnamed.kind) && typeStr(tBase) === typeStr(sBase)
364
+ );
365
+ },
366
+
322
367
  _isUntypedAssignable(target, source) {
323
368
  if (isUntyped(target)) return true;
324
369
  // Named types with a basic underlying type (`type Mode string`) accept
@@ -228,6 +228,7 @@ export const expressionCheckMethods = {
228
228
  _isStructOrNamedMethod(baseType, base, field) {
229
229
  return (
230
230
  (base?.kind === "struct" && base.methods?.has(field)) ||
231
+ (base?.kind === "simd" && base.methods?.has(field)) ||
231
232
  (baseType.kind === "named" && baseType.methods?.has(field))
232
233
  );
233
234
  },
@@ -513,6 +514,8 @@ export const expressionCheckMethods = {
513
514
  }
514
515
  if (typeStr(srcResolved) === typeStr(tgtResolved)) return;
515
516
  if (isNumeric(srcResolved) && isNumeric(tgtResolved)) return;
517
+ if (isString(srcResolved) && isString(tgtResolved)) return;
518
+ if (isBool(srcResolved) && isBool(tgtResolved)) return;
516
519
  if (
517
520
  isString(srcResolved) &&
518
521
  tgtResolved?.kind === "slice" &&
@@ -649,7 +652,8 @@ export const expressionCheckMethods = {
649
652
  return expr;
650
653
  },
651
654
 
652
- _constIntValue(expr) {
655
+ _constIntValue(expr, scope = null) {
656
+ if (!expr) return null;
653
657
  if (expr.kind === "BasicLit" && expr.litKind === "INT")
654
658
  return Number(expr.value);
655
659
  if (
@@ -660,6 +664,19 @@ export const expressionCheckMethods = {
660
664
  ) {
661
665
  return -Number(expr.operand.value);
662
666
  }
667
+ if (expr.kind === "UnaryExpr" && expr.op === "+") {
668
+ return this._constIntValue(expr.operand, scope);
669
+ }
670
+ if (expr.kind === "Ident") {
671
+ const c =
672
+ scope?.lookupConstExpr?.(expr.name) ??
673
+ this.globals.lookupConstExpr?.(expr.name) ??
674
+ this._constExprs?.get(expr.name);
675
+ if (c) return this._constIntValue(c, scope);
676
+ }
677
+ if (expr.kind === "ParenExpr") {
678
+ return this._constIntValue(expr.expr, scope);
679
+ }
663
680
  return null;
664
681
  },
665
682
 
@@ -731,7 +748,7 @@ export const expressionCheckMethods = {
731
748
  return this.err(`Cannot call non-function type ${typeStr(fnType)}`, expr);
732
749
  }
733
750
 
734
- this._checkCallArgs(fnType, argTypes, expr);
751
+ this._checkCallArgs(fnType, argTypes, expr, scope);
735
752
 
736
753
  // Pseudo-generic stdlib helpers (maps.Keys, slices.Clone, ...) declare
737
754
  // how their result derives from the first argument.
@@ -848,7 +865,7 @@ export const expressionCheckMethods = {
848
865
  );
849
866
  },
850
867
 
851
- _checkCallArgs(fnType, argTypes, expr) {
868
+ _checkCallArgs(fnType, argTypes, expr, scope = null) {
852
869
  this._checkArgCount(fnType, argTypes, expr);
853
870
  const maxLen = fnType.variadic
854
871
  ? argTypes.length
@@ -863,6 +880,30 @@ export const expressionCheckMethods = {
863
880
  const node = expr._multiForward ? null : expr.args[i];
864
881
  this.assertAssignable(targetParam, argTypes[i], node);
865
882
  }
883
+
884
+ // Validate Lane and WithLane constant integer lane index
885
+ if (expr.func?.kind === "SelectorExpr") {
886
+ const recvType = expr.func.expr?._type;
887
+ const isSimd =
888
+ recvType?.name === "simd.F32x4" ||
889
+ recvType?.name === "F32x4" ||
890
+ recvType?.underlying?.kind === "simd";
891
+ if (
892
+ isSimd &&
893
+ (expr.func.field === "Lane" || expr.func.field === "WithLane")
894
+ ) {
895
+ const laneArg = expr.args[0];
896
+ if (laneArg) {
897
+ const idx = this._constIntValue(laneArg, scope);
898
+ if (idx === null || idx < 0 || idx > 3) {
899
+ this.err(
900
+ "lane index must be a constant integer in range [0, 3]",
901
+ laneArg,
902
+ );
903
+ }
904
+ }
905
+ }
906
+ }
866
907
  },
867
908
 
868
909
  checkBuiltin(name, expr, argTypes, scope) {
@@ -959,6 +1000,8 @@ export const expressionCheckMethods = {
959
1000
  this._inferArraySize(expr, typeNode, base);
960
1001
  if (base?.kind === "struct")
961
1002
  return this._checkStructCompositeLit(expr, scope, t, base);
1003
+ if (base?.kind === "simd")
1004
+ return this._checkSimdCompositeLit(expr, scope, t, base);
962
1005
  if (this._isSliceOrArray(base))
963
1006
  return this._checkSliceArrayCompositeLit(expr, scope, t, base, typeNode);
964
1007
  if (base?.kind === "map")
@@ -966,6 +1009,17 @@ export const expressionCheckMethods = {
966
1009
  return this._checkUnknownCompositeLit(expr, scope, t);
967
1010
  },
968
1011
 
1012
+ _checkSimdCompositeLit(expr, scope, t, base) {
1013
+ if (expr.elems.length > 4) {
1014
+ this.err("too many values in simd.F32x4 literal", expr);
1015
+ }
1016
+ for (const elem of expr.elems) {
1017
+ const vt = this.checkExpr(elem, scope);
1018
+ this.assertAssignable(base.elem ?? FLOAT32, vt, elem);
1019
+ }
1020
+ return t;
1021
+ },
1022
+
969
1023
  _checkStructPositionalElems(expr, scope, t, base) {
970
1024
  const fieldNames = [...(base.fields?.keys() ?? [])];
971
1025
  for (let i = 0; i < expr.elems.length; i++) {
@@ -987,21 +1041,19 @@ export const expressionCheckMethods = {
987
1041
  for (const elem of expr.elems) {
988
1042
  if (elem.kind === "KeyValueExpr") {
989
1043
  const keyName = elem.key.name;
990
- const fieldType = base.fields?.get(keyName);
1044
+ const embed = base._embeds?.find((e) => {
1045
+ const inner = e.kind === "pointer" ? e.base : e;
1046
+ const name =
1047
+ inner.kind === "named" ? inner.name.split(".").pop() : null;
1048
+ return name === keyName;
1049
+ });
1050
+ if (embed) elem._isEmbedInit = true;
1051
+ const fieldType = base.fields?.get(keyName) ?? embed;
991
1052
  if (fieldType) {
992
1053
  const vt = this.checkExpr(elem.value, scope);
993
1054
  this.assertAssignable(fieldType, vt, elem.value);
994
1055
  } else {
995
- const embed = base._embeds?.find(
996
- (e) => (e.kind === "named" ? e.name : null) === keyName,
997
- );
998
- if (embed) {
999
- const vt = this.checkExpr(elem.value, scope);
1000
- this.assertAssignable(embed, vt, elem.value);
1001
- elem._isEmbedInit = true;
1002
- } else {
1003
- this.err(`Unknown field '${keyName}'`, elem.key);
1004
- }
1056
+ this.err(`Unknown field '${keyName}'`, elem.key);
1005
1057
  }
1006
1058
  } else {
1007
1059
  this.checkExpr(elem, scope);