Skip to content

Commit 99fb92b

Browse files
authored
Merge pull request #388 from OriPekelman/fix/poly-ivar-write-and-nil-return-dispatch
fix(codegen): two silent miscompiles on poly receivers
2 parents 0f92526 + 544a0cb commit 99fb92b

5 files changed

Lines changed: 187 additions & 1 deletion

spinel_codegen.rb

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28558,6 +28558,15 @@ def poly_dispatch_return_type(mname)
2855828558
@needs_rb_value = 1
2855928559
return "poly"
2856028560
end
28561+
# Setters: mname ends with "=" and at least one class has an
28562+
# attr_writer for the bare name. Return type is the ivar type
28563+
# (Ruby returns the rhs from `x = v`); without this, the result
28564+
# tmp's C type defaults to `mrb_int` and `tmp = rhs` mismatches
28565+
# for non-int slots.
28566+
setter_bname = ""
28567+
if mname.length > 1 && mname[mname.length - 1] == "="
28568+
setter_bname = mname[0, mname.length - 1]
28569+
end
2856128570
common = ""
2856228571
ci = 0
2856328572
while ci < @cls_names.length
@@ -28567,6 +28576,9 @@ def poly_dispatch_return_type(mname)
2856728576
elsif cls_has_attr_reader(ci, mname) == 1
2856828577
# An attr_reader returns the ivar type. Issue #119.
2856928578
rt = cls_ivar_type(ci, "@" + mname)
28579+
elsif setter_bname != "" && cls_has_attr_writer(ci, setter_bname) == 1
28580+
# An attr_writer setter returns the ivar's type.
28581+
rt = cls_ivar_type(ci, "@" + setter_bname)
2857028582
end
2857128583
if rt != ""
2857228584
if common == ""
@@ -28851,6 +28863,39 @@ def compile_poly_method_call(nid, rc, mname)
2885128863
else
2885228864
emit(" if (" + recv_tmp + ".cls_id == " + i.to_s + ") " + tmp + " = " + rhs + ";")
2885328865
end
28866+
elsif mname.length > 1 && mname[mname.length - 1] == "=" && cls_has_attr_writer(i, mname[0, mname.length - 1]) == 1
28867+
# An auto-registered attr_writer setter (`obj.x = v`) on a
28868+
# poly-typed receiver. Without this arm the cls_id dispatch
28869+
# finds neither a real method nor an attr_reader, falls
28870+
# through silently, and the assignment never executes —
28871+
# `obj.x = v` becomes a no-op.
28872+
bname = mname[0, mname.length - 1]
28873+
raw_val = arg_compiled.length > 0 ? arg_compiled[0] : "0"
28874+
raw_t = arg_types.length > 0 ? arg_types[0] : ""
28875+
slot_t = cls_ivar_type(i, "@" + bname)
28876+
# Match the rhs to the slot's concrete C type for *this arm*.
28877+
# Three cases:
28878+
# slot poly + arg concrete -> box the arg.
28879+
# slot concrete + arg poly -> unbox the arg.
28880+
# else -> direct assign.
28881+
# Different arms in the dispatch may take different branches
28882+
# (e.g. one slot is `int`, another is `string`).
28883+
store_val = raw_val
28884+
if slot_t == "poly" && raw_t != "poly" && raw_t != ""
28885+
store_val = box_value_to_poly(raw_t, raw_val)
28886+
elsif slot_t != "poly" && raw_t == "poly"
28887+
store_val = unbox_poly_to(slot_t, raw_val)
28888+
end
28889+
ivar_lhs = "((sp_" + cname + " *)" + recv_tmp + ".v.p)->" + sanitize_ivar("@" + bname)
28890+
# `x = v` returns v in Ruby. The result tmp's type is set by
28891+
# poly_dispatch_return_type (poly when slot types diverge,
28892+
# otherwise the common slot type). Box / coerce the stored
28893+
# value into the tmp's expected shape.
28894+
rhs_for_tmp = store_val
28895+
if is_poly_ret == 1
28896+
rhs_for_tmp = box_val_to_poly(store_val, slot_t)
28897+
end
28898+
emit(" if (" + recv_tmp + ".cls_id == " + i.to_s + ") { " + ivar_lhs + " = " + store_val + "; " + tmp + " = " + rhs_for_tmp + "; }")
2885428899
end
2885528900
i = i + 1
2885628901
end
@@ -29254,7 +29299,13 @@ def box_non_nullable_value_to_poly(at, val)
2925429299
return "sp_box_bool(" + val + ")"
2925529300
end
2925629301
if at == "nil"
29257-
return "sp_box_nil()"
29302+
# `val` may be a side-effecting call (e.g. an arm in a
29303+
# cls_id-dispatched table where the user-defined override
29304+
# returns nil because its body is a `puts` / similar). Drop
29305+
# the value but keep the side effect via the comma operator;
29306+
# otherwise the generated dispatch silently elides the call
29307+
# for any subclass whose method's static return type is nil.
29308+
return "((void)(" + val + "), sp_box_nil())"
2925829309
end
2925929310
if at == "symbol"
2926029311
return "sp_box_sym(" + val + ")"

