copied thnad to get a kickstart
This commit is contained in:
parent
f97205300f
commit
d90ea3dd26
27
lib/vm/builtins.rb
Normal file
27
lib/vm/builtins.rb
Normal file
@ -0,0 +1,27 @@
|
||||
require 'java'
|
||||
java_import java.lang.System
|
||||
java_import java.io.PrintStream
|
||||
|
||||
module Thnad
|
||||
module Builtins
|
||||
def add_builtins
|
||||
public_static_method 'print', [], int, int do
|
||||
iload 0
|
||||
|
||||
getstatic System, :out, PrintStream
|
||||
swap
|
||||
invokevirtual PrintStream, :print, [void, int]
|
||||
|
||||
ldc 0
|
||||
ireturn
|
||||
end
|
||||
|
||||
public_static_method 'minus', [], int, int, int do
|
||||
iload 0
|
||||
iload 1
|
||||
isub
|
||||
ireturn
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
74
lib/vm/compiler.rb
Normal file
74
lib/vm/compiler.rb
Normal file
@ -0,0 +1,74 @@
|
||||
require 'bitescript'
|
||||
require 'thnad/parser'
|
||||
require 'thnad/transform'
|
||||
require 'thnad/builtins'
|
||||
|
||||
module Thnad
|
||||
class Compiler
|
||||
def initialize(filename)
|
||||
@filename = filename
|
||||
@classname = File.basename(@filename, '.thnad')
|
||||
end
|
||||
|
||||
def compile
|
||||
tree = parse_source
|
||||
funcs, exprs = split_functions tree
|
||||
classname = @classname
|
||||
|
||||
builder = BiteScript::FileBuilder.build(@filename) do
|
||||
public_class classname, object do |klass|
|
||||
klass.extend(Builtins)
|
||||
klass.add_builtins
|
||||
|
||||
funcs.each do |f|
|
||||
context = Hash.new
|
||||
f.eval(context, klass)
|
||||
end
|
||||
|
||||
klass.public_static_method 'main', [], void, string[] do |method|
|
||||
context = Hash.new
|
||||
exprs.each do |e|
|
||||
e.eval(context, method)
|
||||
end
|
||||
|
||||
method.returnvoid
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
write_result builder
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def parse_source
|
||||
source = File.expand_path(@filename)
|
||||
program = IO.read source
|
||||
|
||||
parser = Parser.new
|
||||
transform = Transform.new
|
||||
syntax = parser.parse(program)
|
||||
tree = transform.apply(syntax)
|
||||
|
||||
Array(tree)
|
||||
end
|
||||
|
||||
def split_functions(tree)
|
||||
first_expr = tree.index { |t| ! t.is_a?(Function) }
|
||||
funcs = first_expr ? tree[0...first_expr] : tree
|
||||
exprs = first_expr ? tree[first_expr..-1] : []
|
||||
|
||||
[funcs, exprs]
|
||||
end
|
||||
|
||||
def write_result(builder)
|
||||
destination = File.expand_path(@classname + '.class')
|
||||
|
||||
builder.generate do |n, b|
|
||||
File.open(destination, 'wb') do |f|
|
||||
f.write b.generate
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
70
lib/vm/copied_parser.rb
Normal file
70
lib/vm/copied_parser.rb
Normal file
@ -0,0 +1,70 @@
|
||||
#start from an example of parslet (assumed to be checked out at the same level as crystal for now)
|
||||
|
||||
$:.unshift File.dirname(__FILE__) + "/../../../parslet/lib"
|
||||
|
||||
require 'pp'
|
||||
require 'rspec'
|
||||
require 'parslet'
|
||||
require 'parslet/rig/rspec'
|
||||
require 'parslet/convenience'
|
||||
|
||||
class InfixExpressionParser < Parslet::Parser
|
||||
root :variable_assignment_list
|
||||
|
||||
rule(:space) { match[' '] }
|
||||
|
||||
def cts atom
|
||||
atom >> space.repeat
|
||||
end
|
||||
def infix *args
|
||||
Infix.new(*args)
|
||||
end
|
||||
|
||||
# This is the heart of the infix expression parser: real simple definitions
|
||||
# for all the pieces we need.
|
||||
rule(:mul_op) { cts match['*/'] }
|
||||
rule(:add_op) { cts match['+-'] }
|
||||
rule(:digit) { match['0-9'] }
|
||||
rule(:integer) { cts digit.repeat(1).as(:int) }
|
||||
|
||||
rule(:expression) { infix_expression(integer,
|
||||
[mul_op, 2, :left],
|
||||
[add_op, 1, :right]) }
|
||||
|
||||
# And now adding variable assignments to that, just to a) demonstrate this
|
||||
# embedded in a bigger parser, and b) make the example interesting.
|
||||
rule(:variable_assignment_list) {
|
||||
variable_assignment.repeat(1) }
|
||||
rule(:variable_assignment) {
|
||||
identifier.as(:ident) >> equal_sign >> expression.as(:exp) >> eol }
|
||||
rule(:identifier) {
|
||||
cts (match['a-z'] >> match['a-zA-Z0-9'].repeat) }
|
||||
rule(:equal_sign) {
|
||||
cts str('=') }
|
||||
rule(:eol) {
|
||||
cts(str("\n")) | any.absent? }
|
||||
end
|
||||
|
||||
class InfixInterpreter < Parslet::Transform
|
||||
rule(int: simple(:int)) { Integer(int) }
|
||||
rule(ident: simple(:ident), exp: simple(:result)) { |d|
|
||||
d[:doc][d[:ident].to_s.strip.to_sym] = d[:result] }
|
||||
|
||||
rule(l: simple(:l), o: /^\*/, r: simple(:r)) { l * r }
|
||||
rule(l: simple(:l), o: /^\+/, r: simple(:r)) { l + r }
|
||||
end
|
||||
|
||||
input = <<ASSIGNMENTS
|
||||
a = 1
|
||||
b = 2
|
||||
c = 3 * 25
|
||||
d = 100 + 3*4
|
||||
ASSIGNMENTS
|
||||
|
||||
puts input
|
||||
|
||||
int_tree = InfixExpressionParser.new.parse_with_debug(input)
|
||||
bindings = {}
|
||||
result = InfixInterpreter.new.apply(int_tree, doc: bindings)
|
||||
|
||||
pp bindings
|
54
lib/vm/nodes.rb
Normal file
54
lib/vm/nodes.rb
Normal file
@ -0,0 +1,54 @@
|
||||
module Thnad
|
||||
class Number < Struct.new :value
|
||||
def eval(context, builder)
|
||||
builder.ldc value
|
||||
end
|
||||
end
|
||||
|
||||
class Name < Struct.new :name
|
||||
def eval(context, builder)
|
||||
param_names = context[:params] || []
|
||||
position = param_names.index(name)
|
||||
raise "Unknown parameter #{name}" unless position
|
||||
|
||||
builder.iload position
|
||||
end
|
||||
end
|
||||
|
||||
class Funcall < Struct.new :name, :args
|
||||
def eval(context, builder)
|
||||
args.each { |a| a.eval(context, builder) }
|
||||
types = [builder.int] * (args.length + 1)
|
||||
builder.invokestatic builder.class_builder, name, types
|
||||
end
|
||||
end
|
||||
|
||||
class Conditional < Struct.new :cond, :if_true, :if_false
|
||||
def eval(context, builder)
|
||||
cond.eval context, builder
|
||||
|
||||
builder.ifeq :else
|
||||
|
||||
if_true.eval context, builder
|
||||
builder.goto :endif
|
||||
|
||||
builder.label :else
|
||||
if_false.eval context, builder
|
||||
|
||||
builder.label :endif
|
||||
end
|
||||
end
|
||||
|
||||
class Function < Struct.new :name, :params, :body
|
||||
def eval(context, builder)
|
||||
param_names = [params].flatten.map(&:name)
|
||||
context[:params] = param_names
|
||||
types = [builder.int] * (param_names.count + 1)
|
||||
|
||||
builder.public_static_method(self.name, [], *types) do |method|
|
||||
self.body.eval(context, method)
|
||||
method.ireturn
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
52
lib/vm/parser.rb
Normal file
52
lib/vm/parser.rb
Normal file
@ -0,0 +1,52 @@
|
||||
require 'parslet'
|
||||
|
||||
module Thnad
|
||||
class Parser < Parslet::Parser
|
||||
rule(:name) { match('[a-z]').repeat(1).as(:name) >> space? }
|
||||
rule(:number) { match('[0-9]').repeat(1).as(:number) >> space? }
|
||||
rule(:space) { match('\s').repeat(1) }
|
||||
rule(:space?) { space.maybe }
|
||||
|
||||
rule(:args) {
|
||||
lparen >>
|
||||
((expression.as(:arg) >> (comma >> expression.as(:arg)).repeat(0)).maybe).as(:args) >>
|
||||
rparen
|
||||
}
|
||||
|
||||
rule(:funcall) { name.as(:funcall) >> args }
|
||||
|
||||
rule(:expression) { cond | funcall | number | name }
|
||||
|
||||
rule(:lparen) { str('(') >> space? }
|
||||
rule(:rparen) { str(')') >> space? }
|
||||
rule(:comma) { str(',') >> space? }
|
||||
|
||||
rule(:cond) {
|
||||
if_kw >> lparen >> expression.as(:cond) >> rparen >>
|
||||
body.as(:if_true) >>
|
||||
else_kw >>
|
||||
body.as(:if_false)
|
||||
}
|
||||
|
||||
rule(:body) { lbrace >> expression.as(:body) >> rbrace }
|
||||
rule(:lbrace) { str('{') >> space? }
|
||||
rule(:rbrace) { str('}') >> space? }
|
||||
rule(:comma) { str(',') >> space? }
|
||||
rule(:if_kw) { str('if') >> space? }
|
||||
rule(:else_kw) { str('else') >> space? }
|
||||
|
||||
rule(:func) {
|
||||
func_kw >> name.as(:func) >> params >> body
|
||||
}
|
||||
|
||||
rule(:func_kw) { str('function') >> space? }
|
||||
|
||||
rule(:params) {
|
||||
lparen >>
|
||||
((name.as(:param) >> (comma >> name.as(:param)).repeat(0)).maybe).as(:params) >>
|
||||
rparen
|
||||
}
|
||||
|
||||
rule(:root) { func.repeat(0) >> expression }
|
||||
end
|
||||
end
|
33
lib/vm/transform.rb
Normal file
33
lib/vm/transform.rb
Normal file
@ -0,0 +1,33 @@
|
||||
require 'parslet'
|
||||
require 'thnad/nodes'
|
||||
|
||||
module Thnad
|
||||
class Transform < Parslet::Transform
|
||||
rule(:number => simple(:value)) { Number.new(value.to_i) }
|
||||
rule(:name => simple(:name)) { Name.new(name.to_s) }
|
||||
|
||||
rule(:arg => simple(:arg)) { arg }
|
||||
rule(:args => sequence(:args)) { args }
|
||||
|
||||
rule(:funcall => simple(:funcall),
|
||||
:args => simple(:args)) { Funcall.new(funcall.name, [args]) }
|
||||
|
||||
rule(:funcall => simple(:funcall),
|
||||
:args => sequence(:args)) { Funcall.new(funcall.name, args) }
|
||||
|
||||
rule(:cond => simple(:cond),
|
||||
:if_true => {:body => simple(:if_true)},
|
||||
:if_false => {:body => simple(:if_false)}) { Conditional.new(cond, if_true, if_false) }
|
||||
|
||||
rule(:param => simple(:param)) { param }
|
||||
rule(:params => sequence(:params)) { params }
|
||||
|
||||
rule(:func => simple(:func),
|
||||
:params => simple(:name),
|
||||
:body => simple(:body)) { Function.new(func.name, [name], body) }
|
||||
|
||||
rule(:func => simple(:func),
|
||||
:params => sequence(:params),
|
||||
:body => simple(:body)) { Function.new(func.name, params, body) }
|
||||
end
|
||||
end
|
21
test/test/fake_builder.rb
Normal file
21
test/test/fake_builder.rb
Normal file
@ -0,0 +1,21 @@
|
||||
class FakeBuilder
|
||||
attr_reader :result
|
||||
|
||||
def initialize
|
||||
@result = ''
|
||||
end
|
||||
|
||||
def class_builder
|
||||
'example'
|
||||
end
|
||||
|
||||
def int
|
||||
'int'
|
||||
end
|
||||
|
||||
def method_missing(name, *args, &block)
|
||||
@result += ([name] + args.flatten).join(', ').sub(',', '')
|
||||
@result += "\n"
|
||||
block.call(self) if name.to_s == 'public_static_method'
|
||||
end
|
||||
end
|
22
test/test/graph_helper.rb
Normal file
22
test/test/graph_helper.rb
Normal file
@ -0,0 +1,22 @@
|
||||
require 'gritty'
|
||||
|
||||
def dump(obj, name)
|
||||
g = digraph do
|
||||
graph_attribs << 'bgcolor=black'
|
||||
|
||||
node_attribs << 'color=white'
|
||||
node_attribs << 'penwidth=2'
|
||||
node_attribs << 'fontcolor=white'
|
||||
node_attribs << 'labelloc=c'
|
||||
node_attribs << 'fontname="Courier New"'
|
||||
node_attribs << 'fontsize=36'
|
||||
|
||||
edge_attribs << 'color=white'
|
||||
edge_attribs << 'penwidth=2'
|
||||
|
||||
builder = Gritty::NodeBuilder.new self
|
||||
builder.build obj, 'root'
|
||||
|
||||
save name, 'pdf'
|
||||
end
|
||||
end
|
77
test/test/test_nodes.rb
Normal file
77
test/test/test_nodes.rb
Normal file
@ -0,0 +1,77 @@
|
||||
$: << File.expand_path(File.dirname(__FILE__) + '/../lib')
|
||||
$: << File.expand_path(File.dirname(__FILE__))
|
||||
|
||||
require 'minitest/autorun'
|
||||
require 'minitest/spec'
|
||||
require 'thnad/nodes'
|
||||
require 'fake_builder'
|
||||
|
||||
include Thnad
|
||||
|
||||
describe 'Nodes' do
|
||||
before do
|
||||
@context = Hash.new
|
||||
@builder = FakeBuilder.new
|
||||
end
|
||||
|
||||
it 'emits a number' do
|
||||
input = Thnad::Number.new 42
|
||||
expected = <<HERE
|
||||
ldc 42
|
||||
HERE
|
||||
input.eval @context, @builder
|
||||
|
||||
@builder.result.must_equal expected
|
||||
end
|
||||
|
||||
it 'emits a function call' do
|
||||
@context[:params] = ['foo']
|
||||
|
||||
input = Thnad::Funcall.new 'baz', [Thnad::Number.new(42),
|
||||
Thnad::Name.new('foo')]
|
||||
expected = <<HERE
|
||||
ldc 42
|
||||
iload 0
|
||||
invokestatic example, baz, int, int, int
|
||||
HERE
|
||||
|
||||
input.eval @context, @builder
|
||||
|
||||
@builder.result.must_equal expected
|
||||
end
|
||||
|
||||
it 'emits a conditional' do
|
||||
input = Thnad::Conditional.new \
|
||||
Thnad::Number.new(0),
|
||||
Thnad::Number.new(42),
|
||||
Thnad::Number.new(667)
|
||||
expected = <<HERE
|
||||
ldc 0
|
||||
ifeq else
|
||||
ldc 42
|
||||
goto endif
|
||||
label else
|
||||
ldc 667
|
||||
label endif
|
||||
HERE
|
||||
|
||||
input.eval @context, @builder
|
||||
@builder.result.must_equal expected
|
||||
end
|
||||
|
||||
it 'emits a function definition' do
|
||||
input = Thnad::Function.new \
|
||||
'foo',
|
||||
Thnad::Name.new('x'),
|
||||
Thnad::Number.new(5)
|
||||
|
||||
expected = <<HERE
|
||||
public_static_method foo, int, int
|
||||
ldc 5
|
||||
ireturn
|
||||
HERE
|
||||
|
||||
input.eval @context, @builder
|
||||
@builder.result.must_equal expected
|
||||
end
|
||||
end
|
72
test/test/test_parser.rb
Normal file
72
test/test/test_parser.rb
Normal file
@ -0,0 +1,72 @@
|
||||
$: << File.expand_path(File.dirname(__FILE__) + '/../lib')
|
||||
|
||||
require 'minitest/autorun'
|
||||
require 'minitest/spec'
|
||||
require 'thnad/parser'
|
||||
|
||||
include Thnad
|
||||
|
||||
describe Parser do
|
||||
before do
|
||||
@parser = Thnad::Parser.new
|
||||
end
|
||||
|
||||
it 'reads a number' do
|
||||
input = '42 '
|
||||
expected = {:number => '42'}
|
||||
|
||||
@parser.number.parse(input).must_equal expected
|
||||
end
|
||||
|
||||
it 'reads a name' do
|
||||
input = 'foo '
|
||||
expected = {:name => 'foo'}
|
||||
|
||||
@parser.name.parse(input).must_equal expected
|
||||
end
|
||||
|
||||
it 'reads an argument list' do
|
||||
input = '(42, foo)'
|
||||
expected = {:args => [{:arg => {:number => '42'}},
|
||||
{:arg => {:name => 'foo'}}]}
|
||||
|
||||
@parser.args.parse(input).must_equal expected
|
||||
end
|
||||
|
||||
it 'reads a function call' do
|
||||
input = 'baz(42, foo)'
|
||||
expected = {:funcall => {:name => 'baz' },
|
||||
:args => [{:arg => {:number => '42'}},
|
||||
{:arg => {:name => 'foo'}}]}
|
||||
|
||||
@parser.funcall.parse(input).must_equal expected
|
||||
end
|
||||
|
||||
it 'reads a conditional' do
|
||||
input = <<HERE
|
||||
if (0) {
|
||||
42
|
||||
} else {
|
||||
667
|
||||
}
|
||||
HERE
|
||||
|
||||
expected = {:cond => {:number => '0'},
|
||||
:if_true => {:body => {:number => '42'}},
|
||||
:if_false => {:body => {:number => '667'}}}
|
||||
|
||||
@parser.cond.parse(input).must_equal expected
|
||||
end
|
||||
|
||||
it 'reads a function definition' do
|
||||
input = <<HERE
|
||||
function foo(x) {
|
||||
5
|
||||
}
|
||||
HERE
|
||||
expected = {:func => {:name => 'foo'},
|
||||
:params => {:param => {:name => 'x'}},
|
||||
:body => {:number => '5'}}
|
||||
@parser.func.parse(input).must_equal expected
|
||||
end
|
||||
end
|
78
test/test/test_transform.rb
Normal file
78
test/test/test_transform.rb
Normal file
@ -0,0 +1,78 @@
|
||||
$: << File.expand_path(File.dirname(__FILE__) + '/../lib')
|
||||
|
||||
require 'minitest/autorun'
|
||||
require 'minitest/spec'
|
||||
require 'thnad/transform'
|
||||
|
||||
include Thnad
|
||||
|
||||
describe Transform do
|
||||
before do
|
||||
@transform = Thnad::Transform.new
|
||||
end
|
||||
|
||||
it 'transforms a number' do
|
||||
input = {:number => '42'}
|
||||
expected = Thnad::Number.new(42)
|
||||
|
||||
@transform.apply(input).must_equal expected
|
||||
end
|
||||
|
||||
it 'transforms a name' do
|
||||
input = {:name => 'foo'}
|
||||
expected = Thnad::Name.new('foo')
|
||||
|
||||
@transform.apply(input).must_equal expected
|
||||
end
|
||||
|
||||
it 'transforms an argument list' do
|
||||
input = {:args => [{:arg => {:number => '42'}},
|
||||
{:arg => {:name => 'foo'}}]}
|
||||
expected = [Thnad::Number.new(42),
|
||||
Thnad::Name.new('foo')]
|
||||
|
||||
@transform.apply(input).must_equal expected
|
||||
end
|
||||
|
||||
it 'transforms a single-argument function call' do
|
||||
input = {:funcall => {:name => 'foo'},
|
||||
:args => [{:arg => {:number => '42'}}]}
|
||||
expected = Thnad::Funcall.new 'foo', [Thnad::Number.new(42)]
|
||||
|
||||
@transform.apply(input).must_equal expected
|
||||
end
|
||||
|
||||
it 'transforms a multi-argument function call' do
|
||||
input = {:funcall => {:name => 'baz'},
|
||||
:args => [{:arg => {:number => '42'}},
|
||||
{:arg => {:name => 'foo'}}]}
|
||||
expected = Thnad::Funcall.new 'baz', [Thnad::Number.new(42),
|
||||
Thnad::Name.new('foo')]
|
||||
|
||||
@transform.apply(input).must_equal expected
|
||||
end
|
||||
|
||||
it 'transforms a conditional' do
|
||||
input = {:cond => {:number => '0'},
|
||||
:if_true => {:body => {:number => '42'}},
|
||||
:if_false => {:body => {:number => '667'}}}
|
||||
expected = Thnad::Conditional.new \
|
||||
Thnad::Number.new(0),
|
||||
Thnad::Number.new(42),
|
||||
Thnad::Number.new(667)
|
||||
|
||||
@transform.apply(input).must_equal expected
|
||||
end
|
||||
|
||||
it 'transforms a function definition' do
|
||||
input = {:func => {:name => 'foo'},
|
||||
:params => {:param => {:name => 'x'}},
|
||||
:body => {:number => '5'}}
|
||||
expected = Thnad::Function.new \
|
||||
'foo',
|
||||
[Thnad::Name.new('x')],
|
||||
Thnad::Number.new(5)
|
||||
|
||||
@transform.apply(input).must_equal expected
|
||||
end
|
||||
end
|
Loading…
Reference in New Issue
Block a user