Follow-up to #141 (three gs/builtin/slice.ts perf fixes, already opened).
While transpiling and running a real ~100k-LOC Go package (interpreter + parser + lexer + types + resolver for a design-tokens expression language) through goscript, we found a handful of additional bugs — mostly in the bundled encoding/json runtime, plus one more builtin/type.ts correctness bug. These are currently vendored locally as a --gs-path override (for encoding/json) and a post-generation text patch (for builtin/type.ts), rather than proposed as PRs yet, to keep review units small. Filing this as a pointer in case there's interest — happy to open individual PRs for any/all of these on request.
Summary of what we found (all root-caused with minimal standalone repros, transpiled and run under Bun):
-
encoding/json omitempty not honored on marshal. Struct fields tagged ,omitempty were emitted unconditionally even when holding a Go zero value (zero number, empty string, false, empty slice/map, nil pointer/interface).
-
encoding/json marshal leaks the boxed interface{} wrapper. A value carrying goscript's boxed-any wrapper (__goValue) serialized as {"__goValue": ...} instead of the underlying primitive/map/slice.
-
encoding/json unmarshal doesn't type-drive Map/Slice struct fields. Only direct []Struct fields decoded into real struct instances; map[string]Struct / map[string][]Struct fields (and further nesting) left plain JS objects/arrays in place, so generated struct-field-accessor getters returned undefined.
-
encoding/json unmarshal doesn't recurse into array elements inside interface{}-typed values. An interface{}-typed array of objects (e.g. Go []any holding map[string]any elements) left element objects as raw plain JS objects instead of the Map representation used elsewhere, breaking Go-side case map[string]any: type switches that call .entries().
-
encoding/json unmarshal ignores json:"..." tags on anonymous (inline, unnamed) struct types. Anonymous struct targets carry no _fields/registered type metadata, so they fell through to the untyped interface{} decode path, which replaces the plain-object struct value with a Map — every field then read back empty.
-
encoding/json RawMessage not handled as a map/slice element type, and no Pointer-to-struct branch in the type-driven decoder (map/slice element and field types like map[string]*Property leaked plain objects with undefined Go field reads).
-
builtin/type.ts structural type matching misclassifies a plain JS Array as map[string]any. matchesMapType and typeAssert's inline map check accept any non-null object; Object.entries() of an array yields string indices that pass key/elem sampling. A Go type switch listing case map[string]any: before case []any: then captures arrays in the map branch, and ranging over the "map" produces numeric keys instead of the correct slice semantics — corrupting []any values into {0: ..., 1: ...}-shaped maps.
Each of these was found because our workload exercises real nested JSON documents (design-token specs) through encoding/json-based Unmarshal/Marshal and Go-side type switches on interface{} — not synthetic. Happy to share minimal repros or open PRs for any of these if useful; didn't want to dump 7 unrelated changes into one giant PR.
🤖 Filed with Claude Code
Follow-up to #141 (three
gs/builtin/slice.tsperf fixes, already opened).While transpiling and running a real ~100k-LOC Go package (interpreter + parser + lexer + types + resolver for a design-tokens expression language) through goscript, we found a handful of additional bugs — mostly in the bundled
encoding/jsonruntime, plus one morebuiltin/type.tscorrectness bug. These are currently vendored locally as a--gs-pathoverride (forencoding/json) and a post-generation text patch (forbuiltin/type.ts), rather than proposed as PRs yet, to keep review units small. Filing this as a pointer in case there's interest — happy to open individual PRs for any/all of these on request.Summary of what we found (all root-caused with minimal standalone repros, transpiled and run under Bun):
encoding/jsonomitemptynot honored on marshal. Struct fields tagged,omitemptywere emitted unconditionally even when holding a Go zero value (zero number, empty string,false, empty slice/map, nil pointer/interface).encoding/jsonmarshal leaks the boxedinterface{}wrapper. A value carrying goscript's boxed-anywrapper (__goValue) serialized as{"__goValue": ...}instead of the underlying primitive/map/slice.encoding/jsonunmarshal doesn't type-drive Map/Slice struct fields. Only direct[]Structfields decoded into real struct instances;map[string]Struct/map[string][]Structfields (and further nesting) left plain JS objects/arrays in place, so generated struct-field-accessor getters returnedundefined.encoding/jsonunmarshal doesn't recurse into array elements insideinterface{}-typed values. Aninterface{}-typed array of objects (e.g. Go[]anyholdingmap[string]anyelements) left element objects as raw plain JS objects instead of the Map representation used elsewhere, breaking Go-sidecase map[string]any:type switches that call.entries().encoding/jsonunmarshal ignoresjson:"..."tags on anonymous (inline, unnamed) struct types. Anonymous struct targets carry no_fields/registered type metadata, so they fell through to the untypedinterface{}decode path, which replaces the plain-object struct value with a Map — every field then read back empty.encoding/jsonRawMessagenot handled as a map/slice element type, and noPointer-to-struct branch in the type-driven decoder (map/slice element and field types likemap[string]*Propertyleaked plain objects with undefined Go field reads).builtin/type.tsstructural type matching misclassifies a plain JSArrayasmap[string]any.matchesMapTypeandtypeAssert's inline map check accept any non-null object;Object.entries()of an array yields string indices that pass key/elem sampling. A Go type switch listingcase map[string]any:beforecase []any:then captures arrays in the map branch, and ranging over the "map" produces numeric keys instead of the correct slice semantics — corrupting[]anyvalues into{0: ..., 1: ...}-shaped maps.Each of these was found because our workload exercises real nested JSON documents (design-token specs) through
encoding/json-based Unmarshal/Marshal and Go-side type switches oninterface{}— not synthetic. Happy to share minimal repros or open PRs for any of these if useful; didn't want to dump 7 unrelated changes into one giant PR.🤖 Filed with Claude Code