Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 48 additions & 1 deletion gs/builtin/panic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import {
import { goSlice, index, makeSlice } from './slice.js'
import { DisposableStack } from './defer.js'


// captureThrow returns whatever value the callback throws, or undefined if it
// did not throw. Go panics surface as a thrown GoPanic in the transpiled
// runtime, so tests assert on the caught value's shape. Catching here models a
Expand Down Expand Up @@ -193,6 +192,54 @@ describe('recover consumes the in-flight panic', () => {
)
})

it('recovers a JavaScript error thrown without panic', () => {
// An error with an Error method is the value, as reflect.ValueError.
class valueError extends Error {
Error(): string {
return this.message
}
}
const thrown = new valueError('reflect: call on zero Value')
let seen: unknown = 'unset'
const outcome = runWithRecover(
() => {
throw thrown
},
() => {
seen = recover()
},
)
expect(seen).toBe(thrown)
expect(outcome.finished).toBe(true)

// Another error is its message.
runWithRecover(
() => {
throw new TypeError('x is undefined')
},
() => {
seen = recover()
},
)
expect(seen).toBe('x is undefined')
})

it('does not recover runtime.Goexit', () => {
const goexit = new Error('runtime.Goexit')
goexit.name = 'GoexitError'
let seen: unknown = 'unset'
const outcome = runWithRecover(
() => {
throw goexit
},
() => {
seen = recover()
},
)
expect(seen).toBeNull()
expect(outcome.rethrew).toBe(goexit)
})

