#!/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
(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)
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
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,
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
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
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 },
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
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
#######################################
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()
"!" => {
bool: [:bool, :bool]
},
- "~" => {
- int: [:int, :int, :int]
- },
}
BinaryOps = {
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
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)
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
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)
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
@func = 0
@protos = StringIO.new
@funcs = StringIO.new
-# gen_protos(block.syms)
@funcs.puts emit_toplevel(block)
end
@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
-# 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
--- /dev/null
+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