Skip to content

Commit dc59098

Browse files
sisshiki1969claude
andauthored
NameError/NoMethodError metadata at raise sites; SystemExit exit status (#894)
core/exception (minus Process.kill-to-self files): 28 -> 13 failures. NameError#name / #receiver now populated where the errors are actually raised (previously only when constructed manually): - undefined constant: receiver is the namespace module (Object for a bare reference), threaded through get_constant_checked / const_missing / the superclass walk. - undefined class variable: receiver is the looked-up class. - bareword 'undefined local variable or method': receiver is self, and the message uses CRuby 4.0's single-quote form. - instance_variable_get / class_variable_get with an invalid name: #name is the *exact* argument object (String/Symbol, so the spec's equal? holds) and #receiver is self — carried via a pre-materialized exception object (name_error_reflection). - NameError#receiver raises ArgumentError 'no receiver is available' when no receiver was recorded (matching CRuby). NoMethodError#args is populated from the failed call's arguments at the method_missing raise site (empty array for a no-arg call), so #args and Exception#dup round-trip. SystemExit (and user subclasses) propagate their exit code: raising a SystemExit-family object syncs MonorubyErrKind::SystemExit's status from the object's /status ivar (it was baked to 0 by from_class_id), so 'raise SystemExit.new(7)' exits 7 and a 'CustomExit < SystemExit' exits with its own status. New differential tests: tests/name_error_api.rs (5) and tests/system_exit_status.rs (3, spawning the binary to check exit codes). Claude-Session: https://claude.ai/code/session_01XpzdoFu46d2ChJpSzeWsNK Co-authored-by: Claude <noreply@anthropic.com>
1 parent 8449fea commit dc59098

10 files changed

Lines changed: 283 additions & 32 deletions

File tree

monoruby/src/builtins.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ mod gc;
2222
mod hash;
2323
mod io;
2424
mod json;
25-
mod kernel;
25+
pub(crate) mod kernel;
2626
mod main_object;
2727
mod marshal;
2828
mod match_data;

monoruby/src/builtins/exception.rs

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -164,11 +164,12 @@ fn exc_receiver(
164164
_: BytecodePtr,
165165
) -> Result<Value> {
166166
let self_val = lfp.self_val();
167-
let v = globals
168-
.store
169-
.get_ivar(self_val, IdentId::get_id("/receiver"))
170-
.unwrap_or_default();
171-
Ok(v)
167+
// CRuby raises ArgumentError when the NameError carries no receiver
168+
// (e.g. `NameError.new.receiver`), rather than returning nil.
169+
match globals.store.get_ivar(self_val, IdentId::get_id("/receiver")) {
170+
Some(v) => Ok(v),
171+
None => Err(MonorubyErr::argumenterr("no receiver is available")),
172+
}
172173
}
173174

174175
/// `UncaughtThrowError#tag` — the tag object of the uncaught `throw`.

monoruby/src/builtins/kernel.rs

Lines changed: 70 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -925,6 +925,29 @@ fn loop_(vm: &mut Executor, globals: &mut Globals, lfp: Lfp, pc: BytecodePtr) ->
925925
/// - fail(error_type, message = nil, [NOT SUPPORTED] backtrace = caller(0), [NOT SUPPORTED] cause: $!) -> ()
926926
///
927927
/// [https://docs.ruby-lang.org/ja/latest/method/Kernel/m/fail.html]
928+
/// A `SystemExit` (or subclass) exception object carries its exit code
929+
/// in the `/status` ivar, but `MonorubyErrKind::SystemExit` is baked to
930+
/// 0 at construction (`from_class_id`). When such an object is raised,
931+
/// sync the kind's status from the ivar so the process exits with the
932+
/// requested code (`raise SystemExit.new(7)` → exit 7).
933+
fn fix_system_exit_status(globals: &mut Globals, err: &mut MonorubyErr, ex: Value) {
934+
// Both `SystemExit` itself (kind already `SystemExit`) and any user
935+
// subclass (kind `Other`, but a SystemExit descendant) exit the
936+
// process silently with the requested code.
937+
let is_system_exit = matches!(err.kind, MonorubyErrKind::SystemExit(_)) || {
938+
let se = globals.store[SYSTEM_EXIT_ERROR_CLASS].get_module();
939+
se.is_ancestor_of(globals.store[ex.class()].get_module())
940+
};
941+
if is_system_exit {
942+
let status = globals
943+
.store
944+
.get_ivar(ex, IdentId::get_id("/status"))
945+
.and_then(|v| v.try_fixnum())
946+
.unwrap_or(0);
947+
err.kind = MonorubyErrKind::SystemExit(status as u8);
948+
}
949+
}
950+
928951
#[monoruby_builtin]
929952
fn raise(vm: &mut Executor, globals: &mut Globals, lfp: Lfp, _: BytecodePtr) -> Result<Value> {
930953
// `cause:` kwarg is at slot 3 (positional_max=3 for the shared
@@ -940,13 +963,16 @@ fn raise(vm: &mut Executor, globals: &mut Globals, lfp: Lfp, _: BytecodePtr) ->
940963
}
941964
let ex = vm.errinfo();
942965
if let Some(inner) = ex.is_exception() {
943-
return Err(MonorubyErr::new_from_exception(inner).with_original(ex));
966+
let mut err = MonorubyErr::new_from_exception(inner);
967+
fix_system_exit_status(globals, &mut err, ex);
968+
return Err(err.with_original(ex));
944969
} else {
945970
return Err(MonorubyErr::runtimeerr(""));
946971
}
947972
}
948973
if let Some(ex) = lfp.arg(0).is_exception() {
949974
let mut err = MonorubyErr::new_from_exception(ex);
975+
fix_system_exit_status(globals, &mut err, lfp.arg(0));
950976
if let Some(arg1) = lfp.try_arg(1) {
951977
if arg1.try_hash_ty().is_none() {
952978
// A message override makes CRuby return a *new* exception
@@ -970,7 +996,9 @@ fn raise(vm: &mut Executor, globals: &mut Globals, lfp: Lfp, _: BytecodePtr) ->
970996
}
971997
let ex =
972998
vm.invoke_method_inner(globals, IdentId::NEW, klass.as_val(), &args, None, None)?;
973-
let err = MonorubyErr::new_from_exception(ex.is_exception().unwrap()).with_original(ex);
999+
let mut err = MonorubyErr::new_from_exception(ex.is_exception().unwrap());
1000+
fix_system_exit_status(globals, &mut err, ex);
1001+
let err = err.with_original(ex);
9741002
return Err(apply_cause(globals, err, Some(ex), cause_kwarg)?);
9751003
}
9761004
} else if let Some(message) = lfp.arg(0).is_rstring() {
@@ -4396,7 +4424,32 @@ fn is_valid_ivar_name(s: &str) -> bool {
43964424
/// semantics: Symbol/String are used directly, anything else is
43974425
/// coerced via `#to_str` (TypeError otherwise), and the resulting
43984426
/// name must be a valid instance-variable name (NameError otherwise).
4399-
fn ivar_name_id(vm: &mut Executor, globals: &mut Globals, arg: Value) -> Result<IdentId> {
4427+
/// Build a `NameError` for the `instance_variable_*` / `class_variable_*`
4428+
/// reflection methods, carrying `name_value` verbatim as `#name` (CRuby
4429+
/// preserves the exact argument object, which the spec checks with
4430+
/// `equal?`) and `receiver` as `#receiver`. The exception object is
4431+
/// pre-materialized and attached as the error's `original` so those
4432+
/// ivars survive being caught.
4433+
pub(crate) fn name_error_reflection(
4434+
globals: &mut Globals,
4435+
msg: String,
4436+
name_value: Value,
4437+
receiver: Value,
4438+
) -> MonorubyErr {
4439+
let exc = Value::new_exception_from(msg.clone(), NAME_ERROR_CLASS);
4440+
let _ = globals.store.set_ivar(exc, IdentId::_NAME, name_value);
4441+
let _ = globals
4442+
.store
4443+
.set_ivar(exc, IdentId::get_id("/receiver"), receiver);
4444+
MonorubyErr::nameerr(msg).with_original(exc)
4445+
}
4446+
4447+
fn ivar_name_id(
4448+
vm: &mut Executor,
4449+
globals: &mut Globals,
4450+
receiver: Value,
4451+
arg: Value,
4452+
) -> Result<IdentId> {
44004453
let name = if let Some(sym) = arg.try_symbol() {
44014454
sym.get_name().to_string()
44024455
} else if let Some(s) = arg.is_str() {
@@ -4411,9 +4464,16 @@ fn ivar_name_id(vm: &mut Executor, globals: &mut Globals, arg: Value) -> Result<
44114464
return Err(MonorubyErr::is_not_symbol_nor_string(&globals.store, arg));
44124465
};
44134466
if !is_valid_ivar_name(&name) {
4414-
return Err(MonorubyErr::nameerr(format!(
4415-
"'{name}' is not allowed as an instance variable name"
4416-
)));
4467+
// CRuby surfaces the given name and the receiver on this
4468+
// NameError (`#name` / `#receiver`); `#name` is the *exact*
4469+
// argument object (a String/Symbol), which the spec checks with
4470+
// `equal?`, so carry the original `arg` value.
4471+
return Err(name_error_reflection(
4472+
globals,
4473+
format!("'{name}' is not allowed as an instance variable name"),
4474+
arg,
4475+
receiver,
4476+
));
44174477
}
44184478
Ok(IdentId::get_id(&name))
44194479
}
@@ -4431,7 +4491,7 @@ fn iv_defined(
44314491
lfp: Lfp,
44324492
_: BytecodePtr,
44334493
) -> Result<Value> {
4434-
let id = ivar_name_id(vm, globals, lfp.arg(0))?;
4494+
let id = ivar_name_id(vm, globals, lfp.self_val(), lfp.arg(0))?;
44354495
let b = globals.store.get_ivar(lfp.self_val(), id).is_some();
44364496
Ok(Value::bool(b))
44374497
}
@@ -4444,7 +4504,7 @@ fn iv_defined(
44444504
/// [https://docs.ruby-lang.org/ja/latest/method/Object/i/instance_variable_set.html]
44454505
#[monoruby_builtin]
44464506
fn iv_set(vm: &mut Executor, globals: &mut Globals, lfp: Lfp, _: BytecodePtr) -> Result<Value> {
4447-
let id = ivar_name_id(vm, globals, lfp.arg(0))?;
4507+
let id = ivar_name_id(vm, globals, lfp.self_val(), lfp.arg(0))?;
44484508
let val = lfp.arg(1);
44494509
globals.store.set_ivar(lfp.self_val(), id, val)?;
44504510
Ok(val)
@@ -4458,7 +4518,7 @@ fn iv_set(vm: &mut Executor, globals: &mut Globals, lfp: Lfp, _: BytecodePtr) ->
44584518
/// [https://docs.ruby-lang.org/ja/latest/method/Object/i/instance_variable_get.html]
44594519
#[monoruby_builtin]
44604520
fn iv_get(vm: &mut Executor, globals: &mut Globals, lfp: Lfp, _: BytecodePtr) -> Result<Value> {
4461-
let id = ivar_name_id(vm, globals, lfp.arg(0))?;
4521+
let id = ivar_name_id(vm, globals, lfp.self_val(), lfp.arg(0))?;
44624522
let v = globals
44634523
.store
44644524
.get_ivar(lfp.self_val(), id)
@@ -4490,7 +4550,7 @@ fn iv(_vm: &mut Executor, globals: &mut Globals, lfp: Lfp, _: BytecodePtr) -> Re
44904550
/// [https://docs.ruby-lang.org/ja/latest/method/Object/i/remove_instance_variable.html]
44914551
#[monoruby_builtin]
44924552
fn iv_remove(vm: &mut Executor, globals: &mut Globals, lfp: Lfp, _: BytecodePtr) -> Result<Value> {
4493-
let id = ivar_name_id(vm, globals, lfp.arg(0))?;
4553+
let id = ivar_name_id(vm, globals, lfp.self_val(), lfp.arg(0))?;
44944554
match globals.store.remove_ivar(lfp.self_val(), id) {
44954555
Some(val) => Ok(val),
44964556
None => Err(MonorubyErr::nameerr(format!(

monoruby/src/builtins/module.rs

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2525,7 +2525,7 @@ fn class_variable_set(
25252525
let self_val = lfp.self_val();
25262526
self_val.ensure_not_frozen(&globals.store)?;
25272527
let class_id = self_val.as_class_id();
2528-
let name = coerce_to_class_var_name(vm, globals, lfp.arg(0))?;
2528+
let name = coerce_to_class_var_name(vm, globals, lfp.self_val(), lfp.arg(0))?;
25292529
let val = lfp.arg(1);
25302530
// A class variable is shared with ancestors: if a superclass already
25312531
// defines it, update *that* definition instead of shadowing a fresh copy
@@ -2556,7 +2556,7 @@ fn class_variable_get(
25562556
_: BytecodePtr,
25572557
) -> Result<Value> {
25582558
let class_id = lfp.self_val().as_class_id();
2559-
let name = coerce_to_class_var_name(vm, globals, lfp.arg(0))?;
2559+
let name = coerce_to_class_var_name(vm, globals, lfp.self_val(), lfp.arg(0))?;
25602560
let module = globals.store[class_id].get_module();
25612561
globals.get_class_variable(module, name).map(|(_, v)| v)
25622562
}
@@ -2575,7 +2575,7 @@ fn class_variable_defined(
25752575
_: BytecodePtr,
25762576
) -> Result<Value> {
25772577
let class_id = lfp.self_val().as_class_id();
2578-
let name = coerce_to_class_var_name(vm, globals, lfp.arg(0))?;
2578+
let name = coerce_to_class_var_name(vm, globals, lfp.self_val(), lfp.arg(0))?;
25792579
let module = globals.store[class_id].get_module();
25802580
Ok(Value::bool(globals.get_class_variable(module, name).is_ok()))
25812581
}
@@ -2587,14 +2587,20 @@ fn class_variable_defined(
25872587
fn coerce_to_class_var_name(
25882588
vm: &mut Executor,
25892589
globals: &mut Globals,
2590+
receiver: Value,
25902591
name_arg: Value,
25912592
) -> Result<IdentId> {
25922593
let id = name_arg.coerce_to_symbol_or_string(vm, globals)?;
25932594
let s = id.get_name();
25942595
if !s.starts_with("@@") || s.len() <= 2 {
2595-
return Err(MonorubyErr::nameerr(format!(
2596-
"`{s}' is not allowed as a class variable name"
2597-
)));
2596+
// CRuby carries the given name (the exact argument object, so the
2597+
// spec's `equal?` holds) and the receiver on this NameError.
2598+
return Err(crate::builtins::kernel::name_error_reflection(
2599+
globals,
2600+
format!("`{s}' is not allowed as a class variable name"),
2601+
name_arg,
2602+
receiver,
2603+
));
25982604
}
25992605
Ok(id)
26002606
}

monoruby/src/builtins/object.rs

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -161,12 +161,15 @@ fn bo_method_missing(
161161
use crate::executor::MethodMissingStyle;
162162
match vm.take_method_missing_style() {
163163
MethodMissingStyle::VCall => {
164-
return Err(MonorubyErr::nameerr_with_name(
164+
// CRuby 4.0: `undefined local variable or method 'name' for
165+
// <recv>` (single quotes), carrying the receiver on `#receiver`.
166+
return Err(MonorubyErr::nameerr_with_name_receiver(
165167
format!(
166-
"undefined local variable or method `{name}' for {}",
168+
"undefined local variable or method '{name}' for {}",
167169
recv.to_s(&globals.store)
168170
),
169171
name,
172+
recv,
170173
));
171174
}
172175
MethodMissingStyle::Super => {
@@ -194,9 +197,40 @@ fn bo_method_missing(
194197
},
195198
_ => MonorubyErr::method_not_found(&globals.store, name, recv),
196199
};
200+
// A genuine NoMethodError (not a visibility NameError) carries the
201+
// call arguments on `#args`. `args[0]` is the method name; the rest
202+
// are the arguments the missing method was called with.
203+
if matches!(err.kind(), MonorubyErrKind::NotMethod { .. }) {
204+
let call_args = Value::array_from_iter(args.iter().skip(1).cloned());
205+
return Err(no_method_error_with_args(globals, err, call_args));
206+
}
197207
Err(err)
198208
}
199209

210+
/// Re-materialize a NoMethodError so it also carries `#args`. The
211+
/// `NotMethod` error's name / receiver are transferred onto a
212+
/// pre-built exception object (via `take_ex_obj`'s standard path) and
213+
/// the arguments array is attached as the hidden `/args` ivar.
214+
fn no_method_error_with_args(globals: &mut Globals, err: MonorubyErr, call_args: Value) -> MonorubyErr {
215+
let (name, receiver) = match err.kind() {
216+
MonorubyErrKind::NotMethod { name, receiver } => (*name, *receiver),
217+
_ => (None, None),
218+
};
219+
let exc = Value::new_exception_from(err.message().to_string(), NO_METHOD_ERROR_CLASS);
220+
if let Some(name) = name {
221+
let _ = globals.store.set_ivar(exc, IdentId::_NAME, Value::symbol(name));
222+
}
223+
if let Some(receiver) = receiver {
224+
let _ = globals
225+
.store
226+
.set_ivar(exc, IdentId::get_id("/receiver"), Value::from_u64(receiver));
227+
}
228+
let _ = globals
229+
.store
230+
.set_ivar(exc, IdentId::get_id("/args"), call_args);
231+
err.with_original(exc)
232+
}
233+
200234
///
201235
/// ### BasicObject#!
202236
///

monoruby/src/executor/constants.rs

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,10 @@ impl Executor {
146146
) -> Result<Value> {
147147
match self.get_constant(globals, class_id, name)? {
148148
Some(v) => Ok(v),
149-
None => Err(MonorubyErr::uninitialized_constant(name)),
149+
None => {
150+
let receiver = globals.store[class_id].get_module().as_val();
151+
Err(MonorubyErr::uninitialized_constant_in(name, receiver))
152+
}
150153
}
151154
}
152155

@@ -178,6 +181,9 @@ impl Executor {
178181
mut module: Module,
179182
name: IdentId,
180183
) -> Result<(Value, ClassId)> {
184+
// The starting module is the one CRuby reports as
185+
// `NameError#receiver`, not whichever ancestor the walk ends on.
186+
let receiver = module.as_val();
181187
loop {
182188
// Snapshot whether the class has the constant before triggering
183189
// autoload, since `get_constant` may transition the entry from
@@ -201,7 +207,7 @@ impl Executor {
201207
},
202208
};
203209
}
204-
Err(MonorubyErr::uninitialized_constant(name))
210+
Err(MonorubyErr::uninitialized_constant_in(name, receiver))
205211
}
206212

207213
/// Ancestor-chain lookup for the segment of a **qualified** reference
@@ -272,7 +278,15 @@ impl Executor {
272278
) {
273279
Ok(v) => Ok(v),
274280
Err(e) if matches!(e.kind, MonorubyErrKind::Name(..)) => {
275-
Err(MonorubyErr::nameerr_with_name(e.message, name))
281+
// The default `Module#const_missing` raises a bare
282+
// NameError; re-attach the name and the receiver (the
283+
// module the lookup started on — `Object` for a bare
284+
// top-level reference) that CRuby reports.
285+
Err(MonorubyErr::nameerr_with_name_receiver(
286+
e.message,
287+
name,
288+
module.as_val(),
289+
))
276290
}
277291
Err(e) => Err(e),
278292
}

monoruby/src/globals/error.rs

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -621,13 +621,26 @@ impl MonorubyErr {
621621
}
622622

623623
pub(crate) fn uninitialized_constant(name: IdentId) -> MonorubyErr {
624-
Self::nameerr(format!("uninitialized constant {name}"))
624+
Self::nameerr_with_name(format!("uninitialized constant {name}"), name)
625625
}
626626

627-
pub(crate) fn uninitialized_cvar(name: IdentId, class_name: String) -> MonorubyErr {
628-
Self::nameerr(format!(
629-
"uninitialized class variable {name} in {class_name}"
630-
))
627+
/// As [`Self::uninitialized_constant`], but also records the module
628+
/// the constant was looked up on as `NameError#receiver` (CRuby
629+
/// reports the namespace class, or `Object` for a bare reference).
630+
pub(crate) fn uninitialized_constant_in(name: IdentId, receiver: Value) -> MonorubyErr {
631+
Self::nameerr_with_name_receiver(format!("uninitialized constant {name}"), name, receiver)
632+
}
633+
634+
pub(crate) fn uninitialized_cvar(
635+
name: IdentId,
636+
class_name: String,
637+
receiver: Value,
638+
) -> MonorubyErr {
639+
Self::nameerr_with_name_receiver(
640+
format!("uninitialized class variable {name} in {class_name}"),
641+
name,
642+
receiver,
643+
)
631644
}
632645

633646
pub(crate) fn identifier_must_be_constant(name: &str) -> MonorubyErr {

monoruby/src/globals/store/class/constants.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -577,6 +577,7 @@ impl Globals {
577577
None => Err(MonorubyErr::uninitialized_cvar(
578578
name,
579579
parent.id().get_name(&self.store),
580+
parent.as_val(),
580581
)),
581582
}
582583
}

0 commit comments

Comments
 (0)