Skip to content

Commit 1a25a72

Browse files
sinsokuclaude
andauthored
Add support for nested multi-target nodes in destructuring assignment (#354)
This commit adds support for nested destructuring assignments like `a, (b, c) = [1, [2, 3]]`, which previously caused a "RuntimeError: not supported yet: multi_target_node" error. Changes: - Add MultiTargetNode class to handle nested destructuring targets - Extend create_target_node to support :multi_target_node type - Add nil guards for `rights` parameter in MAsgnBox and Type::Array#splat_assign - Add comprehensive test cases for nested destructuring patterns 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude <noreply@anthropic.com>
1 parent dc5cd3c commit 1a25a72

5 files changed

Lines changed: 96 additions & 2 deletions

File tree

lib/typeprof/core/ast.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,8 @@ def self.create_target_node(raw_node, lenv)
270270
IndexWriteNode.new(raw_node, dummy_node, lenv)
271271
when :call_target_node
272272
CallWriteNode.new(raw_node, dummy_node, lenv)
273+
when :multi_target_node
274+
MultiTargetNode.new(raw_node, dummy_node, lenv)
273275
else
274276
pp raw_node
275277
raise "not supported yet: #{ raw_node.type }"

lib/typeprof/core/ast/misc.rb

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,63 @@ def retrieve_at(pos, &blk)
139139
end
140140
end
141141

142+
class MultiTargetNode < Node
143+
def initialize(raw_node, rhs, lenv)
144+
super(raw_node, lenv)
145+
@rhs = rhs
146+
@lefts = raw_node.lefts.map do |raw_lhs|
147+
AST.create_target_node(raw_lhs, lenv)
148+
end
149+
if raw_node.rest
150+
case raw_node.rest.type
151+
when :splat_node
152+
if raw_node.rest.expression
153+
@rest = AST.create_target_node(raw_node.rest.expression, lenv)
154+
end
155+
when :implicit_rest_node
156+
# no assignment target
157+
else
158+
raise "unexpected rest node in multi_target: #{raw_node.rest.type}"
159+
end
160+
end
161+
@rights = raw_node.rights.map do |raw_rhs|
162+
AST.create_target_node(raw_rhs, lenv)
163+
end
164+
end
165+
166+
attr_reader :rhs, :lefts, :rest, :rights
167+
168+
def subnodes = { rhs:, lefts:, rest:, rights: }
169+
170+
def install0(genv)
171+
# The rhs should be installed by the parent MultiWriteNode
172+
# Here we set up the multi-assignment box for nested destructuring
173+
value = @rhs.install(genv)
174+
175+
@lefts.each {|lhs| lhs.install(genv) }
176+
@lefts.each {|lhs| lhs.rhs.ret || raise(lhs.rhs.inspect) }
177+
lefts = @lefts.map {|lhs| lhs.rhs.ret }
178+
179+
rest_elem = nil
180+
if @rest
181+
rest_elem = Vertex.new(self)
182+
@rest.install(genv)
183+
@rest.rhs.ret || raise(@rest.rhs.inspect)
184+
@changes.add_edge(genv, Source.new(Type::Instance.new(genv, genv.mod_ary, [rest_elem])), @rest.rhs.ret)
185+
end
186+
187+
rights = nil
188+
if @rights && !@rights.empty?
189+
@rights.each {|rhs| rhs.install(genv) }
190+
@rights.each {|rhs| rhs.rhs.ret || raise(rhs.rhs.inspect) }
191+
rights = @rights.map {|rhs| rhs.rhs.ret }
192+
end
193+
194+
box = @changes.add_masgn_box(genv, value, lefts, rest_elem, rights)
195+
box.ret
196+
end
197+
end
198+
142199
class MatchWriteNode < Node
143200
def initialize(raw_node, lenv)
144201
super(raw_node, lenv)

lib/typeprof/core/graph/box.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1073,7 +1073,7 @@ def run0(genv, changes)
10731073
else
10741074
if @lefts.size >= 1
10751075
edges << [Source.new(ty), @lefts[0]]
1076-
elsif @rights.size >= 1
1076+
elsif @rights && @rights.size >= 1
10771077
edges << [Source.new(ty), @rights[0]]
10781078
else
10791079
edges << [Source.new(ty), @rest_elem]

lib/typeprof/core/type.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ def splat_assign(genv, lefts, rest_elem, rights)
112112
edges = []
113113
state = :left
114114
j = nil
115+
rights_size = rights ? rights.size : 0
115116
@elems.each_with_index do |elem, i|
116117
case state
117118
when :left
@@ -123,7 +124,7 @@ def splat_assign(genv, lefts, rest_elem, rights)
123124
redo
124125
end
125126
when :rest
126-
if @elems.size - i > rights.size
127+
if @elems.size - i > rights_size
127128
edges << [elem, rest_elem]
128129
else
129130
state = :right

scenario/variable/masgn_nested.rb

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
## update
2+
def test_nested_destructuring
3+
a, (b, c) = [1, [2, 3]]
4+
[a, b, c]
5+
end
6+
7+
def test_nested_with_strings
8+
x, (y, z) = ["foo", ["bar", "baz"]]
9+
[x, y, z]
10+
end
11+
12+
def test_deeper_nesting
13+
a, (b, (c, d)) = [1, [2, [3, 4]]]
14+
[a, b, c, d]
15+
end
16+
17+
def test_nested_with_rest
18+
a, (b, *rest) = [1, [2, 3, 4]]
19+
[a, b, rest]
20+
end
21+
22+
def test_nested_with_rest_and_rights
23+
a, (b, *rest, c) = [1, [2, 3, 4, 5]]
24+
[a, b, rest, c]
25+
end
26+
27+
## assert
28+
class Object
29+
def test_nested_destructuring: -> [Integer, Integer, Integer]
30+
def test_nested_with_strings: -> [String, String, String]
31+
def test_deeper_nesting: -> [Integer, Integer, Integer, Integer]
32+
def test_nested_with_rest: -> [Integer, Integer, Array[Integer]]
33+
def test_nested_with_rest_and_rights: -> [Integer, Integer, Array[Integer], Integer]
34+
end

0 commit comments

Comments
 (0)