]> git.mdlowis.com Git - proto/sclpl-rb.git/commitdiff
added tests for type checker
authorMichael D. Lowis <mike@mdlowis.com>
Tue, 7 Jul 2020 02:39:38 +0000 (22:39 -0400)
committerMichael D. Lowis <mike@mdlowis.com>
Tue, 7 Jul 2020 02:39:38 +0000 (22:39 -0400)
.gitignore
dyn.src
lib/dyn.rb
spec/spec_helper.rb
spec/symtable_spec.rb
spec/type_checker_spec.rb [new file with mode: 0644]

index cba7efc8efd27eebb82aa22d38d6dabc0b6e903b..c5760298bece74f40a1b134a14d83bed9e13d00c 100644 (file)
@@ -1 +1,2 @@
 a.out
+coverage/
diff --git a/dyn.src b/dyn.src
index 534152660f7c92f3d5c251ccce53f4d77d8b21db..d082790967565e42511fbb2ec489031b4c7bd29d 100644 (file)
--- a/dyn.src
+++ b/dyn.src
 #  42
 #}
 
-#add : int -> int
-#add = fun(a, b) {
-#  a + b
-#}
+add : int -> int
+add = fun(a, b) {
+  a + b
+}
 
 ## OR:
 #add : int -> int
index 70ebc5d1f12e1806f30fbb0a932d907e304598ac..bb9791a83ed0d794c967772de165159bb9ca4c06 100755 (executable)
@@ -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] == "<input>"
+    raise "<input>: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 || "<input>")
+    @text = (path ? File.read(path) : "")
+    @data = StringScanner.new(@text)
+  end
+
+  def parse_string(str)
+    @file = "<input>"
+    @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
index 251aa51060b0ce3f9e0f98e95d30e14ec841b42e..7ae85b4f8227dfff57249aff137ed3c819881747 100644 (file)
-# 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
index c4073c004c55d2e5d4dab176788b316714b0f65b..14b8b1e50ce77c478bad41af426b4cbed7c18a38 100644 (file)
@@ -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 (file)
index 0000000..e40152c
--- /dev/null
@@ -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