test/poly_attr_writer.rb

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
# Regression: a setter call (`obj.x = val`) on a parameter typed as
2+
# a class union must execute the assignment. Previously the dispatch
3+
# loop emitted no arms for attr_writer setters on poly receivers and
4+
# silently dropped the store.
5+
#
6+
# Three subcases:
7+
# 1. Single class with `x`, second class without -- the union case
8+
# where the dispatch arm is "skipped silently."
9+
# 2. Two classes share an ivar name; same value type. Verifies
10+
# both arms emit and the result temp's C type fits the slot.
11+
# 3. Two classes share an ivar name; differing value types
12+
# (string vs int). Forces the new arm to box the rhs poly-style
13+
# for one cls_id and unbox for the other.
14+
#
15+
# Verifying via the *side effect* (the ivar's value) rather than the
16+
# setter expression's return value -- propagating setter returns
17+
# through the dispatch into the enclosing function's return is a
18+
# separate concern that this PR doesn't try to fix.
19+
20+
# --- 1. Single class (the no-op repro) ---------------------------------
21+
22+
class Foo
23+
attr_accessor :x
24+
def initialize; @x = 0; end
25+
end
26+
27+
class Bar
28+
# No `x`; coexists only to widen `obj` to a class union.
29+
end
30+
31+
def set_x(obj, v)
32+
obj.x = v
33+
end
34+
35+
foo = Foo.new
36+
bar = Bar.new
37+
set_x(foo, 42)
38+
set_x(bar, 99) rescue nil
39+
40+
puts foo.x
41+
42+
43+
# --- 2. Two classes share `body` (string in both) ---------------------
44+
45+
class Req
46+
attr_accessor :body
47+
def initialize; @body = ""; end
48+
end
49+
50+
class Res
51+
attr_accessor :body
52+
def initialize; @body = ""; end
53+
end
54+
55+
def set_body(o, b)
56+
o.body = b
57+
end
58+
59+
req = Req.new
60+
res = Res.new
61+
set_body(req, "REQ")
62+
set_body(res, "RES")
63+
64+
puts req.body
65+
puts res.body
66+
67+
68+
# --- 3. Two classes share `slot` with *different* value types ---------
69+
# Pre-fix poly_dispatch_return_type returned "int" for any setter
70+
# whose name didn't appear as an explicit method, so the result temp
71+
# was mrb_int and the string arm's `tmp = lv` mismatched. The new
72+
# arm now also unboxes the rhs to the slot's concrete type when
73+
# the function param widened to poly.
74+
75+
class IBox
76+
attr_accessor :slot
77+
def initialize; @slot = 0; end
78+
end
79+
80+
class SBox
81+
attr_accessor :slot
82+
def initialize; @slot = ""; end
83+
end
84+
85+
def set_slot(o, v)
86+
o.slot = v
87+
end
88+
89+
ibox = IBox.new
90+
sbox = SBox.new
91+
set_slot(ibox, 7)
92+
set_slot(sbox, "hello")
93+
94+
puts ibox.slot
95+
puts sbox.slot

test/poly_attr_writer.rb.expected

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
42
2+
REQ
3+
RES
4+
7
5+
hello

test/poly_nil_return_dispatch.rb

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# Regression: a subclass override whose static return type is `nil`
2+
# must still be called when dispatched through a poly receiver.
3+
# Previously `box_value_to_poly("nil", call_expr)` dropped `call_expr`
4+
# and emitted a bare `sp_box_nil()` for that arm, so the body of the
5+
# override never ran.
6+
#
7+
# Two checks:
8+
# 1. The override's side effect runs (the `puts` inside Sub#hook).
9+
# 2. The dispatched call's *return value* is correctly observable as
10+
# nil (i.e. the boxing path threads through, not just the side
11+
# effect).
12+
13+
class Base
14+
def hook(arg); end # empty body -> static return "nil"
15+
end
16+
17+
class Sub < Base
18+
def hook(arg)
19+
puts "subclass-ran: " + arg # `puts` returns nil
20+
end
21+
end
22+
23+
class Holder
24+
attr_accessor :h
25+
def initialize; @h = Base.new; end
26+
def set(x); @h = x; end
27+
def call_hook(arg); @h.hook(arg); end
28+
end
29+
30+
h = Holder.new
31+
h.set(Sub.new)
32+
result = h.call_hook("ok")
33+
puts result == nil
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
subclass-ran: ok
2+
true

0 commit comments

Comments
 (0)