From 937b2fe6a4a919b7ace0c383c586d84fc1b55e0f Mon Sep 17 00:00:00 2001 From: "Randall C. O'Reilly" Date: Sat, 24 Oct 2026 21:58:45 +0200 Subject: [PATCH] fix(compiler): keep the type arguments of generic function values A generic function instantiated with several type arguments and used as a value, as in sims := map[string]func(){"neuron": embed[Sim, Config]} lowered to the bare function, so calling the value passed no type arguments: its type parameters fell back to an empty interface, and new(C) did not make a *Config, so asserting it to an interface that *Config implements panicked. lowerIndexExpr already wrapped a function instantiated with one type argument in lowerGenericFunctionValue; lowerExpr lowered an IndexListExpr as its function alone. Lower such an IndexListExpr with lowerGenericFunctionValue too. Since calls do not pass type arguments to override functions that do not take them, such as those of the slices override, lowerGenericFunctionValue leaves those functions as their own values. Add a compliance test for calls, variables and map values of generic functions. Signed-off-by: Randall C. O'Reilly --- compiler/lowering.go | 12 ++ .../generic_func_value_type_args/expected.log | 5 + .../generic_func_value_type_args.go | 43 +++++++ .../generic_func_value_type_args.gs.ts | 112 ++++++++++++++++++ .../generic_func_value_type_args/index.ts | 1 + .../generic_func_value_type_args/lib/index.ts | 3 + .../generic_func_value_type_args/lib/lib.go | 22 ++++ .../lib/lib.gs.ts | 55 +++++++++ .../tsconfig.json | 27 +++++ 9 files changed, 280 insertions(+) create mode 100644 tests/tests/generic_func_value_type_args/expected.log create mode 100644 tests/tests/generic_func_value_type_args/generic_func_value_type_args.go create mode 100644 tests/tests/generic_func_value_type_args/generic_func_value_type_args.gs.ts create mode 100644 tests/tests/generic_func_value_type_args/index.ts create mode 100644 tests/tests/generic_func_value_type_args/lib/index.ts create mode 100644 tests/tests/generic_func_value_type_args/lib/lib.go create mode 100644 tests/tests/generic_func_value_type_args/lib/lib.gs.ts create mode 100644 tests/tests/generic_func_value_type_args/tsconfig.json diff --git a/compiler/lowering.go b/compiler/lowering.go index 4e10c45fa..9cf7b2333 100644 --- a/compiler/lowering.go +++ b/compiler/lowering.go @@ -10331,6 +10331,11 @@ func (o *LoweringOwner) lowerExpr(ctx lowerFileContext, expr ast.Expr) (string, case *ast.TypeAssertExpr: return o.lowerTypeAssertExpr(ctx, typed) case *ast.IndexListExpr: + // A generic function instantiated with several type arguments, used + // as a value, keeps them, as lowerIndexExpr does for one. + if signature := genericFunctionSignature(ctx, typed.X); signature != nil { + return o.lowerGenericFunctionValue(ctx, typed.X, typed.Indices, signature) + } return o.lowerExpr(ctx, typed.X) default: return "undefined", []Diagnostic{loweringUnsupportedAt(ctx, typed, "expression", ctx.semPkg.pkgPath, "unsupported expression kind")} @@ -14705,7 +14710,14 @@ func (o *LoweringOwner) lowerGenericFunctionValue( typeArgExprs []ast.Expr, signature *types.Signature, ) (string, []Diagnostic) { + // An override function that takes no type arguments, such as those of + // the slices override, is its own value, as calls of it pass none. calleeExpr, diagnostics := o.lowerExpr(ctx, callee) + if o.callUsesOverridePackage(ctx, callee) && !o.overrideFacts().TakesTypeArgs(calledFunction(ctx.semPkg.source, callee)) { + return calleeExpr, diagnostics + } + + // Otherwise the value calls the function with the type arguments. typeArgs := o.genericTypeArgsExpr(ctx, callee, typeArgExprs) signatureCtx := ctx.withFunctionTypeDepth(ctx.functionTypeDepth + 1) params := o.tsSignatureParamsFor(signatureCtx, signature, false) diff --git a/tests/tests/generic_func_value_type_args/expected.log b/tests/tests/generic_func_value_type_args/expected.log new file mode 100644 index 000000000..fb7470802 --- /dev/null +++ b/tests/tests/generic_func_value_type_args/expected.log @@ -0,0 +1,5 @@ +call neuron true +variable neuron true +map neuron true +single x +slices 1 diff --git a/tests/tests/generic_func_value_type_args/generic_func_value_type_args.go b/tests/tests/generic_func_value_type_args/generic_func_value_type_args.go new file mode 100644 index 000000000..ff0f98482 --- /dev/null +++ b/tests/tests/generic_func_value_type_args/generic_func_value_type_args.go @@ -0,0 +1,43 @@ +package main + +import ( + "slices" + + "github.com/s4wave/goscript/tests/tests/generic_func_value_type_args/lib" +) + +type Sim struct{} + +type Config struct { + lib.BaseConfig + Rate float32 +} + +func (cfg *Config) Defaults() { cfg.Name = "neuron" } + +// embed needs both of its type arguments. +func embed[S, C any](label string) { + _, cfg := lib.NewConfig[C]() + var s S + println(label, cfg.AsBaseConfig().Name, any(&s) != nil) +} + +func first[T any](v T) T { return v } + +func main() { + // A generic function instantiated with several type arguments keeps them + // when it is used as a value, as in a call. + embed[Sim, Config]("call") + f := embed[Sim, Config] + f("variable") + sims := map[string]func(string){"neuron": embed[Sim, Config]} + sims["neuron"]("map") + + // So does one with a single type argument. + g := first[string] + println("single", g("x")) + + // A function of the slices override, which takes no type arguments. + index := slices.Index[[]int, int] + println("slices", index([]int{4, 5, 6}, 5)) +} diff --git a/tests/tests/generic_func_value_type_args/generic_func_value_type_args.gs.ts b/tests/tests/generic_func_value_type_args/generic_func_value_type_args.gs.ts new file mode 100644 index 000000000..08c5c55d7 --- /dev/null +++ b/tests/tests/generic_func_value_type_args/generic_func_value_type_args.gs.ts @@ -0,0 +1,112 @@ +// Generated file based on generic_func_value_type_args.go +// Updated when compliance tests are re-run, DO NOT EDIT! + +import * as $ from "@goscript/builtin/index.js" +import * as slices from "@goscript/slices/index.js" +import * as lib from "@goscript/github.com/s4wave/goscript/tests/tests/generic_func_value_type_args/lib/index.js" +import "@goscript/slices/index.js" + +export class Sim { + constructor(__goscriptValue?: boolean) { + ;(this as any)[$.structValueMarker] = __goscriptValue === true + } + + public clone(): Sim { + return new Sim(true) + } + + public __goscriptAssign(source: Sim): this { + return this + } + + static __typeInfo = $.registerStructType( + "main.Sim", + () => new Sim(), + () => [], + Sim, + () => [] + ) +} + +export class Config { + public declare BaseConfig: lib.BaseConfig + + public declare Rate: number + + constructor(__goscriptValue?: boolean, BaseConfig$?: lib.BaseConfig, Rate$?: number) { + this.BaseConfig = BaseConfig$ ?? new lib.BaseConfig(true) + this.Rate = Rate$ ?? 0 + ;(this as any)[$.structValueMarker] = __goscriptValue === true + } + + public clone(): Config { + return new Config(true, this.BaseConfig.clone(), this.Rate) + } + + public __goscriptAssign(source: Config): this { + this.BaseConfig.__goscriptAssign(source.BaseConfig) + this.Rate = source.Rate + return this + } + + public Defaults(): void { + const cfg: Config | $.VarRef | null = this; + $.pointerValue(cfg).BaseConfig.Name = "neuron" + } + + public AsBaseConfig(): any { + return lib.BaseConfig.prototype.AsBaseConfig.call($.fieldRef(this, "BaseConfig")) + } + + static __typeInfo = $.registerStructType( + "main.Config", + () => new Config(), + () => [{ name: "Defaults", args: [], returns: [], pointerReceiver: true }, { name: "AsBaseConfig", args: [], returns: [{ type: /* @__PURE__ */ $.pointerType("lib.BaseConfig") }], pointerReceiver: true }], + Config, + () => [{ name: "BaseConfig", key: "BaseConfig", type: "lib.BaseConfig", anonymous: true }, { name: "Rate", key: "Rate", type: /* @__PURE__ */ $.basicType("float32") }] + ) +} + +export async function embed(__typeArgs: $.GenericTypeArgs | undefined, label: string): globalThis.Promise { + let [, cfg] = lib.NewConfig({[$.genericTypeArgsMarker]: $.genericTypeArgsBrand, C: __typeArgs?.["C"] ?? { type: { kind: $.TypeKind.Interface, methods: [] }, zero: () => null }}) + let s: $.VarRef = $.varRef($.genericZero(__typeArgs, "S", null)) + await $.println(label, $.pointerValue(((__goscriptAwait = (cfg !== null && (cfg as any).__isVarRef !== true && ((cfg as any).__goValue === undefined || (cfg as any).__goType?.charCodeAt(0) !== 42) ? (cfg as Exclude) : $.pointerValue>(cfg)).AsBaseConfig(), __goscriptAwait instanceof Promise ? await __goscriptAwait : __goscriptAwait) as lib.BaseConfig | $.VarRef | null)).Name, $.typeParamPointerInterfaceValue(s, __typeArgs?.["S"], "*S", /* @__PURE__ */ $.pointerType(__typeArgs?.["S"]?.type ?? { kind: $.TypeKind.Interface, methods: [] })) != null) +} + +export function first(__typeArgs: $.GenericTypeArgs | undefined, v: any): any { + return $.copyElement(v) +} + +export async function main(): globalThis.Promise { + // A generic function instantiated with several type arguments keeps them + // when it is used as a value, as in a call. + await embed(__goscriptStatic0(), "call") + let f: ((label: string) => void) | null = $.functionValue(async (label: string): globalThis.Promise => await embed(__goscriptStatic0(), label), ({ kind: $.TypeKind.Function, params: [/* @__PURE__ */ $.basicType("string")], results: [] } as $.FunctionTypeInfo)) + void (__goscriptAwait = f!("variable"), __goscriptAwait instanceof Promise ? await __goscriptAwait : __goscriptAwait) + let sims = $.makeMap void) | null>([["neuron", $.functionValue(async (label: string): globalThis.Promise => await embed(__goscriptStatic0(), label), ({ kind: $.TypeKind.Function, params: [/* @__PURE__ */ $.basicType("string")], results: [] } as $.FunctionTypeInfo))]]) + void (__goscriptAwait = $.mapGet void) | null, ((_p0: string) => void) | null>(sims, "neuron", null)[0]!("map"), __goscriptAwait instanceof Promise ? await __goscriptAwait : __goscriptAwait) + + // So does one with a single type argument. + let g: ((v: string) => string | globalThis.Promise) | null = $.functionValue((v: any): any => first(__goscriptStatic1(), v), ({ kind: $.TypeKind.Function, params: [{ kind: $.TypeKind.Interface, methods: [] }], results: [{ kind: $.TypeKind.Interface, methods: [] }] } as $.FunctionTypeInfo)) + await $.println("single", (g!("x") as string)) + + // A function of the slices override, which takes no type arguments. + let index: ((s: $.Slice, v: number) => number | globalThis.Promise) | null = slices.Index + await $.println("slices", (index!($.arrayToSlice([4, 5, 6]), 5) as number)) +} + +var __goscriptStatic0$value: any +function __goscriptStatic0(): any { + return __goscriptStatic0$value ??= ({[$.genericTypeArgsMarker]: $.genericTypeArgsBrand, S: { type: "main.Sim", zero: () => new Sim(true) }, C: { type: "main.Config", zero: () => new Config(true) }}) +} + +var __goscriptStatic1$value: any +function __goscriptStatic1(): any { + return __goscriptStatic1$value ??= ({[$.genericTypeArgsMarker]: $.genericTypeArgsBrand, T: { type: /* @__PURE__ */ $.basicType("string"), zero: () => "" }}) +} + +var __goscriptAwait: any + +if ($.isMainScript(import.meta)) { + await main() +} diff --git a/tests/tests/generic_func_value_type_args/index.ts b/tests/tests/generic_func_value_type_args/index.ts new file mode 100644 index 000000000..a6e33406a --- /dev/null +++ b/tests/tests/generic_func_value_type_args/index.ts @@ -0,0 +1 @@ +export { Config, Sim } from "./generic_func_value_type_args.gs.ts" diff --git a/tests/tests/generic_func_value_type_args/lib/index.ts b/tests/tests/generic_func_value_type_args/lib/index.ts new file mode 100644 index 000000000..121c99eb9 --- /dev/null +++ b/tests/tests/generic_func_value_type_args/lib/index.ts @@ -0,0 +1,3 @@ +export type { Config } from "./lib.gs.ts" +export { BaseConfig, NewConfig } from "./lib.gs.ts" +import "./lib.gs.ts" diff --git a/tests/tests/generic_func_value_type_args/lib/lib.go b/tests/tests/generic_func_value_type_args/lib/lib.go new file mode 100644 index 000000000..32da3688e --- /dev/null +++ b/tests/tests/generic_func_value_type_args/lib/lib.go @@ -0,0 +1,22 @@ +package lib + +// Config is implemented by pointers to configs that embed BaseConfig. +type Config interface { + AsBaseConfig() *BaseConfig + Defaults() +} + +// BaseConfig is embedded in configs. +type BaseConfig struct { + Name string +} + +func (bc *BaseConfig) AsBaseConfig() *BaseConfig { return bc } + +// NewConfig makes a new config of type C, which needs its type argument. +func NewConfig[C any]() (*C, Config) { + cfgC := new(C) + cfg := any(cfgC).(Config) + cfg.Defaults() + return cfgC, cfg +} diff --git a/tests/tests/generic_func_value_type_args/lib/lib.gs.ts b/tests/tests/generic_func_value_type_args/lib/lib.gs.ts new file mode 100644 index 000000000..4bbb53407 --- /dev/null +++ b/tests/tests/generic_func_value_type_args/lib/lib.gs.ts @@ -0,0 +1,55 @@ +// Generated file based on lib.go +// Updated when compliance tests are re-run, DO NOT EDIT! + +import * as $ from "@goscript/builtin/index.js" + +$.registerTypePackagePath("github.com/s4wave/goscript/tests/tests/generic_func_value_type_args/lib", ["lib.Config", "lib.BaseConfig"]) + +export type Config = { + AsBaseConfig(): BaseConfig | $.VarRef | null + Defaults(): void +} + +$.registerInterfaceType( + "lib.Config", + null, + [{ name: "AsBaseConfig", args: [], returns: [{ type: /* @__PURE__ */ $.pointerType("lib.BaseConfig") }] }, { name: "Defaults", args: [], returns: [] }] +); + +export class BaseConfig { + public declare Name: string + + constructor(__goscriptValue?: boolean, Name$?: string) { + this.Name = Name$ ?? "" + ;(this as any)[$.structValueMarker] = __goscriptValue === true + } + + public clone(): BaseConfig { + return new BaseConfig(true, this.Name) + } + + public __goscriptAssign(source: BaseConfig): this { + this.Name = source.Name + return this + } + + public AsBaseConfig(): BaseConfig | $.VarRef | null { + const bc: BaseConfig | $.VarRef | null = this; + return bc + } + + static __typeInfo = $.registerStructType( + "lib.BaseConfig", + () => new BaseConfig(), + () => [{ name: "AsBaseConfig", args: [], returns: [{ type: /* @__PURE__ */ $.pointerType("lib.BaseConfig") }], pointerReceiver: true }], + BaseConfig, + () => [{ name: "Name", key: "Name", type: /* @__PURE__ */ $.basicType("string") }] + ) +} + +export function NewConfig(__typeArgs: $.GenericTypeArgs | undefined): [any, Config | null] { + let cfgC = $.genericNew(__typeArgs, "C", null) + let cfg = $.mustTypeAssert($.typeParamPointerInterfaceValue(cfgC, __typeArgs?.["C"], "*C", /* @__PURE__ */ $.pointerType(__typeArgs?.["C"]?.type ?? { kind: $.TypeKind.Interface, methods: [] })), "lib.Config") + void (cfg !== null && (cfg as any).__isVarRef !== true && ((cfg as any).__goValue === undefined || (cfg as any).__goType?.charCodeAt(0) !== 42) ? (cfg as Exclude) : $.pointerValue>(cfg)).Defaults() + return [cfgC, cfg] +} diff --git a/tests/tests/generic_func_value_type_args/tsconfig.json b/tests/tests/generic_func_value_type_args/tsconfig.json new file mode 100644 index 000000000..7a09a27a7 --- /dev/null +++ b/tests/tests/generic_func_value_type_args/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "paths": { + "*": [ + "./*" + ], + "@go/github.com/s4wave/goscript/*": [ + "../../../*" + ], + "@goscript/*": [ + "../../../tests/tests/generic_func_value_type_args/run/output/@goscript/*", + "../../../gs/*", + "../../../tests/deps/*" + ], + "@goscript/github.com/s4wave/goscript/tests/tests/generic_func_value_type_args/*": [ + "./*" + ] + } + }, + "extends": "../../../tests/tsconfig.base.json", + "include": [ + "generic_func_value_type_args.gs.ts", + "index.ts", + "lib/index.ts", + "lib/lib.gs.ts" + ] +}