diff --git a/lib/caotral/compiler/analyzer.rb b/lib/caotral/compiler/analyzer.rb new file mode 100644 index 00000000..dde3624b --- /dev/null +++ b/lib/caotral/compiler/analyzer.rb @@ -0,0 +1,54 @@ +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_entry(@ast) + 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) + 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 + 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 + + 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 new file mode 100644 index 00000000..6dcec38c --- /dev/null +++ b/lib/caotral/compiler/context.rb @@ -0,0 +1,30 @@ +require_relative "context/function" +require_relative "context/variables" + +module Caotral + class Compiler + class Context + attr_reader :discovered_methods, :label_sequence, :entry, :functions + def initialize + @label_sequence = 0 + @entry_emitted = false + @emitted_methods = Set[] + @discovered_methods = Set[] + @entry = nil + @variables = Caotral::Compiler::Context::Variables.new + @functions = {} + 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) = @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 + def register_function(function) = @functions[function.name] = function + end + end +end diff --git a/lib/caotral/compiler/context/function.rb b/lib/caotral/compiler/context/function.rb new file mode 100644 index 00000000..d8e46cae --- /dev/null +++ b/lib/caotral/compiler/context/function.rb @@ -0,0 +1,21 @@ +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 + def variables = parameters + locals.to_a + end + 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/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..60af2a96 100644 --- a/lib/caotral/compiler/generator.rb +++ b/lib/caotral/compiler/generator.rb @@ -1,288 +1,301 @@ # frozen_string_literal: true +require_relative "analyzer" +require_relative "emitter" + module Caotral class Compiler class Generator REGISTER = %w(rdi rsi rdx rcx r8 r9) - attr_accessor :main + 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 - @doned, @defined_methods, @defined_variables = Set.new, Set.new, Set.new - @seq, @main = 0, false @ast = RubyVM::AbstractSyntaxTree.parse_file(@source) end - def compile_shared_option = %w(-shared -fPIC) def compile - register_var_and_method(@ast) + validate_method_definitions + @context = Caotral::Compiler::Analyzer.analyze(@ast) + @emitter = Caotral::Compiler::Emitter.new(File.open(@precompile, "w")) + entry = @context.entry - output = File.open(@precompile, "w") # prologue - output.puts " .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(".intel_syntax noprefix") + if @context.discovered_methods.empty? + @context.mark_entry_emitted + directive(".globl main") + label("main") + instruction("push", "rbp") + instruction("mov", "rbp", "rsp") + instruction("sub", "rsp", entry.variables.size * 8) + to_asm(@ast, entry) + 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, entry) end - output.close + ensure + @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| @defined_variables << v } - when :DEFN - @defined_methods << variables + private + def compile_shared_option = %w(-shared -fPIC) + def already_build_methods? = @context.all_methods_emitted? + + def validate_method_definitions + errors = [] + collect_method_definition_errors(@ast, errors) + + raise NotImplementedError, errors.join("\n") unless errors.empty? + end + + 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 { |n| register_var_and_method(n) } + node.children.each { |child| collect_method_definition_errors(child, errors) } nil end - 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) - @defined_methods.each do |name| - output.puts ".globl #{name}" - output.puts ".type #{name}, @function" if shared + def prologue_methods + @context.discovered_methods.each do |name| + 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" - unless @defined_variables.empty? - output.puts " 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]}" + def define_method_prologue(function) + instruction("push", "rbp") + instruction("mov", "rbp", "rsp") + 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 end nil end - def method(method, node, output) - output.puts "#{method}:" - define_method_prologue(node, output) - node.children.each do |child| - next unless child.kind_of?(RubyVM::AbstractSyntaxTree::Node) - to_asm(child, output, true) - end - output.puts " pop rax" - ret(output) - @doned << method + def parse_method(name, function) + label(name) + define_method_prologue(function) + + to_asm(function.body, function) + + instruction("pop", "rax") + ret + @context.mark_method_emitted(name) 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, function) + instruction("mov", "rax", "rsp") + instruction("mov", "rdi", 16) + instruction("cqo") + instruction("idiv", "rdi") + instruction("mov", "rax", 0) + instruction("cmp", "rdi", 0) + instruction("jne", ".Lprecall#{sequence}") + instruction("push", 0) + instruction("mov", "rax", 1) + label(".Lprecall#{sequence}") + 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, function) + 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" - @seq += 1 + instruction("call", name) + instruction("pop", "rdi") + instruction("cmp", "rdi", 0) + instruction("je", ".Lpostcall#{sequence}") + instruction("pop", "rdi") + label(".Lpostcall#{sequence}") + instruction("push", "rax") + @context.increment_label_sequence 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, function) + instruction("mov", "rax", "rbp") + instruction("sub", "rax", lvar_offset(var, function) * 8) + instruction("push", "rax") nil end - def lvar_offset(var) - return @defined_variables.size if var.nil? - @defined_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 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, function) 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, function) } return when :SCOPE node.children.each do |child| - if already_build_methods? && !@main + if already_build_methods? && !@context.entry_emitted? return if shared - output.puts "main:" - output.puts " push rbp" - output.puts " mov rbp, rsp" - output.puts " sub rsp, 0" - output.puts " push rax" - @main = true + label("main") + instruction("push", "rbp") + instruction("mov", "rbp", "rsp") + instruction("sub", "rsp", 0) + instruction("push", "rax") + @context.mark_entry_emitted end - to_asm(child, output) + to_asm(child, function) end return when :DEFN name, _ = node.children - method(name, node, output) + parse_method(name, @context.functions.fetch(name)) return when :LVAR - return if method_tree name = node.children.last - lvar(name, output) + lvar(name, function) # 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, function) + to_asm(right, function) - 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") + instruction("push", "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, function) + 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#{sequence}") + to_asm(tblock, function) + instruction("pop", "rax") + instruction("jmp", ".Lend#{sequence}") + label(".Lelse#{sequence}") + to_asm(fblock, function) + instruction("pop", "rax") + label(".Lend#{sequence}") else - if method_tree - to_asm(tblock, output, method_tree) - ret(output) - else - output.puts " je .Lend#{@seq}" - to_asm(tblock, output, method_tree) - output.puts ".Lend#{@seq}:" - end + instruction("je", ".Lend#{sequence}") + to_asm(tblock, function) + label(".Lend#{sequence}") end - @seq += 1 + @context.increment_label_sequence 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}:" - @seq += 1 + label(".Lbegin#{sequence}") + to_asm(cond, function) + instruction("pop", "rax") + instruction("push", "rax") + instruction("cmp", "rax", 0) + instruction("je", ".Lend#{sequence}") + 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, output, method_tree) unless left.nil? + to_asm(left, function) unless left.nil? if left.nil? - call_method(node, output, method_tree) + call_method(node, function) else - to_asm(right, output, method_tree) - output.puts " pop rdi" + to_asm(right, function) + 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 + + 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/sample/args_and_local_variables.rb b/sample/args_and_local_variables.rb new file mode 100644 index 00000000..f094a72f --- /dev/null +++ b/sample/args_and_local_variables.rb @@ -0,0 +1,4 @@ +def foo(a, b) + c = a + b + c +end 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/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/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/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/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/sig/caotral/compiler/analyzer.rbs b/sig/caotral/compiler/analyzer.rbs new file mode 100644 index 00000000..3ed495e9 --- /dev/null +++ b/sig/caotral/compiler/analyzer.rbs @@ -0,0 +1,27 @@ +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_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 new file mode 100644 index 00000000..c453fb38 --- /dev/null +++ b/sig/caotral/compiler/context.rbs @@ -0,0 +1,26 @@ +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? + attr_reader functions: Hash[Symbol, 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 + @functions: Hash[Symbol, Caotral::Compiler::Context::Function] + + 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 + def register_entry: (Caotral::Compiler::Context::Function function) -> void + def register_function: (Caotral::Compiler::Context::Function function) -> void +end diff --git a/sig/caotral/compiler/context/function.rbs b/sig/caotral/compiler/context/function.rbs new file mode 100644 index 00000000..2a054b87 --- /dev/null +++ b/sig/caotral/compiler/context/function.rbs @@ -0,0 +1,19 @@ +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 + def variables: () -> Array[Symbol] +end 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 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..f6ff9f2c 100644 --- a/sig/caotral/compiler/generator.rbs +++ b/sig/caotral/compiler/generator.rbs @@ -1,36 +1,40 @@ class Caotral::Compiler::Generator REGISTER: Array[String] + COUNT_OVER_ARGUMENT_ERROR: String + UNSUPPORTED_ARGUMENT_ERROR: String - # attr_reader 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] - @seq: Integer + @emitter: Caotral::Compiler::Emitter + @precompile: String @shared: bool @ast: RubyVM::AbstractSyntaxTree::Node @source: String - # class methods def initialize: (input: String, ?output: String, ?debug: bool, ?shared: bool) -> void - - # instance private methods - def already_build_methods?: -> bool - def call_method: (RubyVM::AbstractSyntaxTree::Node, File, bool) -> void def compile: () -> void + + private + 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 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 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 already_build_methods?: -> bool + 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 + def epilogue: () -> void + 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?, Caotral::Compiler::Context::Function) -> void + def instruction: (String, *(String | Symbol | Integer)) -> void + def label: (String | Symbol) -> void + def directive: (String) -> void + def sequence: () -> Integer end diff --git a/test/caotral/compiler/analyzer_test.rb b/test/caotral/compiler/analyzer_test.rb new file mode 100644 index 00000000..67d8a4d0 --- /dev/null +++ b/test/caotral/compiler/analyzer_test.rb @@ -0,0 +1,33 @@ +require_relative "../../test_suite" + +class Caotral::Compiler::AnalyzerTest < Test::Unit::TestCase + def test_analyze_methods_and_local_variables + ast = RubyVM::AbstractSyntaxTree.parse_file("sample/method_and_variable.rb") + context = Caotral::Compiler::Analyzer.analyze(ast) + 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(: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 diff --git a/test/caotral/compiler/context/function_test.rb b/test/caotral/compiler/context/function_test.rb new file mode 100644 index 00000000..b35ee9fe --- /dev/null +++ b/test/caotral/compiler/context/function_test.rb @@ -0,0 +1,18 @@ +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) + 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..b9db7dd4 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,86 @@ 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 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 + + 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 + + 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 + + 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 + + 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 check_process(pid) - [ - pid >> 8, # process's exit code - pid & 0x00FF # process's handled error code - ] + 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