├── .ruby-version ├── .dockerignore ├── .gitignore ├── lib ├── mya.rb ├── mya │ ├── compiler │ │ ├── backends │ │ │ ├── llvm_backend │ │ │ │ ├── object_builder.rb │ │ │ │ ├── string_builder.rb │ │ │ │ ├── array_builder.rb │ │ │ │ └── rc_builder.rb │ │ │ ├── vm_backend.rb │ │ │ └── llvm_backend.rb │ │ ├── instruction.rb │ │ └── type_checker.rb │ └── compiler.rb └── tasks │ └── llvm.rake ├── .streerc ├── .solargraph.yml ├── examples ├── countdown.rb ├── fact.rb ├── fib.rb ├── inheritance.rb └── type_annotations.rb ├── spec ├── all.rb ├── spec_helper.rb ├── compiler │ └── backends │ │ ├── vm_backend_spec.rb │ │ └── llvm_backend_spec.rb ├── support │ ├── expectations.rb │ └── shared_backend_examples.rb └── compiler_spec.rb ├── .clang-format ├── .editorconfig ├── Gemfile ├── .github └── workflows │ ├── lint.yml │ └── specs.yml ├── devbox.json ├── Dockerfile ├── Gemfile.lock ├── Rakefile ├── bin └── mya ├── README.md ├── src └── lib.c ├── AmazonQ.md └── devbox.lock /.ruby-version: -------------------------------------------------------------------------------- 1 | 3.4 2 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | vendor 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | vendor 3 | -------------------------------------------------------------------------------- /lib/mya.rb: -------------------------------------------------------------------------------- 1 | require_relative 'mya/compiler' 2 | -------------------------------------------------------------------------------- /.streerc: -------------------------------------------------------------------------------- 1 | --print-width=120 2 | --plugins=plugin/trailing_comma,plugin/single_quotes,plugin/disable_auto_ternary 3 | --ignore-files=vendor/**/* 4 | -------------------------------------------------------------------------------- /.solargraph.yml: -------------------------------------------------------------------------------- 1 | include: 2 | - "**/*.rb" 3 | require: [] 4 | domains: [] 5 | reporters: 6 | - rubocop 7 | #- require_not_found 8 | max_files: 5000 9 | -------------------------------------------------------------------------------- /examples/countdown.rb: -------------------------------------------------------------------------------- 1 | def countdown(n) 2 | while n > 0 3 | puts n.to_s 4 | n = n - 1 5 | end 6 | puts 'Done!' 7 | end 8 | 9 | countdown(5) 10 | -------------------------------------------------------------------------------- /spec/all.rb: -------------------------------------------------------------------------------- 1 | require_relative 'compiler_spec' 2 | require_relative 'compiler/backends/llvm_backend_spec' 3 | require_relative 'compiler/backends/vm_backend_spec' 4 | -------------------------------------------------------------------------------- /.clang-format: -------------------------------------------------------------------------------- 1 | --- 2 | BasedOnStyle: WebKit 3 | PointerAlignment: Right 4 | BreakBeforeBraces: Attach 5 | AllowShortIfStatementsOnASingleLine: WithoutElse 6 | TabWidth: 4 7 | -------------------------------------------------------------------------------- /examples/fact.rb: -------------------------------------------------------------------------------- 1 | def fact(n, result) 2 | if n == 0 3 | result 4 | else 5 | fact(n - 1, result * n) 6 | end 7 | end 8 | 9 | puts fact(10, 1).to_s 10 | -------------------------------------------------------------------------------- /examples/fib.rb: -------------------------------------------------------------------------------- 1 | def fib(n) 2 | if n == 0 3 | 0 4 | elsif n == 1 5 | 1 6 | else 7 | fib(n - 1) + fib(n - 2) 8 | end 9 | end 10 | 11 | puts fib(10).to_s 12 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | indent_style = space 7 | indent_size = 2 8 | 9 | [*.{c,cpp,h,hpp}] 10 | indent_size = 4 11 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'minitest/autorun' 2 | require 'minitest/focus' 3 | require 'minitest/reporters' 4 | 5 | Minitest::Reporters.use! Minitest::Reporters::SpecReporter.new 6 | 7 | require_relative '../lib/mya' 8 | require_relative '../spec/support/expectations' 9 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'minitest' 4 | gem 'minitest-fail-fast' 5 | gem 'minitest-focus' 6 | gem 'minitest-reporters' 7 | gem 'prism' 8 | gem 'rake' 9 | gem 'ruby-llvm', '~> 20' 10 | 11 | group :development do 12 | gem 'syntax_tree' 13 | end 14 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | 9 | jobs: 10 | lint: 11 | runs-on: ubuntu-24.04 12 | steps: 13 | - uses: actions/checkout@v4 14 | with: 15 | submodules: "recursive" 16 | - name: build docker image 17 | run: docker build . -t mya 18 | - name: run lint 19 | run: docker run --entrypoint="" mya bundle exec rake lint 20 | -------------------------------------------------------------------------------- /.github/workflows/specs.yml: -------------------------------------------------------------------------------- 1 | name: Specs 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | 9 | jobs: 10 | specs: 11 | runs-on: ubuntu-24.04 12 | steps: 13 | - uses: actions/checkout@v4 14 | with: 15 | submodules: "recursive" 16 | - name: build docker image 17 | run: docker build . -t mya 18 | - name: run specs 19 | run: docker run --entrypoint="" mya bundle exec rake spec 20 | -------------------------------------------------------------------------------- /lib/mya/compiler/backends/llvm_backend/object_builder.rb: -------------------------------------------------------------------------------- 1 | class Compiler 2 | module Backends 3 | class LLVMBackend 4 | class ObjectBuilder < RcBuilder 5 | def initialize(builder:, mod:, struct:) 6 | super(builder:, mod:, ptr: nil) 7 | 8 | obj = builder.malloc(struct) 9 | obj_ptr = @builder.alloca(LLVM::Type.pointer(LLVM::Int8)) 10 | @builder.store(obj, obj_ptr) 11 | store_ptr(obj_ptr) 12 | end 13 | end 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /devbox.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/jetify-com/devbox/0.12.0/.schema/devbox.schema.json", 3 | "packages": { 4 | "ruby_3_3": "latest", 5 | "llvmPackages_18.libllvm": { 6 | "version": "latest", 7 | "outputs": ["out", "dev", "lib"] 8 | } 9 | }, 10 | "env": { 11 | "LD_LIBRARY_PATH": "$DEVBOX_PACKAGES_DIR/lib" 12 | }, 13 | "shell": { 14 | "init_hook": [ 15 | ], 16 | "scripts": { 17 | "setup": [ 18 | "bundle install" 19 | ], 20 | "test": [ 21 | "rake spec" 22 | ] 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /spec/compiler/backends/vm_backend_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative '../../spec_helper' 2 | require_relative '../../support/shared_backend_examples' 3 | require 'stringio' 4 | 5 | describe Compiler::Backends::VMBackend do 6 | include SharedBackendExamples 7 | 8 | def execute(code, io: $stdout) 9 | instructions = Compiler.new(code).compile 10 | Compiler::Backends::VMBackend.new(instructions, io:).run 11 | end 12 | 13 | def execute_with_output(code) 14 | io = StringIO.new 15 | execute(code, io:) 16 | io.rewind 17 | io.read 18 | end 19 | 20 | def execute_file(path) 21 | execute_with_output(File.read(path)) 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ruby:3.4-trixie 2 | 3 | RUN apt-get update && \ 4 | apt-get install -y -q build-essential clang wget gpg && \ 5 | wget -O - https://apt.llvm.org/llvm-snapshot.gpg.key | gpg --dearmor -o /usr/share/keyrings/llvm-snapshot.gpg && \ 6 | echo "deb [signed-by=/usr/share/keyrings/llvm-snapshot.gpg] http://apt.llvm.org/trixie/ llvm-toolchain-trixie-20 main" >> /etc/apt/sources.list.d/llvm.list && \ 7 | apt-get update && \ 8 | apt-get install -y -q llvm-20-dev 9 | 10 | COPY Gemfile /mya/Gemfile 11 | COPY Gemfile.lock /mya/Gemfile.lock 12 | WORKDIR /mya 13 | 14 | RUN bundle config set --local deployment 'true' && \ 15 | bundle install 16 | 17 | COPY . /mya 18 | 19 | ENTRYPOINT ["./bin/mya"] 20 | -------------------------------------------------------------------------------- /spec/support/expectations.rb: -------------------------------------------------------------------------------- 1 | require 'minitest/assertions' 2 | require 'tempfile' 3 | require 'pp' 4 | 5 | module Minitest::Assertions 6 | def assert_equal_with_diff(expected, actual) 7 | if expected != actual 8 | expected_file = Tempfile.new('expected') 9 | actual_file = Tempfile.new('actual') 10 | PP.pp(expected, expected_file) 11 | PP.pp(actual, actual_file) 12 | expected_file.close 13 | actual_file.close 14 | puts `diff #{expected_file.path} #{actual_file.path}` 15 | expected_file.unlink 16 | actual_file.unlink 17 | end 18 | assert_equal expected, actual 19 | end 20 | end 21 | 22 | require 'minitest/spec' 23 | 24 | module Minitest::Expectations 25 | Enumerable.infect_an_assertion :assert_equal_with_diff, :must_equal_with_diff 26 | end 27 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | ansi (1.5.0) 5 | builder (3.3.0) 6 | ffi (1.17.2-aarch64-linux-gnu) 7 | ffi (1.17.2-arm64-darwin) 8 | ffi (1.17.2-x86_64-linux-gnu) 9 | minitest (5.25.5) 10 | minitest-fail-fast (0.1.0) 11 | minitest (~> 5) 12 | minitest-focus (1.4.0) 13 | minitest (>= 4, < 6) 14 | minitest-reporters (1.7.1) 15 | ansi 16 | builder 17 | minitest (>= 5.0) 18 | ruby-progressbar 19 | prettier_print (1.2.1) 20 | prism (1.4.0) 21 | rake (13.3.0) 22 | ruby-llvm (20.1.7) 23 | ffi (~> 1.13) 24 | rake (>= 12, < 14) 25 | ruby-progressbar (1.13.0) 26 | syntax_tree (6.3.0) 27 | prettier_print (>= 1.2.0) 28 | 29 | PLATFORMS 30 | aarch64-linux 31 | arm64-darwin-23 32 | x86_64-linux 33 | 34 | DEPENDENCIES 35 | minitest 36 | minitest-fail-fast 37 | minitest-focus 38 | minitest-reporters 39 | prism 40 | rake 41 | ruby-llvm (~> 20) 42 | syntax_tree 43 | 44 | BUNDLED WITH 45 | 2.7.1 46 | -------------------------------------------------------------------------------- /spec/compiler/backends/llvm_backend_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative '../../spec_helper' 2 | require_relative '../../support/shared_backend_examples' 3 | require 'tempfile' 4 | 5 | describe Compiler::Backends::LLVMBackend do 6 | include SharedBackendExamples 7 | 8 | def execute(code) 9 | instructions = Compiler.new(code).compile 10 | Compiler::Backends::LLVMBackend.new(instructions).run 11 | end 12 | 13 | def execute_with_output(code) 14 | temp = Tempfile.create('compiled.ll') 15 | temp.close 16 | instructions = Compiler.new(code).compile 17 | Compiler::Backends::LLVMBackend.new(instructions).dump_ir_to_file(temp.path) 18 | `#{lli} #{temp.path} 2>&1` 19 | ensure 20 | File.unlink(temp.path) 21 | end 22 | 23 | def execute_file(path) 24 | execute_with_output(File.read(path)) 25 | end 26 | 27 | private 28 | 29 | def lli 30 | return @lli if @lli 31 | 32 | major_version = LLVM::RUBY_LLVM_VERSION.split('.').first 33 | @lli = (system("command -v lli-#{major_version} 2>/dev/null >/dev/null") ? "lli-#{major_version}" : 'lli') 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | load 'lib/tasks/llvm.rake' 2 | 3 | task default: :spec 4 | 5 | desc 'Build project' 6 | task build: ['build/lib.ll'] 7 | 8 | desc 'Run specs' 9 | task spec: :build do 10 | require 'minitest/fail_fast' if ENV['TESTOPTS'] == '--fail-fast' 11 | require_relative 'spec/all' 12 | end 13 | 14 | desc 'Watch for file changes and run specs' 15 | task :watch do 16 | files = Dir['**/*.rb', 'src/*.c'] 17 | sh %(ls #{files.join(' ')} | entr -c -s 'TESTOPTS="--fail-fast" bundle exec rake spec') 18 | end 19 | 20 | desc 'Run specs in a Docker container' 21 | task :docker_spec do 22 | sh 'docker build -t mya . && docker run mya bundle exec rake spec' 23 | end 24 | 25 | file 'build/lib.ll' => 'src/lib.c' do 26 | mkdir_p 'build' 27 | sh 'clang -o build/lib.ll -S -emit-llvm src/lib.c' 28 | end 29 | 30 | desc 'Format code' 31 | task :format do 32 | sh 'stree write **/*.rb' 33 | sh 'clang-format -i src/lib.c' 34 | end 35 | 36 | desc 'Run lint (syntax-check only for now)' 37 | task :lint do 38 | sh 'stree check **/*.rb' 39 | # sh 'clang-format --dry-run --Werror src/lib.c' 40 | end 41 | 42 | desc 'Run lint in a Docker container' 43 | task :docker_lint do 44 | sh 'docker build -t mya . && docker run mya bundle exec rake lint' 45 | end 46 | -------------------------------------------------------------------------------- /examples/inheritance.rb: -------------------------------------------------------------------------------- 1 | class Animal 2 | def initialize(name) 3 | @name = name 4 | end 5 | 6 | def name 7 | @name 8 | end 9 | 10 | def speak 11 | puts @name + ' makes a sound' 12 | end 13 | 14 | def move 15 | puts @name + ' moves around' 16 | end 17 | end 18 | 19 | class Dog < Animal 20 | def initialize(name, breed) 21 | @name = name 22 | @breed = breed 23 | end 24 | 25 | def speak 26 | puts @name + ' barks: Woof!' 27 | end 28 | 29 | def breed 30 | @breed 31 | end 32 | 33 | def fetch 34 | puts @name + ' fetches the ball' 35 | end 36 | end 37 | 38 | class Cat < Animal 39 | def speak 40 | puts @name + ' meows: Meow!' 41 | end 42 | 43 | def climb 44 | puts @name + ' climbs a tree' 45 | end 46 | end 47 | 48 | animal = Animal.new('Generic Animal') 49 | animal.speak 50 | animal.move 51 | 52 | puts '' 53 | 54 | dog = Dog.new('Buddy', 'Golden Retriever') 55 | puts 'Dog name: ' + dog.name 56 | puts 'Dog breed: ' + dog.breed 57 | dog.speak # Overridden method 58 | dog.move # Inherited method 59 | dog.fetch # Own method 60 | 61 | puts '' 62 | 63 | cat = Cat.new('Whiskers') 64 | puts 'Cat name: ' + cat.name 65 | cat.speak # Overridden method 66 | cat.move # Inherited method 67 | cat.climb # Own method 68 | -------------------------------------------------------------------------------- /lib/mya/compiler/backends/llvm_backend/string_builder.rb: -------------------------------------------------------------------------------- 1 | class Compiler 2 | module Backends 3 | class LLVMBackend 4 | class StringBuilder < RcBuilder 5 | def initialize(builder:, mod:, string: nil, ptr: nil) 6 | super(builder:, mod:, ptr:) 7 | 8 | string = string.to_s 9 | 10 | return if ptr 11 | str = LLVM::ConstantArray.string(string) 12 | str_global = @module.globals.add(LLVM::Type.array(LLVM::Int8, string.bytesize + 1), '') 13 | str_global.initializer = str 14 | str_global.linkage = :private 15 | str_global.global_constant = true 16 | str_ptr = 17 | @builder.gep2( 18 | LLVM::Type.array(LLVM::Int8, string.bytesize + 1), 19 | str_global, 20 | [LLVM.Int(0), LLVM.Int(0)], 21 | 'str', 22 | ) 23 | @builder.call(fn_rc_set_str, @ptr, str_ptr, LLVM.Int(string.bytesize)) 24 | 25 | store_size(string.bytesize) 26 | end 27 | 28 | def fn_rc_set_str 29 | return @fn_rc_set_str if @fn_rc_set_str 30 | 31 | @fn_rc_set_str = 32 | @module.functions['rc_set_str'] || 33 | @module.functions.add( 34 | 'rc_set_str', 35 | [pointer_type, LLVM::Type.pointer(LLVM::Int8), LLVM::Int32], 36 | LLVM::Type.void, 37 | ) 38 | end 39 | end 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /lib/tasks/llvm.rake: -------------------------------------------------------------------------------- 1 | namespace :llvm do 2 | LLVM_VERSION = '20.1.7' 3 | VENDOR_DIR = File.expand_path('../../vendor', __dir__) 4 | LLVM_PROJECT_DIR = "#{VENDOR_DIR}/llvm-project-#{LLVM_VERSION}" 5 | LLVM_BUILD_DIR = "#{LLVM_PROJECT_DIR}/build" 6 | LLVM_INSTALL_DIR = "#{VENDOR_DIR}/llvm-install" 7 | 8 | desc "Download and compile LLVM #{LLVM_VERSION}" 9 | task :install do 10 | FileUtils.mkdir_p(VENDOR_DIR) 11 | 12 | unless File.exist?("#{LLVM_PROJECT_DIR}/llvm/CMakeLists.txt") 13 | puts "Downloading LLVM project #{LLVM_VERSION}..." 14 | sh "curl -L https://github.com/llvm/llvm-project/releases/download/llvmorg-#{LLVM_VERSION}/llvm-project-#{LLVM_VERSION}.src.tar.xz | tar -xJ -C #{VENDOR_DIR}" 15 | FileUtils.mv("#{VENDOR_DIR}/llvm-project-#{LLVM_VERSION}.src", LLVM_PROJECT_DIR) 16 | end 17 | 18 | unless File.exist?("#{LLVM_INSTALL_DIR}/bin/llvm-config") 19 | puts "Building LLVM #{LLVM_VERSION}..." 20 | FileUtils.mkdir_p(LLVM_BUILD_DIR) 21 | 22 | Dir.chdir(LLVM_BUILD_DIR) do 23 | sh "cmake ../llvm -DCMAKE_INSTALL_PREFIX=#{LLVM_INSTALL_DIR} -DCMAKE_BUILD_TYPE=Release -DLLVM_ENABLE_RTTI=ON -DLLVM_BUILD_SHARED_LIBS=OFF -DLLVM_LINK_LLVM_DYLIB=ON -DLLVM_DYLIB_COMPONENTS=all -DLLVM_TARGETS_TO_BUILD=host" 24 | sh "make -j#{`nproc`.strip}" 25 | sh 'make install' 26 | end 27 | end 28 | 29 | puts "LLVM installed to: #{LLVM_INSTALL_DIR}" 30 | end 31 | 32 | desc 'Clean LLVM build files' 33 | task :clean do 34 | FileUtils.rm_rf(LLVM_PROJECT_DIR) 35 | FileUtils.rm_rf(LLVM_INSTALL_DIR) 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/mya/compiler/backends/llvm_backend/array_builder.rb: -------------------------------------------------------------------------------- 1 | class Compiler 2 | module Backends 3 | class LLVMBackend 4 | class ArrayBuilder < RcBuilder 5 | def initialize(builder:, mod:, element_type:, elements: nil, ptr: nil) 6 | super(builder:, mod:, ptr:) 7 | @element_type = element_type 8 | 9 | return if ptr 10 | ary_ptr = builder.array_malloc(element_type, LLVM.Int(elements.size)) 11 | store_ptr(ary_ptr) 12 | 13 | elements.each_with_index do |element, index| 14 | gep = builder.gep2(LLVM::Type.array(element_type), ary_ptr, [LLVM.Int(0), LLVM.Int(index)], '') 15 | builder.store(element, gep) 16 | end 17 | 18 | store_size(elements.size) 19 | end 20 | 21 | def first 22 | fn_name = "array_first_#{type_name}" 23 | unless (fn = @module.functions[fn_name]) 24 | fn = @module.functions.add(fn_name, [RcBuilder.pointer_type], @element_type) 25 | end 26 | @builder.call(fn, @ptr) 27 | end 28 | 29 | def last 30 | fn_name = "array_last_#{type_name}" 31 | unless (fn = @module.functions[fn_name]) 32 | fn = @module.functions.add(fn_name, [RcBuilder.pointer_type], @element_type) 33 | end 34 | @builder.call(fn, @ptr) 35 | end 36 | 37 | def push(value) 38 | fn_name = "array_push_#{type_name}" 39 | unless (fn = @module.functions[fn_name]) 40 | fn = @module.functions.add(fn_name, [RcBuilder.pointer_type, @element_type], RcBuilder.pointer_type) 41 | end 42 | @builder.call(fn, @ptr, value) 43 | end 44 | 45 | private 46 | 47 | def type_name 48 | @element_type.kind 49 | end 50 | 51 | def load_ary_ptr 52 | load_ptr(LLVM::Type.pointer(LLVM::Type.array(@element_type))) 53 | end 54 | end 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /lib/mya/compiler/backends/llvm_backend/rc_builder.rb: -------------------------------------------------------------------------------- 1 | class Compiler 2 | module Backends 3 | class LLVMBackend 4 | class RcBuilder 5 | def initialize(builder:, mod:, ptr: nil) 6 | @builder = builder 7 | @module = mod 8 | if ptr 9 | @ptr = ptr 10 | else 11 | @ptr = builder.malloc(type, self.class.simple_name) 12 | store_ptr(LLVM::Type.ptr.null_pointer) 13 | end 14 | store_ref_count(1) 15 | end 16 | 17 | def to_ptr = @ptr 18 | 19 | def type = self.class.type 20 | def pointer_type = self.class.pointer_type 21 | 22 | def store_ptr(ptr) 23 | @builder.store(ptr, field(0)) 24 | end 25 | 26 | def load_ptr(ptr_type) 27 | @builder.load2(ptr_type, field(0)) 28 | end 29 | 30 | def store_size(size) 31 | @builder.store(LLVM.Int(size), field(1)) 32 | end 33 | 34 | def load_size 35 | @builder.load2(LLVM::Int64, field(1)) 36 | end 37 | 38 | def store_ref_count(count) 39 | @builder.store(LLVM.Int(count), field(2)) 40 | end 41 | 42 | def field(index) 43 | @builder.struct_gep2(type, @ptr, index, '') 44 | end 45 | 46 | def self.type 47 | @type ||= LLVM.Struct(LLVM::Type.ptr, LLVM::Int64, LLVM::Int64, 'rc') 48 | end 49 | 50 | def self.pointer_type 51 | @pointer_type ||= LLVM::Type.pointer(type) 52 | end 53 | 54 | def self.simple_name 55 | name.split('::').last.sub(/Builder$/, '').downcase 56 | end 57 | 58 | private 59 | 60 | def fn_rc_take 61 | @fn_rc_take ||= @module.functions.add('rc_take', [pointer_type], LLVM::Type.void) 62 | end 63 | 64 | def fn_rc_drop 65 | @fn_rc_drop ||= @module.functions.add('rc_drop', [pointer_type], LLVM::Type.void) 66 | end 67 | end 68 | end 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /bin/mya: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require_relative '../lib/mya' 4 | require 'optparse' 5 | 6 | options = {} 7 | parser = 8 | OptionParser.new do |opts| 9 | opts.banner = "Usage: mya [options] file.rb\n mya [options] -e 'code'" 10 | 11 | opts.on('-e CODE', 'Execute code string') { |code| options[:code] = code } 12 | 13 | opts.on('-d DEBUG', %w[ir llvm-ir], 'Show debug output (ir, llvm-ir)') { |debug| options[:debug] = debug } 14 | 15 | opts.on('--backend BACKEND', %w[vm llvm], 'Execute with specified backend (vm, llvm)') do |backend| 16 | options[:backend] = backend 17 | end 18 | 19 | opts.on('-h', '--help', 'Show this help') do 20 | puts opts 21 | exit 22 | end 23 | end 24 | 25 | begin 26 | parser.parse! 27 | rescue OptionParser::InvalidOption => e 28 | puts "Error: #{e.message}" 29 | puts parser 30 | exit 1 31 | rescue OptionParser::InvalidArgument => e 32 | puts "Error: #{e.message}" 33 | puts parser 34 | exit 1 35 | end 36 | 37 | code = 38 | if options[:code] 39 | options[:code] 40 | elsif ARGV.first 41 | begin 42 | File.read(ARGV.first) 43 | rescue Errno::ENOENT 44 | puts "Error: File '#{ARGV.first}' not found" 45 | exit 1 46 | end 47 | else 48 | puts "Error: No code provided. Use -e 'code' or specify a file." 49 | puts parser 50 | exit 1 51 | end 52 | 53 | compiler = Compiler.new(code) 54 | instructions = compiler.compile 55 | 56 | if options[:debug] 57 | case options[:debug] 58 | when 'ir' 59 | instructions.each_with_index { |inst, i| puts "#{i}: #{inst.to_h}" } 60 | when 'llvm-ir' 61 | llvm_backend = Compiler::Backends::LLVMBackend.new(instructions, dump: true) 62 | llvm_backend.dump_ir_to_file('/tmp/mya_output.ll') 63 | llvm_ir = File.read('/tmp/mya_output.ll') 64 | puts llvm_ir 65 | end 66 | exit 0 67 | end 68 | 69 | if options[:backend] == 'vm' 70 | vm_backend = Compiler::Backends::VMBackend.new(instructions) 71 | vm_backend.run 72 | else 73 | llvm_backend = Compiler::Backends::LLVMBackend.new(instructions, dump: false) 74 | llvm_backend.run 75 | end 76 | -------------------------------------------------------------------------------- /examples/type_annotations.rb: -------------------------------------------------------------------------------- 1 | class Person 2 | def initialize(name, age, active) # name:String, age:Integer, active:Boolean 3 | @name = name # @name:String 4 | @age = age # @age:Integer 5 | @active = active # @active:Boolean 6 | @score = 0 # @score:Integer 7 | end 8 | 9 | def name # -> String 10 | @name 11 | end 12 | 13 | def age # -> Integer 14 | @age 15 | end 16 | 17 | def active=(active) # active:Boolean 18 | @active = active 19 | end 20 | 21 | def active # -> Boolean 22 | @active 23 | end 24 | 25 | def score # -> Integer 26 | @score 27 | end 28 | 29 | def add_points(points) # points:Integer -> Integer 30 | @score = @score + points 31 | end 32 | 33 | def info # -> String 34 | @name + ' is ' + @age.to_s + ' years old with ' + @score.to_s + ' points (active: ' + @active.to_s + ')' 35 | end 36 | end 37 | 38 | def process_data # -> Integer 39 | count = 5 # count:Integer 40 | message = 'Count is: ' # message:String 41 | finished = true # finished:Boolean 42 | 43 | count = count * 2 44 | message = message + count.to_s 45 | 46 | puts message 47 | puts 'Finished: ' + finished.to_s 48 | count 49 | end 50 | 51 | def add_numbers(x, y) # x:Integer, y:Integer -> Integer 52 | puts 'Adding ' + x.to_s + ' and ' + y.to_s 53 | x + y 54 | end 55 | 56 | def greet_person(name) # name:String -> String 57 | puts 'Hello, ' + name + '!' 58 | name 59 | end 60 | 61 | def check_status(active) # active:Boolean -> Boolean 62 | puts 'Status is: ' + active.to_s 63 | active 64 | end 65 | 66 | def get_greeting # -> String 67 | 'Welcome to Mya!' 68 | end 69 | 70 | def maybe_process(value) # value:Option[String] -> String 71 | if value 72 | 'Processed: ' + value.value! 73 | else 74 | 'Nothing to process' 75 | end 76 | end 77 | 78 | person = Person.new('Alice', 25, false) 79 | puts person.info 80 | 81 | person.active = true 82 | person.add_points(100) 83 | puts person.info 84 | 85 | sum = add_numbers(10, 20) 86 | puts 'Sum: ' + sum.to_s 87 | 88 | greeting = greet_person('Bob') 89 | puts 'Greeted: ' + greeting 90 | 91 | status = check_status(false) 92 | puts 'Final status: ' + status.to_s 93 | 94 | puts get_greeting 95 | 96 | result1 = maybe_process('data') 97 | result2 = maybe_process(nil) 98 | puts result1 99 | puts result2 100 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Mya 2 | 3 | [![Specs](https://github.com/seven1m/mya/actions/workflows/specs.yml/badge.svg)](https://github.com/seven1m/mya/actions/workflows/specs.yml) 4 | 5 | This is maybe going to be a statically-typed subset of the Ruby language. Or it could become something else entirely! It's mostly a playground for learning about type systems and inference. 6 | 7 | This is also my first foray into using LLVM as a backend, so there's a lot of learning going on here! 8 | 9 | The name "mya" is just a working name... the name will likely change. 10 | 11 | ## Building & Testing 12 | 13 | ```sh 14 | bundle install 15 | bundle exec rake spec 16 | ``` 17 | 18 | ### Building LLVM 19 | 20 | If you don't have LLVM version 20 libraries available on your system, you can build it like this: 21 | 22 | ```sh 23 | bundle exec rake llvm:install 24 | LVM_CONFIG=$(pwd)/vendor/llvm-install/bin/llvm-config bundle install 25 | export LD_LIBRARY_PATH=$(pwd)/vendor/llvm-install/lib 26 | bundle exec rake spec 27 | ``` 28 | 29 | 30 | ## TODO 31 | 32 | ### Basic Ruby Syntax Missing 33 | - **Logical operators**: `&&`, `||`, `!` (and, or, not operators) 34 | - **Control flow**: `break`, `next`, `return` statements 35 | - **Conditional statements**: `unless`, `elsif` (only basic `if`/`else` supported) 36 | - **Pattern matching**: `case`/`when` statements 37 | - **Loop constructs**: `until`, `for` loops (only `while` supported) 38 | - **Exception handling**: `begin`/`rescue`/`ensure`/`raise` 39 | - **Assignment operators**: `+=`, `-=`, `*=`, `/=`, `||=`, `&&=` 40 | - **Range operators**: `..`, `...` (inclusive/exclusive ranges) 41 | - **String interpolation**: `"Hello #{name}"` syntax 42 | - **Symbols**: `:symbol` syntax 43 | - **Hash literals**: `{ key: value }` syntax 44 | - **Block syntax**: `{ |x| ... }` and `do |x| ... end` 45 | - **Iterators**: `.each`, `.map`, `.select`, etc. 46 | - **Multiple assignment**: `a, b = 1, 2` 47 | - **Splat operators**: `*args`, `**kwargs` 48 | - **Constants**: Proper constant definition and scoping 49 | - **Module system**: `module` and `include`/`extend` 50 | 51 | ### Type System & Language Features 52 | - **No generic type syntax documentation**: The supported generic type syntax (e.g., `Option[String]`, `Array[Integer]`, `Array[Array[String]]`) needs better documentation and examples 53 | - **Type annotation error messages**: Error messages for type annotation mismatches could be more descriptive and include context about where the annotation was defined 54 | -------------------------------------------------------------------------------- /src/lib.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | typedef struct { 7 | void *ptr; 8 | size_t size; 9 | size_t ref_count; 10 | } RC; 11 | 12 | void rc_set_str(RC *rc, char *str, size_t size) { 13 | rc->ptr = malloc(sizeof(char) * (size + 1)); 14 | memcpy(rc->ptr, str, size + 1); 15 | } 16 | 17 | void rc_take(RC *rc) { 18 | rc->ref_count++; 19 | } 20 | 21 | void rc_drop(RC *rc) { 22 | if (--rc->ref_count == 0) { 23 | free(rc->ptr); 24 | rc->ptr = (void *)0xdeadbeef; 25 | free(rc); 26 | } 27 | } 28 | 29 | int32_t array_first_integer(RC *rc) { 30 | if (rc->size == 0) { 31 | return 0; // FIXME 32 | } 33 | int32_t *ary = rc->ptr; 34 | return ary[0]; 35 | } 36 | 37 | RC *array_first_pointer(RC *rc) { 38 | if (rc->size == 0) { 39 | return 0; // FIXME 40 | } 41 | RC **ary = rc->ptr; 42 | return ary[0]; 43 | } 44 | 45 | int32_t array_last_integer(RC *rc) { 46 | if (rc->size == 0) { 47 | return 0; // FIXME 48 | } 49 | int32_t *ary = rc->ptr; 50 | return ary[rc->size - 1]; 51 | } 52 | 53 | RC *array_last_pointer(RC *rc) { 54 | if (rc->size == 0) { 55 | return 0; // FIXME 56 | } 57 | RC **ary = rc->ptr; 58 | return ary[rc->size - 1]; 59 | } 60 | 61 | RC *array_push_integer(RC *rc, int32_t value) { 62 | int32_t *ary = rc->ptr; 63 | rc->size++; 64 | ary = rc->ptr = realloc(ary, sizeof(int32_t) * rc->size); 65 | ary[rc->size - 1] = value; 66 | return rc; 67 | } 68 | 69 | RC *array_push_pointer(RC *rc, RC *value) { 70 | RC **ary = rc->ptr; 71 | rc->size++; 72 | ary = rc->ptr = realloc(ary, sizeof(RC *) * rc->size); 73 | ary[rc->size - 1] = value; 74 | return rc; 75 | } 76 | 77 | int32_t puts_string(const RC *rc) { 78 | const char *str = rc->ptr; 79 | return printf("%s\n", str); 80 | } 81 | 82 | RC *int_to_string(int32_t value) { 83 | RC *rc = malloc(sizeof(RC)); 84 | rc->ref_count = 1; 85 | 86 | char *str = malloc(12); 87 | int len = snprintf(str, 12, "%d", value); 88 | 89 | rc->ptr = str; 90 | rc->size = len; 91 | 92 | return rc; 93 | } 94 | 95 | RC *bool_to_string(int8_t value) { 96 | RC *rc = malloc(sizeof(RC)); 97 | rc->ref_count = 1; 98 | 99 | const char *str_value = value ? "true" : "false"; 100 | size_t len = strlen(str_value); 101 | 102 | char *str = malloc(len + 1); 103 | strcpy(str, str_value); 104 | 105 | rc->ptr = str; 106 | rc->size = len; 107 | 108 | return rc; 109 | } 110 | 111 | RC *string_concat(const RC *left, const RC *right) { 112 | const char *left_str = left->ptr; 113 | const char *right_str = right->ptr; 114 | 115 | size_t left_len = strlen(left_str); 116 | size_t right_len = strlen(right_str); 117 | size_t total_len = left_len + right_len; 118 | 119 | RC *result = malloc(sizeof(RC)); 120 | result->ref_count = 1; 121 | result->size = total_len; 122 | 123 | char *new_str = malloc(total_len + 1); 124 | strcpy(new_str, left_str); 125 | strcat(new_str, right_str); 126 | 127 | result->ptr = new_str; 128 | 129 | return result; 130 | } 131 | -------------------------------------------------------------------------------- /AmazonQ.md: -------------------------------------------------------------------------------- 1 | # Amazon Q Developer Rules for Mya Compiler Project 2 | 3 | ## Project Overview 4 | This is a Ruby-based compiler project called "Mya" that compiles a statically-typed subset of Ruby to LLVM IR. The project is experimental and focuses on type systems, type inference, and compiler construction. 5 | 6 | ## Architecture 7 | - **Compiler**: Main compilation pipeline using Prism parser 8 | - **TypeChecker**: Constraint solver 9 | - **VM**: Virtual machine for executing compiled code 10 | - **Instructions**: Intermediate representation for compiled code 11 | - **Backends**: Code generation (currently LLVM) 12 | 13 | ## Code Style and Standards 14 | 15 | ### Ruby Style 16 | - Use **SyntaxTree** formatter with 120 character line width 17 | - Single quotes preferred over double quotes 18 | - Trailing commas enabled in collections 19 | - No auto-ternary operators 20 | - 2-space indentation for Ruby files 21 | - 4-space indentation for C/C++ files 22 | 23 | ### Type System Conventions 24 | - Use Ruby terminology: "methods" not "functions" 25 | - Type representations: `([(param_types)] -> return_type)` 26 | - Concrete types: simple names (int, str, bool, nil) 27 | - Object types: `(object ClassName)` 28 | - Method types: `([(receiver, params)] -> return_type)` 29 | 30 | ### Testing 31 | - Use Minitest with spec-style syntax 32 | - Custom `must_equal_with_diff` matcher for complex comparisons 33 | - Test files end with `_spec.rb` 34 | - Use descriptive test names: `'compiles method definitions'` 35 | - Group related tests in describe blocks 36 | 37 | ### Comments 38 | - Avoid comments that just explain what code does 39 | - Use comments for complex algorithms or non-obvious decisions 40 | - Document public APIs and class purposes 41 | - Prefer self-documenting code over comments 42 | 43 | ### File Organization 44 | - `lib/mya/` - Main library code 45 | - `lib/mya/compiler/` - Compiler components 46 | - `spec/` - Test files 47 | - `examples/` - Example Mya programs 48 | - `src/` - C code for runtime 49 | 50 | ### Dependencies 51 | - **Prism**: Ruby parser 52 | - **ruby-llvm**: LLVM bindings 53 | - **Minitest**: Testing framework 54 | - **SyntaxTree**: Code formatting 55 | 56 | ### Development Workflow 57 | - Use `bundle exec rake spec` to run tests 58 | - Use `bundle exec rake lint` for style checking 59 | - Use `bundle exec rake watch` for continuous testing 60 | - Docker support available for consistent environments 61 | 62 | ## CLI Usage for Debugging and Inspection 63 | 64 | The `bin/mya` binary provides options for examining compiler output and executing code: 65 | 66 | ### Basic Usage 67 | ```bash 68 | # Execute code with LLVM backend (default) 69 | ./bin/mya -e "1 + 2" 70 | ./bin/mya examples/fact.rb 71 | 72 | # Show help 73 | ./bin/mya --help 74 | ``` 75 | 76 | ### Debug Output 77 | ```bash 78 | # Show compiled instructions (our compiler IR) 79 | ./bin/mya -d ir -e "def add(a, b); a + b; end; add(1, 2)" 80 | 81 | # Show generated LLVM IR 82 | ./bin/mya -d llvm-ir -e "1 + 2" 83 | 84 | # Write LLVM IR to file using shell redirection 85 | ./bin/mya -d llvm-ir examples/fact.rb > output.ll 86 | ``` 87 | 88 | ### Backend Selection 89 | ```bash 90 | # Execute with LLVM backend (default) 91 | ./bin/mya --backend llvm examples/fact.rb 92 | 93 | # Execute with VM backend 94 | ./bin/mya --backend vm examples/fact.rb 95 | ``` 96 | 97 | ### Available Options 98 | - `-e CODE` - Execute code string 99 | - `-d DEBUG` - Show debug output (ir, llvm-ir) - does not execute code 100 | - `--backend BACKEND` - Execute with specified backend (vm, llvm) 101 | - `-h, --help` - Show help 102 | 103 | ### Examples 104 | ```bash 105 | # Quick instruction inspection 106 | ./bin/mya -d ir -e "1 + 2" 107 | 108 | # Generate LLVM IR file for analysis 109 | ./bin/mya -d llvm-ir examples/countdown.rb > analysis.ll 110 | 111 | # Execute with specific backend 112 | ./bin/mya --backend vm examples/fact.rb 113 | ``` 114 | -------------------------------------------------------------------------------- /devbox.lock: -------------------------------------------------------------------------------- 1 | { 2 | "lockfile_version": "1", 3 | "packages": { 4 | "llvmPackages_18.libllvm@latest": { 5 | "last_modified": "2024-07-07T16:08:25Z", 6 | "resolved": "github:NixOS/nixpkgs/ab82a9612aa45284d4adf69ee81871a389669a9e#llvmPackages_18.libllvm", 7 | "source": "devbox-search", 8 | "version": "18.1.8", 9 | "systems": { 10 | "aarch64-darwin": { 11 | "outputs": [ 12 | { 13 | "name": "out", 14 | "path": "/nix/store/fadxiq7xp7fgawc91ivkbzz0illrjdrk-llvm-18.1.8", 15 | "default": true 16 | }, 17 | { 18 | "name": "dev", 19 | "path": "/nix/store/lc6cg7nqvpjjivnxp49c3c6i3rdk40ns-llvm-18.1.8-dev" 20 | }, 21 | { 22 | "name": "lib", 23 | "path": "/nix/store/cwishkdfydw04lxf0gbx2i55m10vqnfk-llvm-18.1.8-lib" 24 | }, 25 | { 26 | "name": "python", 27 | "path": "/nix/store/mvad6pw7yzrc5chvpr2kp5qbwqffyn7n-llvm-18.1.8-python" 28 | } 29 | ], 30 | "store_path": "/nix/store/fadxiq7xp7fgawc91ivkbzz0illrjdrk-llvm-18.1.8" 31 | }, 32 | "aarch64-linux": { 33 | "outputs": [ 34 | { 35 | "name": "out", 36 | "path": "/nix/store/3iq0alf4hfvgjpv726ydbxwj3bkzagm9-llvm-18.1.8", 37 | "default": true 38 | }, 39 | { 40 | "name": "python", 41 | "path": "/nix/store/0f17as28bmg4xjrr7dc3zjn943ckwbkc-llvm-18.1.8-python" 42 | }, 43 | { 44 | "name": "dev", 45 | "path": "/nix/store/vqpxjd9yvasna8wi9zhb65vb580w7dgb-llvm-18.1.8-dev" 46 | }, 47 | { 48 | "name": "lib", 49 | "path": "/nix/store/8y9x00lgz83f9s2fn0l07xpp9q7gxa64-llvm-18.1.8-lib" 50 | } 51 | ], 52 | "store_path": "/nix/store/3iq0alf4hfvgjpv726ydbxwj3bkzagm9-llvm-18.1.8" 53 | }, 54 | "x86_64-darwin": { 55 | "outputs": [ 56 | { 57 | "name": "out", 58 | "path": "/nix/store/s0nfnzbdc2s681m1ll4a23xc4v5w4a7c-llvm-18.1.8", 59 | "default": true 60 | }, 61 | { 62 | "name": "python", 63 | "path": "/nix/store/p3z1df0flwlpfjh532ssi6069c244h8f-llvm-18.1.8-python" 64 | }, 65 | { 66 | "name": "dev", 67 | "path": "/nix/store/42nr7l94ryylvg1y0vzb43x526x8zzz8-llvm-18.1.8-dev" 68 | }, 69 | { 70 | "name": "lib", 71 | "path": "/nix/store/43wdpw9nws5jl985r6rdfgdwy30rv6vx-llvm-18.1.8-lib" 72 | } 73 | ], 74 | "store_path": "/nix/store/s0nfnzbdc2s681m1ll4a23xc4v5w4a7c-llvm-18.1.8" 75 | }, 76 | "x86_64-linux": { 77 | "outputs": [ 78 | { 79 | "name": "out", 80 | "path": "/nix/store/16cm18xwbw4rs123hq5wjk3dvwa40vcf-llvm-18.1.8", 81 | "default": true 82 | }, 83 | { 84 | "name": "dev", 85 | "path": "/nix/store/wijipxmz3lnwbcwrwl5r34mw20qlqwi1-llvm-18.1.8-dev" 86 | }, 87 | { 88 | "name": "lib", 89 | "path": "/nix/store/fr74q0nf9wlkfmf1qqmsklykmc09sic6-llvm-18.1.8-lib" 90 | }, 91 | { 92 | "name": "python", 93 | "path": "/nix/store/prajkhjryw40nw6ikfrm60hypidn1f9q-llvm-18.1.8-python" 94 | } 95 | ], 96 | "store_path": "/nix/store/16cm18xwbw4rs123hq5wjk3dvwa40vcf-llvm-18.1.8" 97 | } 98 | } 99 | }, 100 | "ruby_3_3@latest": { 101 | "last_modified": "2024-07-07T07:43:47Z", 102 | "plugin_version": "0.0.2", 103 | "resolved": "github:NixOS/nixpkgs/b60793b86201040d9dee019a05089a9150d08b5b#ruby_3_3", 104 | "source": "devbox-search", 105 | "version": "3.3.2", 106 | "systems": { 107 | "aarch64-darwin": { 108 | "outputs": [ 109 | { 110 | "name": "out", 111 | "path": "/nix/store/qfaw55crdlvajfkpmybjfq4cfmbmm86r-ruby-3.3.2", 112 | "default": true 113 | }, 114 | { 115 | "name": "devdoc", 116 | "path": "/nix/store/vsda7rk49jvwcpjzlxdw13yibvdgjhqd-ruby-3.3.2-devdoc" 117 | } 118 | ], 119 | "store_path": "/nix/store/qfaw55crdlvajfkpmybjfq4cfmbmm86r-ruby-3.3.2" 120 | }, 121 | "aarch64-linux": { 122 | "outputs": [ 123 | { 124 | "name": "out", 125 | "path": "/nix/store/q4jhbigza58sb5bbsygirw3g8ffx567g-ruby-3.3.2", 126 | "default": true 127 | }, 128 | { 129 | "name": "devdoc", 130 | "path": "/nix/store/532fwakiad4xajmmgrjqmvyn1r90n1a0-ruby-3.3.2-devdoc" 131 | } 132 | ], 133 | "store_path": "/nix/store/q4jhbigza58sb5bbsygirw3g8ffx567g-ruby-3.3.2" 134 | }, 135 | "x86_64-darwin": { 136 | "outputs": [ 137 | { 138 | "name": "out", 139 | "path": "/nix/store/5vhq60d1wylzfj47m9lx788zgx95dl8h-ruby-3.3.2", 140 | "default": true 141 | }, 142 | { 143 | "name": "devdoc", 144 | "path": "/nix/store/czvcsmhn6nf9j7mwlj2g94cqk3sldayv-ruby-3.3.2-devdoc" 145 | } 146 | ], 147 | "store_path": "/nix/store/5vhq60d1wylzfj47m9lx788zgx95dl8h-ruby-3.3.2" 148 | }, 149 | "x86_64-linux": { 150 | "outputs": [ 151 | { 152 | "name": "out", 153 | "path": "/nix/store/mlmfwq1bxcbnww5szh5c1kmlzq13pqbr-ruby-3.3.2", 154 | "default": true 155 | }, 156 | { 157 | "name": "devdoc", 158 | "path": "/nix/store/6bnh1i13ggkwly1kahmv7mfmi0gywx64-ruby-3.3.2-devdoc" 159 | } 160 | ], 161 | "store_path": "/nix/store/mlmfwq1bxcbnww5szh5c1kmlzq13pqbr-ruby-3.3.2" 162 | } 163 | } 164 | } 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /lib/mya/compiler/instruction.rb: -------------------------------------------------------------------------------- 1 | class Compiler 2 | class Instruction 3 | def initialize(line: nil) 4 | @line = line 5 | end 6 | 7 | attr_reader :line 8 | 9 | attr_writer :type 10 | 11 | def type! 12 | return @pruned_type if @pruned_type 13 | 14 | raise "No type set on #{inspect}" unless @type 15 | 16 | resolved = @type.resolve! 17 | @pruned_type = resolved 18 | end 19 | 20 | def inspect 21 | "<#{self.class.name} #{instance_variables.map { |iv| "#{iv}=#{instance_variable_get(iv).inspect}" }.join(' ')}>" 22 | end 23 | 24 | def to_h 25 | { type: type!.to_s, instruction: instruction_name } 26 | end 27 | end 28 | 29 | class PushIntInstruction < Instruction 30 | def initialize(value, line:) 31 | super(line:) 32 | @value = value 33 | end 34 | 35 | attr_reader :value 36 | 37 | def instruction_name = :push_int 38 | 39 | def to_h 40 | super.merge(value:) 41 | end 42 | end 43 | 44 | class PushStrInstruction < Instruction 45 | def initialize(value, line:) 46 | super(line:) 47 | @value = value 48 | end 49 | 50 | attr_reader :value 51 | 52 | def instruction_name = :push_str 53 | 54 | def to_h 55 | super.merge(value:) 56 | end 57 | end 58 | 59 | class PushTrueInstruction < Instruction 60 | def instruction_name = :push_true 61 | end 62 | 63 | class PushFalseInstruction < Instruction 64 | def instruction_name = :push_false 65 | end 66 | 67 | class PushNilInstruction < Instruction 68 | def instruction_name = :push_nil 69 | end 70 | 71 | class PushArrayInstruction < Instruction 72 | def initialize(size, line:) 73 | super(line:) 74 | @size = size 75 | end 76 | 77 | attr_reader :size 78 | 79 | def instruction_name = :push_array 80 | 81 | def to_h 82 | super.merge(size:) 83 | end 84 | end 85 | 86 | class PushVarInstruction < Instruction 87 | def initialize(name, line:) 88 | super(line:) 89 | @name = name 90 | end 91 | 92 | attr_reader :name 93 | 94 | def instruction_name = :push_var 95 | 96 | def to_h 97 | super.merge(name:) 98 | end 99 | end 100 | 101 | class SetVarInstruction < Instruction 102 | def initialize(name, line:) 103 | super(line:) 104 | @name = name 105 | @type_annotation = nil 106 | end 107 | 108 | attr_reader :name 109 | attr_accessor :type_annotation 110 | 111 | def instruction_name = :set_var 112 | 113 | def to_h 114 | super.merge(name:) 115 | end 116 | end 117 | 118 | class SetInstanceVarInstruction < Instruction 119 | def initialize(name, line:) 120 | super(line:) 121 | @name = name 122 | @type_annotation = nil 123 | end 124 | 125 | attr_reader :name 126 | attr_accessor :type_annotation 127 | 128 | def instruction_name = :set_ivar 129 | 130 | def to_h 131 | result = super.merge(name:) 132 | result[:type_annotation] = @type_annotation if @type_annotation 133 | result 134 | end 135 | end 136 | 137 | class PushInstanceVarInstruction < Instruction 138 | def initialize(name, line:) 139 | super(line:) 140 | @name = name 141 | end 142 | 143 | attr_reader :name 144 | 145 | def instruction_name = :push_ivar 146 | 147 | def to_h 148 | super.merge(name:) 149 | end 150 | end 151 | 152 | class PushArgInstruction < Instruction 153 | def initialize(index, line:) 154 | super(line:) 155 | @index = index 156 | end 157 | 158 | attr_reader :index 159 | 160 | def instruction_name = :push_arg 161 | 162 | def to_h 163 | super.merge(index:) 164 | end 165 | end 166 | 167 | class PushConstInstruction < Instruction 168 | def initialize(name, line:) 169 | super(line:) 170 | @name = name 171 | end 172 | 173 | attr_reader :name 174 | 175 | def instruction_name = :push_const 176 | 177 | def to_h 178 | super.merge(name:) 179 | end 180 | end 181 | 182 | class CallInstruction < Instruction 183 | def initialize(name, arg_count:, line:) 184 | super(line:) 185 | @name = name 186 | @arg_count = arg_count 187 | end 188 | 189 | attr_reader :name, :arg_count 190 | attr_accessor :method_type 191 | 192 | def instruction_name = :call 193 | 194 | def to_h 195 | result = super.merge(name:, arg_count:) 196 | result[:method_type] = @method_type.to_s if @method_type 197 | result 198 | end 199 | end 200 | 201 | class IfInstruction < Instruction 202 | attr_accessor :if_true, :if_false, :used 203 | 204 | def instruction_name = :if 205 | 206 | def to_h 207 | super.merge(if_true: if_true.map(&:to_h), if_false: if_false.map(&:to_h)) 208 | end 209 | end 210 | 211 | class WhileInstruction < Instruction 212 | attr_accessor :condition, :body 213 | 214 | def instruction_name = :while 215 | 216 | def to_h 217 | super.merge(condition: condition.map(&:to_h), body: body.map(&:to_h)) 218 | end 219 | end 220 | 221 | class DefInstruction < Instruction 222 | def initialize(name, line:) 223 | super(line:) 224 | @name = name 225 | @params = [] 226 | @type_annotations = {} 227 | @return_type_annotation = nil 228 | end 229 | 230 | attr_reader :name 231 | attr_accessor :body, :params, :type_annotations, :return_type_annotation 232 | 233 | def instruction_name = :def 234 | 235 | def receiver_type = type!.self_type 236 | def return_type = type!.return_type 237 | 238 | def to_h 239 | super.merge(name:, params:, body: body.map(&:to_h)) 240 | end 241 | end 242 | 243 | class ClassInstruction < Instruction 244 | def initialize(name, line:, superclass: nil) 245 | super(line:) 246 | @name = name 247 | @superclass = superclass 248 | end 249 | 250 | attr_reader :name, :superclass 251 | attr_accessor :body 252 | 253 | def instruction_name = :class 254 | 255 | def to_h 256 | result = super.merge(name:, body: body.map(&:to_h)) 257 | result[:superclass] = @superclass if @superclass 258 | result 259 | end 260 | end 261 | 262 | class PopInstruction < Instruction 263 | def instruction_name = :pop 264 | end 265 | 266 | class PushSelfInstruction < Instruction 267 | def instruction_name = :push_self 268 | end 269 | end 270 | -------------------------------------------------------------------------------- /lib/mya/compiler/backends/vm_backend.rb: -------------------------------------------------------------------------------- 1 | class Compiler 2 | module Backends 3 | class VMBackend 4 | class ClassType 5 | def initialize(name, superclass: nil) 6 | @name = name 7 | @superclass = superclass 8 | @methods = {} 9 | end 10 | 11 | attr_reader :methods, :name, :superclass 12 | 13 | def new(*args) 14 | ObjectType.new(self) 15 | end 16 | 17 | def find_method(name) 18 | return @methods[name] if @methods.key?(name) 19 | return @superclass.find_method(name) if @superclass 20 | nil 21 | end 22 | end 23 | 24 | class ObjectType 25 | def initialize(klass) 26 | @klass = klass 27 | @ivars = {} 28 | end 29 | 30 | attr_reader :klass 31 | 32 | def methods = klass.methods 33 | 34 | def set_ivar(name, value) 35 | @ivars[name] = value 36 | end 37 | 38 | def get_ivar(name) 39 | @ivars[name] 40 | end 41 | end 42 | 43 | class OptionType 44 | def initialize(value) 45 | @value = value 46 | end 47 | 48 | attr_reader :value 49 | 50 | def methods 51 | { 52 | value!: [{ instruction: :push_option_value }], 53 | is_some: [{ instruction: :push_option_is_some }], 54 | is_none: [{ instruction: :push_option_is_none }], 55 | } 56 | end 57 | 58 | def to_bool 59 | !@value.nil? 60 | end 61 | 62 | def nil? 63 | @value.nil? 64 | end 65 | end 66 | 67 | # Create the base Object class with built-in methods 68 | ObjectClass = ClassType.new('Object') 69 | ObjectClass.methods[:puts] = [{ instruction: :builtin_puts }] 70 | 71 | MainClass = ClassType.new('main', superclass: ObjectClass) 72 | MainObject = ObjectType.new(MainClass) 73 | 74 | def initialize(instructions, io: $stdout) 75 | @instructions = instructions 76 | @stack = [] 77 | @frames = [{ instructions:, return_index: nil }] 78 | @scope_stack = [{ args: [], vars: {}, self_obj: MainObject }] 79 | @if_depth = 0 80 | @classes = { 'Object' => ObjectClass } 81 | @io = io 82 | end 83 | 84 | def run 85 | @index = 0 86 | execute_frame_stack 87 | @stack.pop 88 | end 89 | 90 | private 91 | 92 | def instructions 93 | @frames.last.fetch(:instructions) 94 | end 95 | 96 | BUILT_IN_METHODS = { 97 | puts: ->(arg, io:) do 98 | io.puts(arg) 99 | arg.to_s.size 100 | end, 101 | }.freeze 102 | 103 | def execute(instruction) 104 | send("execute_#{instruction.instruction_name}", instruction) 105 | end 106 | 107 | def execute_class(instruction) 108 | superclass = 109 | if instruction.superclass 110 | @classes[instruction.superclass] 111 | elsif instruction.name != 'Object' 112 | @classes['Object'] 113 | else 114 | nil 115 | end 116 | klass = @classes[instruction.name] = ClassType.new(instruction.name, superclass:) 117 | push_frame(instructions: instruction.body, return_index: @index, with_scope: true) 118 | @scope_stack << { vars: {}, self_obj: klass } 119 | end 120 | 121 | def execute_def(instruction) 122 | self_obj.methods[instruction.name] = instruction.body 123 | end 124 | 125 | def execute_call(instruction) 126 | new_args = @stack.pop(instruction.arg_count) 127 | 128 | receiver = @stack.pop 129 | name = instruction.name 130 | 131 | if receiver.is_a?(OptionType) 132 | case name 133 | when :'value!' 134 | raise RuntimeError, 'Cannot call value! on None option' if receiver.nil? 135 | @stack << receiver.value 136 | return 137 | when :is_some 138 | @stack << !receiver.nil? 139 | return 140 | when :is_none 141 | @stack << receiver.nil? 142 | return 143 | end 144 | end 145 | 146 | if receiver.respond_to?(name) 147 | result = receiver.send(name, *new_args) 148 | @stack << result 149 | 150 | if name == :new && receiver.is_a?(ClassType) && (initialize_method = receiver.find_method(:initialize)) 151 | instance = result 152 | initialize_frame = { instructions: initialize_method, return_index: nil, with_scope: true } 153 | initialize_scope = { args: new_args, vars: {}, self_obj: instance } 154 | execute_frames([initialize_frame], scope: initialize_scope) 155 | @stack << instance 156 | end 157 | 158 | return 159 | end 160 | 161 | if receiver.is_a?(ClassType) && (method_body = receiver.find_method(name)) 162 | if method_body.is_a?(Array) && method_body.first.is_a?(Hash) && 163 | method_body.first[:instruction] == :builtin_puts 164 | @stack << BUILT_IN_METHODS[:puts].call(*new_args, io: @io) 165 | return 166 | end 167 | push_frame(instructions: method_body, return_index: @index, with_scope: true) 168 | @scope_stack << { args: new_args, vars: {}, self_obj: receiver } 169 | return 170 | end 171 | 172 | if receiver.is_a?(ObjectType) && (method_body = receiver.klass.find_method(name)) 173 | if method_body.is_a?(Array) && method_body.first.is_a?(Hash) && 174 | method_body.first[:instruction] == :builtin_puts 175 | @stack << BUILT_IN_METHODS[:puts].call(*new_args, io: @io) 176 | return 177 | end 178 | push_frame(instructions: method_body, return_index: @index, with_scope: true) 179 | @scope_stack << { args: new_args, vars: {}, self_obj: receiver } 180 | return 181 | end 182 | 183 | raise NoMethodError, "Undefined method #{instruction.name} for #{receiver.class}" 184 | end 185 | 186 | def execute_if(instruction) 187 | condition = @stack.pop 188 | 189 | condition = condition.to_bool if condition.is_a?(OptionType) 190 | 191 | body = condition ? instruction.if_true : instruction.if_false 192 | push_frame(instructions: body, return_index: @index) 193 | end 194 | 195 | def execute_while(instruction) 196 | loop do 197 | condition_frame = { instructions: instruction.condition, return_index: nil, with_scope: false } 198 | condition_result = execute_frames([condition_frame]) 199 | 200 | condition_result = condition_result.to_bool if condition_result.is_a?(OptionType) 201 | 202 | break unless condition_result 203 | 204 | body_frame = { instructions: instruction.body, return_index: nil, with_scope: false } 205 | execute_frames([body_frame]) 206 | end 207 | 208 | # While loops always return nil 209 | @stack << nil 210 | end 211 | 212 | def execute_pop(_) 213 | @stack.pop 214 | end 215 | 216 | private 217 | 218 | def execute_frames(frames, scope: nil) 219 | saved_frames = @frames 220 | saved_index = @index 221 | 222 | @scope_stack << scope if scope 223 | 224 | @frames = frames 225 | @index = 0 226 | 227 | execute_frame_stack 228 | 229 | result = @stack.last 230 | 231 | @frames = saved_frames 232 | @index = saved_index 233 | 234 | result 235 | end 236 | 237 | def execute_frame_stack 238 | while @frames.any? 239 | while @index < instructions.size 240 | instruction = instructions[@index] 241 | @index += 1 242 | execute(instruction) 243 | end 244 | frame = @frames.pop 245 | @scope_stack.pop if frame[:with_scope] 246 | @index = frame[:return_index] if frame[:return_index] 247 | end 248 | end 249 | 250 | def execute_push_arg(instruction) 251 | arg = args.fetch(instruction.index) 252 | 253 | if instruction.type!.name == :Option 254 | arg = OptionType.new(arg) unless arg.is_a?(OptionType) 255 | end 256 | 257 | @stack << arg 258 | end 259 | 260 | def execute_push_array(instruction) 261 | ary = @stack.pop(instruction.size) 262 | @stack << ary 263 | end 264 | 265 | def execute_push_const(instruction) 266 | @stack << @classes[instruction.name] 267 | end 268 | 269 | def execute_push_false(_) 270 | @stack << false 271 | end 272 | 273 | def execute_push_int(instruction) 274 | @stack << instruction.value 275 | end 276 | 277 | def execute_push_nil(_) 278 | @stack << nil 279 | end 280 | 281 | def execute_push_str(instruction) 282 | @stack << instruction.value 283 | end 284 | 285 | def execute_push_self(_instruction) 286 | @stack << self_obj 287 | end 288 | 289 | def execute_push_true(_) 290 | @stack << true 291 | end 292 | 293 | def execute_push_var(instruction) 294 | var = vars.fetch(instruction.name) 295 | 296 | if instruction.type!.name == :Option 297 | var = OptionType.new(var) unless var.is_a?(OptionType) 298 | end 299 | 300 | @stack << var 301 | end 302 | 303 | def execute_set_var(instruction) 304 | value = @stack.pop 305 | 306 | if instruction.type!.name == :Option 307 | value = OptionType.new(value) unless value.is_a?(OptionType) 308 | end 309 | 310 | vars[instruction.name] = value 311 | end 312 | 313 | def execute_set_ivar(instruction) 314 | value = @stack.last 315 | self_obj.set_ivar(instruction.name, value) 316 | end 317 | 318 | def execute_push_ivar(instruction) 319 | value = self_obj.get_ivar(instruction.name) 320 | @stack << value 321 | end 322 | 323 | def push_frame(instructions:, return_index:, with_scope: false) 324 | @frames << { instructions:, return_index:, with_scope: } 325 | @index = 0 326 | end 327 | 328 | def scope 329 | @scope_stack.last 330 | end 331 | 332 | def self_obj 333 | scope.fetch(:self_obj) 334 | end 335 | 336 | def vars 337 | scope.fetch(:vars) 338 | end 339 | 340 | def args 341 | scope.fetch(:args) 342 | end 343 | end 344 | end 345 | end 346 | -------------------------------------------------------------------------------- /lib/mya/compiler.rb: -------------------------------------------------------------------------------- 1 | require 'bundler/setup' 2 | require 'prism' 3 | require_relative 'compiler/instruction' 4 | require_relative 'compiler/type_checker' 5 | require_relative 'compiler/backends/llvm_backend' 6 | require_relative 'compiler/backends/vm_backend' 7 | 8 | class Compiler 9 | def initialize(code) 10 | @code = code 11 | @result = Prism.parse(code) 12 | @ast = @result.value 13 | @directives = 14 | @result 15 | .comments 16 | .each_with_object({}) do |comment, directives| 17 | text = comment.location.slice 18 | line = comment.location.start_line 19 | directives[line] ||= {} 20 | 21 | # Parse type annotations like "a:Integer, b:String" 22 | if text =~ /#\s*(.+)/ 23 | annotation_text = $1.strip 24 | type_annotations = parse_type_annotations(annotation_text) 25 | directives[line][:type_annotations] = type_annotations unless type_annotations.empty? 26 | end 27 | 28 | # Parse existing directives 29 | text.split.each do |directive| 30 | next unless directive =~ /^([a-z][a-z_]*):([a-z_]+)$/ 31 | 32 | directives[line][$1.to_sym] ||= [] 33 | directives[line][$1.to_sym] << $2.to_sym 34 | end 35 | end 36 | end 37 | 38 | def parse_type_annotations(text) 39 | return {} if text.nil? || text.strip.empty? 40 | TypeAnnotationParser.new(text).parse 41 | end 42 | 43 | class TypeAnnotationParser 44 | def initialize(text) 45 | @text = text.strip 46 | @pos = 0 47 | end 48 | 49 | def parse 50 | annotations = {} 51 | return annotations if @text.empty? 52 | 53 | # Parse parameters 54 | while @pos < @text.length && !peek_return_arrow? 55 | skip_whitespace 56 | break if @pos >= @text.length || peek_return_arrow? 57 | 58 | # If we can't parse a variable name, we're done with parameters 59 | break unless can_parse_variable_name? 60 | 61 | var_name = parse_variable_name 62 | skip_whitespace 63 | 64 | # If no colon follows, this isn't a type annotation 65 | break unless peek(':') 66 | 67 | expect(':') 68 | skip_whitespace 69 | type_spec = parse_type_spec 70 | 71 | annotations[var_name.to_sym] = type_spec 72 | 73 | skip_whitespace 74 | if peek(',') 75 | consume(',') 76 | skip_whitespace 77 | end 78 | end 79 | 80 | # Parse return type 81 | if peek_return_arrow? 82 | consume('-') 83 | consume('>') 84 | skip_whitespace 85 | annotations[:return_type] = parse_type_spec 86 | end 87 | 88 | annotations 89 | end 90 | 91 | private 92 | 93 | def can_parse_variable_name? 94 | saved_pos = @pos 95 | consume('@') if peek('@') 96 | result = peek(/[a-zA-Z_]/) 97 | @pos = saved_pos 98 | result 99 | end 100 | 101 | def parse_variable_name 102 | start = @pos 103 | consume('@') if peek('@') 104 | 105 | raise "Expected variable name at position #{@pos}" unless peek(/[a-zA-Z_]/) 106 | 107 | @pos += 1 while peek(/[a-zA-Z0-9_]/) 108 | 109 | @text[start...@pos] 110 | end 111 | 112 | def parse_type_spec 113 | type_name = parse_identifier 114 | 115 | if peek('[') 116 | consume('[') 117 | skip_whitespace 118 | inner_type = parse_type_spec 119 | skip_whitespace 120 | expect(']') 121 | { generic: type_name.to_sym, inner: inner_type } 122 | else 123 | type_name.to_sym 124 | end 125 | end 126 | 127 | def parse_identifier 128 | start = @pos 129 | 130 | raise "Expected identifier at position #{@pos}" unless peek(/[a-zA-Z]/) 131 | 132 | @pos += 1 while peek(/[a-zA-Z0-9_]/) 133 | 134 | @text[start...@pos] 135 | end 136 | 137 | def peek(pattern = nil) 138 | return false if @pos >= @text.length 139 | 140 | if pattern.is_a?(Regexp) 141 | @text[@pos] =~ pattern 142 | elsif pattern.is_a?(String) 143 | @text[@pos...@pos + pattern.length] == pattern 144 | else 145 | @text[@pos] 146 | end 147 | end 148 | 149 | def peek_return_arrow? 150 | peek('-') && @pos + 1 < @text.length && @text[@pos + 1] == '>' 151 | end 152 | 153 | def consume(expected) 154 | if peek(expected) 155 | if expected.is_a?(String) 156 | @pos += expected.length 157 | else 158 | @pos += 1 159 | end 160 | else 161 | raise "Expected '#{expected}' at position #{@pos}, got '#{@text[@pos]}'" 162 | end 163 | end 164 | 165 | def expect(expected) 166 | consume(expected) 167 | end 168 | 169 | def skip_whitespace 170 | @pos += 1 while peek(/\s/) 171 | end 172 | end 173 | 174 | def compile 175 | @scope_stack = [{ vars: {} }] 176 | @instructions = [] 177 | transform(@ast, used: true) 178 | type_check 179 | @instructions 180 | end 181 | 182 | private 183 | 184 | def transform(node, used:) 185 | send("transform_#{node.type}", node, used:) 186 | end 187 | 188 | def transform_array_node(node, used:) 189 | node.elements.each { |element| transform(element, used:) } 190 | instruction = PushArrayInstruction.new(node.elements.size, line: node.location.start_line) 191 | @instructions << instruction 192 | @instructions << PopInstruction.new unless used 193 | end 194 | 195 | def transform_call_node(node, used:) 196 | node.receiver ? transform(node.receiver, used: true) : @instructions << PushSelfInstruction.new 197 | args = node.arguments&.arguments || [] 198 | args.each { |arg| transform(arg, used: true) } 199 | instruction = CallInstruction.new(node.name, arg_count: args.size, line: node.location.start_line) 200 | @instructions << instruction 201 | @instructions << PopInstruction.new unless used 202 | end 203 | 204 | def transform_class_node(node, used:) 205 | name = node.constant_path.name 206 | superclass = node.superclass&.name 207 | instruction = ClassInstruction.new(name, line: node.location.start_line, superclass:) 208 | class_instructions = [] 209 | with_instructions_array(class_instructions) { transform(node.body, used: false) } if node.body 210 | instruction.body = class_instructions 211 | @instructions << instruction 212 | @instructions << PushConstInstruction.new(instruction.name, line: node.location.start_line) if used 213 | end 214 | 215 | def transform_constant_read_node(node, used:) 216 | return unless used 217 | 218 | instruction = PushConstInstruction.new(node.name, line: node.location.start_line) 219 | @instructions << instruction 220 | end 221 | 222 | def transform_def_node(node, used:) 223 | @scope_stack << { vars: {} } 224 | params = node.parameters&.requireds || [] 225 | instruction = DefInstruction.new(node.name, line: node.location.start_line) 226 | 227 | if (line_directives = @directives[node.location.start_line]) && (annotations = line_directives[:type_annotations]) 228 | param_annotations = annotations.reject { |key, _| key == :return_type } 229 | instruction.type_annotations = param_annotations unless param_annotations.empty? 230 | instruction.return_type_annotation = annotations[:return_type] if annotations[:return_type] 231 | end 232 | 233 | def_instructions = [] 234 | params.each_with_index do |param, index| 235 | i1 = PushArgInstruction.new(index, line: node.location.start_line) 236 | def_instructions << i1 237 | i2 = SetVarInstruction.new(param.name, line: node.location.start_line) 238 | def_instructions << i2 239 | instruction.params << param.name 240 | end 241 | with_instructions_array(def_instructions) { transform(node.body, used: true) } 242 | instruction.body = def_instructions 243 | @scope_stack.pop 244 | @instructions << instruction 245 | @instructions << PushStrInstruction.new(instruction.name, line: node.location.start_line) if used 246 | end 247 | 248 | def transform_else_node(node, used:) 249 | transform(node.statements, used:) 250 | end 251 | 252 | def transform_false_node(node, used:) 253 | return unless used 254 | 255 | instruction = PushFalseInstruction.new(line: node.location.start_line) 256 | @instructions << instruction 257 | end 258 | 259 | def transform_if_node(node, used:) 260 | transform(node.predicate, used: true) 261 | instruction = IfInstruction.new(line: node.location.start_line) 262 | instruction.used = used 263 | instruction.if_true = [] 264 | with_instructions_array(instruction.if_true) do 265 | transform(node.statements, used: true) 266 | @instructions << PopInstruction.new unless used 267 | end 268 | instruction.if_false = [] 269 | if node.subsequent 270 | with_instructions_array(instruction.if_false) do 271 | transform(node.subsequent, used: true) 272 | @instructions << PopInstruction.new unless used 273 | end 274 | elsif used 275 | raise SyntaxError, "if expression used as value must have an else clause (line #{node.location.start_line})" 276 | else 277 | # If statement without else clause - push nil but it won't be used 278 | with_instructions_array(instruction.if_false) { @instructions << PushNilInstruction.new } 279 | end 280 | @instructions << instruction 281 | @instructions << PopInstruction.new unless used 282 | end 283 | 284 | def transform_instance_variable_write_node(node, used:) 285 | transform(node.value, used: true) 286 | instruction = SetInstanceVarInstruction.new(node.name, line: node.location.start_line) 287 | 288 | if (line_directives = @directives[node.location.start_line]) && 289 | (annotations = line_directives[:type_annotations]) && (type_annotation = annotations[node.name]) 290 | instruction.type_annotation = type_annotation 291 | end 292 | 293 | @instructions << instruction 294 | @instructions << PopInstruction.new unless used 295 | end 296 | 297 | def transform_instance_variable_read_node(node, used:) 298 | return unless used 299 | instruction = PushInstanceVarInstruction.new(node.name, line: node.location.start_line) 300 | @instructions << instruction 301 | end 302 | 303 | def transform_integer_node(node, used:) 304 | return unless used 305 | 306 | instruction = PushIntInstruction.new(node.value, line: node.location.start_line) 307 | @instructions << instruction 308 | end 309 | 310 | def transform_local_variable_read_node(node, used:) 311 | return unless used 312 | 313 | instruction = PushVarInstruction.new(node.name, line: node.location.start_line) 314 | @instructions << instruction 315 | end 316 | 317 | def transform_local_variable_write_node(node, used:) 318 | transform(node.value, used: true) 319 | instruction = SetVarInstruction.new(node.name, line: node.location.start_line) 320 | 321 | if (line_directives = @directives[node.location.start_line]) && 322 | (annotations = line_directives[:type_annotations]) && (type_annotation = annotations[node.name]) 323 | instruction.type_annotation = type_annotation 324 | end 325 | 326 | @instructions << instruction 327 | @instructions << PushVarInstruction.new(node.name, line: node.location.start_line) if used 328 | end 329 | 330 | def transform_nil_node(node, used:) 331 | return unless used 332 | 333 | instruction = PushNilInstruction.new(line: node.location.start_line) 334 | @instructions << instruction 335 | end 336 | 337 | def transform_program_node(node, used:) 338 | transform(node.statements, used:) 339 | end 340 | 341 | def transform_statements_node(node, used:) 342 | node.body.each_with_index { |n, i| transform(n, used: used && i == node.body.size - 1) } 343 | end 344 | 345 | def transform_string_node(node, used:) 346 | return unless used 347 | 348 | instruction = PushStrInstruction.new(node.unescaped, line: node.location.start_line) 349 | @instructions << instruction 350 | end 351 | 352 | def transform_true_node(node, used:) 353 | return unless used 354 | 355 | instruction = PushTrueInstruction.new(line: node.location.start_line) 356 | @instructions << instruction 357 | end 358 | 359 | def transform_while_node(node, used:) 360 | instruction = WhileInstruction.new(line: node.location.start_line) 361 | 362 | instruction.condition = [] 363 | with_instructions_array(instruction.condition) { transform(node.predicate, used: true) } 364 | 365 | # Transform body - the result is not used since while always returns nil 366 | instruction.body = [] 367 | with_instructions_array(instruction.body) { transform(node.statements, used: false) } 368 | 369 | @instructions << instruction 370 | @instructions << PopInstruction.new unless used 371 | end 372 | 373 | def with_instructions_array(array) 374 | array_was = @instructions 375 | @instructions = array 376 | yield 377 | ensure 378 | @instructions = array_was 379 | end 380 | 381 | def type_check 382 | TypeChecker.new.analyze(@instructions) 383 | end 384 | 385 | def scope 386 | @scope_stack.last 387 | end 388 | 389 | def vars 390 | scope.fetch(:vars) 391 | end 392 | end 393 | -------------------------------------------------------------------------------- /lib/mya/compiler/backends/llvm_backend.rb: -------------------------------------------------------------------------------- 1 | require 'llvm/core' 2 | require 'llvm/execution_engine' 3 | require 'llvm/linker' 4 | require_relative 'llvm_backend/rc_builder' 5 | require_relative 'llvm_backend/array_builder' 6 | require_relative 'llvm_backend/object_builder' 7 | require_relative 'llvm_backend/string_builder' 8 | 9 | class Compiler 10 | module Backends 11 | class LLVMBackend 12 | class LLVMClass 13 | attr_reader :struct, :ivar_map, :superclass 14 | 15 | def initialize(struct, ivar_map, superclass: nil) 16 | @struct = struct 17 | @ivar_map = ivar_map 18 | @superclass = superclass 19 | end 20 | 21 | def find_ivar_index(ivar_name) 22 | return @ivar_map[ivar_name] if @ivar_map.key?(ivar_name) 23 | @superclass&.find_ivar_index(ivar_name) 24 | end 25 | end 26 | 27 | LIB_PATH = File.expand_path('../../../../build/lib.ll', __dir__) 28 | 29 | def initialize(instructions, dump: false) 30 | @instructions = instructions 31 | @stack = [] 32 | @scope_stack = [{ vars: {} }] 33 | @call_stack = [] 34 | @if_depth = 0 35 | @methods = build_methods 36 | @classes = {} 37 | @dump = dump 38 | @lib = LLVM::Module.parse_ir(LIB_PATH) 39 | end 40 | 41 | attr_reader :instructions 42 | 43 | def run 44 | build_main_module 45 | execute(@entry) 46 | end 47 | 48 | def dump_ir_to_file(path) 49 | build_main_module 50 | File.write(path, @module.to_s) 51 | end 52 | 53 | private 54 | 55 | def execute(fn) 56 | LLVM.init_jit 57 | 58 | engine = LLVM::JITCompiler.new(@module) 59 | value = engine.run_function(fn) 60 | return_value = llvm_type_to_ruby(value, @return_type) 61 | engine.dispose 62 | 63 | return_value 64 | end 65 | 66 | def build_main_module 67 | @module = LLVM::Module.new('llvm') 68 | @return_type = @instructions.last.type! 69 | @entry = @module.functions.add('main', [], llvm_type(@return_type)) 70 | build_main_function(@entry, @instructions) 71 | @lib.link_into(@module) 72 | @module.dump if @dump || !@module.valid? 73 | @module.verify! 74 | end 75 | 76 | def build_main_function(function, instructions) 77 | function.basic_blocks.append.build do |builder| 78 | unused_for_now = LLVM::Int # need at least one struct member 79 | main_obj_struct = LLVM.Struct(unused_for_now, 'main') 80 | @main_obj = ObjectBuilder.new(builder:, mod: @module, struct: main_obj_struct).to_ptr 81 | @scope_stack << { function:, vars: {}, self_obj: @main_obj } 82 | build_instructions(function, builder, instructions) { |return_value| builder.ret return_value } 83 | @scope_stack.pop 84 | end 85 | end 86 | 87 | def build_instructions(function, builder, instructions) 88 | instructions.each { |instruction| build(instruction, function, builder) } 89 | return_value = @stack.pop 90 | yield return_value if block_given? 91 | end 92 | 93 | def build(instruction, function, builder) 94 | send("build_#{instruction.instruction_name}", instruction, function, builder) 95 | end 96 | 97 | def build_call(instruction, _function, builder) 98 | args = @stack.pop(instruction.arg_count) 99 | receiver = @stack.pop 100 | receiver_type = instruction.method_type.self_type 101 | args.unshift(receiver) 102 | name = instruction.name 103 | fn = @methods.dig(receiver_type.name.to_sym, name.to_sym) or raise(NoMethodError, "Method '#{name}' not found") 104 | fn.respond_to?(:call) ? @stack << fn.call(builder:, instruction:, args:) : @stack << builder.call(fn, *args) 105 | end 106 | 107 | def build_class(instruction, function, builder) 108 | name = instruction.name 109 | 110 | superclass_llvm = nil 111 | if instruction.superclass 112 | superclass_llvm = @classes[instruction.superclass.to_sym] 113 | raise "Superclass #{instruction.superclass} not found" unless superclass_llvm 114 | end 115 | 116 | ivar_map = {} 117 | attr_types = [] 118 | 119 | # First, add superclass instance variables if any 120 | if superclass_llvm 121 | superclass_llvm.ivar_map.each do |ivar_name, index| 122 | ivar_map[ivar_name] = attr_types.size 123 | attr_types << superclass_llvm.struct.element_types[index] 124 | end 125 | end 126 | 127 | # Then add this class's own instance variables 128 | instruction.type!.each_instance_variable.with_index do |(ivar_name, ivar_type), _| 129 | # Skip if already inherited from superclass 130 | next if ivar_map.key?(ivar_name) 131 | 132 | ivar_map[ivar_name] = attr_types.size 133 | attr_types << llvm_type(ivar_type) 134 | end 135 | 136 | # Ensure the struct has at least one field 137 | attr_types << LLVM::Int8 if attr_types.empty? 138 | 139 | struct = LLVM.Struct(*attr_types, name.to_s) 140 | llvm_class = @classes[name.to_sym] = LLVMClass.new(struct, ivar_map, superclass: superclass_llvm) 141 | 142 | @methods[name.to_sym] = if superclass_llvm 143 | @methods[instruction.superclass.to_sym].dup 144 | else 145 | {} 146 | end 147 | @methods[name.to_sym][:new] = method(:build_call_new) 148 | 149 | @scope_stack << { function:, vars: {}, self_obj: llvm_class } 150 | build_instructions(function, builder, instruction.body) 151 | @scope_stack.pop 152 | end 153 | 154 | def build_def(instruction, _function, _builder) 155 | name = instruction.name 156 | param_types = (0...instruction.params.size).map { |i| llvm_type(instruction.body.fetch(i * 2).type!) } 157 | receiver_type = instruction.receiver_type 158 | param_types.unshift(llvm_type(receiver_type)) 159 | return_type = llvm_type(instruction.return_type.resolve!) 160 | @methods[receiver_type.name.to_sym] ||= {} 161 | @methods[receiver_type.name.to_sym][name.to_sym] = fn = @module.functions.add(name, param_types, return_type) 162 | 163 | llvm_class = @classes[receiver_type.name.to_sym] 164 | 165 | fn.basic_blocks.append.build do |builder| 166 | @scope_stack << { function: fn, vars: {}, self_obj: fn.params.first, llvm_class: } 167 | build_instructions(fn, builder, instruction.body) { |return_value| builder.ret return_value } 168 | @scope_stack.pop 169 | end 170 | end 171 | 172 | def build_if(instruction, function, builder) 173 | result = builder.alloca(llvm_type(instruction.type!), "if_line_#{instruction.line}") 174 | then_block = function.basic_blocks.append 175 | else_block = function.basic_blocks.append 176 | result_block = function.basic_blocks.append 177 | condition = @stack.pop 178 | 179 | # Convert Option types to boolean for conditionals 180 | if condition.type == RcBuilder.pointer_type 181 | # Option type: check if it's not null (Some vs None) 182 | condition = builder.icmp(:ne, condition, RcBuilder.pointer_type.null_pointer) 183 | end 184 | 185 | builder.cond(condition, then_block, else_block) 186 | then_block.build do |then_builder| 187 | build_instructions(function, then_builder, instruction.if_true) do |value| 188 | value = RcBuilder.pointer_type.null_pointer if value.nil? 189 | then_builder.store(value, result) 190 | then_builder.br(result_block) 191 | end 192 | end 193 | else_block.build do |else_builder| 194 | build_instructions(function, else_builder, instruction.if_false) do |value| 195 | value = RcBuilder.pointer_type.null_pointer if value.nil? 196 | else_builder.store(value, result) 197 | else_builder.br(result_block) 198 | end 199 | end 200 | builder.position_at_end(result_block) 201 | @stack << builder.load(result) 202 | end 203 | 204 | def build_while(instruction, function, builder) 205 | condition_block = function.basic_blocks.append 206 | body_block = function.basic_blocks.append 207 | exit_block = function.basic_blocks.append 208 | 209 | builder.br(condition_block) 210 | 211 | condition_block.build do |condition_builder| 212 | build_instructions(function, condition_builder, instruction.condition) do |condition_value| 213 | condition_builder.cond(condition_value, body_block, exit_block) 214 | end 215 | end 216 | 217 | body_block.build do |body_builder| 218 | build_instructions(function, body_builder, instruction.body) 219 | body_builder.br(condition_block) # Loop back to condition 220 | end 221 | 222 | builder.position_at_end(exit_block) 223 | 224 | # While loops always return nil 225 | @stack << RcBuilder.pointer_type.null_pointer 226 | end 227 | 228 | def build_pop(_instruction, _function, _builder) 229 | @stack.pop 230 | end 231 | 232 | def build_push_arg(instruction, _function, _builder) 233 | function = @scope_stack.last.fetch(:function) 234 | @stack << function.params[instruction.index + 1] # receiver is always 0 235 | end 236 | 237 | def build_push_array(instruction, _function, builder) 238 | @stack << build_array(builder, instruction) 239 | end 240 | 241 | def build_push_const(instruction, _function, _builder) 242 | llvm_class = @classes.fetch(instruction.name) 243 | @stack << llvm_class.struct 244 | end 245 | 246 | def build_push_false(_instruction, _function, _builder) 247 | @stack << LLVM::FALSE 248 | end 249 | 250 | def build_push_int(instruction, _function, _builder) 251 | @stack << LLVM.Int(instruction.value) 252 | end 253 | 254 | def build_push_nil(_instruction, _function, _builder) 255 | @stack << RcBuilder.pointer_type.null_pointer 256 | end 257 | 258 | def build_push_self(_instruction, _function, _builder) 259 | @stack << self_obj 260 | end 261 | 262 | def build_push_str(instruction, _function, builder) 263 | @stack << build_string(builder, instruction.value) 264 | end 265 | 266 | def build_push_true(_instruction, _function, _builder) 267 | @stack << LLVM::TRUE 268 | end 269 | 270 | def build_push_var(instruction, _function, builder) 271 | variable = vars.fetch(instruction.name) 272 | @stack << builder.load(variable) 273 | end 274 | 275 | def build_push_ivar(instruction, _function, builder) 276 | ivar_name = instruction.name 277 | llvm_class = scope.fetch(:llvm_class) 278 | field_index = llvm_class.find_ivar_index(ivar_name) 279 | raise "Instance variable #{ivar_name} not found" unless field_index 280 | 281 | self_ptr = scope.fetch(:function).params.first 282 | struct_type = llvm_class.struct 283 | field_ptr = builder.struct_gep2(struct_type, self_ptr, field_index, "ivar_#{ivar_name}") 284 | expected_type = llvm_type(instruction.type!) 285 | value = builder.load2(expected_type, field_ptr, "load_#{ivar_name}") 286 | @stack << value 287 | end 288 | 289 | def build_set_ivar(instruction, _function, builder) 290 | ivar_name = instruction.name 291 | llvm_class = scope.fetch(:llvm_class) 292 | field_index = llvm_class.find_ivar_index(ivar_name) 293 | raise "Instance variable #{ivar_name} not found" unless field_index 294 | 295 | value = @stack.last 296 | self_ptr = scope.fetch(:function).params.first 297 | struct_type = llvm_class.struct 298 | field_ptr = builder.struct_gep2(struct_type, self_ptr, field_index, "ivar_#{ivar_name}") 299 | builder.store(value, field_ptr) 300 | end 301 | 302 | def build_set_var(instruction, _function, builder) 303 | value = @stack.pop 304 | if vars[instruction.name] 305 | builder.store(value, vars[instruction.name]) 306 | else 307 | variable = builder.alloca(value.type, "var_#{instruction.name}") 308 | builder.store(value, variable) 309 | vars[instruction.name] = variable 310 | end 311 | end 312 | 313 | def scope 314 | @scope_stack.last 315 | end 316 | 317 | def vars 318 | scope.fetch(:vars) 319 | end 320 | 321 | def self_obj 322 | scope.fetch(:self_obj) 323 | end 324 | 325 | def llvm_type(type) 326 | case type.name 327 | when 'Boolean' 328 | LLVM::Int1.type 329 | when 'Integer' 330 | LLVM::Int32.type 331 | when 'Option' 332 | # Option types are represented as pointers (null for None, non-null for Some) 333 | RcBuilder.pointer_type 334 | else 335 | RcBuilder.pointer_type 336 | end 337 | end 338 | 339 | def llvm_type_to_ruby(value, type) 340 | case type.name 341 | when 'Boolean', 'Integer', 'NilClass' 342 | read_llvm_type_as_ruby(value, type) 343 | when 'String' 344 | ptr = read_rc_pointer(value) 345 | read_llvm_type_as_ruby(ptr, type) 346 | else 347 | raise "Unknown type: #{type.inspect}" 348 | end 349 | end 350 | 351 | def read_rc_pointer(value) 352 | rc_ptr = value.to_ptr.read_pointer 353 | # NOTE: this works because the ptr is the first field of the RC struct. 354 | rc_ptr.read_pointer 355 | end 356 | 357 | def read_llvm_type_as_ruby(value, type) 358 | case type.name 359 | when 'Boolean' 360 | value.to_i == -1 361 | when 'Integer' 362 | value.to_i 363 | when 'String' 364 | value.read_string 365 | when 'NilClass' 366 | nil 367 | else 368 | raise "Unknown type: #{type.inspect}" 369 | end 370 | end 371 | 372 | def build_string(builder, value) 373 | string = StringBuilder.new(builder:, mod: @module, string: value) 374 | string.to_ptr 375 | end 376 | 377 | def build_array(builder, instruction) 378 | elements = @stack.pop(instruction.size) 379 | array_type = instruction.type! 380 | element_type = llvm_type(array_type.element_type) 381 | array = ArrayBuilder.new(builder:, mod: @module, element_type:, elements:) 382 | array.to_ptr 383 | end 384 | 385 | def fn_puts 386 | @fn_puts ||= @module.functions.add('puts_string', [RcBuilder.pointer_type], LLVM::Int32) 387 | end 388 | 389 | def fn_int_to_string 390 | @fn_int_to_string ||= @module.functions.add('int_to_string', [LLVM::Int32], RcBuilder.pointer_type) 391 | end 392 | 393 | def fn_bool_to_string 394 | @fn_bool_to_string ||= @module.functions.add('bool_to_string', [LLVM::Int1], RcBuilder.pointer_type) 395 | end 396 | 397 | def fn_string_concat 398 | @fn_string_concat ||= 399 | @module.functions.add( 400 | 'string_concat', 401 | [RcBuilder.pointer_type, RcBuilder.pointer_type], 402 | RcBuilder.pointer_type, 403 | ) 404 | end 405 | 406 | def build_methods 407 | { 408 | Array: { 409 | first: ->(builder:, instruction:, args:) do 410 | element_type = llvm_type(instruction.type!) 411 | array = ArrayBuilder.new(ptr: args.first, builder:, mod: @module, element_type:) 412 | array.first 413 | end, 414 | last: ->(builder:, instruction:, args:) do 415 | element_type = llvm_type(instruction.type!) 416 | array = ArrayBuilder.new(ptr: args.first, builder:, mod: @module, element_type:) 417 | array.last 418 | end, 419 | '<<': ->(builder:, args:, **) do 420 | element_type = args.last.type 421 | array = ArrayBuilder.new(ptr: args.first, builder:, mod: @module, element_type:) 422 | array.push(args.last) 423 | end, 424 | }, 425 | Integer: { 426 | '+': ->(builder:, args:, **) { builder.add(*args) }, 427 | '-': ->(builder:, args:, **) { builder.sub(*args) }, 428 | '*': ->(builder:, args:, **) { builder.mul(*args) }, 429 | '/': ->(builder:, args:, **) { builder.sdiv(*args) }, 430 | '==': ->(builder:, args:, **) { builder.icmp(:eq, *args) }, 431 | '!=': ->(builder:, args:, **) { builder.icmp(:ne, *args) }, 432 | '<': ->(builder:, args:, **) { builder.icmp(:slt, *args) }, 433 | '<=': ->(builder:, args:, **) { builder.icmp(:sle, *args) }, 434 | '>': ->(builder:, args:, **) { builder.icmp(:sgt, *args) }, 435 | '>=': ->(builder:, args:, **) { builder.icmp(:sge, *args) }, 436 | to_s: ->(builder:, args:, **) { builder.call(fn_int_to_string, args.first) }, 437 | }, 438 | String: { 439 | '+': ->(builder:, args:, **) { builder.call(fn_string_concat, *args) }, 440 | '==': ->(builder:, args:, **) do 441 | raise NotImplementedError, 'String comparison not yet implemented in LLVM backend' 442 | end, 443 | '!=': ->(builder:, args:, **) do 444 | raise NotImplementedError, 'String comparison not yet implemented in LLVM backend' 445 | end, 446 | }, 447 | Boolean: { 448 | '==': ->(builder:, args:, **) { builder.icmp(:eq, *args) }, 449 | '!=': ->(builder:, args:, **) { builder.icmp(:ne, *args) }, 450 | to_s: ->(builder:, args:, **) { builder.call(fn_bool_to_string, args.first) }, 451 | }, 452 | Object: { 453 | puts: ->(builder:, args:, **) do 454 | arg = args[1] 455 | builder.call(fn_puts, arg) 456 | end, 457 | }, 458 | Option: { 459 | value!: ->(builder:, args:, **) do 460 | # For Option types, the value is the pointer itself (when not null) 461 | # This works for Option[String] since strings are already pointers 462 | # Note: Option[Integer] is not supported - integers are native types 463 | args.first 464 | end, 465 | is_some: ->(builder:, args:, **) do 466 | # Check if the Option pointer is not null 467 | builder.icmp(:ne, args.first, RcBuilder.pointer_type.null_pointer) 468 | end, 469 | is_none: ->(builder:, args:, **) do 470 | # Check if the Option pointer is null 471 | builder.icmp(:eq, args.first, RcBuilder.pointer_type.null_pointer) 472 | end, 473 | }, 474 | } 475 | end 476 | 477 | def build_call_new(builder:, args:, instruction:, **) 478 | struct = args.first 479 | instance = ObjectBuilder.new(builder:, mod: @module, struct:).to_ptr 480 | 481 | class_name = instruction.method_type.self_type.name.to_sym 482 | if (initialize_fn = @methods.dig(class_name, :initialize)) 483 | initialize_args = [instance] + args[1..] 484 | builder.call(initialize_fn, *initialize_args) 485 | end 486 | 487 | instance 488 | end 489 | 490 | # usage: 491 | # diff(@module.functions[11].to_s, @module.functions[0].to_s) 492 | def diff(expected, actual) 493 | File.write('/tmp/actual.ll', actual) 494 | File.write('/tmp/expected.ll', expected) 495 | puts `diff -y -W 134 /tmp/expected.ll /tmp/actual.ll` 496 | end 497 | end 498 | end 499 | end 500 | -------------------------------------------------------------------------------- /spec/support/shared_backend_examples.rb: -------------------------------------------------------------------------------- 1 | module SharedBackendExamples 2 | def self.included(base) 3 | base.class_eval do 4 | it 'evaluates integers' do 5 | expect(execute('123')).must_equal(123) 6 | end 7 | 8 | it 'evaluates strings' do 9 | expect(execute('"foo"')).must_equal('foo') 10 | expect(execute('"a longer string works too"')).must_equal('a longer string works too') 11 | end 12 | 13 | it 'evaluates nil' do 14 | expect(execute('nil')).must_be_nil 15 | end 16 | 17 | it 'evaluates booleans' do 18 | expect(execute('true')).must_equal(true) 19 | expect(execute('false')).must_equal(false) 20 | end 21 | 22 | it 'calls `initialize` on new objects' do 23 | code = <<~CODE 24 | class Foo 25 | def initialize(x) 26 | puts "Foo#initialize called with " + x 27 | end 28 | end 29 | Foo.new("bar") 30 | CODE 31 | out = execute_with_output(code) 32 | expect(out).must_equal("Foo#initialize called with bar\n") 33 | end 34 | 35 | it 'evaluates classes with instance variables' do 36 | code = <<~CODE 37 | class Person 38 | def initialize 39 | @name = "" 40 | @age = 0 41 | end 42 | 43 | def name=(name) # name:String 44 | @name = name 45 | end 46 | 47 | def age=(age) # age:Integer 48 | @age = age 49 | end 50 | 51 | def name 52 | @name 53 | end 54 | 55 | def age 56 | @age 57 | end 58 | 59 | def info 60 | @name + " is " + @age.to_s + " years old" 61 | end 62 | end 63 | 64 | person = Person.new 65 | person.name = "Alice" 66 | person.age = 30 67 | person.info 68 | CODE 69 | expect(execute(code)).must_equal('Alice is 30 years old') 70 | end 71 | 72 | it 'evaluates variables set and get' do 73 | expect(execute('a = 1; a + a')).must_equal(2) 74 | end 75 | 76 | it 'evaluates variables with type annotation' do 77 | code = <<~CODE 78 | x = 42 # x:Integer 79 | x + 8 80 | CODE 81 | expect(execute(code)).must_equal(50) 82 | 83 | code = <<~CODE 84 | name = "Alice" # name:String 85 | name + " Smith" 86 | CODE 87 | expect(execute(code)).must_equal('Alice Smith') 88 | 89 | code = <<~CODE 90 | message = "hello" # message:Option[String] 91 | if message 92 | message.value! 93 | else 94 | "no message" 95 | end 96 | CODE 97 | expect(execute(code)).must_equal('hello') 98 | end 99 | 100 | it 'evaluates arrays' do 101 | code = <<~CODE 102 | a = [1, 2, 3] 103 | a.first 104 | CODE 105 | expect(execute(code)).must_equal(1) 106 | 107 | code = <<~CODE 108 | a = [1, 2, 3] 109 | a.last 110 | CODE 111 | expect(execute(code)).must_equal(3) 112 | 113 | code = <<~CODE 114 | a = [] 115 | a << 4 116 | a << 5 117 | a.last 118 | CODE 119 | expect(execute(code)).must_equal(5) 120 | 121 | code = <<~CODE 122 | a = [] 123 | a << "foo" 124 | a << "bar" 125 | a.last 126 | CODE 127 | expect(execute(code)).must_equal('bar') 128 | 129 | code = <<~CODE 130 | a = ["foo", "bar", "baz"] 131 | a.first 132 | CODE 133 | expect(execute(code)).must_equal('foo') 134 | 135 | code = <<~CODE 136 | a = ["foo", "bar", "baz"] 137 | a.last 138 | CODE 139 | expect(execute(code)).must_equal('baz') 140 | end 141 | 142 | it 'evaluates method definitions' do 143 | code = <<~CODE 144 | def foo 145 | 'foo' 146 | end 147 | 148 | foo 149 | CODE 150 | expect(execute(code)).must_equal('foo') 151 | end 152 | 153 | it 'evaluates method definitions with arguments' do 154 | code = <<~CODE 155 | def bar(x) 156 | x 157 | end 158 | 159 | def foo(a, b) 160 | bar(b - 10) 161 | end 162 | 163 | foo('foo', 100) 164 | CODE 165 | expect(execute(code)).must_equal(90) 166 | end 167 | 168 | it 'does not stomp on method arguments' do 169 | code = <<~CODE 170 | def bar(b) 171 | b 172 | end 173 | 174 | def foo(a, b) 175 | bar(b - 10) 176 | b 177 | end 178 | 179 | foo('foo', 100) 180 | CODE 181 | expect(execute(code)).must_equal(100) 182 | end 183 | 184 | it 'evaluates operator expressions' do 185 | expect(execute('1 + 2')).must_equal 3 186 | expect(execute('3 - 1')).must_equal 2 187 | expect(execute('2 * 3')).must_equal 6 188 | expect(execute('6 / 2')).must_equal 3 189 | expect(execute('3 == 3')).must_equal true 190 | expect(execute('3 == 4')).must_equal false 191 | expect(execute('3 != 3')).must_equal false 192 | expect(execute('3 != 4')).must_equal true 193 | expect(execute('1 < 2')).must_equal true 194 | expect(execute('2 < 1')).must_equal false 195 | expect(execute('2 < 2')).must_equal false 196 | expect(execute('1 <= 2')).must_equal true 197 | expect(execute('2 <= 1')).must_equal false 198 | expect(execute('2 <= 2')).must_equal true 199 | expect(execute('2 > 1')).must_equal true 200 | expect(execute('1 > 2')).must_equal false 201 | expect(execute('2 > 2')).must_equal false 202 | expect(execute('2 >= 1')).must_equal true 203 | expect(execute('1 >= 2')).must_equal false 204 | expect(execute('2 >= 2')).must_equal true 205 | end 206 | 207 | it 'evaluates simple if expressions' do 208 | code = <<~CODE 209 | if true 210 | 3 211 | else 212 | 4 213 | end 214 | CODE 215 | expect(execute(code)).must_equal(3) 216 | 217 | code = <<~CODE 218 | if false 219 | 3 220 | else 221 | 4 222 | end 223 | CODE 224 | expect(execute(code)).must_equal(4) 225 | end 226 | 227 | it 'evaluates nested if expressions' do 228 | code = <<~CODE 229 | if false 230 | if true 231 | 1 232 | else 233 | 2 234 | end 235 | else 236 | if true 237 | 3 # <-- this one 238 | else 239 | 4 240 | end 241 | end 242 | CODE 243 | expect(execute(code)).must_equal(3) 244 | 245 | code = <<~CODE 246 | if true 247 | if false 248 | 1 249 | else 250 | 2 # <-- this one 251 | end 252 | else 253 | if false 254 | 3 255 | else 256 | 4 257 | end 258 | end 259 | CODE 260 | expect(execute(code)).must_equal(2) 261 | 262 | code = <<~CODE 263 | if false 264 | if false 265 | 1 266 | else 267 | 2 268 | end 269 | elsif false 270 | 3 271 | else 272 | if false 273 | 4 274 | else 275 | 5 # <-- this one 276 | end 277 | end 278 | CODE 279 | expect(execute(code)).must_equal(5) 280 | end 281 | 282 | it 'evaluates if statements without else clause' do 283 | code = <<~CODE 284 | if true 285 | 42 286 | end 287 | nil 288 | CODE 289 | assert_nil(execute(code)) 290 | end 291 | 292 | it 'evaluates puts for strings' do 293 | code = <<~CODE 294 | puts("hello") 295 | puts("world") 296 | CODE 297 | out = execute_with_output(code) 298 | expect(out).must_equal("hello\nworld\n") 299 | end 300 | 301 | it 'evaluates strings' do 302 | code = <<~CODE 303 | a = "foo" 304 | a 305 | CODE 306 | expect(execute(code)).must_equal('foo') 307 | end 308 | 309 | it 'evaluates while loops' do 310 | code = <<~CODE 311 | i = 0 312 | sum = 0 313 | while i < 5 314 | sum = sum + i 315 | i = i + 1 316 | end 317 | sum 318 | CODE 319 | expect(execute(code)).must_equal(10) 320 | end 321 | 322 | it 'evaluates while loops that never execute' do 323 | code = <<~CODE 324 | i = 10 325 | sum = 42 326 | while i < 5 327 | sum = sum + i 328 | i = i + 1 329 | end 330 | sum 331 | CODE 332 | expect(execute(code)).must_equal(42) 333 | end 334 | 335 | it 'evaluates nested while loops' do 336 | code = <<~CODE 337 | i = 0 338 | result = 0 339 | while i < 3 340 | j = 0 341 | while j < 2 342 | result = result + 1 343 | j = j + 1 344 | end 345 | i = i + 1 346 | end 347 | result 348 | CODE 349 | expect(execute(code)).must_equal(6) 350 | end 351 | 352 | it 'evaluates examples/fib.rb' do 353 | result = execute_file(File.expand_path('../../examples/fib.rb', __dir__)) 354 | expect(result).must_equal("55\n") 355 | end 356 | 357 | it 'evaluates examples/fact.rb' do 358 | result = execute_file(File.expand_path('../../examples/fact.rb', __dir__)) 359 | expect(result).must_equal("3628800\n") 360 | end 361 | 362 | it 'evaluates examples/countdown.rb' do 363 | result = execute_file(File.expand_path('../../examples/countdown.rb', __dir__)) 364 | expect(result).must_equal(<<~END) 365 | 5 366 | 4 367 | 3 368 | 2 369 | 1 370 | Done! 371 | END 372 | end 373 | 374 | it 'evaluates examples/inheritance.rb' do 375 | result = execute_file(File.expand_path('../../examples/inheritance.rb', __dir__)) 376 | expect(result).must_equal(<<~END) 377 | Generic Animal makes a sound 378 | Generic Animal moves around 379 | 380 | Dog name: Buddy 381 | Dog breed: Golden Retriever 382 | Buddy barks: Woof! 383 | Buddy moves around 384 | Buddy fetches the ball 385 | 386 | Cat name: Whiskers 387 | Whiskers meows: Meow! 388 | Whiskers moves around 389 | Whiskers climbs a tree 390 | END 391 | end 392 | 393 | it 'evaluates examples/type_annotations.rb' do 394 | result = execute_file(File.expand_path('../../examples/type_annotations.rb', __dir__)) 395 | expect(result).must_equal(<<~END) 396 | Alice is 25 years old with 0 points (active: false) 397 | Alice is 25 years old with 100 points (active: true) 398 | Adding 10 and 20 399 | Sum: 30 400 | Hello, Bob! 401 | Greeted: Bob 402 | Status is: false 403 | Final status: false 404 | Welcome to Mya! 405 | Processed: data 406 | Nothing to process 407 | END 408 | end 409 | 410 | it 'evaluates Option types with nil' do 411 | code = <<~CODE 412 | def maybe_greet(name) # name:Option[String] 413 | if name 414 | "Hello, " + name.value! 415 | else 416 | "No name provided" 417 | end 418 | end 419 | 420 | maybe_greet(nil) 421 | CODE 422 | expect(execute(code)).must_equal('No name provided') 423 | end 424 | 425 | it 'evaluates Option types with values' do 426 | code = <<~CODE 427 | def maybe_greet(name) # name:Option[String] 428 | if name 429 | "Hello, " + name.value! 430 | else 431 | "No name provided" 432 | end 433 | end 434 | 435 | maybe_greet("Tim") 436 | CODE 437 | expect(execute(code)).must_equal('Hello, Tim') 438 | end 439 | 440 | it 'evaluates Option types in conditional expressions' do 441 | code = <<~CODE 442 | def process_optional(value) # value:Option[String] 443 | if value 444 | value.value! + " processed" 445 | else 446 | "nothing to process" 447 | end 448 | end 449 | 450 | a = process_optional("data") 451 | b = process_optional(nil) 452 | a + " | " + b 453 | CODE 454 | expect(execute(code)).must_equal('data processed | nothing to process') 455 | end 456 | 457 | it 'evaluates basic inheritance' do 458 | code = <<~CODE 459 | class Animal 460 | def speak 461 | "Some animal sound" 462 | end 463 | end 464 | 465 | class Dog < Animal 466 | def bark 467 | "Woof!" 468 | end 469 | end 470 | 471 | dog = Dog.new 472 | dog.speak + " and " + dog.bark 473 | CODE 474 | expect(execute(code)).must_equal('Some animal sound and Woof!') 475 | end 476 | 477 | it 'evaluates method overriding' do 478 | code = <<~CODE 479 | class Animal 480 | def speak 481 | "Some animal sound" 482 | end 483 | 484 | def move 485 | "Animal moves" 486 | end 487 | end 488 | 489 | class Dog < Animal 490 | def speak 491 | "Woof!" 492 | end 493 | end 494 | 495 | animal = Animal.new 496 | dog = Dog.new 497 | animal.speak + " | " + animal.move + " | " + dog.speak + " | " + dog.move 498 | CODE 499 | expect(execute(code)).must_equal('Some animal sound | Animal moves | Woof! | Animal moves') 500 | end 501 | 502 | it 'evaluates inherited initialize methods' do 503 | code = <<~CODE 504 | class Animal 505 | def initialize(name) 506 | @name = name 507 | end 508 | 509 | def name 510 | @name 511 | end 512 | end 513 | 514 | class Dog < Animal 515 | def bark 516 | @name + " says woof!" 517 | end 518 | end 519 | 520 | dog = Dog.new("Buddy") 521 | dog.name + " and " + dog.bark 522 | CODE 523 | expect(execute(code)).must_equal('Buddy and Buddy says woof!') 524 | end 525 | 526 | it 'evaluates overridden initialize methods' do 527 | code = <<~CODE 528 | class Animal 529 | def initialize(name) 530 | @name = name 531 | end 532 | 533 | def name 534 | @name 535 | end 536 | end 537 | 538 | class Dog < Animal 539 | def initialize(name, breed) 540 | @name = name 541 | @breed = breed 542 | end 543 | 544 | def info 545 | @name + " is a " + @breed 546 | end 547 | end 548 | 549 | dog = Dog.new("Buddy", "Golden Retriever") 550 | dog.name + " | " + dog.info 551 | CODE 552 | expect(execute(code)).must_equal('Buddy | Buddy is a Golden Retriever') 553 | end 554 | 555 | it 'evaluates multi-level inheritance' do 556 | code = <<~CODE 557 | class A 558 | def initialize(a) 559 | @a = a 560 | end 561 | 562 | def get_a 563 | @a 564 | end 565 | 566 | def method_a 567 | "A" 568 | end 569 | end 570 | 571 | class B < A 572 | def initialize(a, b) 573 | @a = a 574 | @b = b 575 | end 576 | 577 | def get_b 578 | @b 579 | end 580 | 581 | def method_a 582 | "B overrides A" 583 | end 584 | 585 | def method_b 586 | "B" 587 | end 588 | end 589 | 590 | class C < B 591 | def initialize(a, b, c) 592 | @a = a 593 | @b = b 594 | @c = c 595 | end 596 | 597 | def get_c 598 | @c 599 | end 600 | 601 | def method_c 602 | "C" 603 | end 604 | end 605 | 606 | c = C.new("value_a", "value_b", "value_c") 607 | c.get_a + " | " + c.get_b + " | " + c.get_c + " | " + c.method_a + " | " + c.method_b + " | " + c.method_c 608 | CODE 609 | expect(execute(code)).must_equal('value_a | value_b | value_c | B overrides A | B | C') 610 | end 611 | 612 | it 'evaluates method calls on inherited methods without overriding' do 613 | code = <<~CODE 614 | class Parent 615 | def parent_method 616 | "from parent" 617 | end 618 | end 619 | 620 | class Child < Parent 621 | def child_method 622 | "from child" 623 | end 624 | end 625 | 626 | child = Child.new 627 | child.parent_method + " and " + child.child_method 628 | CODE 629 | expect(execute(code)).must_equal('from parent and from child') 630 | end 631 | 632 | it 'evaluates complex inheritance with mixed instance variables' do 633 | code = <<~CODE 634 | class Vehicle 635 | def initialize(wheels) 636 | @wheels = wheels 637 | end 638 | 639 | def wheels 640 | @wheels 641 | end 642 | 643 | def description 644 | "Vehicle with " + @wheels.to_s + " wheels" 645 | end 646 | end 647 | 648 | class Car < Vehicle 649 | def initialize(wheels, doors) 650 | @wheels = wheels 651 | @doors = doors 652 | end 653 | 654 | def doors 655 | @doors 656 | end 657 | 658 | def description 659 | "Car with " + @wheels.to_s + " wheels and " + @doors.to_s + " doors" 660 | end 661 | end 662 | 663 | class SportsCar < Car 664 | def initialize(wheels, doors, top_speed) 665 | @wheels = wheels 666 | @doors = doors 667 | @top_speed = top_speed 668 | end 669 | 670 | def top_speed 671 | @top_speed 672 | end 673 | 674 | def description 675 | "Sports car: " + @wheels.to_s + " wheels, " + @doors.to_s + " doors, " + @top_speed.to_s + " mph" 676 | end 677 | end 678 | 679 | vehicle = Vehicle.new(2) 680 | car = Car.new(4, 4) 681 | sports_car = SportsCar.new(4, 2, 200) 682 | 683 | vehicle.description + " | " + car.description + " | " + sports_car.description 684 | CODE 685 | expect(execute(code)).must_equal( 686 | 'Vehicle with 2 wheels | Car with 4 wheels and 4 doors | Sports car: 4 wheels, 2 doors, 200 mph', 687 | ) 688 | end 689 | end 690 | end 691 | end 692 | -------------------------------------------------------------------------------- /lib/mya/compiler/type_checker.rb: -------------------------------------------------------------------------------- 1 | class Compiler 2 | class TypeChecker 3 | MAX_CONSTRAINT_ITERATIONS = 100 4 | 5 | class Error < StandardError 6 | end 7 | class TypeClash < Error 8 | end 9 | class UndefinedMethod < Error 10 | end 11 | class UndefinedVariable < Error 12 | end 13 | 14 | class Type 15 | private 16 | 17 | def get_builtin_method_type(method_name, builtin_methods) 18 | return nil unless builtin_methods 19 | 20 | method_def = builtin_methods[method_name] 21 | return nil unless method_def 22 | 23 | if method_def.respond_to?(:call) 24 | method_def.call(self) 25 | else 26 | method_def 27 | end 28 | end 29 | end 30 | 31 | class Constraint 32 | def initialize(target, source, context: nil, context_data: {}) 33 | @target = target 34 | @source = source 35 | @context = context 36 | @context_data = context_data 37 | end 38 | 39 | attr_reader :target, :source, :context, :context_data 40 | 41 | def solve! 42 | target_resolved = @target.resolve! 43 | source_resolved = @source.resolve! 44 | 45 | return false if target_resolved == source_resolved 46 | 47 | if target_resolved.is_a?(TypeVariable) 48 | target_resolved.instance = source_resolved 49 | return true 50 | elsif source_resolved.is_a?(TypeVariable) 51 | # This constraint will be solved later when the source type is resolved 52 | return false 53 | elsif can_coerce_to_option?(target_resolved, source_resolved) 54 | # Allow coercion to Option types: 55 | # - nil can be coerced to Option[T] (representing None) 56 | # - T can be coerced to Option[T] (representing Some(T)) 57 | return false 58 | elsif can_use_as_boolean?(target_resolved, source_resolved) 59 | # Allow Option types to be used as boolean conditions 60 | return false 61 | elsif target_resolved.class != source_resolved.class || target_resolved != source_resolved 62 | case @context 63 | when :if_condition 64 | line_info = @context_data[:line] ? " (line #{@context_data[:line]})" : '' 65 | raise TypeClash, "`if` condition must be Boolean, got #{source_resolved}#{line_info}" 66 | when :while_condition 67 | line_info = @context_data[:line] ? " (line #{@context_data[:line]})" : '' 68 | raise TypeClash, "`while` condition must be Boolean, got #{source_resolved}#{line_info}" 69 | when :if_branches 70 | line_info = @context_data[:line] ? " (line #{@context_data[:line]})" : '' 71 | raise TypeClash, 72 | "one branch of `if` has type #{target_resolved} and the other has type #{source_resolved}#{line_info}" 73 | when :variable_reassignment 74 | var_name = @context_data[:variable_name] 75 | line_info = @context_data[:line] ? " (line #{@context_data[:line]})" : '' 76 | raise TypeClash, 77 | "the variable `#{var_name}` has type #{target_resolved} already; you cannot change it to type #{source_resolved}#{line_info}" 78 | when :method_argument 79 | method_name = @context_data[:method_name] 80 | receiver_type = @context_data[:receiver_type] 81 | arg_index = @context_data[:arg_index] 82 | line_info = @context_data[:line] ? " (line #{@context_data[:line]})" : '' 83 | raise TypeClash, 84 | "#{receiver_type}##{method_name} argument #{arg_index} has type #{target_resolved}, but you passed #{source_resolved}#{line_info}" 85 | else 86 | raise TypeClash, "cannot constrain #{target_resolved} to #{source_resolved}" 87 | end 88 | end 89 | 90 | return false 91 | end 92 | 93 | private 94 | 95 | def can_coerce_to_option?(target, source) 96 | return false unless target.is_a?(OptionType) 97 | 98 | # Allow nil to be coerced to Option[T] (None) 99 | return true if source.name == 'NilClass' 100 | 101 | # Allow T to be coerced to Option[T] (Some(T)) 102 | return true if target.inner_type == source 103 | 104 | false 105 | end 106 | 107 | def can_use_as_boolean?(target, source) 108 | # Allow Option types to be used as boolean conditions 109 | return true if target.name == 'Boolean' && source.is_a?(OptionType) 110 | 111 | false 112 | end 113 | end 114 | 115 | class TypeVariable < Type 116 | def initialize(type_checker, context: nil, context_data: {}) 117 | @type_checker = type_checker 118 | @id = @type_checker.next_variable_id 119 | @context = context 120 | @context_data = context_data 121 | @type_checker.register_type_variable(self) 122 | end 123 | 124 | attr_accessor :id, :instance, :context, :context_data 125 | 126 | def name 127 | @name ||= @type_checker.next_variable_name 128 | end 129 | 130 | def to_s = name.to_s 131 | 132 | def to_sym = name.to_sym 133 | 134 | def inspect = "TypeVariable(id = #{id}, name = #{name})" 135 | 136 | def resolve! 137 | return @instance.resolve! if @instance 138 | self 139 | end 140 | 141 | def get_method_type(method_name) 142 | resolved = resolve! 143 | return nil if resolved == self 144 | resolved.get_method_type(method_name) 145 | end 146 | 147 | def ==(other) 148 | return true if equal?(other) 149 | return @instance == other if @instance 150 | false 151 | end 152 | end 153 | 154 | class MethodCallConstraint < Constraint 155 | def initialize(target:, receiver_type:, method_name:, arg_types:, type_checker:, instruction:) 156 | @target = target 157 | @receiver_type = receiver_type 158 | @method_name = method_name 159 | @arg_types = arg_types 160 | @type_checker = type_checker 161 | @instruction = instruction 162 | end 163 | 164 | attr_reader :target, :receiver_type, :method_name, :arg_types 165 | 166 | def solve! 167 | receiver_resolved = @receiver_type.resolve! 168 | 169 | return false if receiver_resolved.is_a?(TypeVariable) 170 | 171 | method_type = receiver_resolved.get_method_type(@method_name) 172 | unless method_type 173 | # Method definitely not found on this resolved type 174 | raise UndefinedMethod, "undefined method `#{@method_name}` for #{receiver_resolved}" 175 | end 176 | 177 | # Store the full method type separately and set the return type as the main type 178 | @instruction.method_type = method_type 179 | @instruction.type = method_type.return_type 180 | 181 | expected_count = method_type.param_types.length 182 | actual_count = @arg_types.length 183 | if actual_count != expected_count 184 | raise ArgumentError, 185 | "method #{@method_name} expects #{expected_count} argument#{'s' if expected_count != 1}, got #{actual_count}" 186 | end 187 | 188 | @arg_types.each_with_index do |arg_type, index| 189 | constraint = Constraint.new(method_type.param_types[index], arg_type) 190 | @type_checker.add_constraint(constraint) 191 | end 192 | 193 | target_resolved = @target.resolve! 194 | return_resolved = method_type.return_type.resolve! 195 | 196 | return false if target_resolved == return_resolved 197 | 198 | if target_resolved.is_a?(TypeVariable) 199 | target_resolved.instance = return_resolved 200 | return true 201 | elsif return_resolved.is_a?(TypeVariable) 202 | return false 203 | elsif target_resolved.class != return_resolved.class || 204 | (target_resolved.respond_to?(:name) && target_resolved.name != return_resolved.name) 205 | raise TypeClash, "Cannot constrain #{target_resolved} to #{return_resolved}" 206 | end 207 | 208 | return false 209 | end 210 | end 211 | 212 | class ClassType < Type 213 | def initialize(name, native: false, superclass: nil) 214 | @name = name 215 | @methods = {} 216 | @instance_variables = {} 217 | @native = native 218 | @superclass = superclass 219 | end 220 | 221 | attr_reader :name, :superclass 222 | 223 | def native? = @native 224 | 225 | def resolve! = self 226 | 227 | def to_s = name 228 | 229 | def get_method_type(method_name) 230 | return @methods[method_name] if @methods[method_name] 231 | 232 | return @superclass.get_method_type(method_name) if @superclass 233 | 234 | method_type = get_builtin_method_type(method_name, BUILTIN_METHODS[name.to_sym]) 235 | return method_type if method_type 236 | 237 | # Fall back to Object methods only if we don't have a superclass 238 | get_builtin_method_type(method_name, BUILTIN_METHODS[:Object]) unless @superclass 239 | end 240 | 241 | def define_method_type(name, method_type) 242 | @methods[name] = method_type 243 | end 244 | 245 | def each_instance_variable(&block) 246 | return enum_for(:each_instance_variable) unless block_given? 247 | 248 | @instance_variables.each(&block) 249 | @superclass&.each_instance_variable(&block) 250 | end 251 | 252 | def get_instance_variable(name) 253 | return @instance_variables[name] if @instance_variables[name] 254 | @superclass&.get_instance_variable(name) 255 | end 256 | 257 | def define_instance_variable(name, variable_type) 258 | @instance_variables[name] = variable_type 259 | end 260 | 261 | def resolve! = self 262 | end 263 | 264 | class MethodType < Type 265 | def initialize(self_type:, param_types:, return_type:, name:) 266 | @self_type = self_type 267 | @param_types = param_types 268 | @return_type = return_type 269 | @name = name 270 | end 271 | 272 | attr_reader :self_type, :param_types, :return_type, :name 273 | 274 | def resolve! = self 275 | 276 | def to_s 277 | resolved_params = param_types.map(&:resolve!) 278 | resolved_return = return_type.resolve! 279 | if resolved_params.empty? 280 | "#{self_type}##{name}() => #{resolved_return}" 281 | else 282 | param_str = resolved_params.map(&:to_s).join(', ') 283 | "#{self_type}##{name}(#{param_str}) => #{resolved_return}" 284 | end 285 | end 286 | end 287 | 288 | class ArrayType < Type 289 | def initialize(element_type) 290 | @element_type = element_type 291 | end 292 | 293 | attr_reader :element_type 294 | 295 | def resolve! = self 296 | 297 | def to_s = "Array[#{element_type.resolve!}]" 298 | 299 | def name = :Array 300 | 301 | def native? = false 302 | 303 | def get_method_type(method_name) 304 | get_builtin_method_type(method_name, BUILTIN_METHODS[:Array]) 305 | end 306 | 307 | def ==(other) 308 | return false unless other.is_a?(ArrayType) 309 | element_type == other.element_type 310 | end 311 | end 312 | 313 | class OptionType < Type 314 | def initialize(inner_type) 315 | @inner_type = inner_type 316 | end 317 | 318 | attr_reader :inner_type 319 | 320 | def resolve! = self 321 | 322 | def to_s = "Option[#{inner_type.resolve!}]" 323 | 324 | def name = :Option 325 | 326 | def native? = false 327 | 328 | def get_method_type(method_name) 329 | get_builtin_method_type(method_name, BUILTIN_METHODS[:Option]) 330 | end 331 | 332 | def ==(other) 333 | return false unless other.is_a?(OptionType) 334 | inner_type == other.inner_type 335 | end 336 | end 337 | 338 | BoolType = ClassType.new('Boolean', native: true) 339 | IntType = ClassType.new('Integer', native: true) 340 | NilType = ClassType.new('NilClass', native: true) 341 | StrType = ClassType.new('String') 342 | 343 | ObjectClass = ClassType.new('Object') 344 | 345 | class Scope 346 | def initialize(self_type:, method_params:, type_checker:) 347 | @self_type = self_type 348 | @variables = {} 349 | @method_params = method_params 350 | @type_checker = type_checker 351 | end 352 | 353 | attr_reader :self_type, :method_params 354 | 355 | def set_var_type(name, type) 356 | if (existing_type = @variables[name]) 357 | constraint = 358 | Constraint.new(existing_type, type, context: :variable_reassignment, context_data: { variable_name: name }) 359 | constraint.solve! 360 | else 361 | @variables[name] = type 362 | end 363 | end 364 | 365 | def get_var_type(name) 366 | @variables[name] 367 | end 368 | end 369 | 370 | def initialize 371 | @stack = [] 372 | @scope_stack = [Scope.new(self_type: ObjectClass, method_params: [], type_checker: self)] 373 | @classes = {} 374 | @type_variables = [] 375 | @constraints = [] 376 | end 377 | 378 | def scope = @scope_stack.last 379 | 380 | def analyze(instruction) 381 | analyze_instruction(instruction) 382 | solve_constraints 383 | check_unresolved_types 384 | end 385 | 386 | def add_constraint(constraint) 387 | @constraints << constraint 388 | end 389 | 390 | def solve_constraints 391 | iteration = 0 392 | 393 | loop do 394 | iteration += 1 395 | if iteration > MAX_CONSTRAINT_ITERATIONS 396 | raise Error, "Constraint solving did not converge after #{MAX_CONSTRAINT_ITERATIONS} iterations" 397 | end 398 | 399 | break unless @constraints.any?(&:solve!) 400 | end 401 | end 402 | 403 | def register_type_variable(type_variable) 404 | @type_variables << type_variable 405 | end 406 | 407 | def check_unresolved_types 408 | @type_variables.each do |type_var| 409 | resolved = type_var.resolve! 410 | if resolved.is_a?(TypeVariable) 411 | error_message = generate_type_error_message(type_var) 412 | raise TypeError, error_message 413 | end 414 | end 415 | end 416 | 417 | def generate_type_error_message(type_var) 418 | if type_var.context == :method_parameter 419 | method_name = type_var.context_data[:method_name] 420 | param_name = type_var.context_data[:param_name] 421 | line = type_var.context_data[:line] 422 | "Not enough information to infer type of parameter `#{param_name}` for method `#{method_name}` (line #{line})" 423 | else 424 | "Not enough information to infer type of type variable '#{type_var.name}'" 425 | end 426 | end 427 | 428 | def next_variable_id 429 | if @next_variable_id 430 | @next_variable_id += 1 431 | else 432 | @next_variable_id = 0 433 | end 434 | end 435 | 436 | def next_variable_name 437 | if @next_variable_name 438 | @next_variable_name = @next_variable_name.succ 439 | else 440 | @next_variable_name = 'a' 441 | end 442 | end 443 | 444 | private 445 | 446 | def analyze_array_of_instructions(array) 447 | array.each { |instruction| analyze_instruction(instruction) } 448 | @stack.last 449 | end 450 | 451 | def analyze_call(instruction) 452 | arg_types = pop_arguments(instruction.arg_count) 453 | receiver_type = @stack.pop 454 | result_type = TypeVariable.new(self) 455 | 456 | method_type = receiver_type.get_method_type(instruction.name) 457 | 458 | if method_type 459 | handle_known_method_call(instruction, method_type, arg_types, result_type, receiver_type) 460 | else 461 | handle_unknown_method_call(instruction, receiver_type, arg_types, result_type) 462 | end 463 | 464 | @stack << result_type 465 | end 466 | 467 | def analyze_class(instruction) 468 | superclass = 469 | if instruction.superclass 470 | @classes[instruction.superclass.to_sym] || raise("Undefined class #{instruction.superclass}") 471 | elsif instruction.name.to_s != 'Object' 472 | ObjectClass 473 | else 474 | nil 475 | end 476 | 477 | class_type = ClassType.new(instruction.name.to_s, superclass:) 478 | 479 | @classes[instruction.name.to_sym] = class_type 480 | 481 | # Check if superclass has an initialize method to inherit its signature for new 482 | inherited_initialize = superclass&.get_method_type(:initialize) 483 | if inherited_initialize 484 | new_method_type = 485 | MethodType.new( 486 | name: :new, 487 | self_type: class_type, 488 | param_types: inherited_initialize.param_types, 489 | return_type: class_type, 490 | ) 491 | else 492 | new_method_type = MethodType.new(name: :new, self_type: class_type, param_types: [], return_type: class_type) 493 | end 494 | class_type.define_method_type(:new, new_method_type) 495 | 496 | class_scope = Scope.new(self_type: class_type, method_params: [], type_checker: self) 497 | @scope_stack.push(class_scope) 498 | analyze_array_of_instructions(instruction.body) 499 | @scope_stack.pop 500 | 501 | instruction.type = class_type 502 | end 503 | 504 | def analyze_def(instruction) 505 | param_types = 506 | instruction.params.map.with_index do |param_name, index| 507 | # Check if there's a type annotation for this parameter 508 | if instruction.type_annotations && (type_name = instruction.type_annotations[param_name]) 509 | resolve_type_from_name(type_name) 510 | else 511 | TypeVariable.new( 512 | self, 513 | context: :method_parameter, 514 | context_data: { 515 | method_name: instruction.name, 516 | param_name: param_name, 517 | line: instruction.line, 518 | }, 519 | ) 520 | end 521 | end 522 | 523 | method_scope = Scope.new(self_type: scope.self_type, method_params: param_types, type_checker: self) 524 | instruction.params.each_with_index do |param_name, index| 525 | method_scope.set_var_type(param_name, param_types[index]) 526 | end 527 | 528 | @scope_stack.push(method_scope) 529 | inferred_return_type = analyze_array_of_instructions(instruction.body) 530 | @scope_stack.pop 531 | 532 | return_type = 533 | if instruction.return_type_annotation 534 | annotated_return_type = resolve_type_from_name(instruction.return_type_annotation) 535 | # Add constraint to ensure the inferred type matches the annotation 536 | add_constraint( 537 | Constraint.new( 538 | annotated_return_type, 539 | inferred_return_type, 540 | context: :method_return_type, 541 | context_data: { 542 | method_name: instruction.name, 543 | line: instruction.line, 544 | }, 545 | ), 546 | ) 547 | annotated_return_type 548 | else 549 | inferred_return_type 550 | end 551 | 552 | method_type = MethodType.new(name: instruction.name, self_type: scope.self_type, param_types:, return_type:) 553 | scope.self_type.define_method_type(instruction.name, method_type) 554 | 555 | if instruction.name == :initialize 556 | # update the new method to have the same parameters 557 | new_method_type = 558 | MethodType.new(name: :new, self_type: scope.self_type, param_types:, return_type: scope.self_type) 559 | scope.self_type.define_method_type(:new, new_method_type) 560 | end 561 | 562 | instruction.type = method_type 563 | end 564 | 565 | def analyze_if(instruction) 566 | condition_type = @stack.pop 567 | add_constraint( 568 | Constraint.new(BoolType, condition_type, context: :if_condition, context_data: { line: instruction.line }), 569 | ) 570 | 571 | if_true_type = analyze_array_of_instructions(instruction.if_true) 572 | if_false_type = analyze_array_of_instructions(instruction.if_false) 573 | 574 | if instruction.used 575 | result_type = TypeVariable.new(self) 576 | add_constraint( 577 | Constraint.new(result_type, if_true_type, context: :if_branches, context_data: { line: instruction.line }), 578 | ) 579 | add_constraint( 580 | Constraint.new(result_type, if_false_type, context: :if_branches, context_data: { line: instruction.line }), 581 | ) 582 | instruction.type = result_type 583 | @stack << result_type 584 | else 585 | instruction.type = NilType 586 | @stack << NilType 587 | end 588 | end 589 | 590 | def analyze_instruction(instruction) 591 | return analyze_array_of_instructions(instruction) if instruction.is_a?(Array) 592 | 593 | send("analyze_#{instruction.instruction_name}", instruction) 594 | end 595 | 596 | def analyze_pop(instruction) 597 | instruction.type = NilType 598 | @stack.pop 599 | end 600 | 601 | def analyze_push_arg(instruction) 602 | if scope.method_params && instruction.index < scope.method_params.size 603 | param_type = scope.method_params[instruction.index] 604 | @stack << param_type 605 | instruction.type = param_type 606 | else 607 | type_var = TypeVariable.new(self) 608 | @stack << type_var 609 | instruction.type = type_var 610 | end 611 | end 612 | 613 | def analyze_push_array(instruction) 614 | element_types = [] 615 | instruction.size.times { element_types.unshift(@stack.pop) } 616 | 617 | if element_types.empty? 618 | element_type = TypeVariable.new(self) 619 | array_type = ArrayType.new(element_type) 620 | else 621 | first_element_type = element_types.first 622 | element_types[1..].each do |element_type| 623 | if !types_compatible?(first_element_type, element_type) 624 | raise TypeClash, 625 | "the array contains type #{first_element_type} but you are trying to push type #{element_type}" 626 | end 627 | end 628 | 629 | array_type = ArrayType.new(first_element_type) 630 | end 631 | 632 | instruction.type = array_type 633 | @stack << array_type 634 | end 635 | 636 | def analyze_push_const(instruction) 637 | class_type = @classes[instruction.name] 638 | raise UndefinedVariable, "undefined constant #{instruction.name}" unless class_type 639 | 640 | @stack << class_type 641 | instruction.type = class_type 642 | end 643 | 644 | def analyze_push_false(instruction) 645 | @stack << BoolType 646 | instruction.type = BoolType 647 | end 648 | 649 | def analyze_push_int(instruction) 650 | @stack << IntType 651 | instruction.type = IntType 652 | end 653 | 654 | def analyze_push_ivar(instruction) 655 | class_type = scope.self_type 656 | 657 | var_type = class_type.get_instance_variable(instruction.name) 658 | unless var_type 659 | var_type = NilType 660 | class_type.define_instance_variable(instruction.name, var_type) 661 | end 662 | 663 | @stack << var_type 664 | instruction.type = var_type 665 | end 666 | 667 | def analyze_push_nil(instruction) 668 | @stack << NilType 669 | instruction.type = NilType 670 | end 671 | 672 | def analyze_push_self(instruction) 673 | @stack << scope.self_type 674 | instruction.type = scope.self_type 675 | end 676 | 677 | def analyze_push_str(instruction) 678 | @stack << StrType 679 | instruction.type = StrType 680 | end 681 | 682 | def analyze_push_true(instruction) 683 | @stack << BoolType 684 | instruction.type = BoolType 685 | end 686 | 687 | def analyze_push_var(instruction) 688 | value_type = scope.get_var_type(instruction.name) 689 | raise UndefinedVariable, "undefined local variable or method `#{instruction.name}`" unless value_type 690 | @stack << value_type 691 | instruction.type = value_type 692 | end 693 | 694 | def analyze_set_ivar(instruction) 695 | value_type = @stack.pop 696 | 697 | class_type = scope.self_type 698 | 699 | if instruction.type_annotation 700 | annotated_type = resolve_type_from_name(instruction.type_annotation) 701 | 702 | if (existing_type = class_type.get_instance_variable(instruction.name)) 703 | constraint = 704 | Constraint.new( 705 | existing_type, 706 | annotated_type, 707 | context: :type_annotation_mismatch, 708 | context_data: { 709 | variable_name: instruction.name, 710 | annotation: instruction.type_annotation, 711 | }, 712 | ) 713 | add_constraint(constraint) 714 | else 715 | class_type.define_instance_variable(instruction.name, annotated_type) 716 | end 717 | 718 | constraint = 719 | Constraint.new( 720 | annotated_type, 721 | value_type, 722 | context: :type_annotation_assignment, 723 | context_data: { 724 | variable_name: instruction.name, 725 | annotation: instruction.type_annotation, 726 | }, 727 | ) 728 | add_constraint(constraint) 729 | 730 | @stack << annotated_type 731 | instruction.type = annotated_type 732 | else 733 | existing_type = class_type.get_instance_variable(instruction.name) 734 | if existing_type 735 | constraint = 736 | Constraint.new( 737 | existing_type, 738 | value_type, 739 | context: :variable_reassignment, 740 | context_data: { 741 | variable_name: instruction.name, 742 | }, 743 | ) 744 | add_constraint(constraint) 745 | else 746 | class_type.define_instance_variable(instruction.name, value_type) 747 | end 748 | 749 | @stack << (existing_type || value_type) 750 | instruction.type = existing_type || value_type 751 | end 752 | end 753 | 754 | def analyze_set_var(instruction) 755 | value_type = @stack.pop 756 | 757 | if instruction.type_annotation 758 | annotated_type = resolve_type_from_name(instruction.type_annotation) 759 | add_constraint( 760 | Constraint.new( 761 | annotated_type, 762 | value_type, 763 | context: :variable_type_annotation, 764 | context_data: { 765 | name: instruction.name, 766 | line: instruction.line, 767 | }, 768 | ), 769 | ) 770 | scope.set_var_type(instruction.name, annotated_type) 771 | instruction.type = annotated_type 772 | else 773 | existing_type = scope.get_var_type(instruction.name) 774 | scope.set_var_type(instruction.name, value_type) 775 | instruction.type = existing_type || value_type 776 | end 777 | end 778 | 779 | def analyze_while(instruction) 780 | condition_type = analyze_array_of_instructions(instruction.condition) 781 | add_constraint( 782 | Constraint.new(BoolType, condition_type, context: :while_condition, context_data: { line: instruction.line }), 783 | ) 784 | 785 | analyze_array_of_instructions(instruction.body) 786 | 787 | instruction.type = NilType 788 | @stack << NilType 789 | end 790 | 791 | def resolve_type_from_name(type_spec) 792 | # Handle generic types like { generic: :Option, inner: :String } 793 | if type_spec.is_a?(Hash) && type_spec[:generic] 794 | case type_spec[:generic] 795 | when :Option 796 | inner_type = resolve_type_from_name(type_spec[:inner]) 797 | if inner_type.native? 798 | raise NotImplementedError, 799 | "Option[#{inner_type.name}] is not supported since #{inner_type.name} is a native type" 800 | end 801 | 802 | OptionType.new(inner_type) 803 | when :Array 804 | inner_type = resolve_type_from_name(type_spec[:inner]) 805 | ArrayType.new(inner_type) 806 | else 807 | raise UndefinedVariable, "undefined generic type #{type_spec[:generic]}" 808 | end 809 | else 810 | # Handle simple types 811 | case type_spec 812 | when :Integer, :Int 813 | IntType 814 | when :String, :Str 815 | StrType 816 | when :Boolean, :Bool 817 | BoolType 818 | when :NilClass, :Nil 819 | NilType 820 | else 821 | # Check if it's a defined class 822 | @classes[type_spec] || raise(UndefinedVariable, "undefined type #{type_spec}") 823 | end 824 | end 825 | end 826 | 827 | def pop_arguments(arg_count) 828 | arg_types = [] 829 | arg_count.times { arg_types.unshift(@stack.pop) } 830 | arg_types 831 | end 832 | 833 | def handle_known_method_call(instruction, method_type, arg_types, result_type, receiver_type) 834 | validate_argument_count(instruction.name, method_type.param_types.length, instruction.arg_count) 835 | create_argument_constraints(method_type, arg_types, instruction.name, receiver_type) 836 | add_constraint(Constraint.new(result_type, method_type.return_type)) 837 | # Store the full method type separately and set the return type as the main type 838 | instruction.method_type = method_type 839 | instruction.type = method_type.return_type 840 | end 841 | 842 | def handle_unknown_method_call(instruction, receiver_type, arg_types, result_type) 843 | method_call_constraint = 844 | MethodCallConstraint.new( 845 | target: result_type, 846 | receiver_type:, 847 | method_name: instruction.name, 848 | arg_types:, 849 | type_checker: self, 850 | instruction:, 851 | ) 852 | add_constraint(method_call_constraint) 853 | instruction.type = result_type 854 | end 855 | 856 | def validate_argument_count(method_name, expected_count, actual_count) 857 | return if actual_count == expected_count 858 | 859 | raise ArgumentError, 860 | "method #{method_name} expects #{expected_count} argument#{'s' if expected_count != 1}, got #{actual_count}" 861 | end 862 | 863 | def create_argument_constraints(method_type, arg_types, method_name, receiver_type) 864 | arg_types.each_with_index do |arg_type, index| 865 | next unless index < method_type.param_types.length 866 | 867 | add_constraint( 868 | Constraint.new( 869 | method_type.param_types[index], 870 | arg_type, 871 | context: :method_argument, 872 | context_data: { 873 | method_name: method_name, 874 | receiver_type: receiver_type, 875 | arg_index: index + 1, 876 | }, 877 | ), 878 | ) 879 | end 880 | end 881 | 882 | def types_compatible?(type1, type2) 883 | resolved1 = type1.resolve! 884 | resolved2 = type2.resolve! 885 | 886 | return true if resolved1.is_a?(TypeVariable) || resolved2.is_a?(TypeVariable) 887 | 888 | resolved1 == resolved2 889 | end 890 | 891 | BUILTIN_METHODS = { 892 | Array: { 893 | :first => ->(self_type) do 894 | MethodType.new(self_type:, param_types: [], return_type: self_type.element_type, name: :first) 895 | end, 896 | :last => ->(self_type) do 897 | MethodType.new(self_type:, param_types: [], return_type: self_type.element_type, name: :last) 898 | end, 899 | :<< => ->(self_type) do 900 | MethodType.new(self_type:, param_types: [self_type.element_type], return_type: self_type, name: :<<) 901 | end, 902 | }, 903 | Option: { 904 | is_some: ->(self_type) { MethodType.new(self_type:, param_types: [], return_type: BoolType, name: :is_some) }, 905 | is_none: ->(self_type) { MethodType.new(self_type:, param_types: [], return_type: BoolType, name: :is_none) }, 906 | value!: ->(self_type) do 907 | MethodType.new(self_type:, param_types: [], return_type: self_type.inner_type, name: :value!) 908 | end, 909 | value_or: ->(self_type) do 910 | MethodType.new( 911 | self_type:, 912 | param_types: [self_type.inner_type], 913 | return_type: self_type.inner_type, 914 | name: :value_or, 915 | ) 916 | end, 917 | }, 918 | Integer: { 919 | :+ => MethodType.new(self_type: IntType, param_types: [IntType], return_type: IntType, name: :+), 920 | :- => MethodType.new(self_type: IntType, param_types: [IntType], return_type: IntType, name: :-), 921 | :* => MethodType.new(self_type: IntType, param_types: [IntType], return_type: IntType, name: :*), 922 | :/ => MethodType.new(self_type: IntType, param_types: [IntType], return_type: IntType, name: :/), 923 | :== => MethodType.new(self_type: IntType, param_types: [IntType], return_type: BoolType, name: :==), 924 | :!= => MethodType.new(self_type: IntType, param_types: [IntType], return_type: BoolType, name: :!=), 925 | :< => MethodType.new(self_type: IntType, param_types: [IntType], return_type: BoolType, name: :<), 926 | :<= => MethodType.new(self_type: IntType, param_types: [IntType], return_type: BoolType, name: :<=), 927 | :> => MethodType.new(self_type: IntType, param_types: [IntType], return_type: BoolType, name: :>), 928 | :>= => MethodType.new(self_type: IntType, param_types: [IntType], return_type: BoolType, name: :>=), 929 | :to_s => MethodType.new(self_type: IntType, param_types: [], return_type: StrType, name: :to_s), 930 | }, 931 | String: { 932 | :+ => MethodType.new(self_type: StrType, param_types: [StrType], return_type: StrType, name: :+), 933 | :== => MethodType.new(self_type: StrType, param_types: [StrType], return_type: BoolType, name: :==), 934 | :!= => MethodType.new(self_type: StrType, param_types: [StrType], return_type: BoolType, name: :!=), 935 | :length => MethodType.new(self_type: StrType, param_types: [], return_type: IntType, name: :length), 936 | }, 937 | Boolean: { 938 | :== => MethodType.new(self_type: BoolType, param_types: [BoolType], return_type: BoolType, name: :==), 939 | :!= => MethodType.new(self_type: BoolType, param_types: [BoolType], return_type: BoolType, name: :!=), 940 | :to_s => MethodType.new(self_type: BoolType, param_types: [], return_type: StrType, name: :to_s), 941 | }, 942 | Object: { 943 | puts: MethodType.new(self_type: ObjectClass, param_types: [StrType], return_type: IntType, name: :puts), 944 | }, 945 | }.freeze 946 | end 947 | end 948 | -------------------------------------------------------------------------------- /spec/compiler_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative 'spec_helper' 2 | 3 | describe Compiler do 4 | def compile(code) 5 | Compiler.new(code).compile.map(&:to_h) 6 | end 7 | 8 | it 'compiles integers' do 9 | expect(compile('1')).must_equal_with_diff [{ type: 'Integer', instruction: :push_int, value: 1 }] 10 | end 11 | 12 | it 'compiles strings' do 13 | expect(compile('"foo"')).must_equal_with_diff [{ type: 'String', instruction: :push_str, value: 'foo' }] 14 | end 15 | 16 | it 'compiles booleans' do 17 | expect(compile('true')).must_equal_with_diff [{ type: 'Boolean', instruction: :push_true }] 18 | expect(compile('false')).must_equal_with_diff [{ type: 'Boolean', instruction: :push_false }] 19 | end 20 | 21 | it 'compiles variables set and get' do 22 | expect(compile('a = 1; a')).must_equal_with_diff [ 23 | { type: 'Integer', instruction: :push_int, value: 1 }, 24 | { type: 'Integer', instruction: :set_var, name: :a }, 25 | { type: 'Integer', instruction: :push_var, name: :a }, 26 | ] 27 | expect(compile('a = 1; b = a; b')).must_equal_with_diff [ 28 | { type: 'Integer', instruction: :push_int, value: 1 }, 29 | { type: 'Integer', instruction: :set_var, name: :a }, 30 | { type: 'Integer', instruction: :push_var, name: :a }, 31 | { type: 'Integer', instruction: :set_var, name: :b }, 32 | { type: 'Integer', instruction: :push_var, name: :b }, 33 | ] 34 | end 35 | 36 | it 'can set a variable more than once' do 37 | expect(compile('a = 1; a = 2')).must_equal_with_diff [ 38 | { type: 'Integer', instruction: :push_int, value: 1 }, 39 | { type: 'Integer', instruction: :set_var, name: :a }, 40 | { type: 'Integer', instruction: :push_int, value: 2 }, 41 | { type: 'Integer', instruction: :set_var, name: :a }, 42 | { type: 'Integer', instruction: :push_var, name: :a }, 43 | ] 44 | end 45 | 46 | it 'raises error for variable type changes' do 47 | e = expect { compile('a = 1; a = "foo"') }.must_raise Compiler::TypeChecker::TypeClash 48 | expect(e.message).must_equal 'the variable `a` has type Integer already; you cannot change it to type String' 49 | end 50 | 51 | it 'compiles variables with type annotation' do 52 | code = <<~CODE 53 | x = 42 # x:Integer 54 | x 55 | CODE 56 | expect(compile(code)).must_equal_with_diff [ 57 | { type: 'Integer', instruction: :push_int, value: 42 }, 58 | { type: 'Integer', instruction: :set_var, name: :x }, 59 | { type: 'Integer', instruction: :push_var, name: :x }, 60 | ] 61 | 62 | code = <<~CODE 63 | name = "Alice" # name:String 64 | name 65 | CODE 66 | expect(compile(code)).must_equal_with_diff [ 67 | { type: 'String', instruction: :push_str, value: 'Alice' }, 68 | { type: 'String', instruction: :set_var, name: :name }, 69 | { type: 'String', instruction: :push_var, name: :name }, 70 | ] 71 | 72 | code = <<~CODE 73 | message = nil # message:Option[String] 74 | message = "hello" 75 | message = nil 76 | message 77 | CODE 78 | expect(compile(code)).must_equal_with_diff [ 79 | { type: 'NilClass', instruction: :push_nil }, 80 | { type: 'Option[String]', instruction: :set_var, name: :message }, 81 | { type: 'String', instruction: :push_str, value: 'hello' }, 82 | { type: 'Option[String]', instruction: :set_var, name: :message }, 83 | { type: 'NilClass', instruction: :push_nil }, 84 | { type: 'Option[String]', instruction: :set_var, name: :message }, 85 | { type: 'Option[String]', instruction: :push_var, name: :message }, 86 | ] 87 | end 88 | 89 | it 'compiles method definitions' do 90 | code = <<~CODE 91 | def foo 92 | 'foo' 93 | end 94 | def bar 95 | 1 96 | end 97 | foo 98 | bar 99 | CODE 100 | expect(compile(code)).must_equal_with_diff [ 101 | { 102 | type: 'Object#foo() => String', 103 | instruction: :def, 104 | name: :foo, 105 | params: [], 106 | body: [{ type: 'String', instruction: :push_str, value: 'foo' }], 107 | }, 108 | { 109 | type: 'Object#bar() => Integer', 110 | instruction: :def, 111 | name: :bar, 112 | params: [], 113 | body: [{ type: 'Integer', instruction: :push_int, value: 1 }], 114 | }, 115 | { type: 'Object', instruction: :push_self }, 116 | { 117 | type: 'String', 118 | instruction: :call, 119 | name: :foo, 120 | arg_count: 0, 121 | method_type: 'Object#foo() => String', 122 | }, 123 | { type: 'NilClass', instruction: :pop }, 124 | { type: 'Object', instruction: :push_self }, 125 | { 126 | type: 'Integer', 127 | instruction: :call, 128 | name: :bar, 129 | arg_count: 0, 130 | method_type: 'Object#bar() => Integer', 131 | }, 132 | ] 133 | end 134 | 135 | it 'compiles method definitions with arguments' do 136 | code = <<~CODE 137 | def bar(a) 138 | a 139 | end 140 | 141 | def foo(a, b) 142 | a 143 | end 144 | 145 | def baz(a) 146 | temp1 = a 147 | temp2 = temp1 148 | bar(temp2) 149 | end 150 | 151 | foo('foo', 1) 152 | 153 | bar(2) 154 | 155 | baz(3) 156 | CODE 157 | expect(compile(code)).must_equal_with_diff [ 158 | { 159 | type: 'Object#bar(Integer) => Integer', 160 | instruction: :def, 161 | name: :bar, 162 | params: [:a], 163 | body: [ 164 | { type: 'Integer', instruction: :push_arg, index: 0 }, 165 | { type: 'Integer', instruction: :set_var, name: :a }, 166 | { type: 'Integer', instruction: :push_var, name: :a }, 167 | ], 168 | }, 169 | { 170 | type: 'Object#foo(String, Integer) => String', 171 | instruction: :def, 172 | name: :foo, 173 | params: %i[a b], 174 | body: [ 175 | { type: 'String', instruction: :push_arg, index: 0 }, 176 | { type: 'String', instruction: :set_var, name: :a }, 177 | { type: 'Integer', instruction: :push_arg, index: 1 }, 178 | { type: 'Integer', instruction: :set_var, name: :b }, 179 | { type: 'String', instruction: :push_var, name: :a }, 180 | ], 181 | }, 182 | { 183 | type: 'Object#baz(Integer) => Integer', 184 | instruction: :def, 185 | name: :baz, 186 | params: [:a], 187 | body: [ 188 | { type: 'Integer', instruction: :push_arg, index: 0 }, 189 | { type: 'Integer', instruction: :set_var, name: :a }, 190 | { type: 'Integer', instruction: :push_var, name: :a }, 191 | { type: 'Integer', instruction: :set_var, name: :temp1 }, 192 | { type: 'Integer', instruction: :push_var, name: :temp1 }, 193 | { type: 'Integer', instruction: :set_var, name: :temp2 }, 194 | { type: 'Object', instruction: :push_self }, 195 | { type: 'Integer', instruction: :push_var, name: :temp2 }, 196 | { 197 | type: 'Integer', 198 | instruction: :call, 199 | name: :bar, 200 | arg_count: 1, 201 | method_type: 'Object#bar(Integer) => Integer', 202 | }, 203 | ], 204 | }, 205 | { type: 'Object', instruction: :push_self }, 206 | { type: 'String', instruction: :push_str, value: 'foo' }, 207 | { type: 'Integer', instruction: :push_int, value: 1 }, 208 | { 209 | type: 'String', 210 | instruction: :call, 211 | name: :foo, 212 | arg_count: 2, 213 | method_type: 'Object#foo(String, Integer) => String', 214 | }, 215 | { type: 'NilClass', instruction: :pop }, 216 | { type: 'Object', instruction: :push_self }, 217 | { type: 'Integer', instruction: :push_int, value: 2 }, 218 | { 219 | type: 'Integer', 220 | instruction: :call, 221 | name: :bar, 222 | arg_count: 1, 223 | method_type: 'Object#bar(Integer) => Integer', 224 | }, 225 | { type: 'NilClass', instruction: :pop }, 226 | { type: 'Object', instruction: :push_self }, 227 | { type: 'Integer', instruction: :push_int, value: 3 }, 228 | { 229 | type: 'Integer', 230 | instruction: :call, 231 | name: :baz, 232 | arg_count: 1, 233 | method_type: 'Object#baz(Integer) => Integer', 234 | }, 235 | ] 236 | end 237 | 238 | it 'raises error for unknown method parameter type' do 239 | code = <<~CODE 240 | def foo(x) 241 | x 242 | end 243 | CODE 244 | e = expect { compile(code) }.must_raise TypeError 245 | expect(e.message).must_equal('Not enough information to infer type of parameter `x` for method `foo` (line 1)') 246 | end 247 | 248 | it 'raises error for unknown method' do 249 | code = <<~CODE 250 | class Foo 251 | end 252 | 253 | foo = Foo.new 254 | foo.unknown_method 255 | CODE 256 | e = expect { compile(code) }.must_raise Compiler::TypeChecker::UndefinedMethod 257 | expect(e.message).must_equal('undefined method `unknown_method` for Foo') 258 | end 259 | 260 | # NOTE: we don't support monomorphization (yet!) 261 | it 'raises error for method arg with multiple types' do 262 | code = <<~CODE 263 | def foo(x) 264 | x 265 | end 266 | 267 | foo(1) 268 | 269 | foo('bar') 270 | CODE 271 | e = expect { compile(code) }.must_raise Compiler::TypeChecker::TypeClash 272 | expect(e.message).must_equal 'Object#foo argument 1 has type Integer, but you passed String' 273 | end 274 | 275 | it 'raises error for method/call arg count mismatch' do 276 | code = <<~CODE 277 | def foo(x) 278 | x 279 | end 280 | 281 | foo(1, 2) 282 | CODE 283 | e = expect { compile(code) }.must_raise ArgumentError 284 | expect(e.message).must_equal 'method foo expects 1 argument, got 2' 285 | end 286 | 287 | it 'compiles recursive method definitions' do 288 | code = <<~CODE 289 | def countdown(n) # n:Integer 290 | if n == 0 291 | 0 292 | else 293 | countdown(n - 1) 294 | end 295 | end 296 | 297 | countdown(3) 298 | CODE 299 | expect(compile(code)).must_equal_with_diff [ 300 | { 301 | type: 'Object#countdown(Integer) => Integer', 302 | instruction: :def, 303 | name: :countdown, 304 | params: [:n], 305 | body: [ 306 | { type: 'Integer', instruction: :push_arg, index: 0 }, 307 | { type: 'Integer', instruction: :set_var, name: :n }, 308 | { type: 'Integer', instruction: :push_var, name: :n }, 309 | { type: 'Integer', instruction: :push_int, value: 0 }, 310 | { 311 | type: 'Boolean', 312 | instruction: :call, 313 | name: :==, 314 | arg_count: 1, 315 | method_type: 'Integer#==(Integer) => Boolean', 316 | }, 317 | { 318 | type: 'Integer', 319 | instruction: :if, 320 | if_true: [{ type: 'Integer', instruction: :push_int, value: 0 }], 321 | if_false: [ 322 | { type: 'Object', instruction: :push_self }, 323 | { type: 'Integer', instruction: :push_var, name: :n }, 324 | { type: 'Integer', instruction: :push_int, value: 1 }, 325 | { 326 | type: 'Integer', 327 | instruction: :call, 328 | name: :-, 329 | arg_count: 1, 330 | method_type: 'Integer#-(Integer) => Integer', 331 | }, 332 | { 333 | type: 'Integer', 334 | instruction: :call, 335 | name: :countdown, 336 | arg_count: 1, 337 | method_type: 'Object#countdown(Integer) => Integer', 338 | }, 339 | ], 340 | }, 341 | ], 342 | }, 343 | { type: 'Object', instruction: :push_self }, 344 | { type: 'Integer', instruction: :push_int, value: 3 }, 345 | { 346 | type: 'Integer', 347 | instruction: :call, 348 | name: :countdown, 349 | arg_count: 1, 350 | method_type: 'Object#countdown(Integer) => Integer', 351 | }, 352 | ] 353 | end 354 | 355 | it 'compiles operator expressions' do 356 | code = <<~CODE 357 | def num(a) = a 358 | 1 + 2 359 | 3 == 4 360 | num(100) * 2 361 | CODE 362 | expect(compile(code)).must_equal_with_diff [ 363 | { 364 | type: 'Object#num(Integer) => Integer', 365 | instruction: :def, 366 | name: :num, 367 | params: [:a], 368 | body: [ 369 | { type: 'Integer', instruction: :push_arg, index: 0 }, 370 | { type: 'Integer', instruction: :set_var, name: :a }, 371 | { type: 'Integer', instruction: :push_var, name: :a }, 372 | ], 373 | }, 374 | { type: 'Integer', instruction: :push_int, value: 1 }, 375 | { type: 'Integer', instruction: :push_int, value: 2 }, 376 | { 377 | type: 'Integer', 378 | instruction: :call, 379 | name: :+, 380 | arg_count: 1, 381 | method_type: 'Integer#+(Integer) => Integer', 382 | }, 383 | { type: 'NilClass', instruction: :pop }, 384 | { type: 'Integer', instruction: :push_int, value: 3 }, 385 | { type: 'Integer', instruction: :push_int, value: 4 }, 386 | { 387 | type: 'Boolean', 388 | instruction: :call, 389 | name: :==, 390 | arg_count: 1, 391 | method_type: 'Integer#==(Integer) => Boolean', 392 | }, 393 | { type: 'NilClass', instruction: :pop }, 394 | { type: 'Object', instruction: :push_self }, 395 | { type: 'Integer', instruction: :push_int, value: 100 }, 396 | { 397 | type: 'Integer', 398 | instruction: :call, 399 | name: :num, 400 | arg_count: 1, 401 | method_type: 'Object#num(Integer) => Integer', 402 | }, 403 | { type: 'Integer', instruction: :push_int, value: 2 }, 404 | { 405 | type: 'Integer', 406 | instruction: :call, 407 | name: :*, 408 | arg_count: 1, 409 | method_type: 'Integer#*(Integer) => Integer', 410 | }, 411 | ] 412 | end 413 | 414 | it 'compiles classes' do 415 | code = <<~CODE 416 | class Foo 417 | def initialize 418 | @bar = 0 419 | end 420 | 421 | def set_bar(x) 422 | @bar = x 423 | end 424 | 425 | def bar = @bar 426 | end 427 | foo = Foo.new 428 | foo.set_bar(10) 429 | foo.bar 430 | CODE 431 | expect(compile(code)).must_equal_with_diff [ 432 | { 433 | type: 'Foo', 434 | instruction: :class, 435 | name: :Foo, 436 | body: [ 437 | { 438 | type: 'Foo#initialize() => Integer', 439 | instruction: :def, 440 | name: :initialize, 441 | params: [], 442 | body: [ 443 | { type: 'Integer', instruction: :push_int, value: 0 }, 444 | { type: 'Integer', instruction: :set_ivar, name: :@bar }, 445 | ], 446 | }, 447 | { 448 | type: 'Foo#set_bar(Integer) => Integer', 449 | instruction: :def, 450 | name: :set_bar, 451 | params: [:x], 452 | body: [ 453 | { type: 'Integer', instruction: :push_arg, index: 0 }, 454 | { type: 'Integer', instruction: :set_var, name: :x }, 455 | { type: 'Integer', instruction: :push_var, name: :x }, 456 | { type: 'Integer', instruction: :set_ivar, name: :@bar }, 457 | ], 458 | }, 459 | { 460 | type: 'Foo#bar() => Integer', 461 | instruction: :def, 462 | name: :bar, 463 | params: [], 464 | body: [{ type: 'Integer', instruction: :push_ivar, name: :@bar }], 465 | }, 466 | ], 467 | }, 468 | { type: 'Foo', instruction: :push_const, name: :Foo }, 469 | { 470 | type: 'Foo', 471 | instruction: :call, 472 | name: :new, 473 | arg_count: 0, 474 | method_type: 'Foo#new() => Foo', 475 | }, 476 | { type: 'Foo', instruction: :set_var, name: :foo }, 477 | { type: 'Foo', instruction: :push_var, name: :foo }, 478 | { type: 'Integer', instruction: :push_int, value: 10 }, 479 | { 480 | type: 'Integer', 481 | instruction: :call, 482 | name: :set_bar, 483 | arg_count: 1, 484 | method_type: 'Foo#set_bar(Integer) => Integer', 485 | }, 486 | { type: 'NilClass', instruction: :pop }, 487 | { type: 'Foo', instruction: :push_var, name: :foo }, 488 | { 489 | type: 'Integer', 490 | instruction: :call, 491 | name: :bar, 492 | arg_count: 0, 493 | method_type: 'Foo#bar() => Integer', 494 | }, 495 | ] 496 | end 497 | 498 | it 'raises an error if two objects cannot unify' do 499 | code = <<~CODE 500 | class Foo; end 501 | class Bar; end 502 | 503 | def same?(a); nil; end 504 | same?(Foo.new) 505 | same?(Bar.new) 506 | CODE 507 | e = expect { compile(code) }.must_raise Compiler::TypeChecker::TypeClash 508 | expect(e.message).must_equal('Object#same? argument 1 has type Foo, but you passed Bar') 509 | end 510 | 511 | it 'compiles method definitions with type annotations' do 512 | code = <<~CODE 513 | def add(a, b) # a:Integer, b:Integer 514 | a + b 515 | end 516 | 517 | def greet(name) # name:String 518 | "Hello " + name 519 | end 520 | 521 | add(5, 10) 522 | greet("World") 523 | CODE 524 | expect(compile(code)).must_equal_with_diff [ 525 | { 526 | type: 'Object#add(Integer, Integer) => Integer', 527 | instruction: :def, 528 | name: :add, 529 | params: %i[a b], 530 | body: [ 531 | { type: 'Integer', instruction: :push_arg, index: 0 }, 532 | { type: 'Integer', instruction: :set_var, name: :a }, 533 | { type: 'Integer', instruction: :push_arg, index: 1 }, 534 | { type: 'Integer', instruction: :set_var, name: :b }, 535 | { type: 'Integer', instruction: :push_var, name: :a }, 536 | { type: 'Integer', instruction: :push_var, name: :b }, 537 | { 538 | type: 'Integer', 539 | instruction: :call, 540 | name: :+, 541 | arg_count: 1, 542 | method_type: 'Integer#+(Integer) => Integer', 543 | }, 544 | ], 545 | }, 546 | { 547 | type: 'Object#greet(String) => String', 548 | instruction: :def, 549 | name: :greet, 550 | params: [:name], 551 | body: [ 552 | { type: 'String', instruction: :push_arg, index: 0 }, 553 | { type: 'String', instruction: :set_var, name: :name }, 554 | { type: 'String', instruction: :push_str, value: 'Hello ' }, 555 | { type: 'String', instruction: :push_var, name: :name }, 556 | { 557 | type: 'String', 558 | instruction: :call, 559 | name: :+, 560 | arg_count: 1, 561 | method_type: 'String#+(String) => String', 562 | }, 563 | ], 564 | }, 565 | { type: 'Object', instruction: :push_self }, 566 | { type: 'Integer', instruction: :push_int, value: 5 }, 567 | { type: 'Integer', instruction: :push_int, value: 10 }, 568 | { 569 | type: 'Integer', 570 | instruction: :call, 571 | name: :add, 572 | arg_count: 2, 573 | method_type: 'Object#add(Integer, Integer) => Integer', 574 | }, 575 | { type: 'NilClass', instruction: :pop }, 576 | { type: 'Object', instruction: :push_self }, 577 | { type: 'String', instruction: :push_str, value: 'World' }, 578 | { 579 | type: 'String', 580 | instruction: :call, 581 | name: :greet, 582 | arg_count: 1, 583 | method_type: 'Object#greet(String) => String', 584 | }, 585 | ] 586 | end 587 | 588 | it 'raises error for type annotation with wrong type' do 589 | code = <<~CODE 590 | def add(a, b) # a:Integer, b:Integer 591 | a + b 592 | end 593 | 594 | add("hello", 5) 595 | CODE 596 | e = expect { compile(code) }.must_raise Compiler::TypeChecker::TypeClash 597 | expect(e.message).must_equal 'Object#add argument 1 has type Integer, but you passed String' 598 | end 599 | 600 | it 'compiles method with Option type annotations' do 601 | code = <<~CODE 602 | def maybe_greet(name) # name:Option[String] 603 | if name 604 | puts "Hello, " + name.value! 605 | else 606 | 0 607 | end 608 | end 609 | 610 | maybe_greet(nil) 611 | maybe_greet("Tim") 612 | CODE 613 | expect(compile(code)).must_equal_with_diff [ 614 | { 615 | type: 'Object#maybe_greet(Option[String]) => Integer', 616 | instruction: :def, 617 | name: :maybe_greet, 618 | params: [:name], 619 | body: [ 620 | { type: 'Option[String]', instruction: :push_arg, index: 0 }, 621 | { type: 'Option[String]', instruction: :set_var, name: :name }, 622 | { type: 'Option[String]', instruction: :push_var, name: :name }, 623 | { 624 | type: 'Integer', 625 | instruction: :if, 626 | if_true: [ 627 | { type: 'Object', instruction: :push_self }, 628 | { type: 'String', instruction: :push_str, value: 'Hello, ' }, 629 | { type: 'Option[String]', instruction: :push_var, name: :name }, 630 | { 631 | type: 'String', 632 | instruction: :call, 633 | name: :'value!', 634 | arg_count: 0, 635 | method_type: 'Option[String]#value!() => String', 636 | }, 637 | { 638 | type: 'String', 639 | instruction: :call, 640 | name: :+, 641 | arg_count: 1, 642 | method_type: 'String#+(String) => String', 643 | }, 644 | { 645 | type: 'Integer', 646 | instruction: :call, 647 | name: :puts, 648 | arg_count: 1, 649 | method_type: 'Object#puts(String) => Integer', 650 | }, 651 | ], 652 | if_false: [{ type: 'Integer', instruction: :push_int, value: 0 }], 653 | }, 654 | ], 655 | }, 656 | { type: 'Object', instruction: :push_self }, 657 | { type: 'NilClass', instruction: :push_nil }, 658 | { 659 | type: 'Integer', 660 | instruction: :call, 661 | name: :maybe_greet, 662 | arg_count: 1, 663 | method_type: 'Object#maybe_greet(Option[String]) => Integer', 664 | }, 665 | { type: 'NilClass', instruction: :pop }, 666 | { type: 'Object', instruction: :push_self }, 667 | { type: 'String', instruction: :push_str, value: 'Tim' }, 668 | { 669 | type: 'Integer', 670 | instruction: :call, 671 | name: :maybe_greet, 672 | arg_count: 1, 673 | method_type: 'Object#maybe_greet(Option[String]) => Integer', 674 | }, 675 | ] 676 | end 677 | 678 | it 'maintains Option[] type when reassigning method arguments' do 679 | code = <<~CODE 680 | def process_message(message) # message:Option[String] 681 | message = nil 682 | message = "hello" 683 | message 684 | end 685 | 686 | process_message("world") 687 | CODE 688 | expect(compile(code)).must_equal_with_diff [ 689 | { 690 | type: 'Object#process_message(Option[String]) => Option[String]', 691 | instruction: :def, 692 | name: :process_message, 693 | params: [:message], 694 | body: [ 695 | { type: 'Option[String]', instruction: :push_arg, index: 0 }, 696 | { type: 'Option[String]', instruction: :set_var, name: :message }, 697 | { type: 'NilClass', instruction: :push_nil }, 698 | { type: 'Option[String]', instruction: :set_var, name: :message }, 699 | { type: 'String', instruction: :push_str, value: 'hello' }, 700 | { type: 'Option[String]', instruction: :set_var, name: :message }, 701 | { type: 'Option[String]', instruction: :push_var, name: :message }, 702 | ], 703 | }, 704 | { type: 'Object', instruction: :push_self }, 705 | { type: 'String', instruction: :push_str, value: 'world' }, 706 | { 707 | type: 'Option[String]', 708 | instruction: :call, 709 | name: :process_message, 710 | arg_count: 1, 711 | method_type: 'Object#process_message(Option[String]) => Option[String]', 712 | }, 713 | ] 714 | end 715 | 716 | it 'raises for invalid type passed to Option parameter' do 717 | code = <<~CODE 718 | def process_optional(value) # value:Option[String] 719 | value 720 | end 721 | 722 | process_optional(42) 723 | CODE 724 | e = expect { compile(code) }.must_raise Compiler::TypeChecker::TypeClash 725 | expect(e.message).must_equal 'Object#process_optional argument 1 has type Option[String], but you passed Integer' 726 | end 727 | 728 | it 'raises error for Option[Integer] - not supported' do 729 | code = <<~CODE 730 | def process_number(value) # value:Option[Integer] 731 | value 732 | end 733 | CODE 734 | e = expect { compile(code) }.must_raise NotImplementedError 735 | expect(e.message).must_equal 'Option[Integer] is not supported since Integer is a native type' 736 | end 737 | 738 | it 'raises error for Option[Boolean] - not supported' do 739 | code = <<~CODE 740 | def process_flag(value) # value:Option[Boolean] 741 | value 742 | end 743 | CODE 744 | e = expect { compile(code) }.must_raise NotImplementedError 745 | expect(e.message).must_equal 'Option[Boolean] is not supported since Boolean is a native type' 746 | end 747 | 748 | it 'raises error for Option with any native type' do 749 | code = <<~CODE 750 | def process_nil(value) # value:Option[NilClass] 751 | value 752 | end 753 | CODE 754 | e = expect { compile(code) }.must_raise NotImplementedError 755 | expect(e.message).must_equal 'Option[NilClass] is not supported since NilClass is a native type' 756 | end 757 | 758 | it 'compiles arrays' do 759 | code = <<~CODE 760 | a = [1, 2, 3] 761 | a.first 762 | 763 | b = [] 764 | b << "foo" 765 | b << "bar" 766 | 767 | c = [4, 5, 6] 768 | d = [c, c] 769 | CODE 770 | expect(compile(code)).must_equal_with_diff [ 771 | { type: 'Integer', instruction: :push_int, value: 1 }, 772 | { type: 'Integer', instruction: :push_int, value: 2 }, 773 | { type: 'Integer', instruction: :push_int, value: 3 }, 774 | { type: 'Array[Integer]', instruction: :push_array, size: 3 }, 775 | { type: 'Array[Integer]', instruction: :set_var, name: :a }, 776 | { type: 'Array[Integer]', instruction: :push_var, name: :a }, 777 | { 778 | type: 'Integer', 779 | instruction: :call, 780 | name: :first, 781 | arg_count: 0, 782 | method_type: 'Array[Integer]#first() => Integer', 783 | }, 784 | { type: 'NilClass', instruction: :pop }, 785 | { type: 'Array[String]', instruction: :push_array, size: 0 }, 786 | { type: 'Array[String]', instruction: :set_var, name: :b }, 787 | { type: 'Array[String]', instruction: :push_var, name: :b }, 788 | { type: 'String', instruction: :push_str, value: 'foo' }, 789 | { 790 | type: 'Array[String]', 791 | instruction: :call, 792 | name: :<<, 793 | arg_count: 1, 794 | method_type: 'Array[String]#<<(String) => Array[String]', 795 | }, 796 | { type: 'NilClass', instruction: :pop }, 797 | { type: 'Array[String]', instruction: :push_var, name: :b }, 798 | { type: 'String', instruction: :push_str, value: 'bar' }, 799 | { 800 | type: 'Array[String]', 801 | instruction: :call, 802 | name: :<<, 803 | arg_count: 1, 804 | method_type: 'Array[String]#<<(String) => Array[String]', 805 | }, 806 | { type: 'NilClass', instruction: :pop }, 807 | { type: 'Integer', instruction: :push_int, value: 4 }, 808 | { type: 'Integer', instruction: :push_int, value: 5 }, 809 | { type: 'Integer', instruction: :push_int, value: 6 }, 810 | { type: 'Array[Integer]', instruction: :push_array, size: 3 }, 811 | { type: 'Array[Integer]', instruction: :set_var, name: :c }, 812 | { type: 'Array[Integer]', instruction: :push_var, name: :c }, 813 | { type: 'Array[Integer]', instruction: :push_var, name: :c }, 814 | { type: 'Array[Array[Integer]]', instruction: :push_array, size: 2 }, 815 | { type: 'Array[Array[Integer]]', instruction: :set_var, name: :d }, 816 | { type: 'Array[Array[Integer]]', instruction: :push_var, name: :d }, 817 | ] 818 | end 819 | 820 | it 'raises error for mixed array element types' do 821 | code = <<~CODE 822 | [1, "foo"] 823 | CODE 824 | e = expect { compile(code) }.must_raise Compiler::TypeChecker::TypeClash 825 | expect(e.message).must_equal 'the array contains type Integer but you are trying to push type String' 826 | 827 | code = <<~CODE 828 | a = [1] 829 | a << "foo" 830 | CODE 831 | e = expect { compile(code) }.must_raise Compiler::TypeChecker::TypeClash 832 | expect(e.message).must_equal 'Array[Integer]#<< argument 1 has type Integer, but you passed String' 833 | 834 | code = <<~CODE 835 | [ 836 | [1, 2, 3], 837 | ['foo', 'bar', 'baz'] 838 | ] 839 | CODE 840 | e = expect { compile(code) }.must_raise Compiler::TypeChecker::TypeClash 841 | expect(e.message).must_equal 'the array contains type Array[Integer] but you are trying to push type Array[String]' 842 | end 843 | 844 | it 'compiles if expressions' do 845 | code = <<~CODE 846 | if true 847 | 2 848 | else 849 | 3 850 | end 851 | CODE 852 | expect(compile(code)).must_equal_with_diff [ 853 | { type: 'Boolean', instruction: :push_true }, 854 | { 855 | type: 'Integer', 856 | instruction: :if, 857 | if_true: [{ type: 'Integer', instruction: :push_int, value: 2 }], 858 | if_false: [{ type: 'Integer', instruction: :push_int, value: 3 }], 859 | }, 860 | ] 861 | end 862 | 863 | it 'raises error for if branches with different types' do 864 | code = <<~CODE 865 | if true 866 | 2 867 | else 868 | 'foo' 869 | end 870 | CODE 871 | e = expect { compile(code) }.must_raise Compiler::TypeChecker::TypeClash 872 | expect(e.message).must_equal 'one branch of `if` has type Integer and the other has type String (line 1)' 873 | end 874 | 875 | it 'allows if statements without else clause' do 876 | code = <<~CODE 877 | if true 878 | 42 879 | end 880 | nil 881 | CODE 882 | expect(compile(code)).must_equal_with_diff [ 883 | { type: 'Boolean', instruction: :push_true }, 884 | { 885 | type: 'NilClass', 886 | instruction: :if, 887 | if_true: [ 888 | { type: 'Integer', instruction: :push_int, value: 42 }, 889 | { type: 'NilClass', instruction: :pop }, 890 | ], 891 | if_false: [{ type: 'NilClass', instruction: :push_nil }], 892 | }, 893 | { type: 'NilClass', instruction: :pop }, 894 | { type: 'NilClass', instruction: :push_nil }, 895 | ] 896 | end 897 | 898 | it 'raises error for if expressions without else clause' do 899 | code = <<~CODE 900 | x = if true 901 | 42 902 | end 903 | CODE 904 | e = expect { compile(code) }.must_raise SyntaxError 905 | expect(e.message).must_equal 'if expression used as value must have an else clause (line 1)' 906 | end 907 | 908 | it 'raises error for non-boolean if condition' do 909 | code = <<~CODE 910 | if 42 911 | 1 912 | else 913 | 2 914 | end 915 | CODE 916 | e = expect { compile(code) }.must_raise Compiler::TypeChecker::TypeClash 917 | expect(e.message).must_equal '`if` condition must be Boolean, got Integer (line 1)' 918 | end 919 | 920 | it 'compiles while expressions' do 921 | code = <<~CODE 922 | i = 0 923 | while i < 5 924 | i = i + 1 925 | end 926 | CODE 927 | expect(compile(code)).must_equal_with_diff [ 928 | { type: 'Integer', instruction: :push_int, value: 0 }, 929 | { type: 'Integer', instruction: :set_var, name: :i }, 930 | { 931 | type: 'NilClass', 932 | instruction: :while, 933 | condition: [ 934 | { type: 'Integer', instruction: :push_var, name: :i }, 935 | { type: 'Integer', instruction: :push_int, value: 5 }, 936 | { 937 | type: 'Boolean', 938 | instruction: :call, 939 | name: :<, 940 | arg_count: 1, 941 | method_type: 'Integer#<(Integer) => Boolean', 942 | }, 943 | ], 944 | body: [ 945 | { type: 'Integer', instruction: :push_var, name: :i }, 946 | { type: 'Integer', instruction: :push_int, value: 1 }, 947 | { 948 | type: 'Integer', 949 | instruction: :call, 950 | name: :+, 951 | arg_count: 1, 952 | method_type: 'Integer#+(Integer) => Integer', 953 | }, 954 | { type: 'Integer', instruction: :set_var, name: :i }, 955 | ], 956 | }, 957 | ] 958 | end 959 | 960 | it 'raises error for non-boolean while condition' do 961 | code = <<~CODE 962 | while "foo" 963 | 1 964 | end 965 | CODE 966 | e = expect { compile(code) }.must_raise Compiler::TypeChecker::TypeClash 967 | expect(e.message).must_equal '`while` condition must be Boolean, got String (line 1)' 968 | end 969 | 970 | it 'compiles examples/fib.rb' do 971 | code = File.read(File.expand_path('../examples/fib.rb', __dir__)) 972 | expect(compile(code)).must_equal_with_diff [ 973 | { 974 | type: 'Object#fib(Integer) => Integer', 975 | instruction: :def, 976 | name: :fib, 977 | params: [:n], 978 | body: [ 979 | { type: 'Integer', instruction: :push_arg, index: 0 }, 980 | { type: 'Integer', instruction: :set_var, name: :n }, 981 | { type: 'Integer', instruction: :push_var, name: :n }, 982 | { type: 'Integer', instruction: :push_int, value: 0 }, 983 | { 984 | type: 'Boolean', 985 | instruction: :call, 986 | name: :==, 987 | arg_count: 1, 988 | method_type: 'Integer#==(Integer) => Boolean', 989 | }, 990 | { 991 | type: 'Integer', 992 | instruction: :if, 993 | if_true: [{ type: 'Integer', instruction: :push_int, value: 0 }], 994 | if_false: [ 995 | { type: 'Integer', instruction: :push_var, name: :n }, 996 | { type: 'Integer', instruction: :push_int, value: 1 }, 997 | { 998 | type: 'Boolean', 999 | instruction: :call, 1000 | name: :==, 1001 | arg_count: 1, 1002 | method_type: 'Integer#==(Integer) => Boolean', 1003 | }, 1004 | { 1005 | type: 'Integer', 1006 | instruction: :if, 1007 | if_true: [{ type: 'Integer', instruction: :push_int, value: 1 }], 1008 | if_false: [ 1009 | { type: 'Object', instruction: :push_self }, 1010 | { type: 'Integer', instruction: :push_var, name: :n }, 1011 | { type: 'Integer', instruction: :push_int, value: 1 }, 1012 | { 1013 | type: 'Integer', 1014 | instruction: :call, 1015 | name: :-, 1016 | arg_count: 1, 1017 | method_type: 'Integer#-(Integer) => Integer', 1018 | }, 1019 | { 1020 | type: 'Integer', 1021 | instruction: :call, 1022 | name: :fib, 1023 | arg_count: 1, 1024 | method_type: 'Object#fib(Integer) => Integer', 1025 | }, 1026 | { type: 'Object', instruction: :push_self }, 1027 | { type: 'Integer', instruction: :push_var, name: :n }, 1028 | { type: 'Integer', instruction: :push_int, value: 2 }, 1029 | { 1030 | type: 'Integer', 1031 | instruction: :call, 1032 | name: :-, 1033 | arg_count: 1, 1034 | method_type: 'Integer#-(Integer) => Integer', 1035 | }, 1036 | { 1037 | type: 'Integer', 1038 | instruction: :call, 1039 | name: :fib, 1040 | arg_count: 1, 1041 | method_type: 'Object#fib(Integer) => Integer', 1042 | }, 1043 | { 1044 | type: 'Integer', 1045 | instruction: :call, 1046 | name: :+, 1047 | arg_count: 1, 1048 | method_type: 'Integer#+(Integer) => Integer', 1049 | }, 1050 | ], 1051 | }, 1052 | ], 1053 | }, 1054 | ], 1055 | }, 1056 | { type: 'Object', instruction: :push_self }, 1057 | { type: 'Object', instruction: :push_self }, 1058 | { type: 'Integer', instruction: :push_int, value: 10 }, 1059 | { 1060 | type: 'Integer', 1061 | instruction: :call, 1062 | name: :fib, 1063 | arg_count: 1, 1064 | method_type: 'Object#fib(Integer) => Integer', 1065 | }, 1066 | { 1067 | type: 'String', 1068 | instruction: :call, 1069 | name: :to_s, 1070 | arg_count: 0, 1071 | method_type: 'Integer#to_s() => String', 1072 | }, 1073 | { 1074 | type: 'Integer', 1075 | instruction: :call, 1076 | name: :puts, 1077 | arg_count: 1, 1078 | method_type: 'Object#puts(String) => Integer', 1079 | }, 1080 | ] 1081 | end 1082 | 1083 | it 'compiles examples/fact.rb' do 1084 | code = File.read(File.expand_path('../examples/fact.rb', __dir__)) 1085 | expect(compile(code)).must_equal_with_diff [ 1086 | { 1087 | type: 'Object#fact(Integer, Integer) => Integer', 1088 | instruction: :def, 1089 | name: :fact, 1090 | params: %i[n result], 1091 | body: [ 1092 | { type: 'Integer', instruction: :push_arg, index: 0 }, 1093 | { type: 'Integer', instruction: :set_var, name: :n }, 1094 | { type: 'Integer', instruction: :push_arg, index: 1 }, 1095 | { type: 'Integer', instruction: :set_var, name: :result }, 1096 | { type: 'Integer', instruction: :push_var, name: :n }, 1097 | { type: 'Integer', instruction: :push_int, value: 0 }, 1098 | { 1099 | type: 'Boolean', 1100 | instruction: :call, 1101 | name: :==, 1102 | arg_count: 1, 1103 | method_type: 'Integer#==(Integer) => Boolean', 1104 | }, 1105 | { 1106 | type: 'Integer', 1107 | instruction: :if, 1108 | if_true: [{ type: 'Integer', instruction: :push_var, name: :result }], 1109 | if_false: [ 1110 | { type: 'Object', instruction: :push_self }, 1111 | { type: 'Integer', instruction: :push_var, name: :n }, 1112 | { type: 'Integer', instruction: :push_int, value: 1 }, 1113 | { 1114 | type: 'Integer', 1115 | instruction: :call, 1116 | name: :-, 1117 | arg_count: 1, 1118 | method_type: 'Integer#-(Integer) => Integer', 1119 | }, 1120 | { type: 'Integer', instruction: :push_var, name: :result }, 1121 | { type: 'Integer', instruction: :push_var, name: :n }, 1122 | { 1123 | type: 'Integer', 1124 | instruction: :call, 1125 | name: :*, 1126 | arg_count: 1, 1127 | method_type: 'Integer#*(Integer) => Integer', 1128 | }, 1129 | { 1130 | type: 'Integer', 1131 | instruction: :call, 1132 | name: :fact, 1133 | arg_count: 2, 1134 | method_type: 'Object#fact(Integer, Integer) => Integer', 1135 | }, 1136 | ], 1137 | }, 1138 | ], 1139 | }, 1140 | { type: 'Object', instruction: :push_self }, 1141 | { type: 'Object', instruction: :push_self }, 1142 | { type: 'Integer', instruction: :push_int, value: 10 }, 1143 | { type: 'Integer', instruction: :push_int, value: 1 }, 1144 | { 1145 | type: 'Integer', 1146 | instruction: :call, 1147 | name: :fact, 1148 | arg_count: 2, 1149 | method_type: 'Object#fact(Integer, Integer) => Integer', 1150 | }, 1151 | { 1152 | type: 'String', 1153 | instruction: :call, 1154 | name: :to_s, 1155 | arg_count: 0, 1156 | method_type: 'Integer#to_s() => String', 1157 | }, 1158 | { 1159 | type: 'Integer', 1160 | instruction: :call, 1161 | name: :puts, 1162 | arg_count: 1, 1163 | method_type: 'Object#puts(String) => Integer', 1164 | }, 1165 | ] 1166 | end 1167 | 1168 | it 'raises error for variable type annotation mismatch' do 1169 | code = <<~CODE 1170 | x = "hello" # x:Integer 1171 | x 1172 | CODE 1173 | e = expect { compile(code) }.must_raise Compiler::TypeChecker::TypeClash 1174 | expect(e.message).must_include('cannot constrain Integer to String') 1175 | end 1176 | 1177 | it 'compiles instance variables with type annotations' do 1178 | code = <<~CODE 1179 | class Person 1180 | def initialize 1181 | @name = "Alice" # @name:String 1182 | @age = 25 # @age:Integer 1183 | end 1184 | 1185 | def name 1186 | @name 1187 | end 1188 | 1189 | def age 1190 | @age 1191 | end 1192 | end 1193 | 1194 | person = Person.new 1195 | person.name 1196 | person.age 1197 | CODE 1198 | expect(compile(code)).must_equal_with_diff [ 1199 | { 1200 | type: 'Person', 1201 | instruction: :class, 1202 | name: :Person, 1203 | body: [ 1204 | { 1205 | type: 'Person#initialize() => Integer', 1206 | instruction: :def, 1207 | name: :initialize, 1208 | params: [], 1209 | body: [ 1210 | { type: 'String', instruction: :push_str, value: 'Alice' }, 1211 | { type: 'String', instruction: :set_ivar, name: :@name, type_annotation: :String }, 1212 | { type: 'NilClass', instruction: :pop }, 1213 | { type: 'Integer', instruction: :push_int, value: 25 }, 1214 | { 1215 | type: 'Integer', 1216 | instruction: :set_ivar, 1217 | name: :@age, 1218 | type_annotation: :Integer, 1219 | }, 1220 | ], 1221 | }, 1222 | { 1223 | type: 'Person#name() => String', 1224 | instruction: :def, 1225 | name: :name, 1226 | params: [], 1227 | body: [{ type: 'String', instruction: :push_ivar, name: :@name }], 1228 | }, 1229 | { 1230 | type: 'Person#age() => Integer', 1231 | instruction: :def, 1232 | name: :age, 1233 | params: [], 1234 | body: [{ type: 'Integer', instruction: :push_ivar, name: :@age }], 1235 | }, 1236 | ], 1237 | }, 1238 | { type: 'Person', instruction: :push_const, name: :Person }, 1239 | { 1240 | type: 'Person', 1241 | instruction: :call, 1242 | name: :new, 1243 | arg_count: 0, 1244 | method_type: 'Person#new() => Person', 1245 | }, 1246 | { type: 'Person', instruction: :set_var, name: :person }, 1247 | { type: 'Person', instruction: :push_var, name: :person }, 1248 | { 1249 | type: 'String', 1250 | instruction: :call, 1251 | name: :name, 1252 | arg_count: 0, 1253 | method_type: 'Person#name() => String', 1254 | }, 1255 | { type: 'NilClass', instruction: :pop }, 1256 | { type: 'Person', instruction: :push_var, name: :person }, 1257 | { 1258 | type: 'Integer', 1259 | instruction: :call, 1260 | name: :age, 1261 | arg_count: 0, 1262 | method_type: 'Person#age() => Integer', 1263 | }, 1264 | ] 1265 | end 1266 | 1267 | it 'maintains Option[] type when reassigning instance variables' do 1268 | code = <<~CODE 1269 | class Person 1270 | def initialize 1271 | @message = nil # @message:Option[String] 1272 | @message = "hello" 1273 | @message = nil 1274 | end 1275 | 1276 | def message 1277 | @message 1278 | end 1279 | end 1280 | 1281 | person = Person.new 1282 | person.message 1283 | CODE 1284 | expect(compile(code)).must_equal_with_diff [ 1285 | { 1286 | type: 'Person', 1287 | instruction: :class, 1288 | name: :Person, 1289 | body: [ 1290 | { 1291 | type: 'Person#initialize() => Option[String]', 1292 | instruction: :def, 1293 | name: :initialize, 1294 | params: [], 1295 | body: [ 1296 | { type: 'NilClass', instruction: :push_nil }, 1297 | { 1298 | type: 'Option[String]', 1299 | instruction: :set_ivar, 1300 | name: :@message, 1301 | type_annotation: { 1302 | generic: :Option, 1303 | inner: :String, 1304 | }, 1305 | }, 1306 | { type: 'NilClass', instruction: :pop }, 1307 | { type: 'String', instruction: :push_str, value: 'hello' }, 1308 | { type: 'Option[String]', instruction: :set_ivar, name: :@message }, 1309 | { type: 'NilClass', instruction: :pop }, 1310 | { type: 'NilClass', instruction: :push_nil }, 1311 | { type: 'Option[String]', instruction: :set_ivar, name: :@message }, 1312 | ], 1313 | }, 1314 | { 1315 | type: 'Person#message() => Option[String]', 1316 | instruction: :def, 1317 | name: :message, 1318 | params: [], 1319 | body: [{ type: 'Option[String]', instruction: :push_ivar, name: :@message }], 1320 | }, 1321 | ], 1322 | }, 1323 | { type: 'Person', instruction: :push_const, name: :Person }, 1324 | { 1325 | type: 'Person', 1326 | instruction: :call, 1327 | name: :new, 1328 | arg_count: 0, 1329 | method_type: 'Person#new() => Person', 1330 | }, 1331 | { type: 'Person', instruction: :set_var, name: :person }, 1332 | { type: 'Person', instruction: :push_var, name: :person }, 1333 | { 1334 | type: 'Option[String]', 1335 | instruction: :call, 1336 | name: :message, 1337 | arg_count: 0, 1338 | method_type: 'Person#message() => Option[String]', 1339 | }, 1340 | ] 1341 | end 1342 | 1343 | it 'supports nested generic type annotations' do 1344 | code = <<~CODE 1345 | def test(matrix) # matrix:Array[Array[String]] 1346 | matrix.first.first 1347 | end 1348 | 1349 | test([['hello', 'world'], ['foo', 'bar']]) 1350 | CODE 1351 | expect(compile(code)).must_equal_with_diff [ 1352 | { 1353 | type: 'Object#test(Array[Array[String]]) => String', 1354 | instruction: :def, 1355 | name: :test, 1356 | params: [:matrix], 1357 | body: [ 1358 | { type: 'Array[Array[String]]', instruction: :push_arg, index: 0 }, 1359 | { type: 'Array[Array[String]]', instruction: :set_var, name: :matrix }, 1360 | { type: 'Array[Array[String]]', instruction: :push_var, name: :matrix }, 1361 | { 1362 | type: 'Array[String]', 1363 | instruction: :call, 1364 | name: :first, 1365 | arg_count: 0, 1366 | method_type: 'Array[Array[String]]#first() => Array[String]', 1367 | }, 1368 | { 1369 | type: 'String', 1370 | instruction: :call, 1371 | name: :first, 1372 | arg_count: 0, 1373 | method_type: 'Array[String]#first() => String', 1374 | }, 1375 | ], 1376 | }, 1377 | { type: 'Object', instruction: :push_self }, 1378 | { type: 'String', instruction: :push_str, value: 'hello' }, 1379 | { type: 'String', instruction: :push_str, value: 'world' }, 1380 | { type: 'Array[String]', instruction: :push_array, size: 2 }, 1381 | { type: 'String', instruction: :push_str, value: 'foo' }, 1382 | { type: 'String', instruction: :push_str, value: 'bar' }, 1383 | { type: 'Array[String]', instruction: :push_array, size: 2 }, 1384 | { type: 'Array[Array[String]]', instruction: :push_array, size: 2 }, 1385 | { 1386 | type: 'String', 1387 | instruction: :call, 1388 | name: :test, 1389 | arg_count: 1, 1390 | method_type: 'Object#test(Array[Array[String]]) => String', 1391 | }, 1392 | ] 1393 | end 1394 | 1395 | it 'raises error for Array parameter type annotation mismatch' do 1396 | code = <<~CODE 1397 | def process_numbers(nums) # nums:Array[Integer] 1398 | nums.first 1399 | end 1400 | 1401 | process_numbers(['hello', 'world']) 1402 | CODE 1403 | e = expect { compile(code) }.must_raise Compiler::TypeChecker::TypeClash 1404 | expect( 1405 | e.message, 1406 | ).must_equal 'Object#process_numbers argument 1 has type Array[Integer], but you passed Array[String]' 1407 | end 1408 | 1409 | it 'raises error for instance variable type annotation mismatch' do 1410 | code = <<~CODE 1411 | class Person 1412 | def initialize 1413 | @name = 42 # @name:String 1414 | end 1415 | end 1416 | CODE 1417 | e = expect { compile(code) }.must_raise Compiler::TypeChecker::TypeClash 1418 | expect(e.message).must_include('cannot constrain String to Integer') 1419 | end 1420 | 1421 | it 'parses return type annotations' do 1422 | code = <<~CODE 1423 | def foo # -> String 1424 | "hello" 1425 | end 1426 | CODE 1427 | 1428 | compiler = Compiler.new(code) 1429 | instructions = compiler.compile 1430 | def_instruction = instructions.first 1431 | 1432 | expect(def_instruction).must_be_instance_of(Compiler::DefInstruction) 1433 | expect(def_instruction.return_type_annotation).must_equal(:String) 1434 | end 1435 | 1436 | it 'parses return type annotations with parameters' do 1437 | code = <<~CODE 1438 | def add(a, b) # a:Integer, b:Integer -> Integer 1439 | a + b 1440 | end 1441 | CODE 1442 | 1443 | compiler = Compiler.new(code) 1444 | instructions = compiler.compile 1445 | def_instruction = instructions.first 1446 | 1447 | expect(def_instruction).must_be_instance_of(Compiler::DefInstruction) 1448 | expect(def_instruction.return_type_annotation).must_equal(:Integer) 1449 | expect(def_instruction.type_annotations[:a]).must_equal(:Integer) 1450 | expect(def_instruction.type_annotations[:b]).must_equal(:Integer) 1451 | end 1452 | 1453 | it 'parses generic return type annotations' do 1454 | code = <<~CODE 1455 | def maybe_value # -> Option[String] 1456 | "test" 1457 | end 1458 | CODE 1459 | 1460 | compiler = Compiler.new(code) 1461 | instructions = compiler.compile 1462 | def_instruction = instructions.first 1463 | 1464 | expect(def_instruction).must_be_instance_of(Compiler::DefInstruction) 1465 | expect(def_instruction.return_type_annotation).must_equal({ generic: :Option, inner: :String }) 1466 | end 1467 | 1468 | it 'enforces return type annotations during type checking' do 1469 | code = <<~CODE 1470 | def wrong_type # -> String 1471 | 42 1472 | end 1473 | CODE 1474 | 1475 | expect { compile(code) }.must_raise(Compiler::TypeChecker::TypeClash) 1476 | end 1477 | 1478 | it 'allows correct return types' do 1479 | code = <<~CODE 1480 | def correct_type # -> Integer 1481 | 42 1482 | end 1483 | CODE 1484 | 1485 | compiler = Compiler.new(code) 1486 | instructions = compiler.compile 1487 | def_instruction = instructions.first 1488 | 1489 | expect(def_instruction.return_type.to_s).must_equal('Integer') 1490 | end 1491 | 1492 | it 'works without return type annotations' do 1493 | code = <<~CODE 1494 | def no_annotation 1495 | "inferred" 1496 | end 1497 | CODE 1498 | 1499 | compiler = Compiler.new(code) 1500 | instructions = compiler.compile 1501 | def_instruction = instructions.first 1502 | 1503 | expect(def_instruction.return_type_annotation).must_be_nil 1504 | expect(def_instruction.return_type.to_s).must_equal('String') 1505 | end 1506 | end 1507 | --------------------------------------------------------------------------------