├── .clang-format ├── .editorconfig ├── .github └── workflows │ ├── lint.yml │ └── specs.yml ├── .gitignore ├── .rubocop.yml ├── .ruby-version ├── .solargraph.yml ├── .streerc ├── Dockerfile ├── Gemfile ├── Gemfile.lock ├── README.md ├── Rakefile ├── bin └── mya ├── devbox.json ├── devbox.lock ├── examples ├── fact.rb └── fib.rb ├── lib ├── mya.rb └── mya │ ├── compiler.rb │ ├── compiler │ ├── backends │ │ ├── llvm_backend.rb │ │ └── llvm_backend │ │ │ ├── array_builder.rb │ │ │ ├── object_builder.rb │ │ │ ├── rc_builder.rb │ │ │ └── string_builder.rb │ ├── instruction.rb │ └── type_checker.rb │ └── vm.rb ├── spec ├── all.rb ├── compiler │ └── backends │ │ └── llvm_backend_spec.rb ├── compiler_spec.rb ├── spec_helper.rb ├── support │ └── expectations.rb └── vm_spec.rb └── src └── lib.c /.clang-format: -------------------------------------------------------------------------------- 1 | --- 2 | BasedOnStyle: WebKit 3 | PointerAlignment: Right 4 | BreakBeforeBraces: Attach 5 | AllowShortIfStatementsOnASingleLine: WithoutElse 6 | TabWidth: 4 7 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 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 mya bundle exec rake spec 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | inherit_gem: 2 | syntax_tree: config/rubocop.yml 3 | 4 | require: rubocop-minitest 5 | 6 | AllCops: 7 | NewCops: enable 8 | SuggestExtensions: false 9 | 10 | Layout/LeadingCommentSpace: 11 | Enabled: false 12 | 13 | Layout/LineLength: 14 | Max: 120 15 | 16 | Lint/BooleanSymbol: 17 | Enabled: false 18 | 19 | Metrics: 20 | Enabled: false 21 | 22 | Naming/MethodParameterName: 23 | Enabled: false 24 | 25 | Style/ClassAndModuleChildren: 26 | Enabled: false 27 | 28 | Style/ConditionalAssignment: 29 | Enabled: false 30 | 31 | Style/Documentation: 32 | Enabled: false 33 | 34 | Style/FrozenStringLiteralComment: 35 | Enabled: false 36 | 37 | Style/IfUnlessModifier: 38 | Enabled: false 39 | 40 | Style/NumericPredicate: 41 | Enabled: false 42 | 43 | Style/PerlBackrefs: 44 | Enabled: false 45 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 3.3 2 | -------------------------------------------------------------------------------- /.solargraph.yml: -------------------------------------------------------------------------------- 1 | include: 2 | - "**/*.rb" 3 | require: [] 4 | domains: [] 5 | reporters: 6 | - rubocop 7 | #- require_not_found 8 | max_files: 5000 9 | -------------------------------------------------------------------------------- /.streerc: -------------------------------------------------------------------------------- 1 | --print-width=120 2 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ruby:3.3-bullseye 2 | 3 | RUN apt-get update && \ 4 | apt-get install -y -q build-essential lsb-release software-properties-common gnupg clang && \ 5 | wget https://apt.llvm.org/llvm.sh && chmod +x llvm.sh && ./llvm.sh 18 6 | 7 | COPY Gemfile /mya/Gemfile 8 | COPY Gemfile.lock /mya/Gemfile.lock 9 | WORKDIR /mya 10 | 11 | RUN bundle config set --local deployment 'true' && \ 12 | bundle install 13 | 14 | COPY . /mya 15 | -------------------------------------------------------------------------------- /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" 10 | 11 | group :development do 12 | gem "rubocop" 13 | gem "rubocop-minitest", require: false 14 | gem "syntax_tree" 15 | end 16 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | ansi (1.5.0) 5 | ast (2.4.2) 6 | builder (3.3.0) 7 | ffi (1.17.0-aarch64-linux-gnu) 8 | ffi (1.17.0-arm64-darwin) 9 | ffi (1.17.0-x86_64-linux-gnu) 10 | json (2.7.2) 11 | language_server-protocol (3.17.0.3) 12 | minitest (5.24.1) 13 | minitest-fail-fast (0.1.0) 14 | minitest (~> 5) 15 | minitest-focus (1.4.0) 16 | minitest (>= 4, < 6) 17 | minitest-reporters (1.7.1) 18 | ansi 19 | builder 20 | minitest (>= 5.0) 21 | ruby-progressbar 22 | parallel (1.25.1) 23 | parser (3.3.4.0) 24 | ast (~> 2.4.1) 25 | racc 26 | prettier_print (1.2.1) 27 | prism (0.30.0) 28 | racc (1.8.0) 29 | rainbow (3.1.1) 30 | rake (13.2.1) 31 | regexp_parser (2.9.2) 32 | rexml (3.3.2) 33 | strscan 34 | rubocop (1.65.0) 35 | json (~> 2.3) 36 | language_server-protocol (>= 3.17.0) 37 | parallel (~> 1.10) 38 | parser (>= 3.3.0.2) 39 | rainbow (>= 2.2.2, < 4.0) 40 | regexp_parser (>= 2.4, < 3.0) 41 | rexml (>= 3.2.5, < 4.0) 42 | rubocop-ast (>= 1.31.1, < 2.0) 43 | ruby-progressbar (~> 1.7) 44 | unicode-display_width (>= 2.4.0, < 3.0) 45 | rubocop-ast (1.31.3) 46 | parser (>= 3.3.1.0) 47 | rubocop-minitest (0.35.1) 48 | rubocop (>= 1.61, < 2.0) 49 | rubocop-ast (>= 1.31.1, < 2.0) 50 | ruby-llvm (18.1.8) 51 | ffi (~> 1.13) 52 | rake (>= 12, < 14) 53 | ruby-progressbar (1.13.0) 54 | strscan (3.1.0) 55 | syntax_tree (6.2.0) 56 | prettier_print (>= 1.2.0) 57 | unicode-display_width (2.5.0) 58 | 59 | PLATFORMS 60 | aarch64-linux 61 | arm64-darwin-23 62 | x86_64-linux 63 | 64 | DEPENDENCIES 65 | minitest 66 | minitest-fail-fast 67 | minitest-focus 68 | minitest-reporters 69 | prism 70 | rake 71 | rubocop 72 | rubocop-minitest 73 | ruby-llvm 74 | syntax_tree 75 | 76 | BUNDLED WITH 77 | 2.5.16 78 | -------------------------------------------------------------------------------- /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 | I have documented some of my progress on this project via my YouTube channel, in a playlist 10 | called [Compiler Fun](https://www.youtube.com/watch?v=LTMsH69_lmE&list=PLWUx_XkUoGToXnl24MJFaY95f4YHv5g4B). 11 | 12 | The name "mya" is just a working name... it will likely change. 13 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | task default: :spec 2 | 3 | task build: ["build/lib.ll"] 4 | 5 | task spec: :build do 6 | require "minitest/fail_fast" if ENV["TESTOPTS"] == "--fail-fast" 7 | require_relative "spec/all" 8 | end 9 | 10 | task :watch do 11 | files = Dir["**/*.rb", "src/*.c"] 12 | sh %(ls #{files.join(" ")} | entr -c -s 'TESTOPTS="--fail-fast" bundle exec rake spec') 13 | end 14 | 15 | task :docker_spec do 16 | sh "docker build -t mya . && docker run mya bundle exec rake spec" 17 | end 18 | 19 | file "build/lib.ll" => "src/lib.c" do 20 | mkdir_p "build" 21 | sh "clang -o build/lib.ll -S -emit-llvm src/lib.c" 22 | end 23 | 24 | task :lint do 25 | sh "find . -name '*.rb' | egrep -v '^./vendor/' | xargs bundle exec stree check" 26 | sh "bundle exec rubocop" 27 | end 28 | 29 | task :docker_lint do 30 | sh "docker build -t mya . && docker run mya bundle exec rake lint" 31 | end 32 | -------------------------------------------------------------------------------- /bin/mya: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require_relative "../lib/mya" 4 | 5 | args = ARGV.dup 6 | dump_llvm = args.delete("--dump-llvm") 7 | llvm = args.delete("--llvm") || dump_llvm 8 | dump_ir = args.delete("--dump-ir") 9 | 10 | code = 11 | if args.first == "-e" 12 | args[1] 13 | else 14 | File.read(args.first) 15 | end 16 | 17 | instructions = Compiler.new(code).compile 18 | 19 | pp instructions.map(&:to_h) if dump_ir 20 | 21 | llvm ? Compiler::Backends::LLVMBackend.new(instructions, dump: dump_llvm).run : VM.new(instructions).run 22 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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) 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) 12 | -------------------------------------------------------------------------------- /lib/mya.rb: -------------------------------------------------------------------------------- 1 | require_relative "mya/compiler" 2 | require_relative "mya/vm" 3 | -------------------------------------------------------------------------------- /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 | 7 | class Compiler 8 | def initialize(code) 9 | @code = code 10 | @result = Prism.parse(code) 11 | @ast = @result.value 12 | @directives = 13 | @result 14 | .comments 15 | .each_with_object({}) do |comment, directives| 16 | text = comment.location.slice 17 | text.split.each do |directive| 18 | next unless directive =~ /^([a-z][a-z_]*):([a-z_]+)$/ 19 | 20 | line = comment.location.start_line 21 | directives[line] ||= {} 22 | directives[line][$1.to_sym] ||= [] 23 | directives[line][$1.to_sym] << $2.to_sym 24 | end 25 | end 26 | end 27 | 28 | def compile 29 | @scope_stack = [{ vars: {} }] 30 | @instructions = [] 31 | transform(@ast, used: true) 32 | type_check 33 | @instructions 34 | end 35 | 36 | private 37 | 38 | def transform(node, used:) 39 | send("transform_#{node.type}", node, used:) 40 | end 41 | 42 | def transform_array_node(node, used:) 43 | node.elements.each { |element| transform(element, used:) } 44 | instruction = PushArrayInstruction.new(node.elements.size, line: node.location.start_line) 45 | @instructions << instruction 46 | @instructions << PopInstruction.new unless used 47 | end 48 | 49 | def transform_call_node(node, used:) 50 | node.receiver ? transform(node.receiver, used: true) : @instructions << PushSelfInstruction.new 51 | args = node.arguments&.arguments || [] 52 | args.each { |arg| transform(arg, used: true) } 53 | instruction = CallInstruction.new(node.name, arg_count: args.size, line: node.location.start_line) 54 | @instructions << instruction 55 | @instructions << PopInstruction.new unless used 56 | end 57 | 58 | def transform_class_node(node, used:) 59 | name = node.constant_path.name 60 | instruction = ClassInstruction.new(name, line: node.location.start_line) 61 | class_instructions = [] 62 | with_instructions_array(class_instructions) { transform(node.body, used: false) } if node.body 63 | instruction.body = class_instructions 64 | @instructions << instruction 65 | @instructions << PushStrInstruction.new(instruction.name, line: node.location.start_line) if used 66 | end 67 | 68 | def transform_constant_read_node(node, used:) 69 | return unless used 70 | 71 | instruction = PushConstInstruction.new(node.name, line: node.location.start_line) 72 | @instructions << instruction 73 | end 74 | 75 | def transform_def_node(node, used:) 76 | @scope_stack << { vars: {} } 77 | params = node.parameters&.requireds || [] 78 | instruction = DefInstruction.new(node.name, line: node.location.start_line) 79 | def_instructions = [] 80 | params.each_with_index do |param, index| 81 | i1 = PushArgInstruction.new(index, line: node.location.start_line) 82 | def_instructions << i1 83 | i2 = SetVarInstruction.new(param.name, nillable: false, line: node.location.start_line) 84 | def_instructions << i2 85 | instruction.params << param.name 86 | end 87 | with_instructions_array(def_instructions) { transform(node.body, used: true) } 88 | instruction.body = def_instructions 89 | @scope_stack.pop 90 | @instructions << instruction 91 | @instructions << PushStrInstruction.new(instruction.name, line: node.location.start_line) if used 92 | end 93 | 94 | def transform_else_node(node, used:) 95 | transform(node.statements, used:) 96 | end 97 | 98 | def transform_false_node(node, used:) 99 | return unless used 100 | 101 | instruction = PushFalseInstruction.new(line: node.location.start_line) 102 | @instructions << instruction 103 | end 104 | 105 | def transform_if_node(node, used:) 106 | transform(node.predicate, used: true) 107 | instruction = IfInstruction.new(line: node.location.start_line) 108 | instruction.if_true = [] 109 | with_instructions_array(instruction.if_true) { transform(node.statements, used: true) } 110 | instruction.if_false = [] 111 | with_instructions_array(instruction.if_false) { transform(node.consequent, used: true) } 112 | @instructions << instruction 113 | @instructions << PopInstruction.new unless used 114 | end 115 | 116 | def transform_instance_variable_write_node(node, used:) 117 | transform(node.value, used: true) 118 | directives = @directives.dig(node.location.start_line, node.name) || [] 119 | nillable = directives.include?(:nillable) || node.name.match?(/_or_nil$/) 120 | instruction = SetInstanceVarInstruction.new(node.name, nillable:, line: node.location.start_line) 121 | @instructions << instruction 122 | @instructions << PopInstruction.new unless used 123 | end 124 | 125 | def transform_integer_node(node, used:) 126 | return unless used 127 | 128 | instruction = PushIntInstruction.new(node.value, line: node.location.start_line) 129 | @instructions << instruction 130 | end 131 | 132 | def transform_local_variable_read_node(node, used:) 133 | return unless used 134 | 135 | instruction = PushVarInstruction.new(node.name, line: node.location.start_line) 136 | @instructions << instruction 137 | end 138 | 139 | def transform_local_variable_write_node(node, used:) 140 | transform(node.value, used: true) 141 | directives = @directives.dig(node.location.start_line, node.name) || [] 142 | nillable = directives.include?(:nillable) || node.name.match?(/_or_nil$/) 143 | instruction = SetVarInstruction.new(node.name, nillable:, line: node.location.start_line) 144 | @instructions << instruction 145 | @instructions << PushVarInstruction.new(node.name, line: node.location.start_line) if used 146 | end 147 | 148 | def transform_nil_node(node, used:) 149 | return unless used 150 | 151 | instruction = PushNilInstruction.new(line: node.location.start_line) 152 | @instructions << instruction 153 | end 154 | 155 | def transform_program_node(node, used:) 156 | transform(node.statements, used:) 157 | end 158 | 159 | def transform_statements_node(node, used:) 160 | node.body.each_with_index { |n, i| transform(n, used: used && i == node.body.size - 1) } 161 | end 162 | 163 | def transform_string_node(node, used:) 164 | return unless used 165 | 166 | instruction = PushStrInstruction.new(node.unescaped, line: node.location.start_line) 167 | @instructions << instruction 168 | end 169 | 170 | def transform_true_node(node, used:) 171 | return unless used 172 | 173 | instruction = PushTrueInstruction.new(line: node.location.start_line) 174 | @instructions << instruction 175 | end 176 | 177 | def with_instructions_array(array) 178 | array_was = @instructions 179 | @instructions = array 180 | yield 181 | ensure 182 | @instructions = array_was 183 | end 184 | 185 | def type_check 186 | TypeChecker.new.analyze(@instructions) 187 | end 188 | 189 | def scope 190 | @scope_stack.last 191 | end 192 | 193 | def vars 194 | scope.fetch(:vars) 195 | end 196 | end 197 | -------------------------------------------------------------------------------- /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 | LIB_PATH = File.expand_path("../../../../build/lib.ll", __dir__) 13 | 14 | def initialize(instructions, dump: false) 15 | @instructions = instructions 16 | @stack = [] 17 | @scope_stack = [{ vars: {} }] 18 | @call_stack = [] 19 | @if_depth = 0 20 | @methods = build_methods 21 | @classes = {} 22 | @dump = dump 23 | @lib = LLVM::Module.parse_ir(LIB_PATH) 24 | end 25 | 26 | attr_reader :instructions 27 | 28 | def run 29 | build_module 30 | execute(@entry) 31 | end 32 | 33 | def dump_ir_to_file(path) 34 | build_module 35 | File.write(path, @module.to_s) 36 | end 37 | 38 | private 39 | 40 | def execute(fn) 41 | LLVM.init_jit 42 | 43 | engine = LLVM::JITCompiler.new(@module) 44 | value = engine.run_function(fn) 45 | return_value = llvm_type_to_ruby(value, @return_type) 46 | engine.dispose 47 | 48 | return_value 49 | end 50 | 51 | def build_module 52 | @module = LLVM::Module.new("llvm") 53 | @return_type = @instructions.last.type! 54 | @entry = @module.functions.add("main", [], llvm_type(@return_type)) 55 | build_function(@entry, @instructions) 56 | @lib.link_into(@module) 57 | #@module.dump if @dump || !@module.valid? 58 | @module.verify! 59 | end 60 | 61 | def build_function(function, instructions) 62 | function.basic_blocks.append.build do |builder| 63 | unused_for_now = LLVM::Int # need at least one struct member 64 | main_obj_struct = LLVM.Struct(unused_for_now, "main") 65 | @main_obj = ObjectBuilder.new(builder:, mod: @module, struct: main_obj_struct).to_ptr 66 | @scope_stack << { function:, vars: {}, self_obj: @main_obj } 67 | build_instructions(function, builder, instructions) { |return_value| builder.ret return_value } 68 | @scope_stack.pop 69 | end 70 | end 71 | 72 | def build_instructions(function, builder, instructions) 73 | instructions.each { |instruction| build(instruction, function, builder) } 74 | return_value = @stack.pop 75 | yield return_value if block_given? 76 | end 77 | 78 | def build(instruction, function, builder) 79 | send("build_#{instruction.instruction_name}", instruction, function, builder) 80 | end 81 | 82 | def build_call(instruction, _function, builder) 83 | args = @stack.pop(instruction.arg_count) 84 | receiver = @stack.pop 85 | receiver_type = instruction.type!.types.first 86 | args.unshift(receiver) 87 | name = instruction.name 88 | fn = @methods.dig(receiver_type.name_for_method_lookup, name) or 89 | raise(NoMethodError, "Method '#{name}' not found") 90 | fn.respond_to?(:call) ? @stack << fn.call(builder:, instruction:, args:) : @stack << builder.call(fn, *args) 91 | end 92 | 93 | def build_class(instruction, function, builder) 94 | name = instruction.name 95 | attr_types = instruction.type!.attributes.values.map { |t| llvm_type(t) } 96 | klass = @classes[name] = LLVM.Struct(*attr_types, name.to_s) 97 | @methods[name.to_sym] = { new: method(:build_call_new) } 98 | @scope_stack << { function:, vars: {}, self_obj: klass } 99 | build_instructions(function, builder, instruction.body) 100 | @scope_stack.pop 101 | end 102 | 103 | def build_def(instruction, _function, _builder) 104 | name = instruction.name 105 | param_types = (0...instruction.params.size).map { |i| llvm_type(instruction.body.fetch(i * 2).type!) } 106 | receiver_type = instruction.receiver_type 107 | param_types.unshift(llvm_type(receiver_type)) 108 | return_type = llvm_type(instruction.return_type) 109 | @methods[receiver_type.name_for_method_lookup] ||= {} 110 | @methods[receiver_type.name_for_method_lookup][name] = fn = 111 | @module.functions.add(name, param_types, return_type) 112 | build_function(fn, instruction.body) 113 | end 114 | 115 | def build_if(instruction, function, builder) 116 | result = builder.alloca(llvm_type(instruction.type!), "if_line_#{instruction.line}") 117 | then_block = function.basic_blocks.append 118 | else_block = function.basic_blocks.append 119 | result_block = function.basic_blocks.append 120 | condition = @stack.pop 121 | builder.cond(condition, then_block, else_block) 122 | then_block.build do |then_builder| 123 | build_instructions(function, then_builder, instruction.if_true) do |value| 124 | then_builder.store(value, result) 125 | then_builder.br(result_block) 126 | end 127 | end 128 | else_block.build do |else_builder| 129 | build_instructions(function, else_builder, instruction.if_false) do |value| 130 | else_builder.store(value, result) 131 | else_builder.br(result_block) 132 | end 133 | end 134 | builder.position_at_end(result_block) 135 | @stack << builder.load(result) 136 | end 137 | 138 | def build_pop(_instruction, _function, _builder) 139 | @stack.pop 140 | end 141 | 142 | def build_push_arg(instruction, _function, _builder) 143 | function = @scope_stack.last.fetch(:function) 144 | @stack << function.params[instruction.index + 1] # receiver is always 0 145 | end 146 | 147 | def build_push_array(instruction, _function, builder) 148 | @stack << build_array(builder, instruction) 149 | end 150 | 151 | def build_push_const(instruction, _function, _builder) 152 | @stack << @classes.fetch(instruction.name) 153 | end 154 | 155 | def build_push_false(_instruction, _function, _builder) 156 | @stack << LLVM::FALSE 157 | end 158 | 159 | def build_push_int(instruction, _function, _builder) 160 | @stack << LLVM.Int(instruction.value) 161 | end 162 | 163 | def build_push_nil(_instruction, _function, _builder) 164 | @stack << RcBuilder.pointer_type.null_pointer 165 | end 166 | 167 | def build_push_self(_instruction, _function, _builder) 168 | @stack << self_obj 169 | end 170 | 171 | def build_push_str(instruction, _function, builder) 172 | @stack << build_string(builder, instruction.value) 173 | end 174 | 175 | def build_push_true(_instruction, _function, _builder) 176 | @stack << LLVM::TRUE 177 | end 178 | 179 | def build_push_var(instruction, _function, builder) 180 | variable = vars.fetch(instruction.name) 181 | @stack << builder.load(variable) 182 | end 183 | 184 | def build_set_ivar(_instruction, _function, _builder) 185 | # value = @stack.last 186 | # self_obj.set_ivar(instruction.name, value) 187 | end 188 | 189 | def build_set_var(instruction, _function, builder) 190 | value = @stack.pop 191 | variable = builder.alloca(value.type, "var_#{instruction.name}") 192 | builder.store(value, variable) 193 | vars[instruction.name] = variable 194 | end 195 | 196 | def scope 197 | @scope_stack.last 198 | end 199 | 200 | def vars 201 | scope.fetch(:vars) 202 | end 203 | 204 | def self_obj 205 | scope.fetch(:self_obj) 206 | end 207 | 208 | def llvm_type(type) 209 | case type.evaluated_type.to_sym 210 | when :bool 211 | LLVM::Int1.type 212 | when :int 213 | LLVM::Int32.type 214 | else 215 | RcBuilder.pointer_type 216 | end 217 | end 218 | 219 | def llvm_type_to_ruby(value, type) 220 | type = type.types.last if type.is_a?(Compiler::CallType) 221 | 222 | case type.to_sym 223 | when :bool, :int, :nil 224 | read_llvm_type_as_ruby(value, type) 225 | when :str 226 | ptr = read_rc_pointer(value) 227 | read_llvm_type_as_ruby(ptr, type) 228 | else 229 | raise "Unknown type: #{type.inspect}" unless type.name == "nillable" 230 | if (ptr = read_rc_pointer(value, nillable: true)) 231 | read_llvm_type_as_ruby(ptr, type.types.first) 232 | end 233 | end 234 | end 235 | 236 | def read_rc_pointer(value, nillable: false) 237 | rc_ptr = value.to_ptr.read_pointer 238 | if nillable && rc_ptr.null? 239 | nil 240 | else 241 | # NOTE: this works because the ptr is the first field of the RC struct. 242 | rc_ptr.read_pointer 243 | end 244 | end 245 | 246 | def read_llvm_type_as_ruby(value, type) 247 | case type.to_sym 248 | when :bool 249 | value.to_i == -1 250 | when :int 251 | value.to_i 252 | when :str 253 | value.read_string 254 | when :nil 255 | nil 256 | else 257 | raise "Unknown type: #{type.inspect}" 258 | end 259 | end 260 | 261 | def build_string(builder, value) 262 | string = StringBuilder.new(builder:, mod: @module, string: value) 263 | string.to_ptr 264 | end 265 | 266 | def build_array(builder, instruction) 267 | elements = @stack.pop(instruction.size) 268 | array_type = instruction.type! 269 | element_type = llvm_type(array_type.types.first) 270 | array = ArrayBuilder.new(builder:, mod: @module, element_type:, elements:) 271 | array.to_ptr 272 | end 273 | 274 | def fn_puts_int 275 | @fn_puts_int ||= @module.functions.add("puts_int", [LLVM::Int32], LLVM::Int32) 276 | end 277 | 278 | def fn_puts_str 279 | @fn_puts_str ||= @module.functions.add("puts_str", [RcBuilder.pointer_type], LLVM::Int32) 280 | end 281 | 282 | def build_methods 283 | { 284 | array: { 285 | first: ->(builder:, instruction:, args:) do 286 | element_type = llvm_type(instruction.type!) 287 | array = ArrayBuilder.new(ptr: args.first, builder:, mod: @module, element_type:) 288 | array.first 289 | end, 290 | last: ->(builder:, instruction:, args:) do 291 | element_type = llvm_type(instruction.type!) 292 | array = ArrayBuilder.new(ptr: args.first, builder:, mod: @module, element_type:) 293 | array.last 294 | end, 295 | "<<": ->(builder:, args:, **) do 296 | element_type = args.last.type 297 | array = ArrayBuilder.new(ptr: args.first, builder:, mod: @module, element_type:) 298 | array.push(args.last) 299 | end 300 | }, 301 | int: { 302 | "+": ->(builder:, args:, **) { builder.add(*args) }, 303 | "-": ->(builder:, args:, **) { builder.sub(*args) }, 304 | "*": ->(builder:, args:, **) { builder.mul(*args) }, 305 | "/": ->(builder:, args:, **) { builder.sdiv(*args) }, 306 | "==": ->(builder:, args:, **) { builder.icmp(:eq, *args) } 307 | }, 308 | "(object main)": { 309 | puts: ->(builder:, args:, instruction:) do 310 | arg = args[1] # receiver is arg 0 311 | arg_type = instruction.type!.types[1].to_sym 312 | case arg_type 313 | when :int 314 | builder.call(fn_puts_int, arg) 315 | when :str 316 | builder.call(fn_puts_str, arg) 317 | else 318 | raise NoMethodError, "Method 'puts' for type #{arg_type.inspect} not found" 319 | end 320 | end 321 | } 322 | } 323 | end 324 | 325 | def build_call_new(builder:, args:, **) 326 | struct = args.first 327 | ObjectBuilder.new(builder:, mod: @module, struct:).to_ptr 328 | end 329 | 330 | # usage: 331 | # diff(@module.functions[11].to_s, @module.functions[0].to_s) 332 | def diff(expected, actual) 333 | File.write("/tmp/actual.ll", actual) 334 | File.write("/tmp/expected.ll", expected) 335 | puts `diff -y -W 134 /tmp/expected.ll /tmp/actual.ll` 336 | end 337 | end 338 | end 339 | end 340 | -------------------------------------------------------------------------------- /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/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::UInt8)) 10 | @builder.store(obj, obj_ptr) 11 | store_ptr(obj_ptr) 12 | end 13 | end 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /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::UInt64, 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::UInt64, LLVM::UInt64, "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 | -------------------------------------------------------------------------------- /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_ptr = @builder.alloca(LLVM::Type.pointer(LLVM::UInt8)) 13 | @builder.store(str, str_ptr) 14 | @builder.call(fn_rc_set_str, @ptr, str_ptr, LLVM.Int(string.bytesize)) 15 | 16 | store_size(string.bytesize) 17 | end 18 | 19 | def fn_rc_set_str 20 | return @fn_rc_set_str if @fn_rc_set_str 21 | 22 | @fn_rc_set_str = 23 | @module.functions["rc_set_str"] || 24 | @module.functions.add( 25 | "rc_set_str", 26 | [pointer_type, LLVM::Type.pointer(LLVM::UInt8), LLVM::UInt32], 27 | LLVM::Type.void 28 | ) 29 | end 30 | end 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /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 | pruned = @type.prune 17 | raise TypeError, "Not enough information to infer type of #{inspect}" if pruned.is_a?(TypeVariable) 18 | 19 | @pruned_type = pruned 20 | end 21 | 22 | def inspect 23 | "<#{self.class.name} #{instance_variables.map { |iv| "#{iv}=#{instance_variable_get(iv).inspect}" }.join(" ")}>" 24 | end 25 | 26 | def to_h 27 | { type: type!.to_s, instruction: instruction_name } 28 | end 29 | end 30 | 31 | class PushIntInstruction < Instruction 32 | def initialize(value, line:) 33 | super(line:) 34 | @value = value 35 | end 36 | 37 | attr_reader :value 38 | 39 | def instruction_name = :push_int 40 | 41 | def to_h 42 | super.merge(value:) 43 | end 44 | end 45 | 46 | class PushStrInstruction < Instruction 47 | def initialize(value, line:) 48 | super(line:) 49 | @value = value 50 | end 51 | 52 | attr_reader :value 53 | 54 | def instruction_name = :push_str 55 | 56 | def to_h 57 | super.merge(value:) 58 | end 59 | end 60 | 61 | class PushTrueInstruction < Instruction 62 | def instruction_name = :push_true 63 | end 64 | 65 | class PushFalseInstruction < Instruction 66 | def instruction_name = :push_false 67 | end 68 | 69 | class PushNilInstruction < Instruction 70 | def instruction_name = :push_nil 71 | end 72 | 73 | class PushArrayInstruction < Instruction 74 | def initialize(size, line:) 75 | super(line:) 76 | @size = size 77 | end 78 | 79 | attr_reader :size 80 | 81 | def instruction_name = :push_array 82 | 83 | def to_h 84 | super.merge(size:) 85 | end 86 | end 87 | 88 | class PushVarInstruction < Instruction 89 | def initialize(name, line:) 90 | super(line:) 91 | @name = name 92 | end 93 | 94 | attr_reader :name 95 | 96 | def instruction_name = :push_var 97 | 98 | def to_h 99 | super.merge(name:) 100 | end 101 | end 102 | 103 | class SetVarInstruction < Instruction 104 | def initialize(name, nillable:, line:) 105 | super(line:) 106 | @name = name 107 | @nillable = nillable 108 | end 109 | 110 | attr_reader :name 111 | 112 | def nillable? = @nillable 113 | 114 | def instruction_name = :set_var 115 | 116 | def to_h 117 | super.merge(name:, nillable: nillable?) 118 | end 119 | end 120 | 121 | class SetInstanceVarInstruction < Instruction 122 | def initialize(name, nillable:, line:) 123 | super(line:) 124 | @name = name 125 | @nillable = nillable 126 | end 127 | 128 | attr_reader :name 129 | 130 | def nillable? = @nillable 131 | 132 | def instruction_name = :set_ivar 133 | 134 | def to_h 135 | super.merge(name:, nillable: nillable?) 136 | end 137 | end 138 | 139 | class PushArgInstruction < Instruction 140 | def initialize(index, line:) 141 | super(line:) 142 | @index = index 143 | end 144 | 145 | attr_reader :index 146 | 147 | def instruction_name = :push_arg 148 | 149 | def to_h 150 | super.merge(index:) 151 | end 152 | end 153 | 154 | class PushConstInstruction < Instruction 155 | def initialize(name, line:) 156 | super(line:) 157 | @name = name 158 | end 159 | 160 | attr_reader :name 161 | 162 | def instruction_name = :push_const 163 | 164 | def to_h 165 | super.merge(name:) 166 | end 167 | end 168 | 169 | class CallInstruction < Instruction 170 | def initialize(name, arg_count:, line:) 171 | super(line:) 172 | @name = name 173 | @arg_count = arg_count 174 | end 175 | 176 | attr_reader :name, :arg_count 177 | 178 | def instruction_name = :call 179 | 180 | def to_h 181 | super.merge(name:, arg_count:) 182 | end 183 | end 184 | 185 | class IfInstruction < Instruction 186 | attr_accessor :if_true, :if_false 187 | 188 | def instruction_name = :if 189 | 190 | def to_h 191 | super.merge(if_true: if_true.map(&:to_h), if_false: if_false.map(&:to_h)) 192 | end 193 | end 194 | 195 | class DefInstruction < Instruction 196 | def initialize(name, line:) 197 | super(line:) 198 | @name = name 199 | @params = [] 200 | end 201 | 202 | attr_reader :name 203 | attr_accessor :body, :params 204 | 205 | def instruction_name = :def 206 | 207 | def receiver_type = type!.types.first 208 | def return_type = type!.types.last 209 | 210 | def to_h 211 | super.merge(name:, params:, body: body.map(&:to_h)) 212 | end 213 | end 214 | 215 | class ClassInstruction < Instruction 216 | def initialize(name, line:) 217 | super(line:) 218 | @name = name 219 | end 220 | 221 | attr_reader :name 222 | attr_accessor :body 223 | 224 | def instruction_name = :class 225 | 226 | def to_h 227 | super.merge(name:, body: body.map(&:to_h)) 228 | end 229 | end 230 | 231 | class PopInstruction < Instruction 232 | def instruction_name = :pop 233 | end 234 | 235 | class PushSelfInstruction < Instruction 236 | def instruction_name = :push_self 237 | end 238 | end 239 | -------------------------------------------------------------------------------- /lib/mya/compiler/type_checker.rb: -------------------------------------------------------------------------------- 1 | class Compiler 2 | class TypeVariable 3 | def initialize(type_checker) 4 | @type_checker = type_checker 5 | @id = @type_checker.next_variable_id 6 | @generic = true 7 | end 8 | 9 | attr_accessor :id, :instance 10 | 11 | def name 12 | @name ||= @type_checker.next_variable_name 13 | end 14 | 15 | def to_s = name.to_s 16 | 17 | def to_sym = name.to_sym 18 | 19 | def inspect = "TypeVariable(id = #{id})" 20 | 21 | def generic? = @generic 22 | 23 | def non_generic! 24 | @generic = false 25 | self 26 | end 27 | 28 | def prune 29 | return self if @instance.nil? 30 | 31 | @instance = @instance.prune 32 | @instance.non_generic! unless generic? 33 | @instance 34 | end 35 | end 36 | 37 | class TypeOperator 38 | def initialize(name, types) 39 | @name = name 40 | @types = types 41 | end 42 | 43 | attr_accessor :name, :types 44 | 45 | def name_for_method_lookup = name.to_sym 46 | 47 | def to_s 48 | case types.size 49 | when 0 50 | name.to_s 51 | when 2 52 | "(#{types[0]} #{name} #{types[1]})" 53 | else 54 | "#{name} #{types.join(" ")}" 55 | end 56 | end 57 | 58 | def to_sym 59 | to_s.to_sym 60 | end 61 | 62 | def inspect 63 | "#" 64 | end 65 | 66 | def non_generic! 67 | types.each(&:non_generic!) 68 | self 69 | end 70 | 71 | def prune 72 | dup.tap { |new_type| new_type.types = types.map(&:prune) } 73 | end 74 | 75 | def contains?(other_type) 76 | raise "expected TypeVariable" unless other_type.is_a?(TypeVariable) 77 | 78 | types.any? do |candidate| 79 | case candidate 80 | when TypeVariable 81 | candidate == other_type 82 | when TypeOperator 83 | candidate.contains?(other_type) 84 | else 85 | raise "Unexpected type: #{candidate.inspect}" 86 | end 87 | end 88 | end 89 | 90 | def evaluated_type = self 91 | end 92 | 93 | class MethodType < TypeOperator 94 | def initialize(*types) 95 | super("->", types) 96 | end 97 | 98 | def to_s 99 | *arg_types, return_type = types 100 | "([#{arg_types.join(", ")}] -> #{return_type})" 101 | end 102 | 103 | def inspect 104 | "#" 105 | end 106 | 107 | def evaluated_type = types.last 108 | end 109 | 110 | # This just exists for debugging purposes. 111 | # We could easily use MethodType, but would lose some context when puts debugging. :-) 112 | class CallType < MethodType 113 | def inspect 114 | super.sub("MethodType", "CallType") 115 | end 116 | end 117 | 118 | class UnionType < TypeOperator 119 | def initialize(*types) 120 | super("union", types) 121 | end 122 | 123 | def to_s 124 | "(#{types.join(" | ")})" 125 | end 126 | 127 | def select_type(other_type) 128 | types.detect { |candidate| candidate.name == other_type.name && candidate.types.size == other_type.types.size } 129 | end 130 | end 131 | 132 | class AryType < TypeOperator 133 | def initialize(type) 134 | super("array", [type]) 135 | end 136 | 137 | def to_s 138 | "(#{types[0]} array)" 139 | end 140 | end 141 | 142 | class NillableType < TypeOperator 143 | def initialize(type) 144 | super("nillable", [type]) 145 | end 146 | 147 | def to_s 148 | "(nillable #{types[0]})" 149 | end 150 | end 151 | 152 | class ClassType < TypeOperator 153 | def initialize(class_name) 154 | super(class_name.to_s, []) 155 | @attributes = {} 156 | end 157 | 158 | def class_name = name 159 | attr_reader :attributes 160 | 161 | def to_s 162 | attrs = attributes.map { |name, type| "#{name}:#{type}" }.join(", ") 163 | "(class #{class_name} #{attrs})" 164 | end 165 | 166 | def inspect = "#" 167 | 168 | def name_for_method_lookup = class_name.to_sym 169 | end 170 | 171 | class ObjectType < TypeOperator 172 | def initialize(klass) 173 | super("object", [klass]) 174 | end 175 | 176 | def klass = types.first 177 | 178 | def to_s = "(object #{klass.class_name})" 179 | 180 | def inspect = "#" 181 | 182 | def name_for_method_lookup = to_s.to_sym 183 | end 184 | 185 | IntType = TypeOperator.new("int", []) 186 | StrType = TypeOperator.new("str", []) 187 | NilType = TypeOperator.new("nil", []) 188 | BoolType = TypeOperator.new("bool", []) 189 | 190 | class TypeChecker 191 | class Error < StandardError 192 | end 193 | class RecursiveUnification < Error 194 | end 195 | class TypeClash < Error 196 | end 197 | class UndefinedMethod < Error 198 | end 199 | class UndefinedVariable < Error 200 | end 201 | 202 | MainClass = ClassType.new("main") 203 | MainObject = ObjectType.new(MainClass) 204 | 205 | def initialize 206 | @stack = [] 207 | @scope_stack = [{ vars: {}, self_type: MainClass }] 208 | @classes = {} 209 | @calls_to_unify = [] 210 | @methods = build_initial_methods 211 | end 212 | 213 | def analyze(instruction) 214 | analyze_instruction(instruction).prune 215 | @calls_to_unify.each do |call| 216 | type_of_receiver = call.fetch(:type_of_receiver).prune 217 | analyze_call_with_known_receiver(**call, type_of_receiver:) 218 | end 219 | end 220 | 221 | def next_variable_id 222 | if @next_variable_id 223 | @next_variable_id += 1 224 | else 225 | @next_variable_id = 0 226 | end 227 | end 228 | 229 | def next_variable_name 230 | if @next_variable_name 231 | @next_variable_name = @next_variable_name.succ 232 | else 233 | @next_variable_name = "a" 234 | end 235 | end 236 | 237 | private 238 | 239 | def analyze_instruction(instruction) 240 | return analyze_array_of_instructions(instruction) if instruction.is_a?(Array) 241 | 242 | send("analyze_#{instruction.instruction_name}", instruction) 243 | end 244 | 245 | def analyze_array_of_instructions(array) 246 | array.map { |instruction| analyze_instruction(instruction) }.last 247 | end 248 | 249 | def analyze_call(instruction) 250 | type_of_args = pop(instruction.arg_count) 251 | type_of_receiver = pop&.prune 252 | type_of_return = TypeVariable.new(self) 253 | type_of_call = CallType.new(type_of_receiver, *type_of_args, type_of_return) 254 | 255 | if type_of_receiver.is_a?(TypeVariable) 256 | # We cannot unify yet, since we don't know the receiver type. 257 | # Save this call for later unification. 258 | @calls_to_unify << { type_of_receiver:, type_of_call:, instruction: } 259 | instruction.type = type_of_call 260 | @stack << type_of_return 261 | return type_of_return 262 | end 263 | 264 | analyze_call_with_known_receiver(type_of_receiver:, type_of_call:, instruction:) 265 | 266 | instruction.type = type_of_call 267 | @stack << type_of_return 268 | type_of_return 269 | end 270 | 271 | def analyze_call_with_known_receiver(type_of_receiver:, type_of_call:, instruction:) 272 | type_of_method = retrieve_method(type_of_receiver, instruction.name) 273 | unless type_of_method 274 | raise UndefinedMethod, "undefined method #{instruction.name} for type #{type_of_receiver.inspect}" 275 | end 276 | 277 | unify_type(type_of_method, type_of_call, instruction) 278 | end 279 | 280 | def analyze_class(instruction) 281 | class_type = @classes[instruction.name] = ClassType.new(instruction.name) 282 | object_type = ObjectType.new(class_type) 283 | @methods[class_type.name_for_method_lookup] = { new: MethodType.new(class_type, object_type) } 284 | 285 | @scope_stack << { vars: {}, self_type: class_type } 286 | analyze_instruction(instruction.body) 287 | @scope_stack.pop 288 | 289 | instruction.type = class_type 290 | end 291 | 292 | def analyze_def(instruction) 293 | placeholder_var = TypeVariable.new(self) 294 | vars[instruction.name] = placeholder_var.non_generic! 295 | @methods[current_object_type&.name_for_method_lookup][instruction.name] = placeholder_var.non_generic! 296 | 297 | new_vars = {} 298 | parameter_types = instruction.params.map { |param| new_vars[param] = TypeVariable.new(self).non_generic! } 299 | 300 | @scope_stack << scope.merge(parameter_types:, vars: new_vars) 301 | type_of_body = analyze_instruction(instruction.body) 302 | @scope_stack.pop 303 | 304 | type_of_method = MethodType.new(current_object_type, *parameter_types, type_of_body) 305 | unify_type(type_of_method, placeholder_var, instruction) 306 | 307 | @methods[current_object_type&.name_for_method_lookup][instruction.name] = type_of_method.non_generic! 308 | instruction.type = type_of_method 309 | 310 | type_of_method 311 | end 312 | 313 | def analyze_if(instruction) 314 | _condition = pop 315 | type_of_then = analyze_instruction(instruction.if_true) 316 | type_of_else = analyze_instruction(instruction.if_false) 317 | unify_type(type_of_then, type_of_else, instruction) 318 | instruction.type = type_of_then 319 | end 320 | 321 | def analyze_pop(instruction) 322 | instruction.type = NilType 323 | end 324 | 325 | def analyze_push_arg(instruction) 326 | type = scope.fetch(:parameter_types).fetch(instruction.index) 327 | @stack << type 328 | instruction.type = type 329 | end 330 | 331 | def analyze_push_array(instruction) 332 | members = pop(instruction.size) 333 | if members.any?(NilType) 334 | members.map! do |type| 335 | if type == NilType 336 | NillableType.new(TypeVariable.new(self)) 337 | else 338 | NillableType.new(type) 339 | end 340 | end 341 | end 342 | members.each_cons(2) { |a, b| unify_type(a, b, instruction) } 343 | member_type = members.first || TypeVariable.new(self) 344 | member_type.non_generic! 345 | type_of_array = AryType.new(member_type) 346 | @stack << type_of_array 347 | instruction.type = type_of_array 348 | end 349 | 350 | def analyze_push_const(instruction) 351 | klass = @classes[instruction.name] 352 | @stack << klass 353 | instruction.type = klass 354 | end 355 | 356 | def analyze_push_false(instruction) 357 | @stack << BoolType 358 | instruction.type = BoolType 359 | end 360 | 361 | def analyze_push_int(instruction) 362 | @stack << IntType 363 | instruction.type = IntType 364 | end 365 | 366 | def analyze_push_nil(instruction) 367 | @stack << NilType 368 | instruction.type = NilType 369 | end 370 | 371 | def analyze_push_self(instruction) 372 | @stack << current_object_type 373 | instruction.type = current_object_type 374 | end 375 | 376 | def analyze_push_str(instruction) 377 | @stack << StrType 378 | instruction.type = StrType 379 | end 380 | 381 | def analyze_push_true(instruction) 382 | @stack << BoolType 383 | instruction.type = BoolType 384 | end 385 | 386 | def analyze_push_var(instruction) 387 | type = retrieve_var(instruction.name) 388 | raise UndefinedVariable, "undefined variable #{instruction.name.inspect}" unless type 389 | 390 | @stack << type 391 | instruction.type = type 392 | end 393 | 394 | def analyze_set_ivar(instruction) 395 | type = pop 396 | if (existing_type = vars[instruction.name]) 397 | unify_type(existing_type, type, instruction) 398 | type = existing_type 399 | elsif type == NilType 400 | type = NillableType.new(TypeVariable.new(self)) 401 | elsif instruction.nillable? 402 | type = NillableType.new(type) 403 | end 404 | current_class_type.attributes[instruction.name] = type 405 | instruction.type = type 406 | end 407 | 408 | def analyze_set_var(instruction) 409 | type = pop 410 | if (existing_type = vars[instruction.name]) 411 | unify_type(existing_type, type, instruction) 412 | type = existing_type 413 | elsif type == NilType 414 | type = NillableType.new(TypeVariable.new(self)) 415 | elsif instruction.nillable? 416 | type = NillableType.new(type) 417 | end 418 | vars[instruction.name] = type 419 | instruction.type = type 420 | end 421 | 422 | def retrieve_method(type, name) 423 | return unless (method = @methods.dig(type&.name_for_method_lookup, name)) 424 | 425 | fresh_type(method) 426 | end 427 | 428 | def retrieve_var(name) 429 | return unless (type = vars[name]) 430 | 431 | fresh_type(type) 432 | end 433 | 434 | def fresh_type(type, env = {}) 435 | type = type.prune 436 | case type 437 | when TypeVariable 438 | if type.generic? 439 | env[type] ||= TypeVariable.new(self) 440 | else 441 | type 442 | end 443 | when TypeOperator 444 | type.dup.tap { |new_type| new_type.types = type.types.map { |t| fresh_type(t, env) } } 445 | end 446 | end 447 | 448 | def unify_type(a, b, instruction = nil) 449 | a = a.prune 450 | b = b.prune 451 | case a 452 | when TypeVariable 453 | unify_type_variable(a, b, instruction) 454 | when TypeOperator 455 | unify_type_operator(a, b, instruction) 456 | else 457 | raise "Unknown type: #{a.inspect}" 458 | end 459 | end 460 | 461 | def unify_type_variable(a, b, _instruction) 462 | return if a == b 463 | 464 | raise RecursiveUnification, "recursive unification: #{b} contains #{a}" if b.is_a?(TypeOperator) && b.contains?(a) 465 | 466 | a.instance = b 467 | end 468 | 469 | def unify_type_operator(a, b, instruction) 470 | case b 471 | when TypeVariable 472 | unify_type_variable(b, a, instruction) 473 | when TypeOperator 474 | case a 475 | when UnionType 476 | unify_union_type(a, b, instruction) 477 | when NillableType 478 | unify_nillable_type(a, b, instruction) 479 | else 480 | unify_type_operator_with_type_operator(a, b, instruction) 481 | end 482 | else 483 | raise "Unknown type: #{b.inspect}" 484 | end 485 | end 486 | 487 | def unify_nillable_type(a, b, instruction) 488 | return if b.name == "nil" 489 | 490 | if b.is_a?(NillableType) 491 | unify_type(a.types.first, b.types.first, instruction) 492 | elsif a.types.first.is_a?(TypeVariable) 493 | unify_type(a.types.first, b) 494 | else 495 | raise_type_clash_error(a, b, instruction) 496 | end 497 | end 498 | 499 | def unify_union_type(a, b, instruction) 500 | unless (selected = a.select_type(b)) 501 | raise_type_clash_error(a, b, instruction) 502 | end 503 | 504 | unify_type(selected, b, instruction) 505 | end 506 | 507 | def unify_type_operator_with_type_operator(a, b, instruction) 508 | raise_type_clash_error(a, b, instruction) unless a.name == b.name && a.types.size == b.types.size 509 | 510 | begin 511 | unify_args(a.types, b.types, instruction) 512 | rescue TypeClash 513 | # We want to produce an error message for the whole instruction, e.g. 514 | # call, array, etc. -- not for an individual type inside. 515 | raise_type_clash_error(a, b, instruction) 516 | end 517 | end 518 | 519 | def unify_args(list1, list2, instruction) 520 | list1.zip(list2) { |a, b| unify_type(a, b, instruction) } 521 | end 522 | 523 | def raise_type_clash_error(a, b, instruction = nil) 524 | message = 525 | case instruction 526 | when CallInstruction 527 | "#{a} cannot unify with #{b} in call to #{instruction.name} on line #{instruction.line}" 528 | when IfInstruction 529 | "one branch of `if` has type #{a} and the other has type #{b}" 530 | when SetVarInstruction 531 | "the variable #{instruction.name} has type #{a} already; you cannot change it to type #{b}" 532 | when PushArrayInstruction 533 | "the array contains type #{a} but you are trying to push type #{b}" 534 | else 535 | "#{a} cannot unify with #{b} #{instruction.inspect}" 536 | end 537 | raise TypeClash, message 538 | end 539 | 540 | def pop(count = nil) 541 | if count 542 | values = @stack.pop(count) 543 | raise "Not enough values on stack! (Expected #{count} but got #{values.inspect})" unless values.size == count 544 | return values 545 | end 546 | 547 | value = @stack.pop 548 | raise "Nothing on stack!" unless value 549 | value 550 | end 551 | 552 | def scope 553 | @scope_stack.last or raise("No scope!") 554 | end 555 | 556 | def vars 557 | scope.fetch(:vars) 558 | end 559 | 560 | def current_class_type 561 | scope.fetch(:self_type) 562 | end 563 | 564 | def current_object_type 565 | ObjectType.new(current_class_type) if current_class_type 566 | end 567 | 568 | def build_initial_methods 569 | array_type = TypeVariable.new(self) 570 | array = AryType.new(array_type) 571 | { 572 | "(object main)": { 573 | puts: MethodType.new(MainObject, UnionType.new(IntType, StrType), IntType) 574 | }, 575 | int: { 576 | zero?: MethodType.new(IntType, BoolType), 577 | "+": MethodType.new(IntType, IntType, IntType), 578 | "==": MethodType.new(IntType, IntType, BoolType), 579 | "-": MethodType.new(IntType, IntType, IntType), 580 | "*": MethodType.new(IntType, IntType, IntType), 581 | "/": MethodType.new(IntType, IntType, IntType) 582 | }, 583 | array: { 584 | nth: MethodType.new(array, IntType, array_type), 585 | first: MethodType.new(array, array_type), 586 | last: MethodType.new(array, array_type), 587 | "<<": MethodType.new(array, array_type, array), 588 | push: MethodType.new(array, array_type, array_type) 589 | } 590 | }.tap { |hash| hash.default_proc = proc { |h, key| h[key] = {} } } 591 | end 592 | end 593 | end 594 | -------------------------------------------------------------------------------- /lib/mya/vm.rb: -------------------------------------------------------------------------------- 1 | class VM 2 | class ClassType 3 | def initialize(name) 4 | @name = name 5 | @methods = {} 6 | end 7 | 8 | attr_reader :methods, :name 9 | 10 | def new 11 | ObjectType.new(self) 12 | end 13 | end 14 | 15 | class ObjectType 16 | def initialize(klass) 17 | @klass = klass 18 | @ivars = {} 19 | end 20 | 21 | attr_reader :klass 22 | 23 | def methods = klass.methods 24 | 25 | def set_ivar(name, value) 26 | @ivars[name] = value 27 | end 28 | end 29 | 30 | MainClass = ClassType.new("main") 31 | MainObject = ObjectType.new(MainClass) 32 | 33 | def initialize(instructions, io: $stdout) 34 | @instructions = instructions 35 | @stack = [] 36 | @frames = [{ instructions:, return_index: nil }] 37 | @scope_stack = [{ args: [], vars: {}, self_obj: MainObject }] 38 | @if_depth = 0 39 | @classes = {} 40 | @io = io 41 | end 42 | 43 | def run 44 | @index = 0 45 | while @frames.any? 46 | while @index < instructions.size 47 | instruction = instructions[@index] 48 | @index += 1 49 | execute(instruction) 50 | end 51 | frame = @frames.pop 52 | @scope_stack.pop if frame[:with_scope] 53 | @index = frame.fetch(:return_index) 54 | end 55 | @stack.pop 56 | end 57 | 58 | private 59 | 60 | def instructions 61 | @frames.last.fetch(:instructions) 62 | end 63 | 64 | BUILT_IN_METHODS = { 65 | puts: ->(arg, io:) do 66 | io.puts(arg) 67 | arg.to_s.size 68 | end 69 | }.freeze 70 | 71 | def execute(instruction) 72 | send("execute_#{instruction.instruction_name}", instruction) 73 | end 74 | 75 | def execute_class(instruction) 76 | klass = @classes[instruction.name] = ClassType.new(instruction.name) 77 | push_frame(instructions: instruction.body, return_index: @index, with_scope: true) 78 | @scope_stack << { vars: {}, self_obj: klass } 79 | end 80 | 81 | def execute_def(instruction) 82 | self_obj.methods[instruction.name] = instruction.body 83 | end 84 | 85 | def execute_call(instruction) 86 | new_args = @stack.pop(instruction.arg_count) 87 | 88 | receiver = @stack.pop or raise(ArgumentError, "No receiver") 89 | name = instruction.name 90 | if receiver.respond_to?(name) 91 | @stack << receiver.send(name, *new_args) 92 | return 93 | end 94 | if receiver.methods.key?(name) 95 | push_frame(instructions: receiver.methods[name], return_index: @index, with_scope: true) 96 | @scope_stack << { args: new_args, vars: {}, self_obj: receiver } 97 | return 98 | end 99 | 100 | if (built_in_method = BUILT_IN_METHODS[instruction.name]) 101 | @stack << built_in_method.call(*new_args, io: @io) 102 | return 103 | end 104 | 105 | method_body = self_obj.methods[instruction.name] 106 | raise NoMethodError, "Undefined method #{instruction.name}" unless method_body 107 | 108 | push_frame(instructions: method_body, return_index: @index, with_scope: true) 109 | @scope_stack << { args: new_args, vars: {}, self_obj: } 110 | end 111 | 112 | def execute_if(instruction) 113 | condition = @stack.pop 114 | body = condition ? instruction.if_true : instruction.if_false 115 | push_frame(instructions: body, return_index: @index) 116 | end 117 | 118 | def execute_pop(_) 119 | @stack.pop 120 | end 121 | 122 | def execute_push_arg(instruction) 123 | @stack << args.fetch(instruction.index) 124 | end 125 | 126 | def execute_push_array(instruction) 127 | ary = @stack.pop(instruction.size) 128 | @stack << ary 129 | end 130 | 131 | def execute_push_const(instruction) 132 | @stack << @classes[instruction.name] 133 | end 134 | 135 | def execute_push_false(_) 136 | @stack << false 137 | end 138 | 139 | def execute_push_int(instruction) 140 | @stack << instruction.value 141 | end 142 | 143 | def execute_push_nil(_) 144 | @stack << nil 145 | end 146 | 147 | def execute_push_str(instruction) 148 | @stack << instruction.value 149 | end 150 | 151 | def execute_push_self(_instruction) 152 | @stack << self_obj 153 | end 154 | 155 | def execute_push_true(_) 156 | @stack << true 157 | end 158 | 159 | def execute_push_var(instruction) 160 | @stack << vars.fetch(instruction.name) 161 | end 162 | 163 | def execute_set_var(instruction) 164 | vars[instruction.name] = @stack.pop 165 | end 166 | 167 | def execute_set_ivar(instruction) 168 | value = @stack.last 169 | self_obj.set_ivar(instruction.name, value) 170 | end 171 | 172 | def push_frame(instructions:, return_index:, with_scope: false) 173 | @frames << { instructions:, return_index:, with_scope: } 174 | @index = 0 175 | end 176 | 177 | def scope 178 | @scope_stack.last 179 | end 180 | 181 | def self_obj 182 | scope.fetch(:self_obj) 183 | end 184 | 185 | def vars 186 | scope.fetch(:vars) 187 | end 188 | 189 | def args 190 | scope.fetch(:args) 191 | end 192 | end 193 | -------------------------------------------------------------------------------- /spec/all.rb: -------------------------------------------------------------------------------- 1 | require_relative "compiler_spec" 2 | require_relative "compiler/backends/llvm_backend_spec" 3 | require_relative "vm_spec" 4 | -------------------------------------------------------------------------------- /spec/compiler/backends/llvm_backend_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative "../../spec_helper" 2 | require "tempfile" 3 | 4 | describe Compiler::Backends::LLVMBackend do 5 | def execute(code) 6 | instructions = Compiler.new(code).compile 7 | Compiler::Backends::LLVMBackend.new(instructions).run 8 | end 9 | 10 | it "evaluates integers" do 11 | expect(execute("123")).must_equal(123) 12 | end 13 | 14 | it "evaluates strings" do 15 | expect(execute('"foo"')).must_equal("foo") 16 | end 17 | 18 | it "evaluates nil" do 19 | expect(execute("nil")).must_be_nil 20 | end 21 | 22 | it "evaluates booleans" do 23 | expect(execute("true")).must_equal(true) 24 | expect(execute("false")).must_equal(false) 25 | end 26 | 27 | it "evaluates classes" do 28 | code = <<~CODE 29 | class Foo 30 | def bar 31 | @bar = 10 32 | end 33 | end 34 | Foo.new.bar 35 | CODE 36 | expect(execute(code)).must_equal(10) 37 | end 38 | 39 | it "evaluates variables set and get" do 40 | expect(execute("a = 1; a + a")).must_equal(2) 41 | end 42 | 43 | it "evaluates arrays" do 44 | code = <<~CODE 45 | a = [1, 2, 3] 46 | a.first 47 | CODE 48 | expect(execute(code)).must_equal(1) 49 | 50 | code = <<~CODE 51 | a = [1, 2, 3] 52 | a.last 53 | CODE 54 | expect(execute(code)).must_equal(3) 55 | 56 | code = <<~CODE 57 | a = [1, 2, 3] 58 | a.last 59 | CODE 60 | expect(execute(code)).must_equal(3) 61 | 62 | code = <<~CODE 63 | a = [] 64 | a << 4 65 | a << 5 66 | a.last 67 | CODE 68 | expect(execute(code)).must_equal(5) 69 | 70 | code = <<~CODE 71 | a = [] 72 | a << "foo" 73 | a << "bar" 74 | a.last 75 | CODE 76 | expect(execute(code)).must_equal("bar") 77 | 78 | code = <<~CODE 79 | a = ["foo", "bar", "baz"] 80 | a.first 81 | CODE 82 | expect(execute(code)).must_equal("foo") 83 | 84 | code = <<~CODE 85 | a = ["foo", "bar", "baz"] 86 | a.last 87 | CODE 88 | expect(execute(code)).must_equal("baz") 89 | 90 | code = <<~CODE 91 | a = [nil, "foo", "bar"] 92 | a.last 93 | CODE 94 | expect(execute(code)).must_equal("bar") 95 | 96 | code = <<~CODE 97 | a = ["foo", "bar", nil] 98 | a.last 99 | CODE 100 | expect(execute(code)).must_be_nil 101 | 102 | code = <<~CODE 103 | a = [1, 2, 3] 104 | b = [4, 5, 6] 105 | c = [a, b, nil] 106 | c.last 107 | CODE 108 | expect(execute(code)).must_be_nil 109 | end 110 | 111 | it "evaluates method definitions" do 112 | code = <<~CODE 113 | def foo 114 | 'foo' 115 | end 116 | 117 | foo 118 | CODE 119 | expect(execute(code)).must_equal("foo") 120 | end 121 | 122 | it "evaluates method definitions with arguments" do 123 | code = <<~CODE 124 | def bar(x) 125 | x 126 | end 127 | 128 | def foo(a, b) 129 | bar(b - 10) 130 | end 131 | 132 | foo('foo', 100) 133 | CODE 134 | expect(execute(code)).must_equal(90) 135 | end 136 | 137 | it "does not stomp on method arguments" do 138 | code = <<~CODE 139 | def bar(b) 140 | b 141 | end 142 | 143 | def foo(a, b) 144 | bar(b - 10) 145 | b 146 | end 147 | 148 | foo('foo', 100) 149 | CODE 150 | expect(execute(code)).must_equal(100) 151 | end 152 | 153 | it "evaluates operator expressions" do 154 | expect(execute("1 + 2")).must_equal 3 155 | expect(execute("3 - 1")).must_equal 2 156 | expect(execute("2 * 3")).must_equal 6 157 | expect(execute("6 / 2")).must_equal 3 158 | expect(execute("3 == 3")).must_equal true 159 | expect(execute("3 == 4")).must_equal false 160 | end 161 | 162 | it "evaluates simple if expressions" do 163 | code = <<~CODE 164 | if true 165 | 3 166 | else 167 | 4 168 | end 169 | CODE 170 | expect(execute(code)).must_equal(3) 171 | 172 | code = <<~CODE 173 | if false 174 | 3 175 | else 176 | 4 177 | end 178 | CODE 179 | expect(execute(code)).must_equal(4) 180 | end 181 | 182 | it "evaluates nested if expressions" do 183 | code = <<~CODE 184 | if false 185 | if true 186 | 1 187 | else 188 | 2 189 | end 190 | else 191 | if true 192 | 3 # <-- this one 193 | else 194 | 4 195 | end 196 | end 197 | CODE 198 | expect(execute(code)).must_equal(3) 199 | 200 | code = <<~CODE 201 | if true 202 | if false 203 | 1 204 | else 205 | 2 # <-- this one 206 | end 207 | else 208 | if false 209 | 3 210 | else 211 | 4 212 | end 213 | end 214 | CODE 215 | expect(execute(code)).must_equal(2) 216 | 217 | code = <<~CODE 218 | if false 219 | if false 220 | 1 221 | else 222 | 2 223 | end 224 | elsif false 225 | 3 226 | else 227 | if false 228 | 4 229 | else 230 | 5 # <-- this one 231 | end 232 | end 233 | CODE 234 | expect(execute(code)).must_equal(5) 235 | end 236 | 237 | def execute_code(code) 238 | temp = Tempfile.create("compiled.ll") 239 | temp.close 240 | instructions = Compiler.new(code).compile 241 | Compiler::Backends::LLVMBackend.new(instructions).dump_ir_to_file(temp.path) 242 | `#{lli} #{temp.path} 2>&1` 243 | ensure 244 | File.unlink(temp.path) 245 | end 246 | 247 | def lli 248 | return @lli if @lli 249 | 250 | major_version = LLVM::RUBY_LLVM_VERSION.split(".").first 251 | @lli = (system("command -v lli-#{major_version} 2>/dev/null >/dev/null") ? "lli-#{major_version}" : "lli") 252 | end 253 | 254 | it "evaluates puts for both int and str" do 255 | code = <<~CODE 256 | puts(123) 257 | puts("foo") 258 | CODE 259 | out = execute_code(code) 260 | expect(out).must_equal("123\nfoo\n") 261 | end 262 | 263 | def execute_file(path) 264 | execute_code(File.read(path)) 265 | end 266 | 267 | it "evaluates nillable strings" do 268 | code = <<~CODE 269 | a = "foo" # a:nillable 270 | a = nil 271 | a 272 | CODE 273 | expect(execute(code)).must_be_nil 274 | 275 | code = <<~CODE 276 | a = nil 277 | a = "foo" 278 | a 279 | CODE 280 | expect(execute(code)).must_equal("foo") 281 | end 282 | 283 | it "evaluates examples/fib.rb" do 284 | result = execute_file(File.expand_path("../../../examples/fib.rb", __dir__)) 285 | expect(result).must_equal("55\n") 286 | end 287 | 288 | it "evaluates examples/fact.rb" do 289 | result = execute_file(File.expand_path("../../../examples/fact.rb", __dir__)) 290 | expect(result).must_equal("3628800\n") 291 | end 292 | end 293 | -------------------------------------------------------------------------------- /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: "int", instruction: :push_int, value: 1 }] 10 | end 11 | 12 | it "compiles strings" do 13 | expect(compile('"foo"')).must_equal_with_diff [{ type: "str", instruction: :push_str, value: "foo" }] 14 | end 15 | 16 | it "compiles booleans" do 17 | expect(compile("true")).must_equal_with_diff [{ type: "bool", instruction: :push_true }] 18 | expect(compile("false")).must_equal_with_diff [{ type: "bool", instruction: :push_false }] 19 | end 20 | 21 | it "compiles classes" do 22 | code = <<~CODE 23 | class Foo 24 | def bar 25 | @bar = 1 26 | end 27 | end 28 | Foo.new.bar 29 | CODE 30 | expect(compile(code)).must_equal_with_diff [ 31 | { 32 | type: "(class Foo @bar:int)", 33 | instruction: :class, 34 | name: :Foo, 35 | body: [ 36 | { 37 | type: "([(object Foo)] -> int)", 38 | instruction: :def, 39 | name: :bar, 40 | params: [], 41 | body: [ 42 | { type: "int", instruction: :push_int, value: 1 }, 43 | { type: "int", instruction: :set_ivar, name: :@bar, nillable: false } 44 | ] 45 | } 46 | ] 47 | }, 48 | { type: "(class Foo @bar:int)", instruction: :push_const, name: :Foo }, 49 | { 50 | type: "([(class Foo @bar:int)] -> (object Foo))", 51 | instruction: :call, 52 | name: :new, 53 | arg_count: 0 54 | }, 55 | { type: "([(object Foo)] -> int)", instruction: :call, name: :bar, arg_count: 0 } 56 | ] 57 | end 58 | 59 | it "raises an error if two objects cannot unify" do 60 | code = <<~CODE 61 | class Foo; end 62 | class Bar; end 63 | 64 | def same?(a); nil; end 65 | same?(Foo.new) 66 | same?(Bar.new) 67 | CODE 68 | e = expect { compile(code) }.must_raise Compiler::TypeChecker::TypeClash 69 | expect(e.message).must_equal( 70 | "([(object main), (object Foo)] -> nil) cannot unify with " \ 71 | "([(object main), (object Bar)] -> a) in call to same? on line 6" 72 | ) 73 | end 74 | 75 | it "compiles variables set and get" do 76 | expect(compile("a = 1; a")).must_equal_with_diff [ 77 | { type: "int", instruction: :push_int, value: 1 }, 78 | { type: "int", instruction: :set_var, name: :a, nillable: false }, 79 | { type: "int", instruction: :push_var, name: :a } 80 | ] 81 | end 82 | 83 | it "can set a variable more than once" do 84 | expect(compile("a = 1; a = 2")).must_equal_with_diff [ 85 | { type: "int", instruction: :push_int, value: 1 }, 86 | { type: "int", instruction: :set_var, name: :a, nillable: false }, 87 | { type: "int", instruction: :push_int, value: 2 }, 88 | { type: "int", instruction: :set_var, name: :a, nillable: false }, 89 | { type: "int", instruction: :push_var, name: :a } 90 | ] 91 | end 92 | 93 | it "raises an error if the variable type changes" do 94 | e = expect { compile('a = 1; a = "foo"') }.must_raise Compiler::TypeChecker::TypeClash 95 | expect(e.message).must_equal "the variable a has type int already; you cannot change it to type str" 96 | end 97 | 98 | it "compiles arrays" do 99 | code = <<~CODE 100 | a = [1, 2, 3] 101 | a.first 102 | 103 | b = [] 104 | b << "foo" 105 | b << "bar" 106 | 107 | c = [4, 5, 6] 108 | d = [c, c] 109 | CODE 110 | expect(compile(code)).must_equal_with_diff [ 111 | { type: "int", instruction: :push_int, value: 1 }, 112 | { type: "int", instruction: :push_int, value: 2 }, 113 | { type: "int", instruction: :push_int, value: 3 }, 114 | { type: "(int array)", instruction: :push_array, size: 3 }, 115 | { type: "(int array)", instruction: :set_var, name: :a, nillable: false }, 116 | { type: "(int array)", instruction: :push_var, name: :a }, 117 | { type: "([(int array)] -> int)", instruction: :call, name: :first, arg_count: 0 }, 118 | { type: "nil", instruction: :pop }, 119 | { type: "(str array)", instruction: :push_array, size: 0 }, 120 | { type: "(str array)", instruction: :set_var, name: :b, nillable: false }, 121 | { type: "(str array)", instruction: :push_var, name: :b }, 122 | { type: "str", instruction: :push_str, value: "foo" }, 123 | { 124 | type: "([(str array), str] -> (str array))", 125 | instruction: :call, 126 | name: :<<, 127 | arg_count: 1 128 | }, 129 | { type: "nil", instruction: :pop }, 130 | { type: "(str array)", instruction: :push_var, name: :b }, 131 | { type: "str", instruction: :push_str, value: "bar" }, 132 | { 133 | type: "([(str array), str] -> (str array))", 134 | instruction: :call, 135 | name: :<<, 136 | arg_count: 1 137 | }, 138 | { type: "nil", instruction: :pop }, 139 | { type: "int", instruction: :push_int, value: 4 }, 140 | { type: "int", instruction: :push_int, value: 5 }, 141 | { type: "int", instruction: :push_int, value: 6 }, 142 | { type: "(int array)", instruction: :push_array, size: 3 }, 143 | { type: "(int array)", instruction: :set_var, name: :c, nillable: false }, 144 | { type: "(int array)", instruction: :push_var, name: :c }, 145 | { type: "(int array)", instruction: :push_var, name: :c }, 146 | { type: "((int array) array)", instruction: :push_array, size: 2 }, 147 | { type: "((int array) array)", instruction: :set_var, name: :d, nillable: false }, 148 | { type: "((int array) array)", instruction: :push_var, name: :d } 149 | ] 150 | end 151 | 152 | it "raises an error if the array elements do not have the same type" do 153 | code = <<~CODE 154 | [1, "foo"] 155 | CODE 156 | e = expect { compile(code) }.must_raise Compiler::TypeChecker::TypeClash 157 | expect(e.message).must_equal "the array contains type int but you are trying to push type str" 158 | 159 | code = <<~CODE 160 | a = [1] 161 | a << "foo" 162 | CODE 163 | e = expect { compile(code) }.must_raise Compiler::TypeChecker::TypeClash 164 | expect( 165 | e.message 166 | ).must_equal "([(a array), a] -> (a array)) cannot unify with ([(int array), str] -> b) in call to << on line 2" 167 | 168 | code = <<~CODE 169 | [ 170 | [1, 2, 3], 171 | ['foo', 'bar', 'baz'] 172 | ] 173 | CODE 174 | e = expect { compile(code) }.must_raise Compiler::TypeChecker::TypeClash 175 | expect(e.message).must_equal "the array contains type (int array) but you are trying to push type (str array)" 176 | end 177 | 178 | it "compiles method definitions" do 179 | code = <<~CODE 180 | def foo 181 | 'foo' 182 | end 183 | def bar 184 | 1 185 | end 186 | foo 187 | bar 188 | CODE 189 | expect(compile(code)).must_equal_with_diff [ 190 | { 191 | type: "([(object main)] -> str)", 192 | instruction: :def, 193 | name: :foo, 194 | params: [], 195 | body: [{ type: "str", instruction: :push_str, value: "foo" }] 196 | }, 197 | { 198 | type: "([(object main)] -> int)", 199 | instruction: :def, 200 | name: :bar, 201 | params: [], 202 | body: [{ type: "int", instruction: :push_int, value: 1 }] 203 | }, 204 | { type: "(object main)", instruction: :push_self }, 205 | { type: "([(object main)] -> str)", instruction: :call, name: :foo, arg_count: 0 }, 206 | { type: "nil", instruction: :pop }, 207 | { type: "(object main)", instruction: :push_self }, 208 | { type: "([(object main)] -> int)", instruction: :call, name: :bar, arg_count: 0 } 209 | ] 210 | end 211 | 212 | it "compiles method definitions with arguments" do 213 | code = <<~CODE 214 | def bar(a) 215 | a 216 | end 217 | 218 | def foo(a, b) 219 | a 220 | end 221 | 222 | foo('foo', 1) 223 | 224 | bar(2) 225 | CODE 226 | expect(compile(code)).must_equal_with_diff [ 227 | { 228 | type: "([(object main), int] -> int)", 229 | instruction: :def, 230 | name: :bar, 231 | params: [:a], 232 | body: [ 233 | { type: "int", instruction: :push_arg, index: 0 }, 234 | { type: "int", instruction: :set_var, name: :a, nillable: false }, 235 | { type: "int", instruction: :push_var, name: :a } 236 | ] 237 | }, 238 | { 239 | type: "([(object main), str, int] -> str)", 240 | instruction: :def, 241 | name: :foo, 242 | params: %i[a b], 243 | body: [ 244 | { type: "str", instruction: :push_arg, index: 0 }, 245 | { type: "str", instruction: :set_var, name: :a, nillable: false }, 246 | { type: "int", instruction: :push_arg, index: 1 }, 247 | { type: "int", instruction: :set_var, name: :b, nillable: false }, 248 | { type: "str", instruction: :push_var, name: :a } 249 | ] 250 | }, 251 | { type: "(object main)", instruction: :push_self }, 252 | { type: "str", instruction: :push_str, value: "foo" }, 253 | { type: "int", instruction: :push_int, value: 1 }, 254 | { 255 | type: "([(object main), str, int] -> str)", 256 | instruction: :call, 257 | name: :foo, 258 | arg_count: 2 259 | }, 260 | { type: "nil", instruction: :pop }, 261 | { type: "(object main)", instruction: :push_self }, 262 | { type: "int", instruction: :push_int, value: 2 }, 263 | { type: "([(object main), int] -> int)", instruction: :call, name: :bar, arg_count: 1 } 264 | ] 265 | end 266 | 267 | it "raises an error if the method arg type is unknown" do 268 | code = <<~CODE 269 | def foo(a) 270 | a 271 | end 272 | CODE 273 | e = expect { compile(code) }.must_raise TypeError 274 | expect(e.message).must_match(/Not enough information to infer type of/) 275 | end 276 | 277 | # NOTE: we don't support monomorphization (yet!) 278 | it "raises an error if the method arg has more than one type" do 279 | code = <<~CODE 280 | def foo(x) 281 | x 282 | end 283 | 284 | foo(1) 285 | 286 | foo('bar') 287 | CODE 288 | e = expect { compile(code) }.must_raise Compiler::TypeChecker::TypeClash 289 | expect( 290 | e.message 291 | ).must_equal "([(object main), int] -> int) cannot unify with ([(object main), str] -> a) in call to foo on line 7" 292 | end 293 | 294 | it "raises an error if the arg count of method and call do not match" do 295 | code = <<~CODE 296 | def foo(x) 297 | x 298 | end 299 | 300 | foo(1, 2) 301 | CODE 302 | e = expect { compile(code) }.must_raise Compiler::TypeChecker::TypeClash 303 | expect( 304 | e.message 305 | ).must_equal "([(object main), a] -> a) cannot unify with ([(object main), int, int] -> b) in call to foo on line 5" 306 | end 307 | 308 | it "compiles operator expressions" do 309 | code = <<~CODE 310 | 1 + 2 311 | 3 == 4 312 | CODE 313 | expect(compile(code)).must_equal_with_diff [ 314 | { type: "int", instruction: :push_int, value: 1 }, 315 | { type: "int", instruction: :push_int, value: 2 }, 316 | { type: "([int, int] -> int)", instruction: :call, name: :+, arg_count: 1 }, 317 | { type: "nil", instruction: :pop }, 318 | { type: "int", instruction: :push_int, value: 3 }, 319 | { type: "int", instruction: :push_int, value: 4 }, 320 | { type: "([int, int] -> bool)", instruction: :call, name: :==, arg_count: 1 } 321 | ] 322 | end 323 | 324 | it "compiles if expressions" do 325 | code = <<~CODE 326 | if 1 327 | 2 328 | else 329 | 3 330 | end 331 | CODE 332 | expect(compile(code)).must_equal_with_diff [ 333 | { type: "int", instruction: :push_int, value: 1 }, 334 | { 335 | type: "int", 336 | instruction: :if, 337 | if_true: [{ type: "int", instruction: :push_int, value: 2 }], 338 | if_false: [{ type: "int", instruction: :push_int, value: 3 }] 339 | } 340 | ] 341 | end 342 | 343 | it "raises an error if both branches of an if expression do not have the same type" do 344 | code = <<~CODE 345 | if 1 346 | 2 347 | else 348 | 'foo' 349 | end 350 | CODE 351 | e = expect { compile(code) }.must_raise Compiler::TypeChecker::TypeClash 352 | expect(e.message).must_equal "one branch of `if` has type int and the other has type str" 353 | end 354 | 355 | it "compiles calls to puts for both int and str" do 356 | expect(compile("puts(1)")).must_equal_with_diff [ 357 | { type: "(object main)", instruction: :push_self }, 358 | { type: "int", instruction: :push_int, value: 1 }, 359 | { type: "([(object main), int] -> int)", instruction: :call, name: :puts, arg_count: 1 } 360 | ] 361 | expect(compile('puts("foo")')).must_equal_with_diff [ 362 | { type: "(object main)", instruction: :push_self }, 363 | { type: "str", instruction: :push_str, value: "foo" }, 364 | { type: "([(object main), str] -> int)", instruction: :call, name: :puts, arg_count: 1 } 365 | ] 366 | end 367 | 368 | it "raises an error if a variable is changed to nil" do 369 | e = expect { compile('a = "foo"; a = nil') }.must_raise Compiler::TypeChecker::TypeClash 370 | expect(e.message).must_equal "the variable a has type str already; you cannot change it to type nil" 371 | 372 | e = expect { compile('a = ["foo"]; a << nil') }.must_raise Compiler::TypeChecker::TypeClash 373 | expect( 374 | e.message 375 | ).must_equal "([(a array), a] -> (a array)) cannot unify with ([(str array), nil] -> b) in call to << on line 1" 376 | end 377 | 378 | it 'allows assignment of nil when the variable is named with the suffix "_or_nil"' do 379 | code = <<~CODE 380 | a_or_nil = "foo" 381 | a_or_nil = nil 382 | CODE 383 | expect(compile(code)).must_equal_with_diff [ 384 | { type: "str", instruction: :push_str, value: "foo" }, 385 | { type: "(nillable str)", instruction: :set_var, name: :a_or_nil, nillable: true }, 386 | { type: "nil", instruction: :push_nil }, 387 | { type: "(nillable str)", instruction: :set_var, name: :a_or_nil, nillable: true }, 388 | { type: "(nillable str)", instruction: :push_var, name: :a_or_nil } 389 | ] 390 | 391 | code = <<~CODE 392 | a_or_nil = nil 393 | a_or_nil = "foo" 394 | CODE 395 | expect(compile(code)).must_equal_with_diff [ 396 | { type: "nil", instruction: :push_nil }, 397 | { type: "(nillable str)", instruction: :set_var, name: :a_or_nil, nillable: true }, 398 | { type: "str", instruction: :push_str, value: "foo" }, 399 | { type: "(nillable str)", instruction: :set_var, name: :a_or_nil, nillable: true }, 400 | { type: "(nillable str)", instruction: :push_var, name: :a_or_nil } 401 | ] 402 | end 403 | 404 | it 'allows assignment of nil when the variable is marked with the special "nillable" comment' do 405 | code = <<~CODE 406 | a = "foo" # a:nillable 407 | a = nil 408 | CODE 409 | expect(compile(code)).must_equal_with_diff [ 410 | { type: "str", instruction: :push_str, value: "foo" }, 411 | { type: "(nillable str)", instruction: :set_var, name: :a, nillable: true }, 412 | { type: "nil", instruction: :push_nil }, 413 | { type: "(nillable str)", instruction: :set_var, name: :a, nillable: false }, 414 | { type: "(nillable str)", instruction: :push_var, name: :a } 415 | ] 416 | end 417 | 418 | it "allows assignment of nil when the variable is first set to nil and changed to something else" do 419 | code = <<~CODE 420 | a = nil 421 | a = "foo" 422 | CODE 423 | expect(compile(code)).must_equal_with_diff [ 424 | { type: "nil", instruction: :push_nil }, 425 | { type: "(nillable str)", instruction: :set_var, name: :a, nillable: false }, 426 | { type: "str", instruction: :push_str, value: "foo" }, 427 | { type: "(nillable str)", instruction: :set_var, name: :a, nillable: false }, 428 | { type: "(nillable str)", instruction: :push_var, name: :a } 429 | ] 430 | end 431 | 432 | it "raises an error if a method returns nil and is assigned to a variable" do 433 | code = <<~CODE 434 | def foo 435 | nil 436 | end 437 | 438 | a = foo 439 | a = "bar" 440 | CODE 441 | e = expect { compile(code) }.must_raise Compiler::TypeChecker::TypeClash 442 | expect(e.message).must_equal "the variable a has type nil already; you cannot change it to type str" 443 | end 444 | 445 | it "compiles examples/fib.rb" do 446 | code = File.read(File.expand_path("../examples/fib.rb", __dir__)) 447 | expect(compile(code)).must_equal_with_diff [ 448 | { 449 | type: "([(object main), int] -> int)", 450 | instruction: :def, 451 | name: :fib, 452 | params: [:n], 453 | body: [ 454 | { type: "int", instruction: :push_arg, index: 0 }, 455 | { type: "int", instruction: :set_var, name: :n, nillable: false }, 456 | { type: "int", instruction: :push_var, name: :n }, 457 | { type: "int", instruction: :push_int, value: 0 }, 458 | { type: "([int, int] -> bool)", instruction: :call, name: :==, arg_count: 1 }, 459 | { 460 | type: "int", 461 | instruction: :if, 462 | if_true: [{ type: "int", instruction: :push_int, value: 0 }], 463 | if_false: [ 464 | { type: "int", instruction: :push_var, name: :n }, 465 | { type: "int", instruction: :push_int, value: 1 }, 466 | { type: "([int, int] -> bool)", instruction: :call, name: :==, arg_count: 1 }, 467 | { 468 | type: "int", 469 | instruction: :if, 470 | if_true: [{ type: "int", instruction: :push_int, value: 1 }], 471 | if_false: [ 472 | { type: "(object main)", instruction: :push_self }, 473 | { type: "int", instruction: :push_var, name: :n }, 474 | { type: "int", instruction: :push_int, value: 1 }, 475 | { type: "([int, int] -> int)", instruction: :call, name: :-, arg_count: 1 }, 476 | { 477 | type: "([(object main), int] -> int)", 478 | instruction: :call, 479 | name: :fib, 480 | arg_count: 1 481 | }, 482 | { type: "(object main)", instruction: :push_self }, 483 | { type: "int", instruction: :push_var, name: :n }, 484 | { type: "int", instruction: :push_int, value: 2 }, 485 | { type: "([int, int] -> int)", instruction: :call, name: :-, arg_count: 1 }, 486 | { 487 | type: "([(object main), int] -> int)", 488 | instruction: :call, 489 | name: :fib, 490 | arg_count: 1 491 | }, 492 | { type: "([int, int] -> int)", instruction: :call, name: :+, arg_count: 1 } 493 | ] 494 | } 495 | ] 496 | } 497 | ] 498 | }, 499 | { type: "(object main)", instruction: :push_self }, 500 | { type: "(object main)", instruction: :push_self }, 501 | { type: "int", instruction: :push_int, value: 10 }, 502 | { type: "([(object main), int] -> int)", instruction: :call, name: :fib, arg_count: 1 }, 503 | { type: "([(object main), int] -> int)", instruction: :call, name: :puts, arg_count: 1 } 504 | ] 505 | end 506 | 507 | it "compiles examples/fact.rb" do 508 | code = File.read(File.expand_path("../examples/fact.rb", __dir__)) 509 | expect(compile(code)).must_equal_with_diff [ 510 | { 511 | type: "([(object main), int, int] -> int)", 512 | instruction: :def, 513 | name: :fact, 514 | params: %i[n result], 515 | body: [ 516 | { type: "int", instruction: :push_arg, index: 0 }, 517 | { type: "int", instruction: :set_var, name: :n, nillable: false }, 518 | { type: "int", instruction: :push_arg, index: 1 }, 519 | { type: "int", instruction: :set_var, name: :result, nillable: false }, 520 | { type: "int", instruction: :push_var, name: :n }, 521 | { type: "int", instruction: :push_int, value: 0 }, 522 | { type: "([int, int] -> bool)", instruction: :call, name: :==, arg_count: 1 }, 523 | { 524 | type: "int", 525 | instruction: :if, 526 | if_true: [{ type: "int", instruction: :push_var, name: :result }], 527 | if_false: [ 528 | { type: "(object main)", instruction: :push_self }, 529 | { type: "int", instruction: :push_var, name: :n }, 530 | { type: "int", instruction: :push_int, value: 1 }, 531 | { type: "([int, int] -> int)", instruction: :call, name: :-, arg_count: 1 }, 532 | { type: "int", instruction: :push_var, name: :result }, 533 | { type: "int", instruction: :push_var, name: :n }, 534 | { type: "([int, int] -> int)", instruction: :call, name: :*, arg_count: 1 }, 535 | { 536 | type: "([(object main), int, int] -> int)", 537 | instruction: :call, 538 | name: :fact, 539 | arg_count: 2 540 | } 541 | ] 542 | } 543 | ] 544 | }, 545 | { type: "(object main)", instruction: :push_self }, 546 | { type: "(object main)", instruction: :push_self }, 547 | { type: "int", instruction: :push_int, value: 10 }, 548 | { type: "int", instruction: :push_int, value: 1 }, 549 | { 550 | type: "([(object main), int, int] -> int)", 551 | instruction: :call, 552 | name: :fact, 553 | arg_count: 2 554 | }, 555 | { type: "([(object main), int] -> int)", instruction: :call, name: :puts, arg_count: 1 } 556 | ] 557 | end 558 | end 559 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /spec/vm_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative "spec_helper" 2 | require "stringio" 3 | 4 | describe VM do 5 | def execute(code, io: $stdout) 6 | instructions = Compiler.new(code).compile 7 | VM.new(instructions, io:).run 8 | end 9 | 10 | it "evaluates integers" do 11 | expect(execute("1")).must_equal(1) 12 | end 13 | 14 | it "evaluates strings" do 15 | expect(execute('"foo"')).must_equal("foo") 16 | end 17 | 18 | it "evaluates nil" do 19 | expect(execute("nil")).must_be_nil 20 | end 21 | 22 | it "evaluates booleans" do 23 | expect(execute("true")).must_equal(true) 24 | expect(execute("false")).must_equal(false) 25 | end 26 | 27 | it "evaluates classes" do 28 | code = <<~CODE 29 | class Foo 30 | def bar 31 | @bar = 1 32 | end 33 | end 34 | Foo.new.bar 35 | CODE 36 | expect(execute(code)).must_equal(1) 37 | end 38 | 39 | it "evaluates variables set and get" do 40 | expect(execute("a = 1; a")).must_equal(1) 41 | end 42 | 43 | it "evaluates arrays" do 44 | code = <<~CODE 45 | a = [1, 2, 3] 46 | a.first 47 | CODE 48 | expect(execute(code)).must_equal(1) 49 | 50 | code = <<~CODE 51 | a = [1, 2, 3] 52 | a.last 53 | CODE 54 | expect(execute(code)).must_equal(3) 55 | 56 | code = <<~CODE 57 | a = [1, 2, 3] 58 | a.last 59 | CODE 60 | expect(execute(code)).must_equal(3) 61 | 62 | code = <<~CODE 63 | a = [] 64 | a << 4 65 | a << 5 66 | a.last 67 | CODE 68 | expect(execute(code)).must_equal(5) 69 | 70 | code = <<~CODE 71 | a = [] 72 | a << "foo" 73 | a << "bar" 74 | a.last 75 | CODE 76 | expect(execute(code)).must_equal("bar") 77 | 78 | code = <<~CODE 79 | a = ["foo", "bar", "baz"] 80 | a.first 81 | CODE 82 | expect(execute(code)).must_equal("foo") 83 | 84 | code = <<~CODE 85 | a = ["foo", "bar", "baz"] 86 | a.last 87 | CODE 88 | expect(execute(code)).must_equal("baz") 89 | 90 | code = <<~CODE 91 | a = [nil, "foo", "bar"] 92 | a.last 93 | CODE 94 | expect(execute(code)).must_equal("bar") 95 | 96 | code = <<~CODE 97 | a = ["foo", "bar", nil] 98 | a.last 99 | CODE 100 | expect(execute(code)).must_be_nil 101 | 102 | code = <<~CODE 103 | a = [1, 2, 3] 104 | b = [4, 5, 6] 105 | c = [a, b, nil] 106 | c.last 107 | CODE 108 | expect(execute(code)).must_be_nil 109 | end 110 | 111 | it "evaluates method definitions" do 112 | code = <<~CODE 113 | def foo 114 | 'foo' 115 | end 116 | 117 | foo 118 | CODE 119 | expect(execute(code)).must_equal("foo") 120 | end 121 | 122 | it "evaluates method definitions with arguments" do 123 | code = <<~CODE 124 | def bar(x) 125 | x 126 | end 127 | 128 | def foo(a, b) 129 | bar(b - 10) 130 | end 131 | 132 | foo('foo', 100) 133 | CODE 134 | expect(execute(code)).must_equal(90) 135 | end 136 | 137 | it "does not stomp on method arguments" do 138 | code = <<~CODE 139 | def bar(b) 140 | b 141 | end 142 | 143 | def foo(a, b) 144 | bar(b - 10) 145 | b 146 | end 147 | 148 | foo('foo', 100) 149 | CODE 150 | expect(execute(code)).must_equal(100) 151 | end 152 | 153 | it "evaluates operator expressions" do 154 | expect(execute("1 + 2")).must_equal 3 155 | expect(execute("3 == 3")).must_equal true 156 | expect(execute("3 == 4")).must_equal false 157 | end 158 | 159 | it "evaluates if expressions" do 160 | code = <<~CODE 161 | if false 162 | if true 163 | 1 164 | else 165 | 2 166 | end 167 | else 168 | if true 169 | 3 # <-- this one 170 | else 171 | 4 172 | end 173 | end 174 | CODE 175 | expect(execute(code)).must_equal(3) 176 | 177 | code = <<~CODE 178 | if true 179 | if false 180 | 1 181 | else 182 | 2 # <-- this one 183 | end 184 | else 185 | if false 186 | 3 187 | else 188 | 4 189 | end 190 | end 191 | CODE 192 | expect(execute(code)).must_equal(2) 193 | 194 | code = <<~CODE 195 | if false 196 | if false 197 | 1 198 | else 199 | 2 200 | end 201 | elsif false 202 | 3 203 | else 204 | if false 205 | 4 206 | else 207 | 5 # <-- this one 208 | end 209 | end 210 | CODE 211 | expect(execute(code)).must_equal(5) 212 | end 213 | 214 | it "evaluates puts for both int and str" do 215 | code = <<~CODE 216 | puts(123) 217 | puts("foo") 218 | CODE 219 | io = StringIO.new 220 | execute(code, io:) 221 | io.rewind 222 | expect(io.read).must_equal("123\nfoo\n") 223 | end 224 | 225 | def execute_file(path) 226 | code = File.read(path) 227 | io = StringIO.new 228 | execute(code, io:) 229 | io.rewind 230 | io.read 231 | end 232 | 233 | it "evaluates nillable strings" do 234 | code = <<~CODE 235 | a = "foo" # a:nillable 236 | a = nil 237 | a 238 | CODE 239 | expect(execute(code)).must_be_nil 240 | 241 | code = <<~CODE 242 | a = nil 243 | a = "foo" 244 | a 245 | CODE 246 | expect(execute(code)).must_equal("foo") 247 | end 248 | 249 | it "evaluates examples/fib.rb" do 250 | result = execute_file(File.expand_path("../examples/fib.rb", __dir__)) 251 | expect(result).must_equal("55\n") 252 | end 253 | 254 | it "evaluates examples/fact.rb" do 255 | result = execute_file(File.expand_path("../examples/fact.rb", __dir__)) 256 | expect(result).must_equal("3628800\n") 257 | end 258 | end 259 | -------------------------------------------------------------------------------- /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_int(int32_t i) { 78 | return printf("%d\n", i); 79 | } 80 | 81 | int32_t puts_str(const RC *rc) { 82 | const char *str = rc->ptr; 83 | return printf("%s\n", str); 84 | } 85 | --------------------------------------------------------------------------------