From 07305d761254a351f36882f170b6921f237feafc Mon Sep 17 00:00:00 2001 From: "Randall C. O'Reilly" Date: Sun, 25 Oct 2026 10:03:52 +0100 Subject: [PATCH] feat(runtime): add PanicNilError and register runtime.Error runtime.PanicNilError was a stub that panicked when constructed, so code that names it, such as an interpreter that mirrors panic(nil), failed at init. It now has the Error and RuntimeError methods of Go. runtime.Error was not registered with the runtime type system, so a type assertion to it never succeeded. It is now registered, and recovered runtime panics have the RuntimeError method, so they satisfy it as in Go. Signed-off-by: Randall C. O'Reilly --- gs/builtin/panic.ts | 3 ++ gs/runtime/runtime.ts | 36 +++++++++++++++++++ gs/runtime/stubs.gs.ts | 8 ----- .../tests/package_import_runtime/expected.log | 2 ++ .../package_import_runtime.go | 16 +++++++++ .../package_import_runtime.gs.ts | 31 ++++++++++++++++ 6 files changed, 88 insertions(+), 8 deletions(-) diff --git a/gs/builtin/panic.ts b/gs/builtin/panic.ts index 63e1db187..a9e37ec42 100644 --- a/gs/builtin/panic.ts +++ b/gs/builtin/panic.ts @@ -51,6 +51,9 @@ export class RuntimeError extends Error { Error(): string { return this.message } + + // RuntimeError marks the value as a runtime.Error, as in Go. + RuntimeError(): void {} } /** diff --git a/gs/runtime/runtime.ts b/gs/runtime/runtime.ts index dbac5028a..bd199d5af 100644 --- a/gs/runtime/runtime.ts +++ b/gs/runtime/runtime.ts @@ -407,6 +407,17 @@ export interface Error { Error(): string | PromiseLike } +// Error is registered with the runtime type system, so type assertions to +// runtime.Error see its methods. +$.registerInterfaceType('runtime.Error', null, [ + { + name: 'Error', + args: [], + returns: [{ type: { kind: $.TypeKind.Basic, name: 'string' } }], + }, + { name: 'RuntimeError', args: [], returns: [] }, +]) + // TypeAssertionError represents a failed type assertion. export class TypeAssertionError implements Error { constructor( @@ -424,6 +435,31 @@ export class TypeAssertionError implements Error { } } +// PanicNilError is the value of a panic called with a nil argument, as Go +// 1.21 and later panic with. +export class PanicNilError { + constructor(_init?: Partial<{}>) {} + + public clone(): PanicNilError { + return new PanicNilError() + } + + public Error(): string { + return 'runtime error: panic called with nil argument' + } + + public RuntimeError(): void {} + + // PanicNilError is registered with the runtime type system; the compiler + // gives its method signatures, for reflection. + static __typeInfo = $.registerStructType( + 'runtime.PanicNilError', + new PanicNilError(), + [], + PanicNilError, + ) +} + // PanicError represents a panic. export class PanicError implements Error { constructor(public readonly value: any) {} diff --git a/gs/runtime/stubs.gs.ts b/gs/runtime/stubs.gs.ts index c66a1541c..fdc253234 100644 --- a/gs/runtime/stubs.gs.ts +++ b/gs/runtime/stubs.gs.ts @@ -46,14 +46,6 @@ export function MutexProfile(..._args: any[]): any { return $.notImplemented("runtime.MutexProfile") } -export class PanicNilError { - [field: string]: any - - constructor(..._args: any[]) { - $.notImplemented("runtime.PanicNilError") - } -} - export class Pinner { [field: string]: any diff --git a/tests/tests/package_import_runtime/expected.log b/tests/tests/package_import_runtime/expected.log index 953f4a77c..f41e3b757 100644 --- a/tests/tests/package_import_runtime/expected.log +++ b/tests/tests/package_import_runtime/expected.log @@ -21,3 +21,5 @@ Cleanup stopped Finalizer ignored locked goroutine ran main unlocked +PanicNilError: runtime error: panic called with nil argument true +recovered runtime.Error: true diff --git a/tests/tests/package_import_runtime/package_import_runtime.go b/tests/tests/package_import_runtime/package_import_runtime.go index cb2154349..1743bca3b 100644 --- a/tests/tests/package_import_runtime/package_import_runtime.go +++ b/tests/tests/package_import_runtime/package_import_runtime.go @@ -58,6 +58,7 @@ func main() { println("Finalizer ignored") lockOSThread() + panicNilError() } // callers reports frames of its own stack and names a function value. @@ -124,3 +125,18 @@ func lockOSThread() { runtime.UnlockOSThread() println("main unlocked") } + +// panicNilError checks the value Go panics with for panic(nil). +func panicNilError() { + var err error = &runtime.PanicNilError{} + _, isRuntime := err.(runtime.Error) + println("PanicNilError:", err.Error(), isRuntime) + + defer func() { + _, ok := recover().(runtime.Error) + println("recovered runtime.Error:", ok) + }() + var s []int + i := 5 + _ = s[i] +} diff --git a/tests/tests/package_import_runtime/package_import_runtime.gs.ts b/tests/tests/package_import_runtime/package_import_runtime.gs.ts index 94ebe106e..7877bc6ee 100644 --- a/tests/tests/package_import_runtime/package_import_runtime.gs.ts +++ b/tests/tests/package_import_runtime/package_import_runtime.gs.ts @@ -125,6 +125,7 @@ export async function main(): globalThis.Promise { await $.println("Finalizer ignored") await lockOSThread() + await panicNilError() } export async function callers(): globalThis.Promise { @@ -178,6 +179,29 @@ export async function lockOSThread(): globalThis.Promise { await $.println("main unlocked") } +export async function panicNilError(): globalThis.Promise { + const __defer = new $.AsyncDisposableStack() + try { + let err: $.GoError = $.interfaceValue<$.GoError>(new runtime.PanicNilError(), "*runtime.PanicNilError", __goscriptStatic2()) + let [, isRuntime] = $.typeAssertTuple(err, "runtime.Error") + await $.println("PanicNilError:", (__goscriptAwait = (err !== null && (err as any).__isVarRef !== true && ((err as any).__goValue === undefined || (err as any).__goType?.charCodeAt(0) !== 42) ? (err as Exclude<$.GoError, null>) : $.pointerValue>(err)).Error(), __goscriptAwait instanceof Promise ? await __goscriptAwait : __goscriptAwait), isRuntime) + + __defer.defer(async () => { await (async () => { + let [, ok] = $.typeAssertTuple($.recover(), "runtime.Error") + await $.println("recovered runtime.Error:", ok) + })() }) + let s: $.Slice = null! as $.Slice + let i = 5 + $.arrayIndex(s!, i) + await __defer.dispose() + } catch (e) { + await __defer.disposePanic(e) + if (!$.recovered(e)) { + throw e + } + } +} + var __goscriptStatic0$value: any function __goscriptStatic0(): any { return __goscriptStatic0$value ??= ({[$.genericTypeArgsMarker]: $.genericTypeArgsBrand, E: { type: /* @__PURE__ */ $.basicType("int"), zero: () => 0 }}) @@ -188,6 +212,13 @@ function __goscriptStatic1(): any { return __goscriptStatic1$value ??= ([{Get: (receiver: any) => $.pointerValue(receiver).Get(__goscriptStatic0())}, [$.methodSignature("Get", [], [/* @__PURE__ */ $.basicType("int")], true, false)]]) } +var __goscriptStatic2$value: any +function __goscriptStatic2(): any { + return __goscriptStatic2$value ??= ($.overrideTypeMethods(/* @__PURE__ */ $.pointerType("runtime.PanicNilError"), "runtime.PanicNilError", () => [$.methodSignature("Error", [], [/* @__PURE__ */ $.basicType("string")], true), $.methodSignature("RuntimeError", [], [], true)])) +} + +var __goscriptAwait: any + __goscriptInit0() if ($.isMainScript(import.meta)) {