///| Interpreter-specific error handling helpers
///|
///| Re-exports JsError from shared errors package and provides
///| Value conversion helpers for JavaScript try-catch handling.
///|
/// Create a JavaScript Error object Value with proper prototype chain
fn make_error_value_with_env(
name : String,
msg : String,
env : Environment?,
) -> Value {
let err_desc : PropDescriptor = {
writable: true,
enumerable: false,
configurable: true,
getter: None,
setter: None,
is_accessor: false,
}
let err_props : Map[String, Value] = Map([])
err_props["message"] = String_(msg)
err_props["stack"] = String_(name + ": " + msg)
let err_descs : Map[String, PropDescriptor] = {
"message": err_desc,
"stack": err_desc,
}
// Try to look up the proper prototype from the environment
let proto : Value = match env {
Some(e) => {
let ctor : Value = e.get(name) catch { _ => Null }
match ctor {
Object(ctor_data) =>
match ctor_data.bag.properties.get("prototype") {
Some(p) => p
None => Null
}
_ => Null
}
}
None => Null
}
// Fallback: attach `name` as an own data property whenever the resolved
// prototype cannot be trusted to carry it. Covers three cases:
// 1. Env is None or the binding is missing (e.g. InternalError has no
// registered constructor).
// 2. The binding was rebound by user code (`TypeError = function(){}`),
// so `proto` is some user-owned object whose `name` is empty/unrelated.
// 3. The resolved prototype is missing its own `name` entry for any other
// reason.
// Without this, engine-thrown errors would report `err.name === undefined`
// after user rebinding — a user-visible regression of internal error shape.
if !proto_has_name(proto, name) {
err_props["name"] = String_(name)
err_descs["name"] = err_desc
}
Object({
bag: {
properties: err_props,
symbol_properties: Map([]),
descriptors: err_descs,
symbol_descriptors: Map([]),
internal_slots: Map([]),
host_slots: Map([]),
},
prototype: proto,
callable: None,
class_name: name,
extensible: true,
arraybuffer_state: None,
})
}
///|
/// True when `proto` is an object whose own `name` property matches `expected`.
/// Used to decide whether engine-thrown errors can rely on the prototype chain
/// for `err.name` (genuine native error prototype) or must carry `name` as an
/// own property (rebound constructor, missing binding).
fn proto_has_name(proto : Value, expected : String) -> Bool {
match proto {
Object(proto_data) =>
match proto_data.bag.properties.get("name") {
Some(String_(s)) => s == expected
_ => false
}
_ => false
}
}
///|
/// Create an AggregateError with the errors array property
fn make_aggregate_error_value(
msg : String,
errors : Array[Error],
env : Environment?,
) -> Value {
let err_desc : PropDescriptor = {
writable: true,
enumerable: false,
configurable: true,
getter: None,
setter: None,
is_accessor: false,
}
let err_props : Map[String, Value] = Map([])
err_props["message"] = String_(msg)
err_props["stack"] = String_("AggregateError: " + msg)
// Convert errors array to JavaScript array
let js_errors : Array[Value] = errors.map(fn(e) {
js_error_to_value_with_env(e, env)
})
err_props["errors"] = make_array(js_errors)
let err_descs : Map[String, PropDescriptor] = {
"message": err_desc,
"stack": err_desc,
"errors": err_desc,
}
// Try to look up the proper prototype from the environment
let proto : Value = match env {
Some(e) => {
let ctor : Value = e.get("AggregateError") catch { _ => Null }
match ctor {
Object(ctor_data) =>
match ctor_data.bag.properties.get("prototype") {
Some(p) => p
None => Null
}
_ => Null
}
}
None => Null
}
// Same fallback as make_error_value_with_env: if the resolved prototype
// cannot be trusted (missing binding, rebound constructor, missing `name`),
// attach `name` as an own property so `err.name` is still defined.
if !proto_has_name(proto, "AggregateError") {
err_props["name"] = String_("AggregateError")
err_descs["name"] = err_desc
}
Object({
bag: {
properties: err_props,
symbol_properties: Map([]),
descriptors: err_descs,
symbol_descriptors: Map([]),
internal_slots: Map([]),
host_slots: Map([]),
},
prototype: proto,
callable: None,
class_name: "AggregateError",
extensible: true,
arraybuffer_state: None,
})
}
///|
/// Convert any catchable error to a JavaScript Error object Value
pub fn js_error_to_value(err : Error) -> Value {
js_error_to_value_with_env(err, None)
}
///|
/// Convert any catchable error to a JavaScript Error object Value with proper prototype
pub fn js_error_to_value_with_env(err : Error, env : Environment?) -> Value {
match err {
@errors.AggregateError(message~, errors~) =>
make_aggregate_error_value(message, errors, env)
JsException(val) => val // JavaScript throw statement value
ExecutionControlError(StackDepthLimit) =>
mark_engine_stack_depth_error(
make_error_value_with_env("RangeError", STACK_DEPTH_LIMIT_MESSAGE, env),
)
e =>
match @errors.name_message_if_js_error(e) {
Some((name, msg)) => make_error_value_with_env(name, msg, env)
None => make_error_value_with_env("Error", e.to_string(), env)
}
}
}
///|
/// Convert a catchable error to the JS throw value used by this interpreter.
/// Runtime engine errors are surfaced as strings like "TypeError: ...".
pub fn js_error_to_throw_value(err : Error) -> Value {
match err {
@errors.AggregateError(message~, errors~) => {
let error_texts = errors.map(fn(e) {
js_error_to_throw_value(e).to_string()
})
let errors_str = error_texts.join(", ")
String_("AggregateError: " + message + " (errors: " + errors_str + ")")
}
JsException(val) => val
e =>
match @errors.format_if_js_error(e) {
Some(s) => String_(s)
None => String_("Error: " + e.to_string())
}
}
}
///|
/// Raise a JavaScript exception with the given value.
/// This is the only way for external packages to raise JsException since
/// suberror constructors from other packages are read-only in MoonBit.
pub fn raise_js_exception(value : Value) -> Unit raise Error {
raise JsException(value)
}
///|
/// Check if an error is a JavaScript catchable error
pub fn is_js_catchable_error(err : Error) -> Bool {
match err {
JsException(_) => true
ExecutionControlError(StackDepthLimit) => true
e => @errors.name_message_if_js_error(e) != None
}
}