gofront 0.0.5 → 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.
package/README.md CHANGED
@@ -1,13 +1,13 @@
1
1
  ## About
2
2
 
3
- I love Go for the backend — simple, type-safe, no nonsense. I like JavaScript for the
4
- frontend — runs everywhere, no setup. What I don't like is JavaScript's loose typing. And
5
- I don't like TypeScript either. I know, controversial. It's fine, I just don't enjoy it.
3
+ Go for the backend: simple, type-safe, no nonsense. JavaScript for the frontend: runs
4
+ everywhere, no setup. The problem is JavaScript's loose typing — and TypeScript never
5
+ quite felt like home either.
6
6
 
7
- So I built GoFront. Go syntax, type safety, compiles to plain ES modules. Same language
7
+ So I built GoFront. Go syntax and type safety, compiling to plain ES modules. One language
8
8
  front and back, no runtime, no framework, no tsconfig.json.
9
9
 
10
- Is this useful? Probably not, but it was fun to build:D
10
+ Probably not useful. Definitely fun to build.
11
11
 
12
12
  ```go
13
13
  package main
@@ -258,16 +258,18 @@ npm run build:reactive # → example/reactive/app.js
258
258
  gofront <file.go> compile single file → stdout
259
259
  gofront <dir> compile all *.go in directory → stdout
260
260
  gofront <input> -o out.js write output to file (prints elapsed compile time e.g. "15ms")
261
- gofront <input> --check type-check only
262
- gofront <input> --watch watch for changes and recompile
263
- gofront <input> --source-map append inline source map to output
264
- gofront <input> --minify minify output (built-in minifier)
265
- gofront <input> --minify --mangle minify and rename local identifiers
266
- gofront <file.go> --ast dump AST (debug)
267
- gofront <file.go> --tokens dump tokens (debug)
268
- gofront init [dir] scaffold a new project
269
- gofront --version / -v print version
270
- gofront --help / -h print this help
261
+ gofront <input> --check type-check only
262
+ gofront <input> --watch watch for changes and recompile
263
+ gofront <input> -o out.js --serve watch + serve with live reload (default port 3000)
264
+ gofront <input> -o out.js --serve --port 8080 use a custom port
265
+ gofront <input> --source-map append inline source map (single file or directory; multi-file packages emit per-file mappings)
266
+ gofront <input> --minify minify output (built-in minifier)
267
+ gofront <input> --minify --mangle minify and rename local identifiers
268
+ gofront <file.go> --ast dump AST (debug)
269
+ gofront <file.go> --tokens dump tokens (debug)
270
+ gofront init [dir] scaffold a new project
271
+ gofront --version / -v print version
272
+ gofront --help / -h print this help
271
273
  ```
272
274
 
273
275
  ---
@@ -403,14 +405,18 @@ signatures into GoFront's internal type representation.
403
405
 
404
406
  | Package | Functions |
405
407
  |---|---|
406
- | `fmt` | `Sprintf`, `Printf`, `Println`, `Print`, `Errorf` — format verbs: `%v`, `%d`, `%s`, `%t`, `%x`, `%o`, `%b`, `%q`, `%e`, `%g`, `%w`, width/precision |
407
- | `strings` | `Contains`, `HasPrefix`, `HasSuffix`, `Index`, `LastIndex`, `Count`, `Repeat`, `Replace`, `ReplaceAll`, `ToUpper`, `ToLower`, `TrimSpace`, `Trim`, `TrimPrefix`, `TrimSuffix`, `TrimLeft`, `TrimRight`, `Split`, `Join`, `EqualFold` |
408
- | `bytes` | `Contains`, `HasPrefix`, `HasSuffix`, `Index`, `Join`, `Split`, `Replace`, `ToUpper`, `ToLower`, `TrimSpace`, `Equal`, `Count`, `Repeat` |
408
+ | `fmt` | `Sprintf`, `Printf`, `Println`, `Print`, `Errorf`, `Fprintf`, `Fprintln`, `Fprint` — format verbs: `%v`, `%d`, `%s`, `%t`, `%x`, `%o`, `%b`, `%q`, `%e`, `%g`, `%w`, width/precision |
409
+ | `strings` | `Contains`, `HasPrefix`, `HasSuffix`, `Index`, `LastIndex`, `Count`, `Repeat`, `Replace`, `ReplaceAll`, `ToUpper`, `ToLower`, `TrimSpace`, `Trim`, `TrimPrefix`, `TrimSuffix`, `TrimLeft`, `TrimRight`, `Split`, `Join`, `EqualFold`; **`Builder`** type (`WriteString`, `WriteByte`, `WriteRune`, `Write`, `String`, `Len`, `Reset`, `Grow`) |
410
+ | `bytes` | `Contains`, `HasPrefix`, `HasSuffix`, `Index`, `Join`, `Split`, `Replace`, `ToUpper`, `ToLower`, `TrimSpace`, `Equal`, `Count`, `Repeat`; **`Buffer`** type (`WriteString`, `WriteByte`, `Write`, `String`, `Bytes`, `Len`, `Reset`) |
409
411
  | `strconv` | `Itoa`, `Atoi`, `FormatBool`, `FormatInt`, `FormatFloat`, `ParseFloat`, `ParseInt`, `ParseBool` |
410
412
  | `sort` | `Ints`, `Float64s`, `Strings`, `Slice`, `SliceStable`, `SliceIsSorted` |
411
413
  | `math` | `Abs`, `Floor`, `Ceil`, `Round`, `Sqrt`, `Cbrt`, `Pow`, `Log`, `Log2`, `Log10`, trig functions + `Pi`, `E`, `MaxFloat64`, `MaxInt`, `MinInt` |
412
414
  | `errors` | `New`, `Is`, `Unwrap` — custom error types via interface satisfaction |
413
415
  | `time` | `Now`, `Since`, `Sleep` + `Millisecond`, `Second`, `Minute`, `Hour` constants |
416
+ | `html` | `EscapeString`, `UnescapeString` |
417
+ | `maps` | `Keys`, `Values`, `Clone`, `Copy`, `Equal`, `EqualFunc`, `Delete`, `DeleteFunc` |
418
+ | `slices` | `Contains`, `Index`, `Equal`, `Compare`, `Sort`, `SortFunc`, `SortStableFunc`, `IsSorted`, `IsSortedFunc`, `Reverse`, `Max`, `Min`, `MaxFunc`, `MinFunc`, `Clone`, `Compact`, `CompactFunc`, `Concat`, `Delete`, `DeleteFunc`, `Insert`, `Replace`, `Grow`, `Clip` |
419
+ | `regexp` | `MustCompile`, `Compile`, `MatchString`, `QuoteMeta`; **`*Regexp`** methods: `MatchString`, `FindString`, `FindStringIndex`, `FindAllString`, `FindStringSubmatch`, `FindAllStringSubmatch`, `ReplaceAllString`, `ReplaceAllLiteralString`, `Split`, `String`. Inline flags (`(?i)`, `(?m)`, `(?s)`) are extracted automatically into the JS `RegExp` constructor. |
414
420
  | `unicode` | `IsLetter`, `IsDigit`, `IsSpace`, `IsUpper`, `IsLower`, `IsPunct`, `IsControl`, `IsPrint`, `IsGraphic`, `ToUpper`, `ToLower` |
415
421
  | `os` | `Exit`, `Args`, `Getenv` |
416
422
 
@@ -494,7 +500,7 @@ Design documents for planned features are organised by release under `docs/v*/`
494
500
  npm test
