From 9343931352db2ad87b23bfd3f022ecd72f1692ba Mon Sep 17 00:00:00 2001 From: "MATSUMOTO, Katsuyoshi" Date: Tue, 25 Aug 2026 19:45:24 +0900 Subject: [PATCH 01/13] Extract compiler assembly emitter --- lib/caotral/compiler/emitter.rb | 16 ++ lib/caotral/compiler/generator.rb | 274 +++++++++++++++-------------- sig/caotral/compiler/emitter.rbs | 11 ++ sig/caotral/compiler/generator.rbs | 30 ++-- 4 files changed, 187 insertions(+), 144 deletions(-) create mode 100644 lib/caotral/compiler/emitter.rb create mode 100644 sig/caotral/compiler/emitter.rbs diff --git a/lib/caotral/compiler/emitter.rb b/lib/caotral/compiler/emitter.rb new file mode 100644 index 00000000..fbd44405 --- /dev/null +++ b/lib/caotral/compiler/emitter.rb @@ -0,0 +1,16 @@ +module Caotral + class Compiler + class Emitter + INDENT = " " * 2 + def initialize(io) = @io = io + + def instruction(operation, *operands) + ops = operands.empty? ? "" : " #{operands.join(", ")}" + @io.puts("#{INDENT}#{operation}#{ops}") + end + def label(name) = @io.puts("#{name}:") + def directive(row) = @io.puts(row) + def close = @io.close + end + end +end diff --git a/lib/caotral/compiler/generator.rb b/lib/caotral/compiler/generator.rb index b0d39cdb..c7d334e8 100644 --- a/lib/caotral/compiler/generator.rb +++ b/lib/caotral/compiler/generator.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require_relative "emitter" + module Caotral class Compiler class Generator @@ -16,25 +18,26 @@ def initialize(input:, output: File.basename(input, "*") + ".s", debug: false, s def compile_shared_option = %w(-shared -fPIC) def compile register_var_and_method(@ast) + @emitter = Caotral::Compiler::Emitter.new(File.open(@precompile, "w")) - output = File.open(@precompile, "w") # prologue - output.puts " .intel_syntax noprefix" + directive(".intel_syntax noprefix") if @defined_methods.empty? @main = true - output.puts " .globl main" - output.puts "main:" - output.puts " push rbp" - output.puts " mov rbp, rsp" - output.puts " sub rsp, #{@defined_variables.size * 8}" - to_asm(@ast, output) - epilogue(output) + directive(".globl main") + label("main") + instruction("push", "rbp") + instruction("mov", "rbp", "rsp") + instruction("sub", "rsp", @defined_variables.size * 8) + to_asm(@ast) + epilogue else - prologue_methods(output) - output.puts " .globl main" unless @shared - to_asm(@ast, output) + prologue_methods + directive(".globl main") unless @shared + to_asm(@ast) end - output.close + ensure + @emitter&.close end def register_var_and_method(node) @@ -53,92 +56,92 @@ def register_var_and_method(node) def already_build_methods? = @defined_methods.sort == @doned.to_a.sort - def epilogue(output) - output.puts " mov rsp, rbp" - output.puts " pop rbp" + def epilogue + instruction("mov", "rsp", "rbp") + instruction("pop", "rbp") unless @shared - output.puts " mov rdi, rax" - output.puts " mov rax, 0x3C" - output.puts " syscall" + instruction("mov", "rdi", "rax") + instruction("mov", "rax", "0x3C") + instruction("syscall") end - output.puts " ret" + instruction("ret") end - def prologue_methods(output) + def prologue_methods @defined_methods.each do |name| - output.puts ".globl #{name}" - output.puts ".type #{name}, @function" if shared + directive(".globl #{name}") + directive(".type #{name}, @function") if shared end nil end - def define_method_prologue(node, output) - output.puts " push rbp" - output.puts " mov rbp, rsp" + def define_method_prologue(node) + instruction("push", "rbp") + instruction("mov", "rbp", "rsp") unless @defined_variables.empty? - output.puts " sub rsp, #{lvar_offset(nil) * 8}" + instruction("sub", "rsp", lvar_offset(nil) * 8) _name, args, _block = node.children args.children.each_with_index do |_, i| - output.puts " mov [rbp-#{(i + 1) * 8}], #{REGISTER[i]}" + instruction("mov", "[rbp-#{(i + 1) * 8}]", REGISTER[i]) end end nil end - def method(method, node, output) - output.puts "#{method}:" - define_method_prologue(node, output) + def method(method, node) + label(method) + define_method_prologue(node) node.children.each do |child| next unless child.kind_of?(RubyVM::AbstractSyntaxTree::Node) - to_asm(child, output, true) + to_asm(child, true) end - output.puts " pop rax" - ret(output) + instruction("pop", "rax") + ret @doned << method nil end - def call_method(node, output, method_tree) - output.puts " mov rax, rsp" - output.puts " mov rdi, 16" - output.puts " cqo" - output.puts " idiv rdi" - output.puts " mov rax, 0" - output.puts " cmp rdi, 0" - output.puts " jne .Lprecall#{@seq}" - output.puts " push 0" - output.puts " mov rax, 1" - output.puts ".Lprecall#{@seq}:" - output.puts " push rax" + def call_method(node, method_tree) + instruction("mov", "rax", "rsp") + instruction("mov", "rdi", 16) + instruction("cqo") + instruction("idiv", "rdi") + instruction("mov", "rax", 0) + instruction("cmp", "rdi", 0) + instruction("jne", ".Lprecall#{@seq}") + instruction("push", 0) + instruction("mov", "rax", 1) + label(".Lprecall#{@seq}") + instruction("push", "rax") _, name, *args = node.children args.each_with_index do |arg, i| - to_asm(arg, output, method_tree) - output.puts " pop #{REGISTER[i]}" + to_asm(arg, method_tree) + instruction("pop", REGISTER[i]) end - output.puts " call #{name}" - output.puts " pop rdi" - output.puts " cmp rdi, 0" - output.puts " je .Lpostcall#{@seq}" - output.puts " pop rdi" - output.puts ".Lpostcall#{@seq}:" - output.puts " push rax" + instruction("call", name) + instruction("pop", "rdi") + instruction("cmp", "rdi", 0) + instruction("je", ".Lpostcall#{@seq}") + instruction("pop", "rdi") + label(".Lpostcall#{@seq}") + instruction("push", "rax") @seq += 1 nil end - def comp(op, output) - output.puts " cmp rax, rdi" - output.puts " #{op} al" - output.puts " movzb rax, al" - output.puts " push rax" + def comp(op) + instruction("cmp", "rax", "rdi") + instruction(op, "al") + instruction("movzb", "rax", "al") + instruction("push", "rax") nil end - def lvar(var, output) - output.puts " mov rax, rbp" - output.puts " sub rax, #{lvar_offset(var) * 8}" - output.puts " push rax" + def lvar(var) + instruction("mov", "rax", "rbp") + instruction("sub", "rax", lvar_offset(var) * 8) + instruction("push", "rax") nil end @@ -150,139 +153,144 @@ def lvar_offset(var) end end - def ret(output) - output.puts " mov rsp, rbp" - output.puts " pop rbp" - output.puts " ret" + def ret + instruction("mov", "rsp", "rbp") + instruction("pop", "rbp") + instruction("ret") end - def to_asm(node, output, method_tree = false) + def to_asm(node, method_tree = false) return unless node.kind_of?(RubyVM::AbstractSyntaxTree::Node) type = node.type center = case type when :LIT, :INTEGER - output.puts " push 0x#{node.children.last.to_s(16)}" + instruction("push", "0x#{node.children.last.to_s(16)}") return when :LIST, :BLOCK, :BEGIN - node.children.each { |n| to_asm(n, output, method_tree) } + node.children.each { |n| to_asm(n, method_tree) } return when :SCOPE node.children.each do |child| if already_build_methods? && !@main return if shared - output.puts "main:" - output.puts " push rbp" - output.puts " mov rbp, rsp" - output.puts " sub rsp, 0" - output.puts " push rax" + label("main") + instruction("push", "rbp") + instruction("mov", "rbp", "rsp") + instruction("sub", "rsp", 0) + instruction("push", "rax") @main = true end - to_asm(child, output) + to_asm(child) end return when :DEFN name, _ = node.children - method(name, node, output) + method(name, node) return when :LVAR return if method_tree name = node.children.last - lvar(name, output) + lvar(name) # lvar - output.puts " pop rax" - output.puts " mov rax, [rax]" - output.puts " push rax" + instruction("pop", "rax") + instruction("mov", "rax", "[rax]") + instruction("push", "rax") return when :LASGN name, right = node.children # rvar - lvar(name, output) - to_asm(right, output, method_tree) + lvar(name) + to_asm(right, method_tree) - output.puts " pop rdi" - output.puts " pop rax" - output.puts " mov [rax], rdi" - output.puts " push rdi" - output.puts " pop rax" + instruction("pop", "rdi") + instruction("pop", "rax") + instruction("mov", "[rax]", "rdi") + instruction("push", "rdi") + instruction("pop", "rax") return when :IF cond, tblock, fblock = node.children - to_asm(cond, output) - output.puts " pop rax" - output.puts " push rax" - output.puts " cmp rax, 0" + to_asm(cond) + instruction("pop", "rax") + instruction("push", "rax") + instruction("cmp", "rax", 0) if fblock - output.puts " je .Lelse#{@seq}" - to_asm(tblock, output, method_tree) - output.puts " pop rax" - output.puts " jmp .Lend#{@seq}" - output.puts ".Lelse#{@seq}:" - to_asm(fblock, output, method_tree) - output.puts " pop rax" - output.puts ".Lend#{@seq}:" + instruction("je", ".Lelse#{@seq}") + to_asm(tblock, method_tree) + instruction("pop", "rax") + instruction("jmp", ".Lend#{@seq}") + label(".Lelse#{@seq}") + to_asm(fblock, method_tree) + instruction("pop", "rax") + label(".Lend#{@seq}") else if method_tree - to_asm(tblock, output, method_tree) - ret(output) + to_asm(tblock, method_tree) + ret else - output.puts " je .Lend#{@seq}" - to_asm(tblock, output, method_tree) - output.puts ".Lend#{@seq}:" + instruction("je", ".Lend#{@seq}") + to_asm(tblock, method_tree) + label(".Lend#{@seq}") end end @seq += 1 return when :WHILE cond, tblock = node.children - output.puts ".Lbegin#{@seq}:" - to_asm(cond, output, method_tree) - output.puts " pop rax" - output.puts " push rax" - output.puts " cmp rax, 0" - output.puts " je .Lend#{@seq}" - to_asm(tblock, output, method_tree) - output.puts " jmp .Lbegin#{@seq}" - output.puts ".Lend#{@seq}:" + label(".Lbegin#{@seq}") + to_asm(cond, method_tree) + instruction("pop", "rax") + instruction("push", "rax") + instruction("cmp", "rax", 0) + instruction("je", ".Lend#{@seq}") + to_asm(tblock, method_tree) + instruction("jmp", ".Lbegin#{@seq}") + label(".Lend#{@seq}") @seq += 1 return when :OPCALL left, center, right = node.children - to_asm(left, output, method_tree) unless left.nil? + to_asm(left, method_tree) unless left.nil? if left.nil? - call_method(node, output, method_tree) + call_method(node, method_tree) else - to_asm(right, output, method_tree) - output.puts " pop rdi" + to_asm(right, method_tree) + instruction("pop", "rdi") end - output.puts " pop rax" + instruction("pop", "rax") center end case center when :+ - output.puts " add rax, rdi" - output.puts " push rax" + instruction("add", "rax", "rdi") + instruction("push", "rax") when :- - output.puts " sub rax, rdi" - output.puts " push rax" + instruction("sub", "rax", "rdi") + instruction("push", "rax") when :* - output.puts " imul rax, rdi" - output.puts " push rax" + instruction("imul", "rax", "rdi") + instruction("push", "rax") when :/ - output.puts " cqo" - output.puts " idiv rdi" - output.puts " push rax" + instruction("cqo") + instruction("idiv", "rdi") + instruction("push", "rax") when :== - comp("sete", output) + comp("sete") when :!= - comp("setne", output) + comp("setne") when :< - comp("setl", output) + comp("setl") when :<= - comp("setle", output) + comp("setle") end end + + private + def instruction(ope, *operands) = @emitter.instruction(ope, *operands) + def label(name) = @emitter.label(name) + def directive(row) = @emitter.directive(row) end end end diff --git a/sig/caotral/compiler/emitter.rbs b/sig/caotral/compiler/emitter.rbs new file mode 100644 index 00000000..f4509f1d --- /dev/null +++ b/sig/caotral/compiler/emitter.rbs @@ -0,0 +1,11 @@ +class Caotral::Compiler::Emitter + INDENT: String + + @io: IO + + def initialize: (IO io) -> void + def instruction: (String operation, *(String | Symbol | Integer) operands) -> void + def label: (String | Symbol name) -> void + def directive: (String row) -> void + def close: () -> void +end diff --git a/sig/caotral/compiler/generator.rbs b/sig/caotral/compiler/generator.rbs index 3eba8792..a095caad 100644 --- a/sig/caotral/compiler/generator.rbs +++ b/sig/caotral/compiler/generator.rbs @@ -1,14 +1,17 @@ class Caotral::Compiler::Generator REGISTER: Array[String] - # attr_reader + attr_accessor main: bool attr_reader precompile: String + attr_reader shared: bool @main: bool @debug: bool @doned: Set[Symbol] @defined_methods: Set[Symbol] @defined_variables: Set[Symbol] + @emitter: Caotral::Compiler::Emitter + @precompile: String @seq: Integer @shared: bool @ast: RubyVM::AbstractSyntaxTree::Node @@ -19,18 +22,23 @@ class Caotral::Compiler::Generator # instance private methods def already_build_methods?: -> bool - def call_method: (RubyVM::AbstractSyntaxTree::Node, File, bool) -> void + def call_method: (RubyVM::AbstractSyntaxTree::Node, bool) -> void + def comp: (String) -> void def compile: () -> void def compile_shared_option: () -> Array[String] - def define_method_prologue: (RubyVM::AbstractSyntaxTree::Node, File) -> void - def epilogue: (File) -> void - def lvar: (Symbol, File) -> void + def define_method_prologue: (RubyVM::AbstractSyntaxTree::Node) -> void + def epilogue: () -> void + def lvar: (Symbol) -> void def lvar_offset: (Symbol | nil) -> Integer - def method: (Symbol, RubyVM::AbstractSyntaxTree::Node, File) -> void - def prologue: (RubyVM::AbstractSyntaxTree::Node, File) -> void - def prologue_methods: (File) -> void + def method: (Symbol, RubyVM::AbstractSyntaxTree::Node) -> void + def prologue_methods: () -> void def register_var_and_method: (RubyVM::AbstractSyntaxTree::Node?) -> void - def ret: (File) -> void - def to_asm: (RubyVM::AbstractSyntaxTree::Node, File, ?bool) -> void - def variable_or_method?: (Symbol) -> bool + def ret: () -> void + def to_asm: (RubyVM::AbstractSyntaxTree::Node?, ?bool) -> void + + private + + def instruction: (String, *(String | Symbol | Integer)) -> void + def label: (String | Symbol) -> void + def directive: (String) -> void end From a68b086515578ceb4fc30a205d73d813b64d974f Mon Sep 17 00:00:00 2001 From: "MATSUMOTO, Katsuyoshi" Date: Tue, 25 Aug 2026 21:30:40 +0900 Subject: [PATCH 02/13] Extract compiler context --- lib/caotral/compiler/context.rb | 22 ++++++++++ lib/caotral/compiler/generator.rb | 66 +++++++++++++++--------------- sig/caotral/compiler/context.rbs | 20 +++++++++ sig/caotral/compiler/generator.rbs | 8 +--- 4 files changed, 77 insertions(+), 39 deletions(-) create mode 100644 lib/caotral/compiler/context.rb create mode 100644 sig/caotral/compiler/context.rbs diff --git a/lib/caotral/compiler/context.rb b/lib/caotral/compiler/context.rb new file mode 100644 index 00000000..0aee0ccb --- /dev/null +++ b/lib/caotral/compiler/context.rb @@ -0,0 +1,22 @@ +module Caotral + class Compiler + class Context + attr_reader :label_sequence, :discovered_methods, :local_variables + def initialize + @label_sequence = 0 + @entry_emitted = false + @emitted_methods = Set[] + @discovered_methods = Set[] + @local_variables = Set[] + end + + def entry_emitted? = @entry_emitted + def mark_entry_emitted = @entry_emitted = true + def mark_method_emitted(name) = @emitted_methods << name + def discover_method(name) = @discovered_methods << name + def register_local_variable(name) = @local_variables << name + def all_methods_emitted? = @discovered_methods == @emitted_methods + def increment_label_sequence = @label_sequence += 1 + end + end +end diff --git a/lib/caotral/compiler/generator.rb b/lib/caotral/compiler/generator.rb index c7d334e8..2f172063 100644 --- a/lib/caotral/compiler/generator.rb +++ b/lib/caotral/compiler/generator.rb @@ -1,17 +1,16 @@ # frozen_string_literal: true +require_relative "context" require_relative "emitter" module Caotral class Compiler class Generator REGISTER = %w(rdi rsi rdx rcx r8 r9) - attr_accessor :main attr_reader :precompile, :shared def initialize(input:, output: File.basename(input, "*") + ".s", debug: false, shared: false) @source, @precompile, @debug, @shared = input, output, debug, shared - @doned, @defined_methods, @defined_variables = Set.new, Set.new, Set.new - @seq, @main = 0, false + @context = Caotral::Compiler::Context.new @ast = RubyVM::AbstractSyntaxTree.parse_file(@source) end @@ -22,13 +21,13 @@ def compile # prologue directive(".intel_syntax noprefix") - if @defined_methods.empty? - @main = true + if @context.discovered_methods.empty? + @context.mark_entry_emitted directive(".globl main") label("main") instruction("push", "rbp") instruction("mov", "rbp", "rsp") - instruction("sub", "rsp", @defined_variables.size * 8) + instruction("sub", "rsp", @context.local_variables.size * 8) to_asm(@ast) epilogue else @@ -46,15 +45,15 @@ def register_var_and_method(node) variables, *_ = node.children case type when :SCOPE - variables.each { |v| @defined_variables << v } + variables.each { |v| @context.register_local_variable(v) } when :DEFN - @defined_methods << variables + @context.discover_method(variables) end node.children.each { |n| register_var_and_method(n) } nil end - def already_build_methods? = @defined_methods.sort == @doned.to_a.sort + def already_build_methods? = @context.all_methods_emitted? def epilogue instruction("mov", "rsp", "rbp") @@ -68,7 +67,7 @@ def epilogue end def prologue_methods - @defined_methods.each do |name| + @context.discovered_methods.each do |name| directive(".globl #{name}") directive(".type #{name}, @function") if shared end @@ -78,7 +77,7 @@ def prologue_methods def define_method_prologue(node) instruction("push", "rbp") instruction("mov", "rbp", "rsp") - unless @defined_variables.empty? + unless @context.local_variables.empty? instruction("sub", "rsp", lvar_offset(nil) * 8) _name, args, _block = node.children args.children.each_with_index do |_, i| @@ -97,7 +96,7 @@ def method(method, node) end instruction("pop", "rax") ret - @doned << method + @context.mark_method_emitted(method) nil end @@ -108,10 +107,10 @@ def call_method(node, method_tree) instruction("idiv", "rdi") instruction("mov", "rax", 0) instruction("cmp", "rdi", 0) - instruction("jne", ".Lprecall#{@seq}") + instruction("jne", ".Lprecall#{sequence}") instruction("push", 0) instruction("mov", "rax", 1) - label(".Lprecall#{@seq}") + label(".Lprecall#{sequence}") instruction("push", "rax") _, name, *args = node.children args.each_with_index do |arg, i| @@ -122,11 +121,11 @@ def call_method(node, method_tree) instruction("call", name) instruction("pop", "rdi") instruction("cmp", "rdi", 0) - instruction("je", ".Lpostcall#{@seq}") + instruction("je", ".Lpostcall#{sequence}") instruction("pop", "rdi") - label(".Lpostcall#{@seq}") + label(".Lpostcall#{sequence}") instruction("push", "rax") - @seq += 1 + @context.increment_label_sequence nil end @@ -146,8 +145,8 @@ def lvar(var) end def lvar_offset(var) - return @defined_variables.size if var.nil? - @defined_variables.find_index(var).then do |i| + return @context.local_variables.size if var.nil? + @context.local_variables.find_index(var).then do |i| raise "unknown local variable...: #{var}" if i.nil? i + 1 end @@ -171,14 +170,14 @@ def to_asm(node, method_tree = false) return when :SCOPE node.children.each do |child| - if already_build_methods? && !@main + if already_build_methods? && !@context.entry_emitted? return if shared label("main") instruction("push", "rbp") instruction("mov", "rbp", "rsp") instruction("sub", "rsp", 0) instruction("push", "rax") - @main = true + @context.mark_entry_emitted end to_asm(child) end @@ -216,38 +215,38 @@ def to_asm(node, method_tree = false) instruction("push", "rax") instruction("cmp", "rax", 0) if fblock - instruction("je", ".Lelse#{@seq}") + instruction("je", ".Lelse#{sequence}") to_asm(tblock, method_tree) instruction("pop", "rax") - instruction("jmp", ".Lend#{@seq}") - label(".Lelse#{@seq}") + instruction("jmp", ".Lend#{sequence}") + label(".Lelse#{sequence}") to_asm(fblock, method_tree) instruction("pop", "rax") - label(".Lend#{@seq}") + label(".Lend#{sequence}") else if method_tree to_asm(tblock, method_tree) ret else - instruction("je", ".Lend#{@seq}") + instruction("je", ".Lend#{sequence}") to_asm(tblock, method_tree) - label(".Lend#{@seq}") + label(".Lend#{sequence}") end end - @seq += 1 + @context.increment_label_sequence return when :WHILE cond, tblock = node.children - label(".Lbegin#{@seq}") + label(".Lbegin#{sequence}") to_asm(cond, method_tree) instruction("pop", "rax") instruction("push", "rax") instruction("cmp", "rax", 0) - instruction("je", ".Lend#{@seq}") + instruction("je", ".Lend#{sequence}") to_asm(tblock, method_tree) - instruction("jmp", ".Lbegin#{@seq}") - label(".Lend#{@seq}") - @seq += 1 + instruction("jmp", ".Lbegin#{sequence}") + label(".Lend#{sequence}") + @context.increment_label_sequence return when :OPCALL left, center, right = node.children @@ -291,6 +290,7 @@ def to_asm(node, method_tree = false) def instruction(ope, *operands) = @emitter.instruction(ope, *operands) def label(name) = @emitter.label(name) def directive(row) = @emitter.directive(row) + def sequence = @context.label_sequence end end end diff --git a/sig/caotral/compiler/context.rbs b/sig/caotral/compiler/context.rbs new file mode 100644 index 00000000..ef5fa594 --- /dev/null +++ b/sig/caotral/compiler/context.rbs @@ -0,0 +1,20 @@ +class Caotral::Compiler::Context + attr_reader label_sequence: Integer + attr_reader discovered_methods: Set[Symbol] + attr_reader local_variables: Set[Symbol] + + @label_sequence: Integer + @entry_emitted: bool + @emitted_methods: Set[Symbol] + @discovered_methods: Set[Symbol] + @local_variables: Set[Symbol] + + def initialize: () -> void + def entry_emitted?: () -> bool + def mark_entry_emitted: () -> void + def mark_method_emitted: (Symbol name) -> void + def discover_method: (Symbol name) -> void + def register_local_variable: (Symbol name) -> void + def all_methods_emitted?: () -> bool + def increment_label_sequence: () -> void +end diff --git a/sig/caotral/compiler/generator.rbs b/sig/caotral/compiler/generator.rbs index a095caad..35f7c189 100644 --- a/sig/caotral/compiler/generator.rbs +++ b/sig/caotral/compiler/generator.rbs @@ -1,18 +1,13 @@ class Caotral::Compiler::Generator REGISTER: Array[String] - attr_accessor main: bool attr_reader precompile: String attr_reader shared: bool - @main: bool + @context: Caotral::Compiler::Context @debug: bool - @doned: Set[Symbol] - @defined_methods: Set[Symbol] - @defined_variables: Set[Symbol] @emitter: Caotral::Compiler::Emitter @precompile: String - @seq: Integer @shared: bool @ast: RubyVM::AbstractSyntaxTree::Node @source: String @@ -41,4 +36,5 @@ class Caotral::Compiler::Generator def instruction: (String, *(String | Symbol | Integer)) -> void def label: (String | Symbol) -> void def directive: (String) -> void + def sequence: () -> Integer end From 5c6b07f734e37e4d179b340816fb64babb961c08 Mon Sep 17 00:00:00 2001 From: "MATSUMOTO, Katsuyoshi" Date: Tue, 25 Aug 2026 23:36:59 +0900 Subject: [PATCH 03/13] Extract compiler analyzer --- lib/caotral/compiler/analyzer.rb | 36 ++++++++++++++++++++++++++ lib/caotral/compiler/generator.rb | 19 ++------------ sample/method_and_variable.rb | 3 +++ sig/caotral/compiler/analyzer.rbs | 20 ++++++++++++++ sig/caotral/compiler/generator.rbs | 1 - test/caotral/compiler/analyzer_test.rb | 12 +++++++++ 6 files changed, 73 insertions(+), 18 deletions(-) create mode 100644 lib/caotral/compiler/analyzer.rb create mode 100644 sample/method_and_variable.rb create mode 100644 sig/caotral/compiler/analyzer.rbs create mode 100644 test/caotral/compiler/analyzer_test.rb diff --git a/lib/caotral/compiler/analyzer.rb b/lib/caotral/compiler/analyzer.rb new file mode 100644 index 00000000..2c8f5103 --- /dev/null +++ b/lib/caotral/compiler/analyzer.rb @@ -0,0 +1,36 @@ +require_relative "context" + +module Caotral + class Compiler + class Analyzer + def self.analyze(ast, context = Caotral::Compiler::Context.new) + new(ast, context).analyze + end + + def initialize(ast, context) + @ast = ast + @context = context + end + + def analyze + register_variables_and_methods(@ast) + @context + end + + private + def register_variables_and_methods(node) + return unless node.kind_of?(RubyVM::AbstractSyntaxTree::Node) + type = node.type + variables, *_ = node.children + case type + when :SCOPE + variables.each { |v| @context.register_local_variable(v) } + when :DEFN + @context.discover_method(variables) + end + node.children.each { |n| register_variables_and_methods(n) } + nil + end + end + end +end diff --git a/lib/caotral/compiler/generator.rb b/lib/caotral/compiler/generator.rb index 2f172063..67ccff7b 100644 --- a/lib/caotral/compiler/generator.rb +++ b/lib/caotral/compiler/generator.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require_relative "context" +require_relative "analyzer" require_relative "emitter" module Caotral @@ -10,13 +10,12 @@ class Generator attr_reader :precompile, :shared def initialize(input:, output: File.basename(input, "*") + ".s", debug: false, shared: false) @source, @precompile, @debug, @shared = input, output, debug, shared - @context = Caotral::Compiler::Context.new @ast = RubyVM::AbstractSyntaxTree.parse_file(@source) end def compile_shared_option = %w(-shared -fPIC) def compile - register_var_and_method(@ast) + @context = Caotral::Compiler::Analyzer.analyze(@ast) @emitter = Caotral::Compiler::Emitter.new(File.open(@precompile, "w")) # prologue @@ -39,20 +38,6 @@ def compile @emitter&.close end - def register_var_and_method(node) - return unless node.kind_of?(RubyVM::AbstractSyntaxTree::Node) - type = node.type - variables, *_ = node.children - case type - when :SCOPE - variables.each { |v| @context.register_local_variable(v) } - when :DEFN - @context.discover_method(variables) - end - node.children.each { |n| register_var_and_method(n) } - nil - end - def already_build_methods? = @context.all_methods_emitted? def epilogue diff --git a/sample/method_and_variable.rb b/sample/method_and_variable.rb new file mode 100644 index 00000000..9b48f458 --- /dev/null +++ b/sample/method_and_variable.rb @@ -0,0 +1,3 @@ +def foo = 10 +a = 32 +a + foo diff --git a/sig/caotral/compiler/analyzer.rbs b/sig/caotral/compiler/analyzer.rbs new file mode 100644 index 00000000..005a2481 --- /dev/null +++ b/sig/caotral/compiler/analyzer.rbs @@ -0,0 +1,20 @@ +class Caotral::Compiler::Analyzer + @ast: RubyVM::AbstractSyntaxTree::Node + @context: Caotral::Compiler::Context + + def self.analyze: ( + RubyVM::AbstractSyntaxTree::Node ast, + ?Caotral::Compiler::Context context + ) -> Caotral::Compiler::Context + + def initialize: ( + RubyVM::AbstractSyntaxTree::Node ast, + Caotral::Compiler::Context context + ) -> void + + def analyze: () -> Caotral::Compiler::Context + + private + + def register_variables_and_methods: (RubyVM::AbstractSyntaxTree::Node? node) -> void +end diff --git a/sig/caotral/compiler/generator.rbs b/sig/caotral/compiler/generator.rbs index 35f7c189..1ab9c80e 100644 --- a/sig/caotral/compiler/generator.rbs +++ b/sig/caotral/compiler/generator.rbs @@ -27,7 +27,6 @@ class Caotral::Compiler::Generator def lvar_offset: (Symbol | nil) -> Integer def method: (Symbol, RubyVM::AbstractSyntaxTree::Node) -> void def prologue_methods: () -> void - def register_var_and_method: (RubyVM::AbstractSyntaxTree::Node?) -> void def ret: () -> void def to_asm: (RubyVM::AbstractSyntaxTree::Node?, ?bool) -> void diff --git a/test/caotral/compiler/analyzer_test.rb b/test/caotral/compiler/analyzer_test.rb new file mode 100644 index 00000000..67098409 --- /dev/null +++ b/test/caotral/compiler/analyzer_test.rb @@ -0,0 +1,12 @@ +require_relative "../../test_suite" + +class Caotral::Compiler::AnalyzerTest < Test::Unit::TestCase + def test_analyze_methods_and_local_variables + code = File.read("sample/method_and_variable.rb") + ast = RubyVM::AbstractSyntaxTree.parse(code) + context = Caotral::Compiler::Analyzer.analyze(ast) + assert_equal(1, context.local_variables.size) + assert_equal(Set[:a], context.local_variables) + assert_equal(Set[:foo], context.discovered_methods) + end +end From c8584d4225ff6b2264c1d28e0d090e8a51631f5a Mon Sep 17 00:00:00 2001 From: "MATSUMOTO, Katsuyoshi" Date: Wed, 26 Aug 2026 00:39:56 +0900 Subject: [PATCH 04/13] Extract compiler variables context --- lib/caotral/compiler/context.rb | 9 ++++++--- lib/caotral/compiler/context/variables.rb | 11 +++++++++++ sig/caotral/compiler/context.rbs | 2 +- sig/caotral/compiler/context/variables.rbs | 8 ++++++++ 4 files changed, 26 insertions(+), 4 deletions(-) create mode 100644 lib/caotral/compiler/context/variables.rb create mode 100644 sig/caotral/compiler/context/variables.rbs diff --git a/lib/caotral/compiler/context.rb b/lib/caotral/compiler/context.rb index 0aee0ccb..7e87f475 100644 --- a/lib/caotral/compiler/context.rb +++ b/lib/caotral/compiler/context.rb @@ -1,22 +1,25 @@ +require_relative "context/variables" + module Caotral class Compiler class Context - attr_reader :label_sequence, :discovered_methods, :local_variables + attr_reader :discovered_methods, :label_sequence def initialize @label_sequence = 0 @entry_emitted = false @emitted_methods = Set[] @discovered_methods = Set[] - @local_variables = Set[] + @variables = Caotral::Compiler::Context::Variables.new end def entry_emitted? = @entry_emitted def mark_entry_emitted = @entry_emitted = true def mark_method_emitted(name) = @emitted_methods << name def discover_method(name) = @discovered_methods << name - def register_local_variable(name) = @local_variables << name + def register_local_variable(name) = @variables.register_local(name) def all_methods_emitted? = @discovered_methods == @emitted_methods def increment_label_sequence = @label_sequence += 1 + def local_variables = @variables.locals end end end diff --git a/lib/caotral/compiler/context/variables.rb b/lib/caotral/compiler/context/variables.rb new file mode 100644 index 00000000..6e78921c --- /dev/null +++ b/lib/caotral/compiler/context/variables.rb @@ -0,0 +1,11 @@ +module Caotral + class Compiler + class Context + class Variables + attr_reader :locals + def initialize = @locals = Set[] + def register_local(name) = @locals.add?(name) + end + end + end +end diff --git a/sig/caotral/compiler/context.rbs b/sig/caotral/compiler/context.rbs index ef5fa594..fa14130f 100644 --- a/sig/caotral/compiler/context.rbs +++ b/sig/caotral/compiler/context.rbs @@ -7,7 +7,7 @@ class Caotral::Compiler::Context @entry_emitted: bool @emitted_methods: Set[Symbol] @discovered_methods: Set[Symbol] - @local_variables: Set[Symbol] + @variables: Caotral::Compiler::Context::Variables def initialize: () -> void def entry_emitted?: () -> bool diff --git a/sig/caotral/compiler/context/variables.rbs b/sig/caotral/compiler/context/variables.rbs new file mode 100644 index 00000000..02d3fd3e --- /dev/null +++ b/sig/caotral/compiler/context/variables.rbs @@ -0,0 +1,8 @@ +class Caotral::Compiler::Context::Variables + attr_reader locals: Set[Symbol] + + @locals: Set[Symbol] + + def initialize: () -> void + def register_local: (Symbol name) -> void +end From ecee1d94bf035bcbf65ef976311d9198234d8ed0 Mon Sep 17 00:00:00 2001 From: "MATSUMOTO, Katsuyoshi" Date: Wed, 26 Aug 2026 01:10:05 +0900 Subject: [PATCH 05/13] Add compiler function context --- lib/caotral/compiler/context.rb | 1 + lib/caotral/compiler/context/function.rb | 20 +++++++++++++++++++ sig/caotral/compiler/context/function.rbs | 18 +++++++++++++++++ .../caotral/compiler/context/function_test.rb | 17 ++++++++++++++++ 4 files changed, 56 insertions(+) create mode 100644 lib/caotral/compiler/context/function.rb create mode 100644 sig/caotral/compiler/context/function.rbs create mode 100644 test/caotral/compiler/context/function_test.rb diff --git a/lib/caotral/compiler/context.rb b/lib/caotral/compiler/context.rb index 7e87f475..7d6b1b0b 100644 --- a/lib/caotral/compiler/context.rb +++ b/lib/caotral/compiler/context.rb @@ -1,3 +1,4 @@ +require_relative "context/function" require_relative "context/variables" module Caotral diff --git a/lib/caotral/compiler/context/function.rb b/lib/caotral/compiler/context/function.rb new file mode 100644 index 00000000..b8f6e178 --- /dev/null +++ b/lib/caotral/compiler/context/function.rb @@ -0,0 +1,20 @@ +require_relative "variables" + +module Caotral + class Compiler + class Context + class Function + attr_reader :name, :parameters, :body + def initialize(name:, parameters: [], locals: [], body:) + @name = name + @parameters = parameters + @variables = Caotral::Compiler::Context::Variables.new + locals.each { |name| @variables.register_local(name) } + @body = body + end + + def locals = @variables.locals + end + end + end +end diff --git a/sig/caotral/compiler/context/function.rbs b/sig/caotral/compiler/context/function.rbs new file mode 100644 index 00000000..aba3aa16 --- /dev/null +++ b/sig/caotral/compiler/context/function.rbs @@ -0,0 +1,18 @@ +class Caotral::Compiler::Context::Function + attr_reader name: Symbol? + attr_reader parameters: Array[Symbol] + attr_reader body: RubyVM::AbstractSyntaxTree::Node + attr_reader locals: Set[Symbol] + + @name: Symbol? + @parameters: Array[Symbol] + @variables: Caotral::Compiler::Context::Variables + @body: RubyVM::AbstractSyntaxTree::Node + + def initialize: ( + name: Symbol?, + ?parameters: Array[Symbol], + ?locals: Array[Symbol], + body: RubyVM::AbstractSyntaxTree::Node + ) -> void +end diff --git a/test/caotral/compiler/context/function_test.rb b/test/caotral/compiler/context/function_test.rb new file mode 100644 index 00000000..93df942d --- /dev/null +++ b/test/caotral/compiler/context/function_test.rb @@ -0,0 +1,17 @@ +require_relative "../../../test_suite" + +class Caotral::Compiler::Context::FunctionTest < Test::Unit::TestCase + def test_context_function + body = RubyVM::AbstractSyntaxTree.parse("") + function = Caotral::Compiler::Context::Function.new( + name: :foo, + parameters: %i(a b c), + locals: %i(z y z), + body: + ) + assert_equal(:foo, function.name) + assert_equal(Set[:z, :y], function.locals) + assert_equal([:a, :b, :c], function.parameters) + assert_equal(body, function.body) + end +end From 7062e588c6f8a98195919f74527e0c4887e86f4e Mon Sep 17 00:00:00 2001 From: "MATSUMOTO, Katsuyoshi" Date: Wed, 26 Aug 2026 11:01:14 +0900 Subject: [PATCH 06/13] Add compiler entry context --- lib/caotral/compiler/analyzer.rb | 7 +++++++ lib/caotral/compiler/context.rb | 4 +++- sig/caotral/compiler/analyzer.rbs | 1 + sig/caotral/compiler/context.rbs | 3 +++ test/caotral/compiler/analyzer_test.rb | 14 ++++++++++++-- 5 files changed, 26 insertions(+), 3 deletions(-) diff --git a/lib/caotral/compiler/analyzer.rb b/lib/caotral/compiler/analyzer.rb index 2c8f5103..4726e020 100644 --- a/lib/caotral/compiler/analyzer.rb +++ b/lib/caotral/compiler/analyzer.rb @@ -13,6 +13,7 @@ def initialize(ast, context) end def analyze + register_entry(@ast) register_variables_and_methods(@ast) @context end @@ -31,6 +32,12 @@ def register_variables_and_methods(node) node.children.each { |n| register_variables_and_methods(n) } nil end + + def register_entry(scope) + locals, _args, body = scope.children + entry = Caotral::Compiler::Context::Function.new(name: nil, locals:, body:) + @context.register_entry(entry) + end end end end diff --git a/lib/caotral/compiler/context.rb b/lib/caotral/compiler/context.rb index 7d6b1b0b..bee44db8 100644 --- a/lib/caotral/compiler/context.rb +++ b/lib/caotral/compiler/context.rb @@ -4,12 +4,13 @@ module Caotral class Compiler class Context - attr_reader :discovered_methods, :label_sequence + attr_reader :discovered_methods, :label_sequence, :entry def initialize @label_sequence = 0 @entry_emitted = false @emitted_methods = Set[] @discovered_methods = Set[] + @entry = nil @variables = Caotral::Compiler::Context::Variables.new end @@ -21,6 +22,7 @@ def register_local_variable(name) = @variables.register_local(name) def all_methods_emitted? = @discovered_methods == @emitted_methods def increment_label_sequence = @label_sequence += 1 def local_variables = @variables.locals + def register_entry(function) = @entry = function end end end diff --git a/sig/caotral/compiler/analyzer.rbs b/sig/caotral/compiler/analyzer.rbs index 005a2481..a63e8351 100644 --- a/sig/caotral/compiler/analyzer.rbs +++ b/sig/caotral/compiler/analyzer.rbs @@ -16,5 +16,6 @@ class Caotral::Compiler::Analyzer private + def register_entry: (RubyVM::AbstractSyntaxTree::Node scope) -> void def register_variables_and_methods: (RubyVM::AbstractSyntaxTree::Node? node) -> void end diff --git a/sig/caotral/compiler/context.rbs b/sig/caotral/compiler/context.rbs index fa14130f..1a0161f5 100644 --- a/sig/caotral/compiler/context.rbs +++ b/sig/caotral/compiler/context.rbs @@ -2,11 +2,13 @@ class Caotral::Compiler::Context attr_reader label_sequence: Integer attr_reader discovered_methods: Set[Symbol] attr_reader local_variables: Set[Symbol] + attr_reader entry: Caotral::Compiler::Context::Function? @label_sequence: Integer @entry_emitted: bool @emitted_methods: Set[Symbol] @discovered_methods: Set[Symbol] + @entry: Caotral::Compiler::Context::Function? @variables: Caotral::Compiler::Context::Variables def initialize: () -> void @@ -17,4 +19,5 @@ class Caotral::Compiler::Context def register_local_variable: (Symbol name) -> void def all_methods_emitted?: () -> bool def increment_label_sequence: () -> void + def register_entry: (Caotral::Compiler::Context::Function function) -> void end diff --git a/test/caotral/compiler/analyzer_test.rb b/test/caotral/compiler/analyzer_test.rb index 67098409..854e1aab 100644 --- a/test/caotral/compiler/analyzer_test.rb +++ b/test/caotral/compiler/analyzer_test.rb @@ -5,8 +5,18 @@ def test_analyze_methods_and_local_variables code = File.read("sample/method_and_variable.rb") ast = RubyVM::AbstractSyntaxTree.parse(code) context = Caotral::Compiler::Analyzer.analyze(ast) - assert_equal(1, context.local_variables.size) - assert_equal(Set[:a], context.local_variables) + entry = context.entry + last_node = ast.children.last + + assert_not_nil(entry) + assert_nil(entry.name) + assert_equal(1, entry.locals.size) + assert_equal(Set[:a], entry.locals) + assert_equal(last_node.type, entry.body.type) + assert_equal(last_node.first_lineno, entry.body.first_lineno) + assert_equal(last_node.first_column, entry.body.first_column) + assert_equal(last_node.last_lineno, entry.body.last_lineno) + assert_equal(last_node.last_column, entry.body.last_column) assert_equal(Set[:foo], context.discovered_methods) end end From 8c789cfad74a3612fb9ea78bd4a5038f9416e73b Mon Sep 17 00:00:00 2001 From: "MATSUMOTO, Katsuyoshi" Date: Wed, 26 Aug 2026 11:51:58 +0900 Subject: [PATCH 07/13] Track compiler functions --- lib/caotral/compiler/analyzer.rb | 11 +++++++++++ lib/caotral/compiler/context.rb | 4 +++- sample/args_and_local_variables.rb | 6 ++++++ sig/caotral/compiler/analyzer.rbs | 6 ++++++ sig/caotral/compiler/context.rbs | 3 +++ test/caotral/compiler/analyzer_test.rb | 12 ++++++++++++ 6 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 sample/args_and_local_variables.rb diff --git a/lib/caotral/compiler/analyzer.rb b/lib/caotral/compiler/analyzer.rb index 4726e020..dde3624b 100644 --- a/lib/caotral/compiler/analyzer.rb +++ b/lib/caotral/compiler/analyzer.rb @@ -28,6 +28,8 @@ def register_variables_and_methods(node) variables.each { |v| @context.register_local_variable(v) } when :DEFN @context.discover_method(variables) + function = Caotral::Compiler::Context::Function.new(**analyze_function_scope(node)) + @context.register_function(function) end node.children.each { |n| register_variables_and_methods(n) } nil @@ -38,6 +40,15 @@ def register_entry(scope) entry = Caotral::Compiler::Context::Function.new(name: nil, locals:, body:) @context.register_entry(entry) end + + def analyze_function_scope(node) + name, function_scope = node.children + locals, args, body = function_scope.children + parameter_count = args.children.first + parameters = locals.take(parameter_count) + locals = locals.drop(parameter_count) + { name:, parameters:, locals:, body: } + end end end end diff --git a/lib/caotral/compiler/context.rb b/lib/caotral/compiler/context.rb index bee44db8..6dcec38c 100644 --- a/lib/caotral/compiler/context.rb +++ b/lib/caotral/compiler/context.rb @@ -4,7 +4,7 @@ module Caotral class Compiler class Context - attr_reader :discovered_methods, :label_sequence, :entry + attr_reader :discovered_methods, :label_sequence, :entry, :functions def initialize @label_sequence = 0 @entry_emitted = false @@ -12,6 +12,7 @@ def initialize @discovered_methods = Set[] @entry = nil @variables = Caotral::Compiler::Context::Variables.new + @functions = {} end def entry_emitted? = @entry_emitted @@ -23,6 +24,7 @@ def all_methods_emitted? = @discovered_methods == @emitted_methods def increment_label_sequence = @label_sequence += 1 def local_variables = @variables.locals def register_entry(function) = @entry = function + def register_function(function) = @functions[function.name] = function end end end diff --git a/sample/args_and_local_variables.rb b/sample/args_and_local_variables.rb new file mode 100644 index 00000000..75912aff --- /dev/null +++ b/sample/args_and_local_variables.rb @@ -0,0 +1,6 @@ +def foo(a, b) + c = a + b + c +end + +foo(1, 2) diff --git a/sig/caotral/compiler/analyzer.rbs b/sig/caotral/compiler/analyzer.rbs index a63e8351..3ed495e9 100644 --- a/sig/caotral/compiler/analyzer.rbs +++ b/sig/caotral/compiler/analyzer.rbs @@ -18,4 +18,10 @@ class Caotral::Compiler::Analyzer def register_entry: (RubyVM::AbstractSyntaxTree::Node scope) -> void def register_variables_and_methods: (RubyVM::AbstractSyntaxTree::Node? node) -> void + def analyze_function_scope: (RubyVM::AbstractSyntaxTree::Node node) -> { + name: Symbol, + parameters: Array[Symbol], + locals: Array[Symbol], + body: RubyVM::AbstractSyntaxTree::Node + } end diff --git a/sig/caotral/compiler/context.rbs b/sig/caotral/compiler/context.rbs index 1a0161f5..c453fb38 100644 --- a/sig/caotral/compiler/context.rbs +++ b/sig/caotral/compiler/context.rbs @@ -3,6 +3,7 @@ class Caotral::Compiler::Context attr_reader discovered_methods: Set[Symbol] attr_reader local_variables: Set[Symbol] attr_reader entry: Caotral::Compiler::Context::Function? + attr_reader functions: Hash[Symbol, Caotral::Compiler::Context::Function] @label_sequence: Integer @entry_emitted: bool @@ -10,6 +11,7 @@ class Caotral::Compiler::Context @discovered_methods: Set[Symbol] @entry: Caotral::Compiler::Context::Function? @variables: Caotral::Compiler::Context::Variables + @functions: Hash[Symbol, Caotral::Compiler::Context::Function] def initialize: () -> void def entry_emitted?: () -> bool @@ -20,4 +22,5 @@ class Caotral::Compiler::Context def all_methods_emitted?: () -> bool def increment_label_sequence: () -> void def register_entry: (Caotral::Compiler::Context::Function function) -> void + def register_function: (Caotral::Compiler::Context::Function function) -> void end diff --git a/test/caotral/compiler/analyzer_test.rb b/test/caotral/compiler/analyzer_test.rb index 854e1aab..de500c58 100644 --- a/test/caotral/compiler/analyzer_test.rb +++ b/test/caotral/compiler/analyzer_test.rb @@ -17,6 +17,18 @@ def test_analyze_methods_and_local_variables assert_equal(last_node.first_column, entry.body.first_column) assert_equal(last_node.last_lineno, entry.body.last_lineno) assert_equal(last_node.last_column, entry.body.last_column) + assert_equal(:foo, context.functions[:foo].name) + end + + def test_analyze_method_with_parameters_and_local_variables + ast = RubyVM::AbstractSyntaxTree.parse_file("sample/args_and_local_variables.rb") + context = Caotral::Compiler::Analyzer.analyze(ast) + functions = context.functions + foo = functions[:foo] + + assert_equal(:foo, foo.name) + assert_equal([:a, :b], foo.parameters) + assert_equal(Set[:c], foo.locals) assert_equal(Set[:foo], context.discovered_methods) end end From 67f5e5ca897218b4c6fe99d48a4d1860bbaf26a5 Mon Sep 17 00:00:00 2001 From: "MATSUMOTO, Katsuyoshi" Date: Wed, 26 Aug 2026 12:13:51 +0900 Subject: [PATCH 08/13] Generate methods from compiler functions --- lib/caotral/compiler/generator.rb | 26 ++++++++++++-------------- sig/caotral/compiler/generator.rbs | 15 ++++++--------- test/caotral/compiler/analyzer_test.rb | 3 +-- 3 files changed, 19 insertions(+), 25 deletions(-) diff --git a/lib/caotral/compiler/generator.rb b/lib/caotral/compiler/generator.rb index 67ccff7b..bee42aaf 100644 --- a/lib/caotral/compiler/generator.rb +++ b/lib/caotral/compiler/generator.rb @@ -13,7 +13,6 @@ def initialize(input:, output: File.basename(input, "*") + ".s", debug: false, s @ast = RubyVM::AbstractSyntaxTree.parse_file(@source) end - def compile_shared_option = %w(-shared -fPIC) def compile @context = Caotral::Compiler::Analyzer.analyze(@ast) @emitter = Caotral::Compiler::Emitter.new(File.open(@precompile, "w")) @@ -38,6 +37,8 @@ def compile @emitter&.close end + private + def compile_shared_option = %w(-shared -fPIC) def already_build_methods? = @context.all_methods_emitted? def epilogue @@ -59,29 +60,27 @@ def prologue_methods nil end - def define_method_prologue(node) + def define_method_prologue(function) instruction("push", "rbp") instruction("mov", "rbp", "rsp") unless @context.local_variables.empty? instruction("sub", "rsp", lvar_offset(nil) * 8) - _name, args, _block = node.children - args.children.each_with_index do |_, i| + function.parameters.each_with_index do |_, i| instruction("mov", "[rbp-#{(i + 1) * 8}]", REGISTER[i]) end end nil end - def method(method, node) - label(method) - define_method_prologue(node) - node.children.each do |child| - next unless child.kind_of?(RubyVM::AbstractSyntaxTree::Node) - to_asm(child, true) - end + def parse_method(name, function) + label(name) + define_method_prologue(function) + + to_asm(function.body, true) + instruction("pop", "rax") ret - @context.mark_method_emitted(method) + @context.mark_method_emitted(name) nil end @@ -169,7 +168,7 @@ def to_asm(node, method_tree = false) return when :DEFN name, _ = node.children - method(name, node) + parse_method(name, @context.functions.fetch(name)) return when :LVAR return if method_tree @@ -271,7 +270,6 @@ def to_asm(node, method_tree = false) end end - private def instruction(ope, *operands) = @emitter.instruction(ope, *operands) def label(name) = @emitter.label(name) def directive(row) = @emitter.directive(row) diff --git a/sig/caotral/compiler/generator.rbs b/sig/caotral/compiler/generator.rbs index 1ab9c80e..a35a70d6 100644 --- a/sig/caotral/compiler/generator.rbs +++ b/sig/caotral/compiler/generator.rbs @@ -12,26 +12,23 @@ class Caotral::Compiler::Generator @ast: RubyVM::AbstractSyntaxTree::Node @source: String - # class methods def initialize: (input: String, ?output: String, ?debug: bool, ?shared: bool) -> void + def compile: () -> void + + private - # instance private methods + def compile_shared_option: () -> Array[String] def already_build_methods?: -> bool def call_method: (RubyVM::AbstractSyntaxTree::Node, bool) -> void def comp: (String) -> void - def compile: () -> void - def compile_shared_option: () -> Array[String] - def define_method_prologue: (RubyVM::AbstractSyntaxTree::Node) -> void + def define_method_prologue: (Caotral::Compiler::Context::Function) -> void def epilogue: () -> void def lvar: (Symbol) -> void def lvar_offset: (Symbol | nil) -> Integer - def method: (Symbol, RubyVM::AbstractSyntaxTree::Node) -> void + def parse_method: (Symbol, Caotral::Compiler::Context::Function) -> void def prologue_methods: () -> void def ret: () -> void def to_asm: (RubyVM::AbstractSyntaxTree::Node?, ?bool) -> void - - private - def instruction: (String, *(String | Symbol | Integer)) -> void def label: (String | Symbol) -> void def directive: (String) -> void diff --git a/test/caotral/compiler/analyzer_test.rb b/test/caotral/compiler/analyzer_test.rb index de500c58..67d8a4d0 100644 --- a/test/caotral/compiler/analyzer_test.rb +++ b/test/caotral/compiler/analyzer_test.rb @@ -2,8 +2,7 @@ class Caotral::Compiler::AnalyzerTest < Test::Unit::TestCase def test_analyze_methods_and_local_variables - code = File.read("sample/method_and_variable.rb") - ast = RubyVM::AbstractSyntaxTree.parse(code) + ast = RubyVM::AbstractSyntaxTree.parse_file("sample/method_and_variable.rb") context = Caotral::Compiler::Analyzer.analyze(ast) entry = context.entry last_node = ast.children.last From 79656d54aa60b8565fe88e6935078f0b8a113bec Mon Sep 17 00:00:00 2001 From: "MATSUMOTO, Katsuyoshi" Date: Wed, 26 Aug 2026 13:27:09 +0900 Subject: [PATCH 09/13] Support method-local variables --- lib/caotral/compiler/context/function.rb | 1 + lib/caotral/compiler/generator.rb | 62 +++++++++---------- sample/args_and_local_variables.rb | 2 - sig/caotral/compiler/context/function.rbs | 1 + sig/caotral/compiler/generator.rbs | 8 +-- .../caotral/compiler/context/function_test.rb | 1 + test/caotral/compiler_test.rb | 51 ++++++++------- 7 files changed, 68 insertions(+), 58 deletions(-) diff --git a/lib/caotral/compiler/context/function.rb b/lib/caotral/compiler/context/function.rb index b8f6e178..d8e46cae 100644 --- a/lib/caotral/compiler/context/function.rb +++ b/lib/caotral/compiler/context/function.rb @@ -14,6 +14,7 @@ def initialize(name:, parameters: [], locals: [], body:) end def locals = @variables.locals + def variables = parameters + locals.to_a end end end diff --git a/lib/caotral/compiler/generator.rb b/lib/caotral/compiler/generator.rb index bee42aaf..deea7640 100644 --- a/lib/caotral/compiler/generator.rb +++ b/lib/caotral/compiler/generator.rb @@ -16,6 +16,7 @@ def initialize(input:, output: File.basename(input, "*") + ".s", debug: false, s def compile @context = Caotral::Compiler::Analyzer.analyze(@ast) @emitter = Caotral::Compiler::Emitter.new(File.open(@precompile, "w")) + entry = @context.entry # prologue directive(".intel_syntax noprefix") @@ -25,13 +26,13 @@ def compile label("main") instruction("push", "rbp") instruction("mov", "rbp", "rsp") - instruction("sub", "rsp", @context.local_variables.size * 8) - to_asm(@ast) + instruction("sub", "rsp", entry.variables.size * 8) + to_asm(@ast, entry) epilogue else prologue_methods directive(".globl main") unless @shared - to_asm(@ast) + to_asm(@ast, entry) end ensure @emitter&.close @@ -63,8 +64,8 @@ def prologue_methods def define_method_prologue(function) instruction("push", "rbp") instruction("mov", "rbp", "rsp") - unless @context.local_variables.empty? - instruction("sub", "rsp", lvar_offset(nil) * 8) + unless function.variables.empty? + instruction("sub", "rsp", lvar_offset(nil, function) * 8) function.parameters.each_with_index do |_, i| instruction("mov", "[rbp-#{(i + 1) * 8}]", REGISTER[i]) end @@ -76,7 +77,7 @@ def parse_method(name, function) label(name) define_method_prologue(function) - to_asm(function.body, true) + to_asm(function.body, function) instruction("pop", "rax") ret @@ -84,7 +85,7 @@ def parse_method(name, function) nil end - def call_method(node, method_tree) + def call_method(node, function) instruction("mov", "rax", "rsp") instruction("mov", "rdi", 16) instruction("cqo") @@ -98,7 +99,7 @@ def call_method(node, method_tree) instruction("push", "rax") _, name, *args = node.children args.each_with_index do |arg, i| - to_asm(arg, method_tree) + to_asm(arg, function) instruction("pop", REGISTER[i]) end @@ -121,16 +122,16 @@ def comp(op) nil end - def lvar(var) + def lvar(var, function) instruction("mov", "rax", "rbp") - instruction("sub", "rax", lvar_offset(var) * 8) + instruction("sub", "rax", lvar_offset(var, function) * 8) instruction("push", "rax") nil end - def lvar_offset(var) - return @context.local_variables.size if var.nil? - @context.local_variables.find_index(var).then do |i| + def lvar_offset(var, function) + return function.variables.size if var.nil? + function.variables.find_index(var).then do |i| raise "unknown local variable...: #{var}" if i.nil? i + 1 end @@ -142,7 +143,7 @@ def ret instruction("ret") end - def to_asm(node, method_tree = false) + def to_asm(node, function) return unless node.kind_of?(RubyVM::AbstractSyntaxTree::Node) type = node.type center = case type @@ -150,7 +151,7 @@ def to_asm(node, method_tree = false) instruction("push", "0x#{node.children.last.to_s(16)}") return when :LIST, :BLOCK, :BEGIN - node.children.each { |n| to_asm(n, method_tree) } + node.children.each { |n| to_asm(n, function) } return when :SCOPE node.children.each do |child| @@ -163,7 +164,7 @@ def to_asm(node, method_tree = false) instruction("push", "rax") @context.mark_entry_emitted end - to_asm(child) + to_asm(child, function) end return when :DEFN @@ -171,9 +172,8 @@ def to_asm(node, method_tree = false) parse_method(name, @context.functions.fetch(name)) return when :LVAR - return if method_tree name = node.children.last - lvar(name) + lvar(name, function) # lvar instruction("pop", "rax") instruction("mov", "rax", "[rax]") @@ -183,8 +183,8 @@ def to_asm(node, method_tree = false) name, right = node.children # rvar - lvar(name) - to_asm(right, method_tree) + lvar(name, function) + to_asm(right, function) instruction("pop", "rdi") instruction("pop", "rax") @@ -194,26 +194,26 @@ def to_asm(node, method_tree = false) return when :IF cond, tblock, fblock = node.children - to_asm(cond) + to_asm(cond, function) instruction("pop", "rax") instruction("push", "rax") instruction("cmp", "rax", 0) if fblock instruction("je", ".Lelse#{sequence}") - to_asm(tblock, method_tree) + to_asm(tblock, function) instruction("pop", "rax") instruction("jmp", ".Lend#{sequence}") label(".Lelse#{sequence}") - to_asm(fblock, method_tree) + to_asm(fblock, function) instruction("pop", "rax") label(".Lend#{sequence}") else - if method_tree - to_asm(tblock, method_tree) + if function&.name + to_asm(tblock, function) ret else instruction("je", ".Lend#{sequence}") - to_asm(tblock, method_tree) + to_asm(tblock, function) label(".Lend#{sequence}") end end @@ -222,23 +222,23 @@ def to_asm(node, method_tree = false) when :WHILE cond, tblock = node.children label(".Lbegin#{sequence}") - to_asm(cond, method_tree) + to_asm(cond, function) instruction("pop", "rax") instruction("push", "rax") instruction("cmp", "rax", 0) instruction("je", ".Lend#{sequence}") - to_asm(tblock, method_tree) + to_asm(tblock, function) instruction("jmp", ".Lbegin#{sequence}") label(".Lend#{sequence}") @context.increment_label_sequence return when :OPCALL left, center, right = node.children - to_asm(left, method_tree) unless left.nil? + to_asm(left, function) unless left.nil? if left.nil? - call_method(node, method_tree) + call_method(node, function) else - to_asm(right, method_tree) + to_asm(right, function) instruction("pop", "rdi") end instruction("pop", "rax") diff --git a/sample/args_and_local_variables.rb b/sample/args_and_local_variables.rb index 75912aff..f094a72f 100644 --- a/sample/args_and_local_variables.rb +++ b/sample/args_and_local_variables.rb @@ -2,5 +2,3 @@ def foo(a, b) c = a + b c end - -foo(1, 2) diff --git a/sig/caotral/compiler/context/function.rbs b/sig/caotral/compiler/context/function.rbs index aba3aa16..2a054b87 100644 --- a/sig/caotral/compiler/context/function.rbs +++ b/sig/caotral/compiler/context/function.rbs @@ -15,4 +15,5 @@ class Caotral::Compiler::Context::Function ?locals: Array[Symbol], body: RubyVM::AbstractSyntaxTree::Node ) -> void + def variables: () -> Array[Symbol] end diff --git a/sig/caotral/compiler/generator.rbs b/sig/caotral/compiler/generator.rbs index a35a70d6..ce2ca8c1 100644 --- a/sig/caotral/compiler/generator.rbs +++ b/sig/caotral/compiler/generator.rbs @@ -19,16 +19,16 @@ class Caotral::Compiler::Generator def compile_shared_option: () -> Array[String] def already_build_methods?: -> bool - def call_method: (RubyVM::AbstractSyntaxTree::Node, bool) -> void + def call_method: (RubyVM::AbstractSyntaxTree::Node, Caotral::Compiler::Context::Function) -> void def comp: (String) -> void def define_method_prologue: (Caotral::Compiler::Context::Function) -> void def epilogue: () -> void - def lvar: (Symbol) -> void - def lvar_offset: (Symbol | nil) -> Integer + def lvar: (Symbol, Caotral::Compiler::Context::Function) -> void + def lvar_offset: (Symbol?, Caotral::Compiler::Context::Function) -> Integer def parse_method: (Symbol, Caotral::Compiler::Context::Function) -> void def prologue_methods: () -> void def ret: () -> void - def to_asm: (RubyVM::AbstractSyntaxTree::Node?, ?bool) -> void + def to_asm: (RubyVM::AbstractSyntaxTree::Node?, Caotral::Compiler::Context::Function) -> void def instruction: (String, *(String | Symbol | Integer)) -> void def label: (String | Symbol) -> void def directive: (String) -> void diff --git a/test/caotral/compiler/context/function_test.rb b/test/caotral/compiler/context/function_test.rb index 93df942d..b35ee9fe 100644 --- a/test/caotral/compiler/context/function_test.rb +++ b/test/caotral/compiler/context/function_test.rb @@ -13,5 +13,6 @@ def test_context_function assert_equal(Set[:z, :y], function.locals) assert_equal([:a, :b, :c], function.parameters) assert_equal(body, function.body) + assert_equal([:a, :b, :c, :z, :y], function.variables) end end diff --git a/test/caotral/compiler_test.rb b/test/caotral/compiler_test.rb index 54100a91..d4995572 100644 --- a/test/caotral/compiler_test.rb +++ b/test/caotral/compiler_test.rb @@ -1,17 +1,20 @@ -require "caotral" -require "test/unit" +require "fiddle" +require_relative "../test_suite" class Caotral::CompilerTest < Test::Unit::TestCase - def setup = @generated = ["tmp.s", "tmp.o"] - def teardown - File.delete("tmp") if File.exist?("tmp") - @generated.map { File.delete(_1) if File.exist?(_1) } + include CommonSetupHelper + include TestProcessHelper + + def setup + super + @generated.concat(["tmp.s", "tmp.o", "tmp"]) + @output = "./tmp" end def test_sample_plus @file = "sample/plus.rb" @caotral = Caotral.compile!(input: @file, assembler: "self", linker: "self") - File.chmod(755, "tmp") - IO.popen("./tmp").close + File.chmod(755, @output) + IO.popen(@output).close exit_code, handle_code = check_process($?.to_i) assert_equal(9, exit_code) assert_equal(0, handle_code) @@ -20,8 +23,8 @@ def test_sample_plus def test_sample_variable @file = "sample/variable.rb" @caotral = Caotral.compile!(input: @file, assembler: "self", linker: "self") - File.chmod(755, "tmp") - IO.popen("./tmp").close + File.chmod(755, @output) + IO.popen(@output).close exit_code, handle_code = check_process($?.to_i) assert_equal(1, exit_code) assert_equal(0, handle_code) @@ -30,7 +33,7 @@ def test_sample_variable def test_sample_if @file = "sample/if.rb" @caotral = Caotral.compile!(input: @file) - IO.popen("./tmp").close + IO.popen(@output).close exit_code, handle_code = check_process($?.to_i) assert_equal(1, exit_code) assert_equal(0, handle_code) @@ -39,7 +42,7 @@ def test_sample_if def test_sample_else @file = "sample/else.rb" @caotral = Caotral.compile!(input: @file) - IO.popen("./tmp").close + IO.popen(@output).close exit_code, handle_code = check_process($?.to_i) assert_equal(2, exit_code) assert_equal(0, handle_code) @@ -48,7 +51,7 @@ def test_sample_else def test_sample_while @file = "sample/while.rb" @caotral = Caotral.compile!(input: @file) - IO.popen("./tmp").close + IO.popen(@output).close exit_code, handle_code = check_process($?.to_i) assert_equal(55, exit_code) assert_equal(0, handle_code) @@ -57,17 +60,23 @@ def test_sample_while def test_sample_call_method @generated = ["libtmp.so", "libtmp.so.o", "libtmp.so.s"] @file = "sample/method.rb" - @caotral = Caotral.compile!(input: @file, output: "./libtmp.so", shared: true, linker: "mold", assembler: "as") + @output = "./libtmp.so" + @caotral = Caotral.compile!(input: @file, output: @output, shared: true, linker: "mold", assembler: "as") require './sample/fiddle.rb' assert_equal(10, X.aibo) end - private - - def check_process(pid) - [ - pid >> 8, # process's exit code - pid & 0x00FF # process's handled error code - ] + def test_sample_call_method_with_arguments + @generated = ["libargs.so", "libargs.so.o", "libargs.so.s"] + @file = "sample/args_and_local_variables.rb" + @output = "./libargs.so" + @caotral = Caotral.compile!(input: @file, output: @output, shared: true, linker: "mold", assembler: "as") + handle = Fiddle.dlopen(@output) + foo = Fiddle::Function.new( + handle["foo"], + [Fiddle::TYPE_LONG, Fiddle::TYPE_LONG], + Fiddle::TYPE_LONG + ) + assert_equal(42, foo.call(40, 2)) end end From fb95d74017e7d5ab9530250f8926dfc5c000c2ca Mon Sep 17 00:00:00 2001 From: "MATSUMOTO, Katsuyoshi" Date: Wed, 26 Aug 2026 13:53:25 +0900 Subject: [PATCH 10/13] Preserve method conditional control flow --- lib/caotral/compiler/generator.rb | 11 +++-------- sample/not_early_return.rb | 6 ++++++ test/caotral/compiler_test.rb | 16 ++++++++++++++++ 3 files changed, 25 insertions(+), 8 deletions(-) create mode 100644 sample/not_early_return.rb diff --git a/lib/caotral/compiler/generator.rb b/lib/caotral/compiler/generator.rb index deea7640..70aa7dc6 100644 --- a/lib/caotral/compiler/generator.rb +++ b/lib/caotral/compiler/generator.rb @@ -208,14 +208,9 @@ def to_asm(node, function) instruction("pop", "rax") label(".Lend#{sequence}") else - if function&.name - to_asm(tblock, function) - ret - else - instruction("je", ".Lend#{sequence}") - to_asm(tblock, function) - label(".Lend#{sequence}") - end + instruction("je", ".Lend#{sequence}") + to_asm(tblock, function) + label(".Lend#{sequence}") end @context.increment_label_sequence return diff --git a/sample/not_early_return.rb b/sample/not_early_return.rb new file mode 100644 index 00000000..0843a663 --- /dev/null +++ b/sample/not_early_return.rb @@ -0,0 +1,6 @@ +def not_early_return_if(a) + if a == 1 + 47 + end + 42 +end diff --git a/test/caotral/compiler_test.rb b/test/caotral/compiler_test.rb index d4995572..27309336 100644 --- a/test/caotral/compiler_test.rb +++ b/test/caotral/compiler_test.rb @@ -79,4 +79,20 @@ def test_sample_call_method_with_arguments ) assert_equal(42, foo.call(40, 2)) end + + def test_sample_method_if_without_else_continues + @generated = ["libif.so", "libif.so.o", "libif.so.s"] + @file = "sample/not_early_return.rb" + @output = "./libif.so" + @caotral = Caotral.compile!(input: @file, output: @output, shared: true, linker: "mold", assembler: "as") + handle = Fiddle.dlopen(@output) + not_early_if = Fiddle::Function.new( + handle["not_early_return_if"], + [Fiddle::TYPE_LONG], + Fiddle::TYPE_LONG + ) + + assert_equal(42, not_early_if.call(1)) + assert_equal(42, not_early_if.call(9)) + end end From 81bb367ee2efb66c72c91a387c51fc72238fa161 Mon Sep 17 00:00:00 2001 From: "MATSUMOTO, Katsuyoshi" Date: Wed, 26 Aug 2026 16:37:57 +0900 Subject: [PATCH 11/13] Reject unsupported method argument counts --- lib/caotral/compiler/generator.rb | 15 +++++++++++++++ sample/max_arguments.rb | 1 + sig/caotral/compiler/generator.rbs | 1 + test/caotral/compiler_test.rb | 11 +++++++++++ 4 files changed, 28 insertions(+) create mode 100644 sample/max_arguments.rb diff --git a/lib/caotral/compiler/generator.rb b/lib/caotral/compiler/generator.rb index 70aa7dc6..6ebe01a7 100644 --- a/lib/caotral/compiler/generator.rb +++ b/lib/caotral/compiler/generator.rb @@ -15,6 +15,7 @@ def initialize(input:, output: File.basename(input, "*") + ".s", debug: false, s def compile @context = Caotral::Compiler::Analyzer.analyze(@ast) + validate_method_argument_counts @emitter = Caotral::Compiler::Emitter.new(File.open(@precompile, "w")) entry = @context.entry @@ -42,6 +43,20 @@ def compile def compile_shared_option = %w(-shared -fPIC) def already_build_methods? = @context.all_methods_emitted? + def validate_method_argument_counts + errors = [] + max = REGISTER.size + @context.functions.each do |name, function| + next unless function.parameters.size > max + parameter_size = function.parameters.size + errors << "#{name} has #{parameter_size} required positional arguments; maximum supported is #{max}" + end + + unless errors.empty? + raise NotImplementedError, errors.join("\n") + end + end + def epilogue instruction("mov", "rsp", "rbp") instruction("pop", "rbp") diff --git a/sample/max_arguments.rb b/sample/max_arguments.rb new file mode 100644 index 00000000..1d20d447 --- /dev/null +++ b/sample/max_arguments.rb @@ -0,0 +1 @@ +def max_args(a, b, c, d, e, f, g) = 42 diff --git a/sig/caotral/compiler/generator.rbs b/sig/caotral/compiler/generator.rbs index ce2ca8c1..c489f139 100644 --- a/sig/caotral/compiler/generator.rbs +++ b/sig/caotral/compiler/generator.rbs @@ -19,6 +19,7 @@ class Caotral::Compiler::Generator def compile_shared_option: () -> Array[String] def already_build_methods?: -> bool + def validate_method_argument_counts: () -> void def call_method: (RubyVM::AbstractSyntaxTree::Node, Caotral::Compiler::Context::Function) -> void def comp: (String) -> void def define_method_prologue: (Caotral::Compiler::Context::Function) -> void diff --git a/test/caotral/compiler_test.rb b/test/caotral/compiler_test.rb index 27309336..446cefa8 100644 --- a/test/caotral/compiler_test.rb +++ b/test/caotral/compiler_test.rb @@ -95,4 +95,15 @@ def test_sample_method_if_without_else_continues assert_equal(42, not_early_if.call(1)) assert_equal(42, not_early_if.call(9)) end + + def test_rejects_more_than_six_required_positional_arguments + @file = "sample/max_arguments.rb" + error = assert_raise(NotImplementedError) do + Caotral.compile!(input: @file, output: @output, shared: true, linker: "mold", assembler: "as") + end + assert_equal( + "max_args has 7 required positional arguments; maximum supported is 6", + error.message + ) + end end From 8c60bac661cfb47a65d4fc6f539c91f21cf57366 Mon Sep 17 00:00:00 2001 From: "MATSUMOTO, Katsuyoshi" Date: Wed, 26 Aug 2026 17:04:36 +0900 Subject: [PATCH 12/13] Preserve local assignment results --- lib/caotral/compiler/generator.rb | 1 + sample/trailing_local_assignment.rb | 5 +++++ test/caotral/compiler_test.rb | 15 +++++++++++++++ 3 files changed, 21 insertions(+) create mode 100644 sample/trailing_local_assignment.rb diff --git a/lib/caotral/compiler/generator.rb b/lib/caotral/compiler/generator.rb index 6ebe01a7..ea8f437b 100644 --- a/lib/caotral/compiler/generator.rb +++ b/lib/caotral/compiler/generator.rb @@ -206,6 +206,7 @@ def to_asm(node, function) instruction("mov", "[rax]", "rdi") instruction("push", "rdi") instruction("pop", "rax") + instruction("push", "rax") return when :IF cond, tblock, fblock = node.children diff --git a/sample/trailing_local_assignment.rb b/sample/trailing_local_assignment.rb new file mode 100644 index 00000000..0fb3818b --- /dev/null +++ b/sample/trailing_local_assignment.rb @@ -0,0 +1,5 @@ +def trailing_local_assignment(a) + x = a + 2 + y = 99 + x = a + 1 +end diff --git a/test/caotral/compiler_test.rb b/test/caotral/compiler_test.rb index 446cefa8..079a348c 100644 --- a/test/caotral/compiler_test.rb +++ b/test/caotral/compiler_test.rb @@ -106,4 +106,19 @@ def test_rejects_more_than_six_required_positional_arguments error.message ) end + + def test_sample_return_value_with_local_variable + @generated = ["libtla.so", "libtla.so.o", "libtla.so.s"] + @file = "sample/trailing_local_assignment.rb" + @output = "./libtla.so" + @caotral = Caotral.compile!(input: @file, output: @output, shared: true, linker: "mold", assembler: "as") + handle = Fiddle.dlopen(@output) + tla = Fiddle::Function.new( + handle["trailing_local_assignment"], + [Fiddle::TYPE_LONG], + Fiddle::TYPE_LONG + ) + + assert_equal(42, tla.call(41)) + end end From 65e012e71419661a7f96235fae759f655489722d Mon Sep 17 00:00:00 2001 From: "MATSUMOTO, Katsuyoshi" Date: Wed, 26 Aug 2026 18:11:55 +0900 Subject: [PATCH 13/13] Reject unsupported method parameters --- lib/caotral/compiler/generator.rb | 31 +++++++++++++++-------- sample/optional_and_too_many_arguments.rb | 2 ++ sample/optional_arguments.rb | 1 + sig/caotral/compiler/generator.rbs | 5 +++- test/caotral/compiler_test.rb | 21 +++++++++++++++ 5 files changed, 49 insertions(+), 11 deletions(-) create mode 100644 sample/optional_and_too_many_arguments.rb create mode 100644 sample/optional_arguments.rb diff --git a/lib/caotral/compiler/generator.rb b/lib/caotral/compiler/generator.rb index ea8f437b..60af2a96 100644 --- a/lib/caotral/compiler/generator.rb +++ b/lib/caotral/compiler/generator.rb @@ -7,6 +7,8 @@ module Caotral class Compiler class Generator REGISTER = %w(rdi rsi rdx rcx r8 r9) + COUNT_OVER_ARGUMENT_ERROR = "%s has %s required positional arguments; maximum supported is #{REGISTER.size}" + UNSUPPORTED_ARGUMENT_ERROR = "%s has unsupported parameters; only required positional parameters are supported" attr_reader :precompile, :shared def initialize(input:, output: File.basename(input, "*") + ".s", debug: false, shared: false) @source, @precompile, @debug, @shared = input, output, debug, shared @@ -14,8 +16,8 @@ def initialize(input:, output: File.basename(input, "*") + ".s", debug: false, s end def compile + validate_method_definitions @context = Caotral::Compiler::Analyzer.analyze(@ast) - validate_method_argument_counts @emitter = Caotral::Compiler::Emitter.new(File.open(@precompile, "w")) entry = @context.entry @@ -43,18 +45,27 @@ def compile def compile_shared_option = %w(-shared -fPIC) def already_build_methods? = @context.all_methods_emitted? - def validate_method_argument_counts + def validate_method_definitions errors = [] - max = REGISTER.size - @context.functions.each do |name, function| - next unless function.parameters.size > max - parameter_size = function.parameters.size - errors << "#{name} has #{parameter_size} required positional arguments; maximum supported is #{max}" - end + collect_method_definition_errors(@ast, errors) + + raise NotImplementedError, errors.join("\n") unless errors.empty? + end - unless errors.empty? - raise NotImplementedError, errors.join("\n") + def collect_method_definition_errors(node, errors) + return unless RubyVM::AbstractSyntaxTree::Node === node + if node.type == :DEFN + name, scope = node.children + _l, arguments, _b = scope.children + case arguments.children + in [Integer => count, nil, nil, nil, 0, nil, nil, nil, nil, nil] + errors << COUNT_OVER_ARGUMENT_ERROR % [name, count] if count > REGISTER.size + else + errors << UNSUPPORTED_ARGUMENT_ERROR % [name] + end end + node.children.each { |child| collect_method_definition_errors(child, errors) } + nil end def epilogue diff --git a/sample/optional_and_too_many_arguments.rb b/sample/optional_and_too_many_arguments.rb new file mode 100644 index 00000000..b5603e6a --- /dev/null +++ b/sample/optional_and_too_many_arguments.rb @@ -0,0 +1,2 @@ +def max_args(a, b, c, d, e, f, g) = 42 +def optional_arguments(a = 1000, b) = 42 diff --git a/sample/optional_arguments.rb b/sample/optional_arguments.rb new file mode 100644 index 00000000..905334fd --- /dev/null +++ b/sample/optional_arguments.rb @@ -0,0 +1 @@ +def optional_arguments(a = 1, b) = b diff --git a/sig/caotral/compiler/generator.rbs b/sig/caotral/compiler/generator.rbs index c489f139..f6ff9f2c 100644 --- a/sig/caotral/compiler/generator.rbs +++ b/sig/caotral/compiler/generator.rbs @@ -1,5 +1,7 @@ class Caotral::Compiler::Generator REGISTER: Array[String] + COUNT_OVER_ARGUMENT_ERROR: String + UNSUPPORTED_ARGUMENT_ERROR: String attr_reader precompile: String attr_reader shared: bool @@ -19,7 +21,8 @@ class Caotral::Compiler::Generator def compile_shared_option: () -> Array[String] def already_build_methods?: -> bool - def validate_method_argument_counts: () -> void + def validate_method_definitions: () -> void + def collect_method_definition_errors: (RubyVM::AbstractSyntaxTree::Node?, Array[String]) -> void def call_method: (RubyVM::AbstractSyntaxTree::Node, Caotral::Compiler::Context::Function) -> void def comp: (String) -> void def define_method_prologue: (Caotral::Compiler::Context::Function) -> void diff --git a/test/caotral/compiler_test.rb b/test/caotral/compiler_test.rb index 079a348c..b9db7dd4 100644 --- a/test/caotral/compiler_test.rb +++ b/test/caotral/compiler_test.rb @@ -121,4 +121,25 @@ def test_sample_return_value_with_local_variable assert_equal(42, tla.call(41)) end + + def test_rejects_optional_arguments + @file = "sample/optional_arguments.rb" + error = assert_raise(NotImplementedError) do + Caotral.compile!(input: @file, output: @output, shared: true, linker: "mold", assembler: "as") + end + assert_equal( + "optional_arguments has unsupported parameters; only required positional parameters are supported", + error.message + ) + end + + def test_reports_all_method_definition_errors + @file = "sample/optional_and_too_many_arguments.rb" + error = assert_raise(NotImplementedError) do + Caotral.compile!(input: @file, output: @output, shared: true, linker: "mold", assembler: "as") + end + error_str = "max_args has 7 required positional arguments; maximum supported is 6\n" + error_str += "optional_arguments has unsupported parameters; only required positional parameters are supported" + assert_equal(error_str, error.message) + end end