From: Michael D. Lowis Date: Tue, 7 Jul 2020 02:39:38 +0000 (-0400) Subject: added tests for type checker X-Git-Url: https://git.mdlowis.com/?a=commitdiff_plain;h=adc828da2415f59f712773a2f8b051f56721a77f;p=proto%2Fsclpl-rb.git added tests for type checker --- diff --git a/.gitignore b/.gitignore index cba7efc..c576029 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ a.out +coverage/ diff --git a/dyn.src b/dyn.src index 5341526..d082790 100644 --- a/dyn.src +++ b/dyn.src @@ -35,10 +35,10 @@ # 42 #} -#add : int -> int -#add = fun(a, b) { -# a + b -#} +add : int -> int +add = fun(a, b) { + a + b +} ## OR: #add : int -> int diff --git a/lib/dyn.rb b/lib/dyn.rb index 70ebc5d..bb9791a 100755 --- a/lib/dyn.rb +++ b/lib/dyn.rb @@ -1,16 +1,27 @@ #!/usr/bin/env ruby - +# TODO: +# * Implement detection of free variables in lambdas +# * Implement expression block syntax +# * Add support for structs +# * Add support for checked unions +# * Add support for arrays +# * Implement module system for namespacing +# * Implement generic/template system or parametric polymorphism require 'strscan' $debug = false def error(loc, msg) - lines = File.read(loc[0])[0..(loc[1])].split("\n") - $stderr.puts "#{loc[0]}:#{lines.length}: #{msg}" - raise "" if $debug -# $stderr.puts "#{lines.last}" -# $stderr.puts (" " * lines.last.length) + "^" - exit 1 + if loc[0] == "" + raise ":0: error: #{msg}" + else + lines = File.read(loc[0])[0..(loc[1])].split("\n") + $stderr.puts "#{loc[0]}:#{lines.length}: error: #{msg}" + raise "" if $debug + # $stderr.puts "#{lines.last}" + # $stderr.puts (" " * lines.last.length) + "^" + end +# exit 1 end class SymTable < Hash @@ -28,16 +39,20 @@ class SymTable < Hash (super(key) || @parent[key]) end - def []=(key, value) - existing = method(:[]).super_method.call(key) - if (not existing.nil?) and existing[:set!] - error(value[:loc], "symbol '#{key}' is multiply defined in scope") - end - super(key,value) + def local(key) + method(:[]).super_method.call(key) + end + + def defined?(key) + (not (self[key] || {})[:type].nil?) + end + + def defined_locally?(key) + (not (local(key) || {})[:type].nil?) end def block_local?(key) - (not method(:[]).super_method.call(key).nil?) + (not local(key).nil?) end def global?(key) @@ -63,6 +78,15 @@ class SymTable < Hash def merge!(env) env.each {|k,v| self[k] = v } end + + def annotate(key, type) + self[key] ||= {} + self[key][:ann] = type + end + + def annotation(key) + (method(:[]).super_method.call(key) || {})[:ann] + end end class Lexer @@ -70,8 +94,9 @@ class Lexer SPACE = /([ \t\v\n\r]+|#.*\n)/ IDENT = /[_a-zA-Z][_a-zA-Z0-9]*/ BRACES = /[\(\)\[\]\{\}\.]/ - OPERATORS = /[:,<>*\/=+\-\$?]+/ - NUMBER = /[0-9]+(\.[0-9]+)?/ + OPERATORS = /[:,<>*\/=+\-\$?!]+/ + INTEGER = /[0-9]+/ + FLOATING = /[0-9]+(\.[0-9]+)?/ STRING = /"(\\"|[^"])*"/ ID_TYPES = { "true" => :bool, @@ -86,9 +111,15 @@ class Lexer attr_accessor :file attr_accessor :data - def initialize(path) - @file = path - @text = File.read(path) + def initialize(path = nil) + @file = (path || "") + @text = (path ? File.read(path) : "") + @data = StringScanner.new(@text) + end + + def parse_string(str) + @file = "" + @text = str @data = StringScanner.new(@text) end @@ -103,7 +134,9 @@ class Lexer type = :eof if @data.scan(IDENT) type = get_id_type(@data.matched) - elsif @data.scan(NUMBER) +# elsif @data.scan(FLOATING) +# type = :float + elsif @data.scan(INTEGER) type = :int elsif @data.scan(STRING) type = :string @@ -127,33 +160,62 @@ class Parser LEVELS = { none: 0, assign: 1, - or: 2, - and: 3, - equality: 4, - compare: 5, - term: 6, - factor: 7, - unary: 8, - call: 9, - primary: 10, - eof: 11, + log_or: 2, + log_and: 3, + or: 4, + xor: 5, + and: 6, + equality: 7, + compare: 8, + shift: 9, + term: 10, + factor: 11, + unary: 12, # unary + - ! ~ + call: 13, # function, array subscript, member access, inc/dec + primary: 14, + eof: 15, } LVLNAMES = LEVELS.keys RULES = { - ":" => { prefix: nil, infix: :annotation, level: :primary }, - "(" => { prefix: :grouping, infix: :func_call, level: :call }, - "+" => { prefix: :unary, infix: :binary, level: :term }, - "-" => { prefix: :unary, infix: :binary, level: :term }, - "*" => { prefix: nil, infix: :binary, level: :factor }, - "/" => { prefix: nil, infix: :binary, level: :factor }, - "=" => { prefix: nil, infix: :definition, level: :assign }, + ":" => { prefix: nil, infix: :annotation, level: :primary }, + + "(" => { prefix: :grouping, infix: :func_call, level: :call }, + "[" => { prefix: :block, infix: :subscript, level: :call }, + "." => { prefix: nil, infix: :member, level: :call }, + + "*" => { prefix: nil, infix: :binary, level: :factor }, + "/" => { prefix: nil, infix: :binary, level: :factor }, + "%" => { prefix: nil, infix: :binary, level: :factor }, + + "+" => { prefix: :unary, infix: :binary, level: :term }, + "-" => { prefix: :unary, infix: :binary, level: :term }, + "!" => { prefix: :unary, infix: nil, level: :term }, + "~" => { prefix: :unary, infix: nil, level: :term }, + + "<<" => { prefix: nil, infix: :binary, level: :shift }, + ">>" => { prefix: nil, infix: :binary, level: :shift }, + + "<" => { prefix: nil, infix: :binary, level: :compare }, + ">" => { prefix: nil, infix: :binary, level: :compare }, + "<=" => { prefix: nil, infix: :binary, level: :compare }, + ">=" => { prefix: nil, infix: :binary, level: :compare }, + + "==" => { prefix: nil, infix: :binary, level: :equality }, + "!=" => { prefix: nil, infix: :binary, level: :equality }, + + "&" => { prefix: nil, infix: :binary, level: :and }, + "^" => { prefix: nil, infix: :binary, level: :xor }, + "|" => { prefix: nil, infix: :binary, level: :or }, + "&&" => { prefix: nil, infix: :binary, level: :log_and }, + "||" => { prefix: nil, infix: :binary, level: :log_or }, + "=" => { prefix: nil, infix: :definition, level: :assign }, + + "{" => { prefix: :block, infix: nil, level: :none }, :fun => { prefix: :function, infix: nil, level: :none }, :if => { prefix: :if_expr, infix: nil, level: :none }, :ident => { prefix: :variable, infix: nil, level: :none }, - "[" => { prefix: :constant, infix: :subscript, level: :call }, - "{" => { prefix: :constant, infix: nil, level: :none }, :bool => { prefix: :constant, infix: nil, level: :none }, :int => { prefix: :constant, infix: nil, level: :none }, :string => { prefix: :constant, infix: nil, level: :none }, @@ -168,9 +230,21 @@ class Parser Call = Struct.new(:loc, :type, :func, :args) IfExpr = Struct.new(:loc, :type, :cond, :br1, :br2) Ann = Struct.new(:loc, :type, :expr) - Block = Struct.new(:loc, :type, :syms, :exprs) + Block = Struct.new(:loc, :type, :exprs) + + def initialize(path = nil) + parse_file(path) + end - def initialize(path) + def parse_string(str) + @lex = Lexer.new() + @lex.parse_string(str) + @prev = nil + @next = nil + toplevel + end + + def parse_file(path) @lex = Lexer.new(path) @prev = nil @next = nil @@ -184,27 +258,14 @@ class Parser end def block(toplevel = false) - block = Block.new(location(), nil, {}, []) - syms = {} + block = Block.new(location(), nil, []) exprs = [] expect("{") if not toplevel while !matches(:eof) and !matches("}") expr = expression() - if expr.is_a? Def - error("symbol '#{expr.name}' multiply defined in scope", expr.loc) if syms[expr.name] and syms[expr.name][:loc] - syms[expr.name.name] ||= {} - syms[expr.name.name][:loc] = expr.loc - exprs << Call.new(expr.loc, nil, :set!, [expr.name, expr.value]) - elsif expr.is_a? Ann and expr.expr.is_a? Ident - error("symbol '#{expr.expr.name}' multiply annotated in scope", expr.loc) if syms[expr.expr.name] and syms[expr.expr.name][:type] - syms[expr.expr.name] ||= {} - syms[expr.expr.name][:type] = expr.type - else - exprs << expr; - end + exprs << expr; end expect("}") if not toplevel - block.syms = syms block.exprs = exprs block end @@ -345,9 +406,10 @@ class Parser ####################################### def error(str, loc = nil) file, pos = (loc ? loc : [@lex.file, (@next || @prev).pos]) - puts "#{file}:#{@lex.linenum(pos)}: #{str}" - raise "" if $debug - exit 1 + raise "#{file}:#{@lex.linenum(pos)}: #{str}" +# puts "#{file}:#{@lex.linenum(pos)}: #{str}" +# raise "" if $debug +# exit 1 end def peek() @@ -407,9 +469,6 @@ module TypeChecker "!" => { bool: [:bool, :bool] }, - "~" => { - int: [:int, :int, :int] - }, } BinaryOps = { @@ -475,7 +534,7 @@ module TypeChecker check_func(env, expr, type) else etype = infer(env, expr) - error(expr.loc, "expected #{type}, recieved #{etype}") if type != etype + error(expr.loc, "expected #{type}, received #{etype}") if type != etype end expr.type = type end @@ -488,10 +547,13 @@ module TypeChecker def self.check_func(env, expr, type) env = env.clone + error(expr.loc, "wrong number of arguments for function definition. expected #{type.length-1}, got #{expr.args.length}") if type.length-1 != expr.args.length type[0..-2].each_with_index do |t, i| - env[expr.args[i].name] = { :loc => expr.loc, :type => t, :set! => true } + env[expr.args[i].name] = { :loc => expr.loc, :type => t } end - check(env, expr.body, type.last) + itype = infer_block(env, expr.body, false) + error(expr.loc, "expected #{type}, received #{etype}") if itype != type.last + type.last end def self.check_call(env, expr, type) @@ -507,6 +569,8 @@ module TypeChecker infer_value(env, expr) elsif expr.is_a? Parser::Ident infer_symbol(env, expr) + elsif expr.is_a? Parser::Def + infer_definition(env, expr) elsif expr.is_a? Parser::Ann infer_annotation(env, expr) elsif expr.is_a? Parser::Block @@ -525,25 +589,46 @@ module TypeChecker end def self.infer_symbol(env, expr) - error(expr.loc, "undefined symbol '#{expr.name}'") if env[expr.name].nil? or not env[expr.name][:set!] - error(expr.loc, "symbol '#{expr.name}' has unknown type") if env[expr.name][:type].nil? + error(expr.loc, "undefined symbol '#{expr.name}'") if not env.defined? expr.name expr.type = env[expr.name][:type] end - def self.infer_annotation(env, expr) - check(env, expr.expr, expr.type) + def self.infer_definition(env, expr) + name = expr.name.name + type = env.annotation(name) + error(expr.loc, "symbol '#{name}' defined multiple times in scope") if env.defined_locally? name + if type + check(env, expr.value, type) + else + type = infer(env, expr.value) + end + env[name] = { loc: expr.loc, type: type } + :void end - def self.infer_block(env, expr) - env.merge!(expr.syms) - types = expr.exprs.map {|e| infer(env, e) } + def self.infer_annotation(env, expr, block = false) + if block and expr.expr.is_a? Parser::Ident + env.annotate(expr.expr.name, expr.type) + :void + else + check(env, expr.expr, expr.type) + end + end + + def self.infer_block(env, expr, clone = true) + env = env.clone if clone + types = expr.exprs.map do |e| + if e.is_a? Parser::Ann + infer_annotation(env, e, true) + else + infer(env, e) + end + end types.last end def self.infer_call(env, expr) - if expr.func == :set! - infer_def(env, expr) - elsif expr.func.is_a? String + if expr.func.is_a? String infer_opcall(env, expr) else type = infer(env, expr.func) @@ -551,20 +636,6 @@ module TypeChecker end end - def self.infer_def(env, expr) - name = expr.args[0].name - type = env[name][:type] - if (type) - error(expr.loc, "symbol '#{name}' is multiply defined in scope") if env[name][:set!] - check(env, expr.args[1], type) - else - type = infer(env, expr.args[1]) - env[name][:type] = type - end - env[name][:set!] = true - expr.type = :void - end - def self.infer_opcall(env, expr) ltype = infer(env, expr.args[0]) if expr.args.length == 1 @@ -591,7 +662,6 @@ class Codegen @func = 0 @protos = StringIO.new @funcs = StringIO.new -# gen_protos(block.syms) @funcs.puts emit_toplevel(block) end @@ -733,46 +803,4 @@ class Codegen @funcs.puts newout.string var end - -# def get_vars(bsyms) -# syms = {} -# bsyms.each do |k,v| -# syms[v[:type]] ||= [] -# syms[v[:type]] << k -# end -# syms -# end -# -# def gen_protos(syms) -# get_vars(syms).each do |k,v| -# if k.is_a? Symbol -# @protos.puts "#{k.to_s} #{v.join(", ")};" -# else -# v.each do |name| -# @protos.puts "#{k.last} #{name}(#{k[0..-2].join(", ")});" -# end -# end -# end -# end end - -#block = Parser.new("dyn.src").toplevel -#block.type = TypeChecker.infer(SymTable.new, block) -#puts Codegen.new(block) -##gen = Codegen.new -##gen << block -##pp block -# -#syms = {} -#block.syms.each do |k,v| -# syms[v[:type]] ||= [] -# syms[v[:type]] << k -#end -#pp syms - -# TODO: -# * Add support for structs -# * Add support for checked unions -# * Add support for arrays -# * Implement module system for namespacing -# * Implement generic/template system for polymorphism \ No newline at end of file diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 251aa51..7ae85b4 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,100 +1,16 @@ -# This file was generated by the `rspec --init` command. Conventionally, all -# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. -# The generated `.rspec` file contains `--require spec_helper` which will cause -# this file to always be loaded, without a need to explicitly require it in any -# files. -# -# Given that it is always loaded, you are encouraged to keep this file as -# light-weight as possible. Requiring heavyweight dependencies from this file -# will add to the boot time of your test suite on EVERY test run, even for an -# individual file that may not need all of that loaded. Instead, consider making -# a separate helper file that requires the additional dependencies and performs -# the additional setup, and require it from the spec files that actually need -# it. -# -# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration +require "simplecov" +SimpleCov.start + +require "dyn" + RSpec.configure do |config| - # rspec-expectations config goes here. You can use an alternate - # assertion/expectation library such as wrong or the stdlib/minitest - # assertions if you prefer. config.expect_with :rspec do |expectations| - # This option will default to `true` in RSpec 4. It makes the `description` - # and `failure_message` of custom matchers include text for helper methods - # defined using `chain`, e.g.: - # be_bigger_than(2).and_smaller_than(4).description - # # => "be bigger than 2 and smaller than 4" - # ...rather than: - # # => "be bigger than 2" expectations.include_chain_clauses_in_custom_matcher_descriptions = true end - # rspec-mocks config goes here. You can use an alternate test double - # library (such as bogus or mocha) by changing the `mock_with` option here. config.mock_with :rspec do |mocks| - # Prevents you from mocking or stubbing a method that does not exist on - # a real object. This is generally recommended, and will default to - # `true` in RSpec 4. mocks.verify_partial_doubles = true end - # This option will default to `:apply_to_host_groups` in RSpec 4 (and will - # have no way to turn it off -- the option exists only for backwards - # compatibility in RSpec 3). It causes shared context metadata to be - # inherited by the metadata hash of host groups and examples, rather than - # triggering implicit auto-inclusion in groups with matching metadata. config.shared_context_metadata_behavior = :apply_to_host_groups - -# The settings below are suggested to provide a good initial experience -# with RSpec, but feel free to customize to your heart's content. -=begin - # This allows you to limit a spec run to individual examples or groups - # you care about by tagging them with `:focus` metadata. When nothing - # is tagged with `:focus`, all examples get run. RSpec also provides - # aliases for `it`, `describe`, and `context` that include `:focus` - # metadata: `fit`, `fdescribe` and `fcontext`, respectively. - config.filter_run_when_matching :focus - - # Allows RSpec to persist some state between runs in order to support - # the `--only-failures` and `--next-failure` CLI options. We recommend - # you configure your source control system to ignore this file. - config.example_status_persistence_file_path = "spec/examples.txt" - - # Limits the available syntax to the non-monkey patched syntax that is - # recommended. For more details, see: - # - http://rspec.info/blog/2012/06/rspecs-new-expectation-syntax/ - # - http://www.teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/ - # - http://rspec.info/blog/2014/05/notable-changes-in-rspec-3/#zero-monkey-patching-mode - config.disable_monkey_patching! - - # This setting enables warnings. It's recommended, but in some cases may - # be too noisy due to issues in dependencies. - config.warnings = true - - # Many RSpec users commonly either run the entire suite or an individual - # file, and it's useful to allow more verbose output when running an - # individual spec file. - if config.files_to_run.one? - # Use the documentation formatter for detailed output, - # unless a formatter has already been configured - # (e.g. via a command-line flag). - config.default_formatter = "doc" - end - - # Print the 10 slowest examples and example groups at the - # end of the spec run, to help surface which specs are running - # particularly slow. - config.profile_examples = 10 - - # Run specs in random order to surface order dependencies. If you find an - # order dependency and want to debug it, you can fix the order by providing - # the seed, which is printed after each run. - # --seed 1234 - config.order = :random - - # Seed global randomization in this process using the `--seed` CLI option. - # Setting this allows you to use `--seed` to deterministically reproduce - # test failures related to randomization by passing the same `--seed` value - # as the one that triggered the failure. - Kernel.srand config.seed -=end end diff --git a/spec/symtable_spec.rb b/spec/symtable_spec.rb index c4073c0..14b8b1e 100644 --- a/spec/symtable_spec.rb +++ b/spec/symtable_spec.rb @@ -1,5 +1,3 @@ -require "dyn" - describe SymTable do subject { symtab1 = SymTable.new diff --git a/spec/type_checker_spec.rb b/spec/type_checker_spec.rb new file mode 100644 index 0000000..e40152c --- /dev/null +++ b/spec/type_checker_spec.rb @@ -0,0 +1,157 @@ +describe TypeChecker do + def parse_and_check(str) + TypeChecker.infer(SymTable.new, Parser.new.parse_string(str)) + end + + context "constants" do + it "will recognize 'true' as a bool" do + expect(parse_and_check("true")).to eq :bool + end + it "will recognize 'false' as a bool" do + expect(parse_and_check("false")).to eq :bool + end + it "will recognize '123' as an int" do + expect(parse_and_check("123")).to eq :int + end + it "will recognize '123.0' as a float" + it "will recognize '\"abc\"' as a string" do + expect(parse_and_check("\"abc\"")).to eq :string + end + end + + context "unary operators" do + it "will recognize unary '+' operator" do + expect(parse_and_check("+1")).to eq :int + end + + it "will recognize unary '-' operator" do + expect(parse_and_check("-1")).to eq :int + end + + it "will recognize unary '!' operator" do + expect(parse_and_check("!true")).to eq :bool + end + end + + context "binary operators" do + it "will recognize binary '+' operator for ints" do + expect(parse_and_check("1 + 1")).to eq :int + end + + it "will recognize binary '-' operator for ints" do + expect(parse_and_check("1 - 1")).to eq :int + end + + it "will recognize binary '*' operator for ints" do + expect(parse_and_check("1 * 1")).to eq :int + end + + it "will recognize binary '/' operator for ints" do + expect(parse_and_check("1 / 1")).to eq :int + end + + it "will recognize binary '%' operator for ints" do + expect(parse_and_check("1 % 1")).to eq :int + end + + it "will recognize binary '<' operator for ints" do + expect(parse_and_check("1 < 1")).to eq :bool + end + + it "will recognize binary '>' operator for ints" do + expect(parse_and_check("1 > 1")).to eq :bool + end + + it "will recognize binary '<=' operator for ints" do + expect(parse_and_check("1 <= 1")).to eq :bool + end + + it "will recognize binary '>=' operator for ints" do + expect(parse_and_check("1 >= 1")).to eq :bool + end + + it "will recognize binary '==' operator for ints" do + expect(parse_and_check("1 == 1")).to eq :bool + end + + it "will recognize binary '!=' operator for ints" do + expect(parse_and_check("1 != 1")).to eq :bool + end + + it "will recognize binary '&&' operator for ints" do + expect(parse_and_check("true && false")).to eq :bool + end + + + end + + context "variables/annotations" do + it "will return the type assigned/inferred at definition time when a variable is referenced" do + type = parse_and_check <<-eos + a = "123" + a + eos + expect(type).to eq :string + end + + it "will allow definitions to shadow definitions in outer scopes" do + type = parse_and_check <<-eos + a = 123 + if (true) { + a = "123" + a + } else { + "123" + } + eos + expect(type).to eq :string + end + + it "will allow inner scopes to reference definitions in outer scopes" do + type = parse_and_check <<-eos + a = 123 + if (true) { a } else { a } + eos + expect(type).to eq :int + end + + it "will error if inferred type does not match the annotated type of definition" do + expect do + type = parse_and_check <<-eos + a : int + a = "123" + a + eos + end.to raise_error(/expected int, received string/) + end + + it "will error if inferred type does not match the annotated type of expression" do + expect do + type = parse_and_check <<-eos + ("123" : int + 1) + eos + end.to raise_error(/expected int, received string/) + end + + it "will error if multiple definitions in scope" do + expect do + type = parse_and_check <<-eos + a = "123" + a = "123" + eos + end.to raise_error(/'a' defined multiple times in scope/) + end + end + + context "functions" do + it "will recognize functions and function calls" do + type = parse_and_check <<-eos + add1 : int -> int + add1 = fun(a) { + a + 1 + } + add1(1) + eos + end + end +end \ No newline at end of file