From 0103e00fc32c397f2e22c251044eff72486a29da Mon Sep 17 00:00:00 2001 From: "Randall C. O'Reilly" Date: Sat, 24 Oct 2026 16:39:53 +0200 Subject: [PATCH] feat(reflect): add the missing Value methods reflect.Value lacked SetIterKey, SetIterValue, SetCap, Clear, CanInt, CanUint, CanFloat, CanComplex, OverflowComplex, Comparable, Fields, Methods, TryRecv, TrySend, InterfaceData and SetPointer. Code that calls them failed at run time, and a type that embeds reflect.Value, as encoding/json/v2 does, failed TypeScript type checking on the promoted methods. Implement them as Go defines them. TryRecv and TrySend are async like Recv and Send. InterfaceData and SetPointer need memory layouts and unsafe pointers that values do not have, so they panic. Add a compliance test for the implemented methods. Signed-off-by: Randall C. O'Reilly --- gs/reflect/meta.json | 4 +- gs/reflect/type.ts | 178 ++++++++++++++++++ .../reflect_value_methods_more/expected.log | 11 ++ .../tests/reflect_value_methods_more/index.ts | 1 + .../reflect_value_methods_more.go | 57 ++++++ .../reflect_value_methods_more.gs.ts | 122 ++++++++++++ .../reflect_value_methods_more/tsconfig.json | 25 +++ 7 files changed, 397 insertions(+), 1 deletion(-) create mode 100644 tests/tests/reflect_value_methods_more/expected.log create mode 100644 tests/tests/reflect_value_methods_more/index.ts create mode 100644 tests/tests/reflect_value_methods_more/reflect_value_methods_more.go create mode 100644 tests/tests/reflect_value_methods_more/reflect_value_methods_more.gs.ts create mode 100644 tests/tests/reflect_value_methods_more/tsconfig.json diff --git a/gs/reflect/meta.json b/gs/reflect/meta.json index fa0f564ca..d73080f07 100644 --- a/gs/reflect/meta.json +++ b/gs/reflect/meta.json @@ -7,7 +7,9 @@ "Value.Call": true, "Value.CallSlice": true, "Value.Recv": true, - "Value.Send": true + "Value.Send": true, + "Value.TryRecv": true, + "Value.TrySend": true }, "funcValueMakers": { "MakeFunc": true, diff --git a/gs/reflect/type.ts b/gs/reflect/type.ts index 9323739cb..a7c03eb2e 100644 --- a/gs/reflect/type.ts +++ b/gs/reflect/type.ts @@ -1983,6 +1983,184 @@ export class Value { } // SetZero sets v to be the zero value of v's type + // SetIterKey assigns to v the key of iter's current map entry, as + // v.Set(iter.Key()) does. + public SetIterKey(iter: MapIter | null): void { + this.Set(iter!.Key()) + } + + // SetIterValue assigns to v the value of iter's current map entry, as + // v.Set(iter.Value()) does. + public SetIterValue(iter: MapIter | null): void { + this.Set(iter!.Value()) + } + + // SetCap sets v's capacity to n. It panics if v's Kind is not Slice, or if + // n is smaller than the length or greater than the capacity of the slice. + public SetCap(n: number): void { + if (!this.CanSet()) { + throw new Error( + 'reflect: call of reflect.Value.SetCap on unaddressable value', + ) + } + if (this.Kind() !== Slice) { + throw new ValueError({ Kind: this.Kind(), Method: 'SetCap' }) + } + const value = this.currentValue() as $.Slice + if (n < $.len(value) || n > $.cap(value)) { + throw new Error('reflect: slice capacity out of range in SetCap') + } + this.storeValue($.goSlice(value, 0, $.len(value), n) as ReflectValue) + } + + // Clear clears the contents of a map or zeros the contents of a slice, as + // the clear builtin does. + public Clear(): void { + const k = this.Kind() + if (k === Map) { + ;(this.currentValue() as globalThis.Map | null)?.clear() + return + } + if (k !== Slice) { + throw new ValueError({ Kind: k, Method: 'Clear' }) + } + for (let i = 0; i < this.Len(); i++) { + this.Index(i).SetZero() + } + } + + // CanInt reports whether Int can be used without panicking. + public CanInt(): boolean { + const k = this.Kind() + return k === Int || k === Int8 || k === Int16 || k === Int32 || k === Int64 + } + + // CanUint reports whether Uint can be used without panicking. + public CanUint(): boolean { + const k = this.Kind() + return ( + k === Uint || + k === Uint8 || + k === Uint16 || + k === Uint32 || + k === Uint64 || + k === Uintptr + ) + } + + // CanFloat reports whether Float can be used without panicking. + public CanFloat(): boolean { + const k = this.Kind() + return k === Float32 || k === Float64 + } + + // CanComplex reports whether Complex can be used without panicking. + public CanComplex(): boolean { + const k = this.Kind() + return k === Complex64 || k === Complex128 + } + + // OverflowComplex reports whether the complex128 x cannot be represented by + // v's type. It panics if v's Kind is not Complex64 or Complex128. + public OverflowComplex(x: $.Complex): boolean { + const k = this.Kind() + if (k === Complex64) { + const f32max = 3.4028234663852886e38 + const overflows = (f: number) => isFinite(f) && Math.abs(f) > f32max + return overflows($.real(x)) || overflows($.imag(x)) + } + if (k === Complex128) { + return false + } + throw new ValueError({ Kind: k, Method: 'OverflowComplex' }) + } + + // Comparable reports whether v is comparable. For an interface, array or + // struct, it depends on the values it holds, as in Go. + public Comparable(): boolean { + switch (this.Kind()) { + case Invalid: + return false + case Interface: + return this.IsNil() || this.Elem().Comparable() + case Array: + case Struct: { + const n = this.Kind() === Array ? this.Len() : this.NumField() + for (let i = 0; i < n; i++) { + const elem = this.Kind() === Array ? this.Index(i) : this.Field(i) + if (!elem.Comparable()) { + return false + } + } + return true + } + default: + return this.Type().Comparable() + } + } + + // Fields returns an iterator over the fields of the struct v, yielding + // each field's StructField and Value. + public Fields(): iter.Seq2 { + return async ( + _yield: ((f: StructField, v: Value) => iter.YieldResult) | null, + ) => { + const t = this.Type() + for (let i = 0; i < this.NumField(); i++) { + if (!(await _yield!(t.Field(i), this.Field(i)))) { + return + } + } + } + } + + // Methods returns an iterator over the exported methods of v, yielding + // each method's Method and its value bound to v. + public Methods(): iter.Seq2 { + return async ( + _yield: ((m: Method, v: Value) => iter.YieldResult) | null, + ) => { + const t = this.Type() + for (let i = 0; i < this.NumMethod(); i++) { + if (!(await _yield!(t.Method(i), this.Method(i)))) { + return + } + } + } + } + + // TryRecv receives a value from the channel v if it can without blocking, + // reporting whether it did; ok is false for a closed channel. + public async TryRecv(): Promise<[Value, boolean]> { + const ch = this.recvChan() + if (ch === null || !ch.canReceiveNonBlocking()) { + return [new Value(null, invalidTypeInstance), false] + } + return this.Recv() + } + + // TrySend sends x on the channel v if it can without blocking, reporting + // whether it did. + public async TrySend(x: Value): Promise { + const ch = this.sendChan() + if (ch === null || !ch.canSendNonBlocking()) { + return false + } + await this.Send(x) + return true + } + + // InterfaceData is deprecated and needs the memory layout of an interface, + // which values do not have here. + public InterfaceData(): number[] { + throw new Error('reflect: InterfaceData is not supported') + } + + // SetPointer needs unsafe pointers, which values do not have here. + public SetPointer(_x: unknown): void { + throw new Error('reflect: SetPointer is not supported') + } + public SetZero(): void { if (!this.CanSet()) { throw new Error( diff --git a/tests/tests/reflect_value_methods_more/expected.log b/tests/tests/reflect_value_methods_more/expected.log new file mode 100644 index 000000000..5bab00f7c --- /dev/null +++ b/tests/tests/reflect_value_methods_more/expected.log @@ -0,0 +1,11 @@ +can: true false true true true false +overflow: true false +comparable: false true +clear: 0 0 2 4 +field: A int +field: B string +method: Sum 3 +iter: k 7 +trysend: true false +tryrecv: 3 true +empty: false false diff --git a/tests/tests/reflect_value_methods_more/index.ts b/tests/tests/reflect_value_methods_more/index.ts new file mode 100644 index 000000000..79d27c30e --- /dev/null +++ b/tests/tests/reflect_value_methods_more/index.ts @@ -0,0 +1 @@ +export { pair } from "./reflect_value_methods_more.gs.ts" diff --git a/tests/tests/reflect_value_methods_more/reflect_value_methods_more.go b/tests/tests/reflect_value_methods_more/reflect_value_methods_more.go new file mode 100644 index 000000000..606c5b341 --- /dev/null +++ b/tests/tests/reflect_value_methods_more/reflect_value_methods_more.go @@ -0,0 +1,57 @@ +package main + +import "reflect" + +type pair struct { + A int + B string +} + +func (p pair) Sum() int { return p.A + len(p.B) } + +func main() { + // The Can methods report the kinds that their accessors accept. + i, u, f, c := reflect.ValueOf(1), reflect.ValueOf(uint8(2)), reflect.ValueOf(1.5), reflect.ValueOf(1i) + println("can:", i.CanInt(), i.CanUint(), u.CanUint(), f.CanFloat(), c.CanComplex(), f.CanComplex()) + println("overflow:", reflect.ValueOf(complex64(0)).OverflowComplex(1e40), c.OverflowComplex(1e40)) + + // Comparable looks at the values an interface or struct holds. + var a any = []int{1} + println("comparable:", reflect.ValueOf(&a).Elem().Comparable(), reflect.ValueOf(pair{}).Comparable()) + + // Clear empties a map and zeros a slice; SetCap shrinks a slice's capacity. + m := map[string]int{"a": 1} + reflect.ValueOf(m).Clear() + s := make([]int, 2, 8) + s[0] = 5 + sv := reflect.ValueOf(&s).Elem() + sv.Clear() + sv.SetCap(4) + println("clear:", len(m), s[0], len(s), cap(s)) + + // Fields and Methods iterate with names and values. + p := pair{A: 1, B: "xy"} + for sf, v := range reflect.ValueOf(p).Fields() { + println("field:", sf.Name, v.Kind().String()) + } + for meth, v := range reflect.ValueOf(p).Methods() { + println("method:", meth.Name, v.Call(nil)[0].Int()) + } + + // SetIterKey and SetIterValue copy the current map entry. + k, e := reflect.New(reflect.TypeFor[string]()).Elem(), reflect.New(reflect.TypeFor[int]()).Elem() + it := reflect.ValueOf(map[string]int{"k": 7}).MapRange() + for it.Next() { + k.SetIterKey(it) + e.SetIterValue(it) + } + println("iter:", k.String(), e.Int()) + + // TrySend and TryRecv do not block. + ch := reflect.ValueOf(make(chan int, 1)) + println("trysend:", ch.TrySend(reflect.ValueOf(3)), ch.TrySend(reflect.ValueOf(4))) + x, ok := ch.TryRecv() + println("tryrecv:", x.Int(), ok) + x, ok = ch.TryRecv() + println("empty:", x.IsValid(), ok) +} diff --git a/tests/tests/reflect_value_methods_more/reflect_value_methods_more.gs.ts b/tests/tests/reflect_value_methods_more/reflect_value_methods_more.gs.ts new file mode 100644 index 000000000..2c129d1c3 --- /dev/null +++ b/tests/tests/reflect_value_methods_more/reflect_value_methods_more.gs.ts @@ -0,0 +1,122 @@ +// Generated file based on reflect_value_methods_more.go +// Updated when compliance tests are re-run, DO NOT EDIT! + +import * as $ from "@goscript/builtin/index.js" +import * as reflect from "@goscript/reflect/index.js" +import type * as iter from "@goscript/iter/index.js" + +export class pair { + public declare A: number + + public declare B: string + + constructor(__goscriptValue?: boolean, A$?: number, B$?: string) { + this.A = A$ ?? 0 + this.B = B$ ?? "" + ;(this as any)[$.structValueMarker] = __goscriptValue === true + } + + public clone(): pair { + return new pair(true, this.A, this.B) + } + + public __goscriptAssign(source: pair): this { + this.A = source.A + this.B = source.B + return this + } + + public Sum(): number { + const p = this; + return p.A + $.stringLen(p.B) + } + + static __typeInfo = $.registerStructType( + "main.pair", + () => new pair(), + () => [$.methodSignature("Sum", [], [/* @__PURE__ */ $.basicType("int")])], + pair, + () => [/* @__PURE__ */ $.structField("A", /* @__PURE__ */ $.basicType("int"), [0], 0, true), /* @__PURE__ */ $.structField("B", /* @__PURE__ */ $.basicType("string"), [1], 8, true)] + ) +} + +export async function main(): globalThis.Promise { + // The Can methods report the kinds that their accessors accept. + let i = $.markAsStructValue($.cloneStructValue(reflect.ValueOf($.basicInterfaceValue(1, "int")))) + let u = $.markAsStructValue($.cloneStructValue(reflect.ValueOf($.basicInterfaceValue(2, "uint8")))) + let f = $.markAsStructValue($.cloneStructValue(reflect.ValueOf($.basicInterfaceValue(1.5, "float64")))) + let c = $.markAsStructValue($.cloneStructValue(reflect.ValueOf($.basicInterfaceValue($.complex(0, 1), "complex128")))) + await $.println("can:", $.markAsStructValue($.cloneStructValue(i)).CanInt(), $.markAsStructValue($.cloneStructValue(i)).CanUint(), $.markAsStructValue($.cloneStructValue(u)).CanUint(), $.markAsStructValue($.cloneStructValue(f)).CanFloat(), $.markAsStructValue($.cloneStructValue(c)).CanComplex(), $.markAsStructValue($.cloneStructValue(f)).CanComplex()) + await $.println("overflow:", $.markAsStructValue($.cloneStructValue(reflect.ValueOf($.basicInterfaceValue($.complex(0, 0), "complex64")))).OverflowComplex($.complex(1e+40, 0)), $.markAsStructValue($.cloneStructValue(c)).OverflowComplex($.complex(1e+40, 0))) + + // Comparable looks at the values an interface or struct holds. + let a: $.VarRef = $.varRef($.interfaceValue($.arrayToSlice([1]), "[]int", /* @__PURE__ */ $.sliceType(/* @__PURE__ */ $.basicType("int")))) + await $.println("comparable:", $.markAsStructValue($.cloneStructValue($.markAsStructValue($.cloneStructValue(reflect.ValueOf($.interfaceValue(a, "*interface{}", /* @__PURE__ */ $.pointerType({ kind: $.TypeKind.Interface, methods: [] }))))).Elem())).Comparable(), $.markAsStructValue($.cloneStructValue(reflect.ValueOf($.structInterfaceValue(new pair(true), "main.pair", "main.pair")))).Comparable()) + + // Clear empties a map and zeros a slice; SetCap shrinks a slice's capacity. + let m = $.makeMap([["a", 1]]) + $.markAsStructValue($.cloneStructValue(reflect.ValueOf($.interfaceValue(m, "map[string]int", /* @__PURE__ */ $.mapType(/* @__PURE__ */ $.basicType("string"), /* @__PURE__ */ $.basicType("int")))))).Clear() + let s: $.VarRef<$.Slice> = $.varRef($.makeSlice(2, 8, "number")) + $.sliceElems(s.value!)[$.sliceSlot(s.value!, 0)] = 5 + let sv = $.markAsStructValue($.cloneStructValue($.markAsStructValue($.cloneStructValue(reflect.ValueOf($.interfaceValue(s, "*[]int", /* @__PURE__ */ $.pointerType(/* @__PURE__ */ $.sliceType(/* @__PURE__ */ $.basicType("int"))))))).Elem())) + $.markAsStructValue($.cloneStructValue(sv)).Clear() + $.markAsStructValue($.cloneStructValue(sv)).SetCap(4) + await $.println("clear:", $.len(m), $.arrayIndex(s.value!, 0), $.sliceLen(s.value), $.cap(s.value)) + + // Fields and Methods iterate with names and values. + let p = new pair(true, 1, "xy") + let __goscriptRangeReturn0 = false + ;await (async () => { + await $.markAsStructValue($.cloneStructValue(reflect.ValueOf($.structInterfaceValue((p).clone(), "main.pair", "main.pair")))).Fields()!(async (sf, v) => { + await $.println("field:", sf.Name, reflect.Kind_String($.markAsStructValue($.cloneStructValue(v)).Kind())) + return true + }) + })() + if (__goscriptRangeReturn0) { + return + } + let __goscriptRangeReturn1 = false + ;await (async () => { + await $.markAsStructValue($.cloneStructValue(reflect.ValueOf($.structInterfaceValue((p).clone(), "main.pair", "main.pair")))).Methods()!(async (meth, v) => { + await $.println("method:", meth.Name, $.markAsStructValue($.cloneStructValue($.arrayIndex((await $.markAsStructValue($.cloneStructValue(v)).Call(null))!, 0))).Int()) + return true + }) + })() + if (__goscriptRangeReturn1) { + return + } + + // SetIterKey and SetIterValue copy the current map entry. + let k = $.markAsStructValue($.cloneStructValue($.markAsStructValue($.cloneStructValue(reflect.New($.pointerValueOrNil(reflect.TypeFor(__goscriptStatic0()))!))).Elem())) + let e = $.markAsStructValue($.cloneStructValue($.markAsStructValue($.cloneStructValue(reflect.New($.pointerValueOrNil(reflect.TypeFor(__goscriptStatic1()))!))).Elem())) + let it = $.markAsStructValue($.cloneStructValue(reflect.ValueOf($.interfaceValue($.makeMap([["k", 7]]), "map[string]int", /* @__PURE__ */ $.mapType(/* @__PURE__ */ $.basicType("string"), /* @__PURE__ */ $.basicType("int")))))).MapRange() + while ((it !== null && (it as any).__isVarRef !== true && (it as any).__goValue === undefined ? ((it as any) as reflect.MapIter).Next() : reflect.MapIter.prototype.Next.call($.pointerValue(it)))) { + $.markAsStructValue($.cloneStructValue(k)).SetIterKey(it) + $.markAsStructValue($.cloneStructValue(e)).SetIterValue(it) + } + await $.println("iter:", $.markAsStructValue($.cloneStructValue(k)).String(), $.markAsStructValue($.cloneStructValue(e)).Int()) + + // TrySend and TryRecv do not block. + let ch = $.markAsStructValue($.cloneStructValue(reflect.ValueOf($.interfaceValue($.makeChannel(1, 0, "both"), "chan int", /* @__PURE__ */ $.channelType(/* @__PURE__ */ $.basicType("int"), "both"))))) + await $.println("trysend:", await $.markAsStructValue($.cloneStructValue(ch)).TrySend($.markAsStructValue($.cloneStructValue(reflect.ValueOf($.basicInterfaceValue(3, "int"))))), await $.markAsStructValue($.cloneStructValue(ch)).TrySend($.markAsStructValue($.cloneStructValue(reflect.ValueOf($.basicInterfaceValue(4, "int")))))) + let [x, ok] = await $.markAsStructValue($.cloneStructValue(ch)).TryRecv() + await $.println("tryrecv:", $.markAsStructValue($.cloneStructValue(x)).Int(), ok) + let __goscriptTuple0: any = await $.markAsStructValue($.cloneStructValue(ch)).TryRecv() + x = $.assignStruct(x, __goscriptTuple0[0]) + ok = __goscriptTuple0[1] + await $.println("empty:", $.markAsStructValue($.cloneStructValue(x)).IsValid(), ok) +} + +var __goscriptStatic0$value: any +function __goscriptStatic0(): any { + return __goscriptStatic0$value ??= ({[$.genericTypeArgsMarker]: $.genericTypeArgsBrand, T: { type: /* @__PURE__ */ $.basicType("string"), zero: () => "" }}) +} + +var __goscriptStatic1$value: any +function __goscriptStatic1(): any { + return __goscriptStatic1$value ??= ({[$.genericTypeArgsMarker]: $.genericTypeArgsBrand, T: { type: /* @__PURE__ */ $.basicType("int"), zero: () => 0 }}) +} + +if ($.isMainScript(import.meta)) { + await main() +} diff --git a/tests/tests/reflect_value_methods_more/tsconfig.json b/tests/tests/reflect_value_methods_more/tsconfig.json new file mode 100644 index 000000000..475df6348 --- /dev/null +++ b/tests/tests/reflect_value_methods_more/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "paths": { + "*": [ + "./*" + ], + "@go/github.com/s4wave/goscript/*": [ + "../../../*" + ], + "@goscript/*": [ + "../../../tests/tests/reflect_value_methods_more/run/output/@goscript/*", + "../../../gs/*", + "../../../tests/deps/*" + ], + "@goscript/github.com/s4wave/goscript/tests/tests/reflect_value_methods_more/*": [ + "./*" + ] + } + }, + "extends": "../../../tests/tsconfig.base.json", + "include": [ + "index.ts", + "reflect_value_methods_more.gs.ts" + ] +}