495
501
  ```
496
502
 
497
- 869 tests covering language features, type errors, edge cases, DOM (jsdom), external
503
+ 940 tests covering language features, type errors, edge cases, DOM (jsdom), external
498
504
  `.d.ts`, npm resolver, multi-file compilation, embedded structs, string formatting, map
499
505
  iteration order, integer overflow semantics, unused variable detection, unused import
500
506
  detection, semantic difference verification, stdlib shim packages, generics (type params,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gofront",
3
- "version": "0.0.5",
3
+ "version": "0.0.6",
4
4
  "author": "Luuk van Venrooij",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -443,6 +443,19 @@ export const expressionGenMethods = {
443
443
  if (result !== undefined) return result;
444
444
  }
445
445
 
446
+ // strings.Builder / bytes.Buffer / regexp.Regexp method dispatch
447
+ if (expr.func.kind === "SelectorExpr") {
448
+ const recvType = expr.func.expr._type;
449
+ const recvName = recvType?.name ?? recvType?.base?.name;
450
+ if (recvName === "strings.Builder" || recvName === "bytes.Buffer") {
451
+ const typeName = recvType?.name ?? recvType?.base?.name;
452
+ return this._genBuilderCall(typeName, expr.func.field, expr);
453
+ }
454
+ if (recvName === "regexp.Regexp") {
455
+ return this._genRegexpMethodCall(expr.func.field, expr);
456
+ }
457
+ }
458
+
446
459
  // Mark the callee so SelectorExpr knows it's being called, not used as a value
447
460
  expr.func._callee = true;
448
461
  const rawFn = this.genExpr(expr.func);
@@ -662,6 +675,8 @@ export const expressionGenMethods = {
662
675
  case "TypeName": {
663
676
  const basic = this._zeroForBasicName(typeNode.name);
664
677
  if (basic !== null) return basic;
678
+ if (typeNode.name === "strings.Builder") return '{ _buf: "" }';
679
+ if (typeNode.name === "bytes.Buffer") return "{ _buf: [] }";
665
680
  if (this.structNames.has(typeNode.name))
666
681
  return `new ${typeNode.name}()`;
667
682
  return "null";
@@ -706,6 +721,8 @@ export const expressionGenMethods = {
706
721
  return `{ ${fields} }`;
707
722
  }
708
723
  case "named":
724
+ if (t.name === "strings.Builder") return '{ _buf: "" }';
725
+ if (t.name === "bytes.Buffer") return "{ _buf: [] }";
709
726
  if (this.structNames.has(t.name)) return `new ${t.name}()`;
710
727
  return "null";
711
728
  default:
@@ -776,6 +793,268 @@ export const expressionGenMethods = {
776
793
  return base?.kind === "struct" || base?.kind === "array";
777
794
  },
778
795
 
796
+ _genBuilderCall(typeName, method, expr) {
797
+ // Resolve the receiver — may be a direct var or a &var (pointer)
798
+ const recv = expr.func.expr;
799
+ const isPtr = recv._type?.kind === "pointer";
800
+ const base = isPtr ? `${this.genExpr(recv)}.value` : this.genExpr(recv);
801
+ const args = expr.args.map((a) => this.genExpr(a));
802
+
803
+ if (typeName === "strings.Builder") {
804
+ switch (method) {
805
+ case "WriteString":
806
+ return `(${base}._buf += ${args[0]}, [${args[0]}.length, null])`;
807
+ case "WriteByte":
808
+ case "WriteRune":
809
+ return `(${base}._buf += String.fromCodePoint(${args[0]}))`;
810
+ case "Write":
811
+ return `(${base}._buf += String.fromCharCode(...${args[0]}))`;
812
+ case "String":
813
+ return `${base}._buf`;
814
+ case "Len":
815
+ return `${base}._buf.length`;
816
+ case "Reset":
817
+ return `(${base}._buf = "")`;
818
+ case "Grow":
819
+ return "undefined";
820
+ }
821
+ }
822
+
823
+ if (typeName === "bytes.Buffer") {
824
+ switch (method) {
825
+ case "WriteString":
826
+ return `(${base}._buf.push(...new TextEncoder().encode(${args[0]})), [${args[0]}.length, null])`;
827
+ case "WriteByte":
828
+ return `${base}._buf.push(${args[0]})`;
829
+ case "Write":
830
+ return `(${base}._buf.push(...${args[0]}), [${args[0]}.length, null])`;
831
+ case "String":
832
+ return `new TextDecoder().decode(new Uint8Array(${base}._buf))`;
833
+ case "Bytes":
834
+ return `${base}._buf.slice()`;
835
+ case "Len":
836
+ return `${base}._buf.length`;
837
+ case "Reset":
838
+ return `(${base}._buf = [])`;
839
+ case "Grow":
840
+ return "undefined";
841
+ }
842
+ }
843
+
844
+ return undefined;
845
+ },
846
+
847
+ _genRegexp(fn, a) {
848
+ // Converts a Go regexp pattern string (which may contain inline flags like (?i))
849
+ // into a JS RegExp, extracting the flags into the second constructor argument.
850
+ const makeRegExp = (pat) =>
851
+ `((p) => { const m = /^\\(\\?([gimsuy]+)\\)/.exec(p); return m ? new RegExp(p.slice(m[0].length), m[1]) : new RegExp(p); })(${pat})`;
852
+ switch (fn) {
853
+ case "MustCompile":
854
+ return makeRegExp(a()[0]);
855
+ case "Compile":
856
+ return `[${makeRegExp(a()[0])}, null]`;
857
+ case "MatchString": {
858
+ const [pat, s] = a();
859
+ return `[new RegExp(${pat}).test(${s}), null]`;
860
+ }
861
+ case "QuoteMeta":
862
+ return `${a()[0]}.replace(/[.*+?^\${}()|[\\]\\\\]/g, '\\\\$&')`;
863
+ }
864
+ return undefined;
865
+ },
866
+
867
+ _genRegexpMethodCall(method, expr) {
868
+ const recv = expr.func.expr;
869
+ // *Regexp is a plain JS RegExp — no .value boxing needed
870
+ const re = this.genExpr(recv);
871
+ const args = expr.args.map((a) => this.genExpr(a));
872
+
873
+ switch (method) {
874
+ case "MatchString":
875
+ return `${re}.test(${args[0]})`;
876
+ case "FindString":
877
+ return `(${re}.exec(${args[0]})?.[0] ?? "")`;
878
+ case "FindStringIndex":
879
+ return `((m => m ? [m.index, m.index + m[0].length] : null)(${re}.exec(${args[0]})))`;
880
+ case "FindAllString":
881
+ return `[...${args[0]}.matchAll(new RegExp(${re}.source, ${re}.flags.includes("g") ? ${re}.flags : ${re}.flags + "g"))].slice(0, ${args[1]} < 0 ? undefined : ${args[1]}).map(m => m[0])`;
882
+ case "FindStringSubmatch":
883
+ return `[...(${re}.exec(${args[0]}) ?? [])]`;
884
+ case "FindAllStringSubmatch":
885
+ return `[...${args[0]}.matchAll(new RegExp(${re}.source, ${re}.flags.includes("g") ? ${re}.flags : ${re}.flags + "g"))].slice(0, ${args[1]} < 0 ? undefined : ${args[1]}).map(m => [...m])`;
886
+ case "ReplaceAllString":
887
+ return `${args[0]}.replaceAll(new RegExp(${re}.source, ${re}.flags.includes("g") ? ${re}.flags : ${re}.flags + "g"), ${args[1]})`;
888
+ case "ReplaceAllLiteralString":
889
+ return `${args[0]}.replaceAll(new RegExp(${re}.source, ${re}.flags.includes("g") ? ${re}.flags : ${re}.flags + "g"), ${args[1]}.replace(/\\$/g, '$$$$'))`;
890
+ case "Split":
891
+ return `${args[0]}.split(${re}).slice(0, ${args[1]} < 0 ? undefined : ${args[1]})`;
892
+ case "String":
893
+ return `${re}.source`;
894
+ }
895
+ return undefined;
896
+ },
897
+
898
+ _genSlices(fn, a) {
899
+ switch (fn) {
900
+ case "Contains": {
901
+ const [s, v] = a();
902
+ return `${s}.includes(${v})`;
903
+ }
904
+ case "Index": {
905
+ const [s, v] = a();
906
+ return `${s}.indexOf(${v})`;
907
+ }
908
+ case "Equal": {
909
+ this._usesEqual = true;
910
+ const [a1, b1] = a();
911
+ return `__equal(${a1}, ${b1})`;
912
+ }
913
+ case "Compare": {
914
+ const [a1, b1] = a();
915
+ return `((a, b) => { for (let i = 0; i < a.length && i < b.length; i++) { if (a[i] < b[i]) return -1; if (a[i] > b[i]) return 1; } return a.length - b.length; })(${a1}, ${b1})`;
916
+ }
917
+ case "Sort": {
918
+ const [s] = a();
919
+ return `${s}.sort((a, b) => a < b ? -1 : a > b ? 1 : 0)`;
920
+ }
921
+ case "SortFunc": {
922
+ const [s, cmp] = a();
923
+ return `${s}.sort(${cmp})`;
924
+ }
925
+ case "SortStableFunc": {
926
+ const [s, cmp] = a();
927
+ return `${s}.sort(${cmp})`;
928
+ }
929
+ case "IsSorted": {
930
+ const [s] = a();
931
+ return `((s) => { for (let i = 1; i < s.length; i++) { if (s[i] < s[i-1]) return false; } return true; })(${s})`;
932
+ }
933
+ case "IsSortedFunc": {
934
+ const [s, cmp] = a();
935
+ return `((s, f) => { for (let i = 1; i < s.length; i++) { if (f(s[i], s[i-1]) < 0) return false; } return true; })(${s}, ${cmp})`;
936
+ }
937
+ case "Reverse": {
938
+ const [s] = a();
939
+ return `${s}.reverse()`;
940
+ }
941
+ case "Max": {
942
+ const [s] = a();
943
+ return `Math.max(...${s})`;
944
+ }
945
+ case "Min": {
946
+ const [s] = a();
947
+ return `Math.min(...${s})`;
948
+ }
949
+ case "MaxFunc": {
950
+ const [s, cmp] = a();
951
+ return `((s, f) => s.reduce((m, x) => f(x, m) > 0 ? x : m))(${s}, ${cmp})`;
952
+ }
953
+ case "MinFunc": {
954
+ const [s, cmp] = a();
955
+ return `((s, f) => s.reduce((m, x) => f(x, m) < 0 ? x : m))(${s}, ${cmp})`;
956
+ }
957
+ case "Clone": {
958
+ const [s] = a();
959
+ return `${s}.slice()`;
960
+ }
961
+ case "Compact": {
962
+ const [s] = a();
963
+ return `((s) => s.filter((v, i) => i === 0 || v !== s[i-1]))(${s})`;
964
+ }
965
+ case "CompactFunc": {
966
+ const [s, eq] = a();
967
+ return `((s, f) => s.filter((v, i) => i === 0 || !f(v, s[i-1])))(${s}, ${eq})`;
968
+ }
969
+ case "Concat": {
970
+ const args = a();
971
+ return `[].concat(${args.join(", ")})`;
972
+ }
973
+ case "Delete": {
974
+ const [s, i, j] = a();
975
+ return `[...${s}.slice(0, ${i}), ...${s}.slice(${j})]`;
976
+ }
977
+ case "DeleteFunc": {
978
+ const [s, fn2] = a();
979
+ return `${s}.filter((v) => !${fn2}(v))`;
980
+ }
981
+ case "Insert": {
982
+ const args = a();
983
+ const s = args[0];
984
+ const i = args[1];
985
+ const vs = args.slice(2);
986
+ return `[...${s}.slice(0, ${i}), ${vs.join(", ")}, ...${s}.slice(${i})]`;
987
+ }
988
+ case "Replace": {
989
+ const args = a();
990
+ const s = args[0];
991
+ const i = args[1];
992
+ const j = args[2];
993
+ const vs = args.slice(3);
994
+ return `[...${s}.slice(0, ${i}), ${vs.join(", ")}, ...${s}.slice(${j})]`;
995
+ }
996
+ case "Grow":
997
+ case "Clip": {
998
+ const [s] = a();
999
+ return `${s}.slice()`;
1000
+ }
1001
+ }
1002
+ return undefined;
1003
+ },
1004
+
1005
+ _genHtml(fn, a) {
1006
+ switch (fn) {
1007
+ case "EscapeString": {
1008
+ const [s] = a();
1009
+ return `${s}.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&#34;").replace(/'/g,"&#39;")`;
1010
+ }
1011
+ case "UnescapeString": {
1012
+ const [s] = a();
1013
+ return `${s}.replace(/&#39;/g,"'").replace(/&#34;/g,'"').replace(/&gt;/g,">").replace(/&lt;/g,"<").replace(/&amp;/g,"&")`;
1014
+ }
1015
+ }
1016
+ return undefined;
1017
+ },
1018
+
1019
+ _genMaps(fn, a) {
1020
+ switch (fn) {
1021
+ case "Keys": {
1022
+ const [m] = a();
1023
+ return `Object.keys(${m})`;
1024
+ }
1025
+ case "Values": {
1026
+ const [m] = a();
1027
+ return `Object.values(${m})`;
1028
+ }
1029
+ case "Clone": {
1030
+ const [m] = a();
1031
+ return `({...${m}})`;
1032
+ }
1033
+ case "Copy": {
1034
+ const [dst, src] = a();
1035
+ return `Object.assign(${dst}, ${src})`;
1036
+ }
1037
+ case "Equal": {
1038
+ this._usesEqual = true;
1039
+ const [a1, b1] = a();
1040
+ return `__equal(${a1}, ${b1})`;
1041
+ }
1042
+ case "EqualFunc": {
1043
+ const [m1, m2, eq] = a();
1044
+ return `((a, b, f) => { const ka = Object.keys(a); if (ka.length !== Object.keys(b).length) return false; return ka.every(k => k in b && f(a[k], b[k])); })(${m1}, ${m2}, ${eq})`;
1045
+ }
1046
+ case "Delete": {
1047
+ const [m, k] = a();
1048
+ return `(delete ${m}[${k}], undefined)`;
1049
+ }
1050
+ case "DeleteFunc": {
1051
+ const [m, fn2] = a();
1052
+ return `Object.keys(${m}).forEach(k => { if (${fn2}(k, ${m}[k])) delete ${m}[k]; })`;
1053
+ }
1054
+ }
1055
+ return undefined;
1056
+ },
1057
+
779
1058
  typeComment(typeNode) {
780
1059
  if (!typeNode) return "unknown";
781
1060
  switch (typeNode.kind) {
@@ -815,6 +1094,14 @@ export const expressionGenMethods = {
815
1094
  return this._genErrors(fn, a);
816
1095
  case "time":
817
1096
  return this._genTime(fn, a);
1097
+ case "regexp":
1098
+ return this._genRegexp(fn, a);
1099
+ case "slices":
1100
+ return this._genSlices(fn, a);
1101
+ case "maps":
1102
+ return this._genMaps(fn, a);
1103
+ case "html":
1104
+ return this._genHtml(fn, a);
818
1105
  default:
819
1106
  return undefined;
820
1107
  }
@@ -843,6 +1130,52 @@ export const expressionGenMethods = {
843
1130
  case "Println":
844
1131
  this._usesSprintf = true;
845
1132
  return `console.log(__sprintf(${fmtArgs}))`;
1133
+ case "Fprintf":
1134
+ case "Fprintln":
1135
+ case "Fprint": {
1136
+ this._usesSprintf = true;
1137
+ const writerArg = expr.args[0];
1138
+ const rest = expr.args.slice(1);
1139
+ const restJs = rest.map((e) => this.genExpr(e)).join(", ");
1140
+ // Resolve the writer to its underlying buffer expression
1141
+ const writerType = writerArg._type;
1142
+ const targetTypeName =
1143
+ writerType?.name ??
1144
+ (writerType?.kind === "pointer" ? writerType.base?.name : null);
1145
+ let buf;
1146
+ if (targetTypeName === "strings.Builder") {
1147
+ const w = this.genExpr(writerArg);
1148
+ buf =
1149
+ writerType?.kind === "pointer" ? `${w}.value._buf` : `${w}._buf`;
1150
+ } else if (targetTypeName === "bytes.Buffer") {
1151
+ const w = this.genExpr(writerArg);
1152
+ const base = writerType?.kind === "pointer" ? `${w}.value` : w;
1153
+ buf = null;
1154
+ if (fn === "Fprintf") {
1155
+ return `((__b,__s)=>{ __b._buf.push(...new TextEncoder().encode(__s)); })(${base}, __sprintf(${restJs}))`;
1156
+ }
1157
+ const valJs =
1158
+ fn === "Fprintln"
1159
+ ? `__sprintf("%v\\n", ${restJs})`
1160
+ : `__sprintf("%v", ${restJs})`;
1161
+ return `((__b,__s)=>{ __b._buf.push(...new TextEncoder().encode(__s)); })(${base}, ${valJs})`;
1162
+ } else {
1163
+ // Generic io.Writer fallback — call .WriteString if available
1164
+ const w = this.genExpr(writerArg);
1165
+ const formatted =
1166
+ fn === "Fprintf"
1167
+ ? `__sprintf(${restJs})`
1168
+ : fn === "Fprintln"
1169
+ ? `__sprintf("%v\\n", ${restJs})`
1170
+ : `__sprintf("%v", ${restJs})`;
1171
+ return `${w}.WriteString(${formatted})`;
1172
+ }
1173
+ // strings.Builder path
1174
+ if (fn === "Fprintf") return `(${buf} += __sprintf(${restJs}))`;
1175
+ if (fn === "Fprintln")
1176
+ return `(${buf} += __sprintf("%v\\n", ${restJs}))`;
1177
+ return `(${buf} += __sprintf("%v", ${restJs}))`;
1178
+ }
846
1179
  default:
847
1180
  return undefined;
848
1181
  }
@@ -862,7 +1195,7 @@ export const expressionGenMethods = {
862
1195
  case "LastIndex":
863
1196
  return `${args[0]}.lastIndexOf(${args[1]})`;
864
1197
  case "Count":
865
- return `${args[0]}.split(${args[1]}).length - 1`;
1198
+ return `((s, sep) => sep === "" ? s.length + 1 : s.split(sep).length - 1)(${args[0]}, ${args[1]})`;
866
1199
  case "Repeat":
867
1200
  return `${args[0]}.repeat(${args[1]})`;
868
1201
  case "Replace":
@@ -878,9 +1211,9 @@ export const expressionGenMethods = {
878
1211
  case "Trim":
879
1212
  return `${args[0]}.replace(new RegExp(\`^[\${${args[1]}}]+|[\${${args[1]}}]+$\`, "g"), "")`;
880
1213
  case "TrimPrefix":
881
- return `(${args[0]}.startsWith(${args[1]}) ? ${args[0]}.slice(${args[1]}.length) : ${args[0]})`;
1214
+ return `((s, pre) => s.startsWith(pre) ? s.slice(pre.length) : s)(${args[0]}, ${args[1]})`;
882
1215
  case "TrimSuffix":
883
- return `(${args[0]}.endsWith(${args[1]}) ? ${args[0]}.slice(0, -${args[1]}.length) : ${args[0]})`;
1216
+ return `((s, suf) => !suf.length || !s.endsWith(suf) ? s : s.slice(0, -suf.length))(${args[0]}, ${args[1]})`;
884
1217
  case "TrimLeft":
885
1218
  return `${args[0]}.replace(new RegExp(\`^[\${${args[1]}}]+\`), "")`;
886
1219
  case "TrimRight":
@@ -910,7 +1243,7 @@ export const expressionGenMethods = {
910
1243
  case "Index":
911
1244
  return `${__bs}(${args[0]}).indexOf(${__bs}(${args[1]}))`;
912
1245
  case "Count":
913
- return `${__bs}(${args[0]}).split(${__bs}(${args[1]})).length - 1`;
1246
+ return `((b, sep) => sep.length === 0 ? b.length + 1 : ${__bs}(b).split(${__bs}(sep)).length - 1)(${args[0]}, ${args[1]})`;
914
1247
  case "Repeat":
915
1248
  return `${__sb}(${__bs}(${args[0]}).repeat(${args[1]}))`;
916
1249
  case "Replace":
@@ -16,21 +16,29 @@ function vlqEncode(value) {
16
16
  return result;
17
17
  }
18
18
 
19
- export function buildSourceMap(sourceName, mappings) {
20
- // mappings: Array<{ genLine: number, srcLine: number }> (0-based)
19
+ export function buildSourceMap(sources, mappings) {
20
+ // sources: string[]
21
+ // mappings: Array<{ genLine: number, srcLine: number, srcFileIdx?: number }> (0-based)
21
22
  // Emits one segment per generated line, column 0 → source line (delta-encoded).
22
23
  const lines = [];
23
24
  let prevSrcLine = 0;
25
+ let prevSrcFile = 0;
24
26
  const maxGen = mappings.reduce((m, e) => Math.max(m, e.genLine), -1);
25
27
  const byLine = new Map(mappings.map((e) => [e.genLine, e]));
26
28
  for (let g = 0; g <= maxGen; g++) {
27
29
  const entry = byLine.get(g);
28
30
  if (entry) {
31
+ const fileIdx = entry.srcFileIdx ?? 0;
32
+ const fileDelta = fileIdx - prevSrcFile;
29
33
  const srcDelta = entry.srcLine - prevSrcLine;
30
- // Segment: [genCol=0, srcFileIdx=0, srcLineDelta, srcCol=0]
34
+ // Segment: [genCol=0, srcFileIdxDelta, srcLineDelta, srcCol=0]
31
35
  lines.push(
32
- vlqEncode(0) + vlqEncode(0) + vlqEncode(srcDelta) + vlqEncode(0),
36
+ vlqEncode(0) +
37
+ vlqEncode(fileDelta) +
38
+ vlqEncode(srcDelta) +
39
+ vlqEncode(0),
33
40
  );
41
+ prevSrcFile = fileIdx;
34
42
  prevSrcLine = entry.srcLine;
35
43
  } else {
36
44
  lines.push(""); // no mapping for this generated line
@@ -38,7 +46,7 @@ export function buildSourceMap(sourceName, mappings) {
38
46
  }
39
47
  return JSON.stringify({
40
48
  version: 3,
41
- sources: [sourceName],
49
+ sources,
42
50
  names: [],
43
51
  mappings: lines.join(";"),
44
52
  });
package/src/codegen.js CHANGED
@@ -38,7 +38,8 @@ export class CodeGen {
38
38
  this.jsImports = jsImports;
39
39
  this.bundledPackages = bundledPackages;
40
40
  this.namedReturnVars = null; // names of current function's named return vars
41
- this._srcMappings = []; // { genLine, srcLine } for source map
41
+ this._srcMappings = []; // { genLine, srcLine, srcFileIdx } for source map
42
+ this._currentSrcFileIdx = 0; // updated as each top-level decl is generated
42
43
  this._boxedVars = new Set(); // address-taken scalar variables that need boxing
43
44
  // Runtime helper usage tracking — only emit helpers that are actually used
44
45
  this._usesLen = false;
@@ -67,8 +68,9 @@ export class CodeGen {
67
68
  if (srcLine != null) {
68
69
  this._srcMappings.push({
69
70
  genLine: this.out.length,
70
- srcLine: srcLine - 1,
71
- }); // 0-based
71
+ srcLine: srcLine - 1, // 0-based
72
+ srcFileIdx: this._currentSrcFileIdx,
73
+ });
72
74
  }
73
75
  this.out.push(" ".repeat(this.indent) + s);
74
76
  }
@@ -109,6 +111,7 @@ export class CodeGen {
109
111
 
110
112
  for (const d of program.decls) {
111
113
  if (d.kind === "TypeDecl") {
114
+ this._currentSrcFileIdx = d._srcFileIdx ?? 0;
112
115
  this.genTypeDeclWithMethods(d, methods.get(d.name) ?? []);
113
116
  this.blank();
114
117
  }
@@ -116,10 +119,12 @@ export class CodeGen {
116
119
 
117
120
  for (const d of program.decls) {
118
121
  if (d.kind === "VarDecl") {
122
+ this._currentSrcFileIdx = d._srcFileIdx ?? 0;
119
123
  this.genVarDecl(d);
120
124
  this.blank();
121
125
  }
122
126
  if (d.kind === "ConstDecl") {
127
+ this._currentSrcFileIdx = d._srcFileIdx ?? 0;
123
128
  this.genConstDecl(d);
124
129
  this.blank();
125
130
  }
@@ -130,6 +135,7 @@ export class CodeGen {
130
135
  const initNames = [];
131
136
  for (const d of program.decls) {
132
137
  if (d.kind === "FuncDecl") {
138
+ this._currentSrcFileIdx = d._srcFileIdx ?? 0;
133
139
  if (d.name === "init") {
134
140
  const renamed = initCount === 0 ? "init" : `init$${initCount}`;
135
141
  initNames.push(renamed);
@@ -152,29 +158,31 @@ export class CodeGen {
152
158
  // Prepend runtime helpers that were actually used
153
159
  const helpers = [];
154
160
  if (this._usesLen)
155
- helpers.push("function __len(a) { return a?.length ?? 0; }");
161
+ helpers.push(
162
+ "var __len = __len || function(a) { if (a && typeof a === 'object' && !Array.isArray(a)) return Object.keys(a).length; return a?.length ?? 0; };",
163
+ );
156
164
  if (this._usesAppend)
157
165
  helpers.push(
158
- "function __append(a, ...b) { return a ? [...a, ...b] : b; }",
166
+ "var __append = __append || function(a, ...b) { return a ? [...a, ...b] : b; };",
159
167
  );
160
168
  if (this._usesSliceGuard)
161
- helpers.push("function __s(a) { return a || []; }");
169
+ helpers.push("var __s = __s || function(a) { return a || []; };");
162
170
  if (this._usesEqual)
163
171
  helpers.push(
164
- 'function __equal(a,b){if(a===b)return true;if(a===null||b===null)return false;if(Array.isArray(a)&&Array.isArray(b)){if(a.length!==b.length)return false;for(let i=0;i<a.length;i++)if(!__equal(a[i],b[i]))return false;return true;}if(typeof a==="object"&&typeof b==="object"){const ka=Object.keys(a),kb=Object.keys(b);if(ka.length!==kb.length)return false;for(const k of ka)if(!__equal(a[k],b[k]))return false;return true;}return false;}',
172
+ 'var __equal = __equal || function __equal(a,b){if(a===b)return true;if(a===null||b===null)return false;if(Array.isArray(a)&&Array.isArray(b)){if(a.length!==b.length)return false;for(let i=0;i<a.length;i++)if(!__equal(a[i],b[i]))return false;return true;}if(typeof a==="object"&&typeof b==="object"){const ka=Object.keys(a),kb=Object.keys(b);if(ka.length!==kb.length)return false;for(const k of ka)if(!__equal(a[k],b[k]))return false;return true;}return false;};',
165
173
  );
166
174
  if (this._usesCmul)
167
175
  helpers.push(
168
- "function __cmul(a, b) { return { re: a.re * b.re - a.im * b.im, im: a.re * b.im + a.im * b.re }; }",
176
+ "var __cmul = __cmul || function(a, b) { return { re: a.re * b.re - a.im * b.im, im: a.re * b.im + a.im * b.re }; };",
169
177
  );
170
178
  if (this._usesCdiv)
171
179
  helpers.push(
172
- "function __cdiv(a, b) { const d = b.re * b.re + b.im * b.im; return { re: (a.re * b.re + a.im * b.im) / d, im: (a.im * b.re - a.re * b.im) / d }; }",
180
+ "var __cdiv = __cdiv || function(a, b) { const d = b.re * b.re + b.im * b.im; return { re: (a.re * b.re + a.im * b.im) / d, im: (a.im * b.re - a.re * b.im) / d }; };",
173
181
  );
174
182
  if (this._usesSprintf)
175
183
  helpers.push(
176
184
  [
177
- "function __sprintf(f,...a){let i=0;return f.replace(/%([#+\\- 0]*)([0-9]*)\\.?([0-9]*)[sdvftxXqobeEgGw%]/g,(m)=>{",
185
+ "var __sprintf = __sprintf || function(f,...a){let i=0;return f.replace(/%([#+\\- 0]*)([0-9]*)\\.?([0-9]*)[sdvftxXqobeEgGw%]/g,(m)=>{",
178
186
  "if(m==='%%')return'%';const fl=m.slice(1,-1),verb=m.slice(-1),v=a[i++];",
179
187
  "const pad=(s,w,z)=>{w=parseInt(w)||0;if(!w)return s;const p=(z?'0':' ').repeat(Math.max(0,w-s.length));return fl.includes('-')?s+p:p+s;};",
180
188
  "const [,flags,width,prec]=m.match(/^%([#+\\- 0]*)([0-9]*)\\.?([0-9]*)/)||[];",
@@ -194,16 +202,16 @@ export class CodeGen {
194
202
  "case'g':case'G':{const n=Number(v);return pad(prec!==''?n.toPrecision(parseInt(prec)):String(n),width,zero);}",
195
203
  "case'w':return pad(String(v==null?'<nil>':typeof v==='object'&&v.Error?v.Error():v),width,false);",
196
204
  "default:return m;}});",
197
- "}",
205
+ "};",
198
206
  ].join(""),
199
207
  );
200
208
  if (this._usesError)
201
209
  helpers.push(
202
- "function __error(msg, cause) { return { Error() { return msg; }, toString() { return msg; }, _msg: msg, _cause: cause ?? null }; }",
210
+ "var __error = __error || function(msg, cause) { return { Error() { return msg; }, toString() { return msg; }, _msg: msg, _cause: cause ?? null }; };",
203
211
  );
204
212
  if (this._usesErrorIs)
205
213
  helpers.push(
206
- 'function __errorIs(err, target) { while (err !== null && err !== undefined) { if (err === target) return true; if (typeof err === "object" && typeof target === "object" && err._msg !== undefined && target._msg !== undefined && err._msg === target._msg) return true; err = err?._cause ?? null; } return false; }',
214
+ 'var __errorIs = __errorIs || function(err, target) { while (err !== null && err !== undefined) { if (err === target) return true; if (typeof err === "object" && typeof target === "object" && err._msg !== undefined && target._msg !== undefined && err._msg === target._msg) return true; err = err?._cause ?? null; } return false; };',
207
215
  );
208
216
 
209
217
  if (helpers.length > 0) this.out.unshift(...helpers, "");
@@ -214,16 +222,22 @@ export class CodeGen {
214
222
  }
215
223
 
216
224
  // Generate a single bundle from multiple programs (same-package multi-file).
217
- // Merges all decls and generates as if it were one program.
225
+ // Annotates each decl with its source file index before merging.
218
226
  generateAll(programs) {
227
+ for (let i = 0; i < programs.length; i++) {
228
+ for (const decl of programs[i].decls) {
229
+ decl._srcFileIdx = i;
230
+ }
231
+ }
219
232
  const merged = { decls: programs.flatMap((p) => p.decls) };
220
233
  return this.generate(merged);
221
234
  }
222
235
 
223
236
  // Returns a source map JSON string for the last generate() call.
224
- // sourceName: the .go filename to reference in the map.
225
- getSourceMap(sourceName) {
226
- return buildSourceMap(sourceName, this._srcMappings);
237
+ // sources: string[] of source filenames (relative to the output file).
238
+ getSourceMap(sources) {
239
+ const srcArray = Array.isArray(sources) ? sources : [sources];
240
+ return buildSourceMap(srcArray, this._srcMappings);
227
241
  }
228
242
 
229
243
  // ── Type declarations ────────────────────────────────────────
package/src/compiler.js CHANGED
@@ -16,7 +16,7 @@
16
16
  // de-qualifies it because the dependency is inlined.
17
17
 
18
18
  import { readdirSync, readFileSync } from "node:fs";
19
- import { basename, dirname, join, resolve } from "node:path";
19
+ import { basename, dirname, join, relative, resolve } from "node:path";
20
20
  import { CodeGen } from "./codegen.js";
21
21
  import { parseDts } from "./dts-parser.js";
22
22
  import { Lexer } from "./lexer.js";
@@ -165,8 +165,15 @@ export function compileFiles(files, options = {}) {
165
165
  const codegen = new CodeGen(checker, jsImports, bundledPackages);
166
166
  const mainJs = codegen.generateAll(programs);
167
167
 
168
- const js =
169
- preambles.length > 0 ? `${preambles.join("\n")}\n${mainJs}` : mainJs;
168
+ let js = preambles.length > 0 ? `${preambles.join("\n")}\n${mainJs}` : mainJs;
169
+
170
+ if (options.sourceMap) {
171
+ const outputDir = options.outputDir ?? fromDir;
172
+ const sourceFiles = files.map((f) => relative(outputDir, f));
173
+ const map = codegen.getSourceMap(sourceFiles);
174
+ const b64 = Buffer.from(map).toString("base64");
175
+ js += `\n//# sourceMappingURL=data:application/json;base64,${b64}`;
176
+ }
170
177
 
171
178
  return {
172
179
  pkgName,
@@ -0,0 +1,83 @@
1
+ // Minimal dev server for gofront --serve watch mode.
2
+ // Serves static files and pushes a reload event via SSE after each build.
3
+
4
+ import { existsSync, readFileSync } from "node:fs";
5
+ import { createServer } from "node:http";
6
+ import { extname, join } from "node:path";
7
+
8
+ const MIME = {
9
+ ".html": "text/html; charset=utf-8",
10
+ ".js": "application/javascript; charset=utf-8",
11
+ ".css": "text/css; charset=utf-8",
12
+ ".json": "application/json; charset=utf-8",
13
+ ".svg": "image/svg+xml",
14
+ ".png": "image/png",
15
+ ".jpg": "image/jpeg",
16
+ ".ico": "image/x-icon",
17
+ ".woff2": "font/woff2",
18
+ ".woff": "font/woff",
19
+ };
20
+
21
+ // Injected at the bottom of compiled JS in serve mode.
22
+ export const liveReloadClient = `(function(){var es=new EventSource('/_gofront/events');es.addEventListener('reload',function(){location.reload();});})();`;
23
+
24
+ export function createDevServer(serveDir, port) {
25
+ const clients = new Set();
26
+
27
+ const server = createServer((req, res) => {
28
+ // SSE endpoint — browser connects here to receive reload events
29
+ if (req.url === "/_gofront/events") {
30
+ res.writeHead(200, {
31
+ "Content-Type": "text/event-stream",
32
+ "Cache-Control": "no-cache",
33
+ Connection: "keep-alive",
34
+ });
35
+ res.write(": connected\n\n");
36
+ clients.add(res);
37
+ req.on("close", () => clients.delete(res));
38
+ return;
39
+ }
40
+
41
+ let urlPath = req.url.split("?")[0];
42
+ if (urlPath === "/" || urlPath === "") urlPath = "/index.html";
43
+
44
+ const filePath = join(serveDir, urlPath);
45
+ if (!existsSync(filePath)) {
46
+ res.writeHead(404, { "Content-Type": "text/plain" });
47
+ res.end("Not found");
48
+ return;
49
+ }
50
+
51
+ try {
52
+ const data = readFileSync(filePath);
53
+ const mime = MIME[extname(filePath)] ?? "application/octet-stream";
54
+ res.writeHead(200, { "Content-Type": mime });
55
+ res.end(data);
56
+ } catch {
57
+ res.writeHead(500, { "Content-Type": "text/plain" });
58
+ res.end("Server error");
59
+ }
60
+ });
61
+
62
+ server.on("error", (err) => {
63
+ if (err.code === "EADDRINUSE") {
64
+ console.error(
65
+ `gofront: port ${port} already in use — try --port <number>`,
66
+ );
67
+ } else {
68
+ console.error(`gofront: dev server error: ${err.message}`);
69
+ }
70
+ process.exit(1);
71
+ });
72
+ server.listen(port, () => {
73
+ console.error(`gofront: dev server → http://localhost:${port}`);
74
+ });
75
+
76
+ function notify() {
77
+ for (const client of clients) {
78
+ client.write("event: reload\ndata: {}\n\n");
79
+ }
80
+ }
81
+
82
+ return { notify };
83
+ }
package/src/index.js CHANGED
@@ -26,9 +26,10 @@ import { createRequire } from "node:module";
26
26
  const _require = createRequire(import.meta.url);
27
27
  const { version } = _require("../package.json");
28
28
 
29
- import { basename, dirname, join, resolve } from "node:path";
29
+ import { basename, dirname, join, relative, resolve } from "node:path";
30
30
  import { CodeGen } from "./codegen.js";
31
31
  import { compileDir } from "./compiler.js";
32
+ import { createDevServer, liveReloadClient } from "./dev-server.js";
32
33
  import { parseDts } from "./dts-parser.js";
33
34
  import { Lexer } from "./lexer.js";
34
35
  import { minify } from "./minifier.js";
@@ -55,6 +56,8 @@ Usage:
55
56
  gofront <input> -o out.js Compile and write to file
56
57
  gofront <input> --check Type-check only
57
58
  gofront <input> --watch Watch for changes and recompile
59
+ gofront <input> -o out.js --serve Watch + serve with live reload (default port 3000)
60
+ gofront <input> -o out.js --serve --port 8080 Use a custom port
58
61
  gofront <input> --source-map Append inline source map to output
59
62
  gofront <input> --minify Minify output
60
63
  gofront <input> --minify --mangle Minify and mangle identifiers
@@ -120,9 +123,12 @@ const checkOnly = args.includes("--check");
120
123
  const dumpAst = args.includes("--ast");
121
124
  const dumpTokens = args.includes("--tokens");
122
125
  const sourceMap = args.includes("--source-map");
123
- const watchMode = args.includes("--watch");
126
+ const serveMode = args.includes("--serve");
127
+ const watchMode = args.includes("--watch") || serveMode;
124
128
  const minifyOutput = args.includes("--minify");
125
129
  const mangleOutput = args.includes("--mangle");
130
+ const portFlag = args.indexOf("--port");
131
+ const servePort = portFlag !== -1 ? parseInt(args[portFlag + 1], 10) : 3000;
126
132
 
127
133
  // ── Determine input mode ─────────────────────────────────────
128
134
 
@@ -139,7 +145,8 @@ try {
139
145
 
140
146
  function runCompile() {
141
147
  if (isDir) {
142
- return compileDir(inputPath);
148
+ const outputDir = outputFile ? dirname(resolve(outputFile)) : resolve(".");
149
+ return compileDir(inputPath, { sourceMap, outputDir });
143
150
  }
144
151
 
145
152
  // Single-file mode
@@ -223,7 +230,9 @@ function runCompile() {
223
230
  let js = cg.generate(ast);
224
231
 
225
232
  if (sourceMap) {
226
- const map = cg.getSourceMap(inputArg);
233
+ const outputDir = outputFile ? dirname(resolve(outputFile)) : resolve(".");
234
+ const srcPath = relative(outputDir, inputPath);
235
+ const map = cg.getSourceMap(srcPath);
227
236
  const b64 = Buffer.from(map).toString("base64");
228
237
  js += `\n//# sourceMappingURL=data:application/json;base64,${b64}`;
229
238
  }
@@ -291,11 +300,31 @@ function timestamp() {
291
300
  return new Date().toLocaleTimeString();
292
301
  }
293
302
 
303
+ // Start dev server before first build so the browser can connect immediately
304
+ let devServer = null;
305
+ if (serveMode) {
306
+ if (!outputFile) {
307
+ console.error("gofront: --serve requires -o <output file>");
308
+ process.exit(1);
309
+ }
310
+ const serveDir = dirname(resolve(outputFile));
311
+ devServer = createDevServer(serveDir, servePort);
312
+ }
313
+
294
314
  function buildOnce() {
295
315
  try {
296
316
  const startMs = performance.now();
297
317
  const result = runCompile();
298
- const js = maybeMinify(result.js);
318
+ let js = maybeMinify(result.js);
319
+ if (serveMode) {
320
+ // Insert live reload client before the source map comment so it stays last
321
+ const smIdx = js.lastIndexOf("\n//# sourceMappingURL=");
322
+ if (smIdx !== -1) {
323
+ js = `${js.slice(0, smIdx)}\n${liveReloadClient}${js.slice(smIdx)}`;
324
+ } else {
325
+ js += `\n${liveReloadClient}`;
326
+ }
327
+ }
299
328
  const elapsedMs = (performance.now() - startMs).toFixed(0);
300
329
  if (outputFile) {
301
330
  writeFileSync(outputFile, `${js}\n`);
@@ -308,6 +337,7 @@ function buildOnce() {
308
337
  console.log(js);
309
338
  console.error(`[${timestamp()}] gofront: OK (${elapsedMs}ms)`);
310
339
  }
340
+ devServer?.notify();
311
341
  } catch (e) {
312
342
  console.error(`[${timestamp()}] gofront: ERROR`);
313
343
  for (const line of e.message.split("\n")) console.error(` ${line}`);
@@ -146,6 +146,24 @@ export class TypeChecker {
146
146
  Printf: fmtVariadic(VOID),
147
147
  Println: fmtVariadic(VOID),
148
148
  Print: fmtVariadic(VOID),
149
+ Fprintf: {
150
+ kind: "func",
151
+ params: [ANY, STRING],
152
+ returns: [INT, ERROR],
153
+ variadic: true,
154
+ },
155
+ Fprintln: {
156
+ kind: "func",
157
+ params: [ANY],
158
+ returns: [INT, ERROR],
159
+ variadic: true,
160
+ },
161
+ Fprint: {
162
+ kind: "func",
163
+ params: [ANY],
164
+ returns: [INT, ERROR],
165
+ variadic: true,
166
+ },
149
167
  },
150
168
  });
151
169
 
@@ -431,6 +449,195 @@ export class TypeChecker {
431
449
  },
432
450
  });
433
451
 
452
+ // slices package (Go 1.21)
453
+ // Functions are generic — use ANY for slice params so any concrete slice type
454
+ // is accepted without an assignability error (same pattern as sort.Slice).
455
+ this.globals.define("slices", {
456
+ kind: "namespace",
457
+ name: "slices",
458
+ members: {
459
+ Contains: { kind: "func", params: [ANY, ANY], returns: [BOOL] },
460
+ Index: { kind: "func", params: [ANY, ANY], returns: [INT] },
461
+ Equal: { kind: "func", params: [ANY, ANY], returns: [BOOL] },
462
+ Compare: { kind: "func", params: [ANY, ANY], returns: [INT] },
463
+ Sort: { kind: "func", params: [ANY], returns: [VOID] },
464
+ SortFunc: { kind: "func", params: [ANY, ANY], returns: [VOID] },
465
+ SortStableFunc: { kind: "func", params: [ANY, ANY], returns: [VOID] },
466
+ IsSorted: { kind: "func", params: [ANY], returns: [BOOL] },
467
+ IsSortedFunc: { kind: "func", params: [ANY, ANY], returns: [BOOL] },
468
+ Reverse: { kind: "func", params: [ANY], returns: [VOID] },
469
+ Max: { kind: "func", params: [ANY], returns: [ANY] },
470
+ Min: { kind: "func", params: [ANY], returns: [ANY] },
471
+ MaxFunc: { kind: "func", params: [ANY, ANY], returns: [ANY] },
472
+ MinFunc: { kind: "func", params: [ANY, ANY], returns: [ANY] },
473
+ Clone: { kind: "func", params: [ANY], returns: [ANY] },
474
+ Compact: { kind: "func", params: [ANY], returns: [ANY] },
475
+ CompactFunc: { kind: "func", params: [ANY, ANY], returns: [ANY] },
476
+ Concat: { kind: "func", params: [ANY], returns: [ANY], variadic: true },
477
+ Delete: { kind: "func", params: [ANY, INT, INT], returns: [ANY] },
478
+ DeleteFunc: { kind: "func", params: [ANY, ANY], returns: [ANY] },
479
+ Insert: {
480
+ kind: "func",
481
+ params: [ANY, INT, ANY],
482
+ returns: [ANY],
483
+ variadic: true,
484
+ },
485
+ Replace: {
486
+ kind: "func",
487
+ params: [ANY, INT, INT, ANY],
488
+ returns: [ANY],
489
+ variadic: true,
490
+ },
491
+ Grow: { kind: "func", params: [ANY, INT], returns: [ANY] },
492
+ Clip: { kind: "func", params: [ANY], returns: [ANY] },
493
+ },
494
+ });
495
+
496
+ // html package
497
+ this.globals.define("html", {
498
+ kind: "namespace",
499
+ name: "html",
500
+ members: {
501
+ EscapeString: { kind: "func", params: [STRING], returns: [STRING] },
502
+ UnescapeString: { kind: "func", params: [STRING], returns: [STRING] },
503
+ },
504
+ });
505
+
506
+ // maps package (Go 1.21)
507
+ this.globals.define("maps", {
508
+ kind: "namespace",
509
+ name: "maps",
510
+ members: {
511
+ Keys: { kind: "func", params: [ANY], returns: [ANY] },
512
+ Values: { kind: "func", params: [ANY], returns: [ANY] },
513
+ Clone: { kind: "func", params: [ANY], returns: [ANY] },
514
+ Copy: { kind: "func", params: [ANY, ANY], returns: [VOID] },
515
+ Equal: { kind: "func", params: [ANY, ANY], returns: [BOOL] },
516
+ EqualFunc: { kind: "func", params: [ANY, ANY, ANY], returns: [BOOL] },
517
+ Delete: { kind: "func", params: [ANY, ANY], returns: [VOID] },
518
+ DeleteFunc: { kind: "func", params: [ANY, ANY], returns: [VOID] },
519
+ },
520
+ });
521
+
522
+ // regexp package
523
+ const REGEXP_T = {
524
+ kind: "named",
525
+ name: "regexp.Regexp",
526
+ underlying: { kind: "struct", fields: new Map(), methods: new Map() },
527
+ };
528
+ const REGEXP_PTR = { kind: "pointer", base: REGEXP_T };
529
+ const STR_SLICE = { kind: "slice", elem: STRING };
530
+ const INT_SLICE = { kind: "slice", elem: INT };
531
+ const regexpMethods = new Map([
532
+ ["MatchString", { kind: "func", params: [STRING], returns: [BOOL] }],
533
+ ["FindString", { kind: "func", params: [STRING], returns: [STRING] }],
534
+ [
535
+ "FindStringIndex",
536
+ { kind: "func", params: [STRING], returns: [INT_SLICE] },
537
+ ],
538
+ [
539
+ "FindAllString",
540
+ { kind: "func", params: [STRING, INT], returns: [STR_SLICE] },
541
+ ],
542
+ [
543
+ "FindStringSubmatch",
544
+ { kind: "func", params: [STRING], returns: [STR_SLICE] },
545
+ ],
546
+ [
547
+ "FindAllStringSubmatch",
548
+ {
549
+ kind: "func",
550
+ params: [STRING, INT],
551
+ returns: [{ kind: "slice", elem: STR_SLICE }],
552
+ },
553
+ ],
554
+ [
555
+ "ReplaceAllString",
556
+ { kind: "func", params: [STRING, STRING], returns: [STRING] },
557
+ ],
558
+ [
559
+ "ReplaceAllLiteralString",
560
+ { kind: "func", params: [STRING, STRING], returns: [STRING] },
561
+ ],
562
+ ["Split", { kind: "func", params: [STRING, INT], returns: [STR_SLICE] }],
563
+ ["String", { kind: "func", params: [], returns: [STRING] }],
564
+ ]);
565
+ REGEXP_T.underlying.methods = regexpMethods;
566
+ this.types.set("regexp.Regexp", REGEXP_T);
567
+ this.globals.define("regexp", {
568
+ kind: "namespace",
569
+ name: "regexp",
570
+ members: {
571
+ MustCompile: { kind: "func", params: [STRING], returns: [REGEXP_PTR] },
572
+ Compile: {
573
+ kind: "func",
574
+ params: [STRING],
575
+ returns: [REGEXP_PTR, ERROR],
576
+ },
577
+ MatchString: {
578
+ kind: "func",
579
+ params: [STRING, STRING],
580
+ returns: [BOOL, ERROR],
581
+ },
582
+ QuoteMeta: { kind: "func", params: [STRING], returns: [STRING] },
583
+ },
584
+ });
585
+
586
+ // strings.Builder
587
+ const BYTE_SLICE_T = { kind: "slice", elem: INT };
588
+ const strBuilderMethods = new Map([
589
+ [
590
+ "WriteString",
591
+ { kind: "func", params: [STRING], returns: [INT, ERROR] },
592
+ ],
593
+ ["WriteByte", { kind: "func", params: [INT], returns: [ERROR] }],
594
+ ["WriteRune", { kind: "func", params: [INT], returns: [INT, ERROR] }],
595
+ [
596
+ "Write",
597
+ { kind: "func", params: [BYTE_SLICE_T], returns: [INT, ERROR] },
598
+ ],
599
+ ["String", { kind: "func", params: [], returns: [STRING] }],
600
+ ["Len", { kind: "func", params: [], returns: [INT] }],
601
+ ["Reset", { kind: "func", params: [], returns: [VOID] }],
602
+ ["Grow", { kind: "func", params: [INT], returns: [VOID] }],
603
+ ]);
604
+ this.types.set("strings.Builder", {
605
+ kind: "named",
606
+ name: "strings.Builder",
607
+ underlying: {
608
+ kind: "struct",
609
+ fields: new Map(),
610
+ methods: strBuilderMethods,
611
+ },
612
+ });
613
+
614
+ // bytes.Buffer
615
+ const bytesBufferMethods = new Map([
616
+ [
617
+ "WriteString",
618
+ { kind: "func", params: [STRING], returns: [INT, ERROR] },
619
+ ],
620
+ ["WriteByte", { kind: "func", params: [INT], returns: [ERROR] }],
621
+ [
622
+ "Write",
623
+ { kind: "func", params: [BYTE_SLICE_T], returns: [INT, ERROR] },
624
+ ],
625
+ ["String", { kind: "func", params: [], returns: [STRING] }],
626
+ ["Bytes", { kind: "func", params: [], returns: [BYTE_SLICE_T] }],
627
+ ["Len", { kind: "func", params: [], returns: [INT] }],
628
+ ["Reset", { kind: "func", params: [], returns: [VOID] }],
629
+ ["Grow", { kind: "func", params: [INT], returns: [VOID] }],
630
+ ]);
631
+ this.types.set("bytes.Buffer", {
632
+ kind: "named",
633
+ name: "bytes.Buffer",
634
+ underlying: {
635
+ kind: "struct",
636
+ fields: new Map(),
637
+ methods: bytesBufferMethods,
638
+ },
639
+ });
640
+
434
641
  this.globals.define("append", { kind: "builtin", name: "append" });
435
642
  this.globals.define("len", { kind: "builtin", name: "len" });
436
643
  this.globals.define("cap", { kind: "builtin", name: "cap" });
@@ -1360,6 +1567,15 @@ export class TypeChecker {
1360
1567
  fieldType(baseType, field, node) {
1361
1568
  baseType = this.resolveType(baseType);
1362
1569
  if (!baseType || isAny(baseType)) return ANY;
1570
+ // Transparent pointer dereference for named-type method calls: (*T).Method works like T.Method
1571
+ // (only unwrap when the pointer's base is a named type that has methods)
1572
+ if (
1573
+ baseType.kind === "pointer" &&
1574
+ field !== "value" &&
1575
+ baseType.base?.kind === "named"
1576
+ ) {
1577
+ baseType = this.resolveType(baseType.base);
1578
+ }
1363
1579
  let base = baseType.kind === "named" ? baseType.underlying : baseType;
1364
1580
  base = this.resolveType(base);
1365
1581
  if (base?.kind === "struct") {