it('normal defer disposal cannot recover an unrelated panic', () => {
let seen: unknown = 'unset'
try {
Expand Down
36 changes: 33 additions & 3 deletions gs/builtin/panic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,44 @@ let activeRecoverPanic: GoPanic | null = null

export function withRecoveringPanic<T>(err: unknown, fn: () => T): T {
const previous = activeRecoverPanic
activeRecoverPanic = err instanceof GoPanic ? err : null
activeRecoverPanic = asGoPanic(err)
try {
return fn()
} finally {
activeRecoverPanic = previous
}
}

// errorPanics holds the GoPanic that stands for a JavaScript error thrown
// without panic, such as a reflect.ValueError of the reflect package or a
// TypeError, which Go code recovers as it does a panic. It is keyed by the
// error, which the generated catch block passes to recovered.
const errorPanics = new WeakMap<Error, GoPanic>()

// asGoPanic returns the GoPanic that err unwinds as, or null if err is not
// a panic: runtime.Goexit and os.Exit throw errors that recover does not
// stop. The value of a panic from an error without an Error method is the
// error's message.
function asGoPanic(err: unknown): GoPanic | null {
if (err instanceof GoPanic) {
return err
}
if (
!(err instanceof Error) ||
err.name === 'GoexitError' ||
err.name === 'ProcessExitError'
) {
return null
}
let goPanic = errorPanics.get(err)
if (goPanic === undefined) {
const hasError = typeof (err as { Error?: unknown }).Error === 'function'
goPanic = new GoPanic(hasError ? err : err.message)
errorPanics.set(err, goPanic)
}
return goPanic
}

// RuntimeError is the value carried by a panic from a Go runtime fault: index
// out of range, slice bounds out of range, integer divide by zero, or nil
// pointer dereference. It implements the Go error interface (Error()), so a
Expand Down Expand Up @@ -129,11 +159,11 @@ export function recover(): any {
return goPanic.value
}

// recovered reports whether err is a GoPanic that a deferred recover() consumed.
// recovered reports whether err is a panic that a deferred recover() consumed.
// The catch block generated for a
// defer+recover function calls this: true means swallow the panic and return the
// function's named results; false means rethrow so an outer frame can recover or
// the program crashes with the Go panic message.
export function recovered(err: unknown): boolean {
return err instanceof GoPanic && err.recovered
return asGoPanic(err)?.recovered === true
}
37 changes: 34 additions & 3 deletions gs/reflect/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -286,11 +286,42 @@ export class ValueError extends Error {
public Method: string

constructor(init: { Kind: Kind; Method: string }) {
super(
`reflect: call of reflect.Value.${init.Method} on ${Kind_String(init.Kind)} Value`,
)
// Go calls a Value of kind Invalid the zero Value.
const kind = init.Kind === 0 ? 'zero' : Kind_String(init.Kind)
super(`reflect: call of reflect.Value.${init.Method} on ${kind} Value`)
this.Kind = init.Kind
this.Method = init.Method
this.name = 'ValueError'
}

// Error returns the message, as the Error method of *ValueError.
Error(): string {
return this.message
}

// Register the type, so that a recovered *ValueError asserts as one.
static __typeInfo = $.registerStructType(
'reflect.ValueError',
null,
[
{
name: 'Error',
args: [],
returns: [{ type: { kind: $.TypeKind.Basic, name: 'string' } }],
},
],
ValueError,
[
{
name: 'Kind',
key: 'Kind',
type: { kind: $.TypeKind.Basic, name: 'int' },
},
{
name: 'Method',
key: 'Method',
type: { kind: $.TypeKind.Basic, name: 'string' },
},
],
)
}
2 changes: 2 additions & 0 deletions tests/tests/reflect_recover_value_error/expected.log
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
ok
recovered: true reflect: call of reflect.Value.Field on zero Value
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package main

import (
"fmt"
"reflect"
)

// field returns the first field of v, or the panic it recovers.
func field(v reflect.Value) (s string) {
defer func() {
if r := recover(); r != nil {
_, isErr := r.(*reflect.ValueError)
s = fmt.Sprint("recovered: ", isErr, " ", r)
}
}()
return v.Field(0).String()
}

func main() {
type T struct{ S string }
println(field(reflect.ValueOf(T{S: "ok"})))
println(field(reflect.Value{}))
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// Generated file based on reflect_recover_value_error.go
// Updated when compliance tests are re-run, DO NOT EDIT!

import * as $ from "@goscript/builtin/index.js"
import * as fmt from "@goscript/fmt/index.js"
import * as reflect from "@goscript/reflect/index.js"

export function field(v: reflect.Value): string {
let s = ""
const __defer = new $.DisposableStack()
try {
__defer.defer(() => { (() => {
{
let r = $.recover()
if (r != null) {
let [, isErr] = $.typeAssertTuple<reflect.ValueError | $.VarRef<reflect.ValueError> | null>(r, /* @__PURE__ */ $.pointerType("reflect.ValueError"))
s = (fmt.Sprint("recovered: ", isErr, " ", r) as string)
}
}
})() })
const __goscriptReturn0: string = $.markAsStructValue($.cloneStructValue($.markAsStructValue($.cloneStructValue(v)).Field(0))).String()
s = __goscriptReturn0
__defer.dispose()
return s
__defer.dispose()
} catch (e) {
__defer.disposePanic(e)
if (!$.recovered(e)) {
throw e
}
}
return s
}

export async function main(): globalThis.Promise<void> {
class T {
public declare S: string

constructor(init?: Partial<{S?: string}>, __goscriptValue?: boolean) {
this.S = init?.S ?? ("" as string)
;(this as any)[$.structValueMarker] = __goscriptValue === true
}

public clone(): T {
return new T(this, true)
}

public __goscriptAssign(source: T): this {
this.S = source.S
return this
}

static __typeInfo = $.registerStructType(
"main.T",
() => new T(),
() => [],
T,
() => [/* @__PURE__ */ $.structField("S", /* @__PURE__ */ $.basicType("string"), [0], 0, true)]
)
}
await $.println(field($.markAsStructValue($.cloneStructValue(reflect.ValueOf($.structInterfaceValue($.markAsStructValue(new T({S: "ok"})), "main.T", "main.T"))))))
await $.println(field($.markAsStructValue(new reflect.Value())))
}

if ($.isMainScript(import.meta)) {
await main()
}
25 changes: 25 additions & 0 deletions tests/tests/reflect_recover_value_error/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"compilerOptions": {
"paths": {
"*": [
"./*"
],
"@go/github.com/s4wave/goscript/*": [
"../../../*"
],
"@goscript/*": [
"../../../tests/tests/reflect_recover_value_error/run/output/@goscript/*",
"../../../gs/*",
"../../../tests/deps/*"
],
"@goscript/github.com/s4wave/goscript/tests/tests/reflect_recover_value_error/*": [
"./*"
]
}
},
"extends": "../../../tests/tsconfig.base.json",
"include": [
"index.ts",
"reflect_recover_value_error.gs.ts"
]
}
Loading