Fix poly-recv attr_writer dispatch (silently dropped assignments)#391
Fix poly-recv attr_writer dispatch (silently dropped assignments)#391OriPekelman wants to merge 2 commits intomatz:masterfrom
Conversation
`recv.attr = v` where `recv`'s static type is poly (sp_RbVal --
typically a function parameter widened across two call sites of
different classes) was silently a no-op. The poly-recv dispatch
builder (`compile_poly_method_call`) walked user classes and
matched two cases:
1. an explicit method named mname on the class, or
2. an attr_reader / attr_accessor reader.
Both reader-shaped. A class that exposes `attr =` only via
`attr_accessor` has no explicit method named `attr=`, and the
reader fallback only matches the bare name (`"attr"`, not
`"attr="`). So when mname ended in `=`, no per-class arm was
emitted -- the SP_TAG_OBJ block was empty and the result temp
held its zero-init default.
Pre-fix C for `o.flag = false` on a poly `o`:
sp_RbVal _t1 = recv;
mrb_int _t2 = 0;
if (_t1.tag == SP_TAG_OBJ) {
} /* <-- empty */
_t2;
Two coordinated changes:
1. `compile_poly_method_call` learns a third arm after the
reader fallback. When mname ends in `=` (excluding `==`,
`!=`, `<=`, `>=`) and the class has an attr_writer for the
bare name, emit a direct ivar assignment:
if (recv.cls_id == N) tmp = ((sp_C *)recv.v.p)->iv_x = arg0;
The result temp gets the assigned value (Ruby semantics).
Boxing parallels the user-method arm: when the ivar
widened to `poly` and the call site supplied a concrete
value, box via box_value_to_poly.
2. `poly_dispatch_return_type` learns the writer case -- the
per-class union of ivar types decides the result temp's
static type (str / int / float / poly). Without this, the
temp falls through to the int default and a `const char *`
arm trips a C compile error.
Real-world bite: tep's App#dispatch sets `req.passed = false`
at the top of every `pass`-fallthrough loop iteration. With
this bug, the flag stayed true after the first handler called
`pass`, every subsequent iteration also "passed", the loop ran
out of routes, and the request 404'd instead of falling through
to the next matching route.
Regression test: test/poly_attr_writer.rb -- two classes share
an attr_accessor through a function-typed parameter, with both
true->false and false->true transitions.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review
This pull request introduces a regression test for polymorphic attribute writers to address a bug where assignments were silently dropped. The review feedback suggests expanding the test coverage to include non-boolean types and verifying the return value of the assignment expression to ensure the fix correctly handles Ruby semantics and avoids potential C compilation errors.
| def trip(o); o.flag = true; end | ||
| def clear(o); o.flag = false; end | ||
|
|
||
| a = Box.new | ||
| b = Bag.new | ||
|
|
||
| trip(a) | ||
| trip(b) | ||
| puts a.flag ? "1" : "0" | ||
| puts b.flag ? "1" : "0" | ||
|
|
||
| clear(a) | ||
| clear(b) | ||
| puts a.flag ? "1" : "0" | ||
| puts b.flag ? "1" : "0" |
There was a problem hiding this comment.
The regression test currently only exercises boolean assignments. The PR description mentions that the fix also addresses a C compilation error occurring when the return type of a polymorphic assignment is incorrectly inferred (e.g., when dealing with const char *). To fully verify this fix and the "Ruby semantics" of the return value, the test should be expanded to include non-integer types and verify that the assignment expression returns the correct value.
def trip(o, v); o.flag = v; end
a = Box.new
b = Bag.new
puts trip(a, true) ? "1" : "0"
puts trip(b, "ok")
puts a.flag ? "1" : "0"
puts b.flag
puts trip(a, false) ? "1" : "0"
puts trip(b, false) ? "1" : "0"
puts a.flag ? "1" : "0"
puts b.flag ? "1" : "0"| 1 | ||
| 1 | ||
| 0 | ||
| 0 |
Per matz#391 review: extend test/poly_attr_writer.rb with three subcases. 1. bool slot (the original tep symptom) 2. string slot with both classes agreeing 3. divergent slots: IBox{int} + SBox{string} sharing the `assign(o, v)` call site, so spinel widens both `o` and `v` to poly. Each subcase also captures and checks the assignment expression's return value to confirm Ruby `obj.x = v` semantics (yields v). Subcase 3 surfaced a hole in the original arm: when the rhs is poly (`sp_RbVal`) and the slot is concrete (mrb_int / const char *), the per-arm write needs to unbox the rhs into the slot's C type. Without that the C compiler errors with "assigning to const char * from sp_RbVal" -- because the divergent ivars demand different concrete unboxes. Mirrors the fix already in PR matz#388 for poly setter dispatch. The arm now matches three rhs/slot shapes: - slot poly, rhs concrete: box rhs. - slot concrete, rhs poly: unbox rhs into the slot's type. - both agree: pass through. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
Addressed both points from @gemini-code-assist's review:
Pushed as 4ae9e4a on top of the original commit. Spinel |
|
Thanks Ori! Closing as a duplicate of #388, which is now merged on master. #388 already includes:
Verified on master HEAD (5014bc8): Both PRs were the right diagnosis arrived at independently; #388 happened to land first. Thanks again for the dig! |
recv.attr = von a poly receiver was silently a no-opWhen the poly-recv method-dispatch builder
(
compile_poly_method_call) walks user classes for an arm toemit, it has two cases:
attr_reader/attr_accessorreader fallback.Both are reader-shaped. The writer side wasn't covered: a class
that exposes
flag=only viaattr_accessor :flaghas noexplicit method named
flag=, and the reader fallback onlymatches the bare reader name (
"flag", not"flag="). So whenthe call site landed on poly dispatch, no per-class arm was
emitted at all, the SP_TAG_OBJ block was empty, and the
result temp held its zero-init default. The assignment was
silently elided.
The reader-side worked because it falls into the
cls_has_attr_readerarm. The writer side just had no arm tofall into.
The bite
Surfaced as a hung dispatch loop in tep, but it's the kind of
bug any code with a polymorphic helper hits:
Pre-fix C for
o.flag = false:Real-world repro: tep's
App#dispatchsetsreq.passed = falseat the top of every iteration of the
pass-fallthrough loop.With this bug, the flag stayed
trueafter the first handlercalled
pass, every subsequent iteration also "passed", theloop ran out of routes, and the request 404'd instead of
falling through to the next matching route. (The local
test_passregression in the tep tree caught it.)Patch
Two coordinated changes:
compile_poly_method_calllearns a third arm after thereader fallback. When
mnameends in=(and isn't==,!=,<=,>=) and the class has anattr_writerforthe bare name, emit a direct ivar assignment:
The result temp gets the assigned value (Ruby semantics).
Boxing parallels the user-method arm: when the ivar widened
to
polyand the call site supplied a concrete value, boxvia
box_value_to_poly.poly_dispatch_return_typealso learns the writer case --the per-class union of ivar types decides the result
temp's static type (str / int / float / poly). Without this
the temp falls through to the
intdefault and aconst char * = ...arm trips a C compile error.Regression test:
test/poly_attr_writer.rbexercises twoclasses sharing an attr_accessor through a function-typed
parameter, with both true→false and false→true transitions.
Reader-only and explicit-
def x=paths stay on their existingarms; this only adds the missing fallback.
🤖 Generated with Claude Code