From d90ea3dd26a9ba3372d34592a3797ed5471b82bd Mon Sep 17 00:00:00 2001 From: Torsten Ruger Date: Thu, 24 Apr 2014 15:43:20 +0300 Subject: [PATCH] copied thnad to get a kickstart --- lib/vm/builtins.rb | 27 +++++++++++++ lib/vm/compiler.rb | 74 +++++++++++++++++++++++++++++++++++ lib/vm/copied_parser.rb | 70 +++++++++++++++++++++++++++++++++ lib/vm/nodes.rb | 54 +++++++++++++++++++++++++ lib/vm/parser.rb | 52 +++++++++++++++++++++++++ lib/vm/transform.rb | 33 ++++++++++++++++ test/test/fake_builder.rb | 21 ++++++++++ test/test/graph_helper.rb | 22 +++++++++++ test/test/test_nodes.rb | 77 ++++++++++++++++++++++++++++++++++++ test/test/test_parser.rb | 72 ++++++++++++++++++++++++++++++++++ test/test/test_transform.rb | 78 +++++++++++++++++++++++++++++++++++++ 11 files changed, 580 insertions(+) create mode 100644 lib/vm/builtins.rb create mode 100644 lib/vm/compiler.rb create mode 100644 lib/vm/copied_parser.rb create mode 100644 lib/vm/nodes.rb create mode 100644 lib/vm/parser.rb create mode 100644 lib/vm/transform.rb create mode 100644 test/test/fake_builder.rb create mode 100644 test/test/graph_helper.rb create mode 100644 test/test/test_nodes.rb create mode 100644 test/test/test_parser.rb create mode 100644 test/test/test_transform.rb diff --git a/lib/vm/builtins.rb b/lib/vm/builtins.rb new file mode 100644 index 00000000..b0e0d83c --- /dev/null +++ b/lib/vm/builtins.rb @@ -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 diff --git a/lib/vm/compiler.rb b/lib/vm/compiler.rb new file mode 100644 index 00000000..1043797b --- /dev/null +++ b/lib/vm/compiler.rb @@ -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 diff --git a/lib/vm/copied_parser.rb b/lib/vm/copied_parser.rb new file mode 100644 index 00000000..80aeb294 --- /dev/null +++ b/lib/vm/copied_parser.rb @@ -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 = <> 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 diff --git a/lib/vm/transform.rb b/lib/vm/transform.rb new file mode 100644 index 00000000..99a6fbb6 --- /dev/null +++ b/lib/vm/transform.rb @@ -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 diff --git a/test/test/fake_builder.rb b/test/test/fake_builder.rb new file mode 100644 index 00000000..b8765b56 --- /dev/null +++ b/test/test/fake_builder.rb @@ -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 diff --git a/test/test/graph_helper.rb b/test/test/graph_helper.rb new file mode 100644 index 00000000..ce98bbfc --- /dev/null +++ b/test/test/graph_helper.rb @@ -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 diff --git a/test/test/test_nodes.rb b/test/test/test_nodes.rb new file mode 100644 index 00000000..2bca688b --- /dev/null +++ b/test/test/test_nodes.rb @@ -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 = < '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 = < {: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 = < {:name => 'foo'}, + :params => {:param => {:name => 'x'}}, + :body => {:number => '5'}} + @parser.func.parse(input).must_equal expected + end +end diff --git a/test/test/test_transform.rb b/test/test/test_transform.rb new file mode 100644 index 00000000..4fc59db0 --- /dev/null +++ b/test/test/test_transform.rb @@ -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