From 76ca5e2b230f897c4a41ad025d9d4a4e52db64eb Mon Sep 17 00:00:00 2001 From: Takumi Shotoku Date: Sun, 2 Nov 2025 21:06:17 +0900 Subject: [PATCH] Add support for nested multi-target nodes in destructuring assignment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- lib/typeprof/core/ast.rb | 2 ++ lib/typeprof/core/ast/misc.rb | 57 +++++++++++++++++++++++++++++++ lib/typeprof/core/graph/box.rb | 2 +- lib/typeprof/core/type.rb | 3 +- scenario/variable/masgn_nested.rb | 34 ++++++++++++++++++ 5 files changed, 96 insertions(+), 2 deletions(-) create mode 100644 scenario/variable/masgn_nested.rb diff --git a/lib/typeprof/core/ast.rb b/lib/typeprof/core/ast.rb index 039851bc..18ee88fa 100644 --- a/lib/typeprof/core/ast.rb +++ b/lib/typeprof/core/ast.rb @@ -270,6 +270,8 @@ def self.create_target_node(raw_node, lenv) IndexWriteNode.new(raw_node, dummy_node, lenv) when :call_target_node CallWriteNode.new(raw_node, dummy_node, lenv) + when :multi_target_node + MultiTargetNode.new(raw_node, dummy_node, lenv) else pp raw_node raise "not supported yet: #{ raw_node.type }" diff --git a/lib/typeprof/core/ast/misc.rb b/lib/typeprof/core/ast/misc.rb index 2a226100..bcd9cc31 100644 --- a/lib/typeprof/core/ast/misc.rb +++ b/lib/typeprof/core/ast/misc.rb @@ -139,6 +139,63 @@ def retrieve_at(pos, &blk) end end + class MultiTargetNode < Node + def initialize(raw_node, rhs, lenv) + super(raw_node, lenv) + @rhs = rhs + @lefts = raw_node.lefts.map do |raw_lhs| + AST.create_target_node(raw_lhs, lenv) + end + if raw_node.rest + case raw_node.rest.type + when :splat_node + if raw_node.rest.expression + @rest = AST.create_target_node(raw_node.rest.expression, lenv) + end + when :implicit_rest_node + # no assignment target + else + raise "unexpected rest node in multi_target: #{raw_node.rest.type}" + end + end + @rights = raw_node.rights.map do |raw_rhs| + AST.create_target_node(raw_rhs, lenv) + end + end + + attr_reader :rhs, :lefts, :rest, :rights + + def subnodes = { rhs:, lefts:, rest:, rights: } + + def install0(genv) + # The rhs should be installed by the parent MultiWriteNode + # Here we set up the multi-assignment box for nested destructuring + value = @rhs.install(genv) + + @lefts.each {|lhs| lhs.install(genv) } + @lefts.each {|lhs| lhs.rhs.ret || raise(lhs.rhs.inspect) } + lefts = @lefts.map {|lhs| lhs.rhs.ret } + + rest_elem = nil + if @rest + rest_elem = Vertex.new(self) + @rest.install(genv) + @rest.rhs.ret || raise(@rest.rhs.inspect) + @changes.add_edge(genv, Source.new(Type::Instance.new(genv, genv.mod_ary, [rest_elem])), @rest.rhs.ret) + end + + rights = nil + if @rights && !@rights.empty? + @rights.each {|rhs| rhs.install(genv) } + @rights.each {|rhs| rhs.rhs.ret || raise(rhs.rhs.inspect) } + rights = @rights.map {|rhs| rhs.rhs.ret } + end + + box = @changes.add_masgn_box(genv, value, lefts, rest_elem, rights) + box.ret + end + end + class MatchWriteNode < Node def initialize(raw_node, lenv) super(raw_node, lenv) diff --git a/lib/typeprof/core/graph/box.rb b/lib/typeprof/core/graph/box.rb index eeeb7734..103bb0d9 100644 --- a/lib/typeprof/core/graph/box.rb +++ b/lib/typeprof/core/graph/box.rb @@ -1073,7 +1073,7 @@ def run0(genv, changes) else if @lefts.size >= 1 edges << [Source.new(ty), @lefts[0]] - elsif @rights.size >= 1 + elsif @rights && @rights.size >= 1 edges << [Source.new(ty), @rights[0]] else edges << [Source.new(ty), @rest_elem] diff --git a/lib/typeprof/core/type.rb b/lib/typeprof/core/type.rb index 24a07d01..960840d3 100644 --- a/lib/typeprof/core/type.rb +++ b/lib/typeprof/core/type.rb @@ -112,6 +112,7 @@ def splat_assign(genv, lefts, rest_elem, rights) edges = [] state = :left j = nil + rights_size = rights ? rights.size : 0 @elems.each_with_index do |elem, i| case state when :left @@ -123,7 +124,7 @@ def splat_assign(genv, lefts, rest_elem, rights) redo end when :rest - if @elems.size - i > rights.size + if @elems.size - i > rights_size edges << [elem, rest_elem] else state = :right diff --git a/scenario/variable/masgn_nested.rb b/scenario/variable/masgn_nested.rb new file mode 100644 index 00000000..ae81c7d7 --- /dev/null +++ b/scenario/variable/masgn_nested.rb @@ -0,0 +1,34 @@ +## update +def test_nested_destructuring + a, (b, c) = [1, [2, 3]] + [a, b, c] +end + +def test_nested_with_strings + x, (y, z) = ["foo", ["bar", "baz"]] + [x, y, z] +end + +def test_deeper_nesting + a, (b, (c, d)) = [1, [2, [3, 4]]] + [a, b, c, d] +end + +def test_nested_with_rest + a, (b, *rest) = [1, [2, 3, 4]] + [a, b, rest] +end + +def test_nested_with_rest_and_rights + a, (b, *rest, c) = [1, [2, 3, 4, 5]] + [a, b, rest, c] +end + +## assert +class Object + def test_nested_destructuring: -> [Integer, Integer, Integer] + def test_nested_with_strings: -> [String, String, String] + def test_deeper_nesting: -> [Integer, Integer, Integer, Integer] + def test_nested_with_rest: -> [Integer, Integer, Array[Integer]] + def test_nested_with_rest_and_rights: -> [Integer, Integer, Array[Integer], Integer] +end