move compiler to typed

starting to get rid of soml, bit by bit
This commit is contained in:
Torsten Ruger
2016-12-08 15:25:20 +02:00
parent c3a28d2abc
commit da553f996f
30 changed files with 36 additions and 35 deletions

23
lib/typed/ast_helper.rb Normal file
View File

@ -0,0 +1,23 @@
AST::Node.class_eval do
def [](name)
#puts self.inspect
children.each do |child|
if child.is_a?(AST::Node)
#puts child.type
if (child.type == name)
return child.children
end
else
#puts child.class
end
end
nil
end
def first_from( node_name )
from = self[node_name]
return nil unless from
from.first
end
end

207
lib/typed/compiler.rb Normal file
View File

@ -0,0 +1,207 @@
module Typed
# Compiling is the conversion of the AST into 2 things:
# - code (ie sequences of Instructions inside Methods)
# - an object graph containing all the Methods, their classes and Constants
#
# Some compile methods just add code, some may add Instructions while
# others instantiate Class and Method objects
#
# Everything in ruby is an statement, ie returns a value. So the effect of every compile
# is that a value is put into the ReturnSlot of the current Message.
# The compile method (so every compile method) returns the value that it deposits.
#
# The process uses a visitor pattern (from AST::Processor) to dispatch according to the
# type the statement. So a s(:if xx) will become an on_if(node) call.
# This makes the dispatch extensible, ie Expressions may be added by external code,
# as long as matching compile methods are supplied too.
#
# A compiler can also be used to generate code for a method without AST nodes. In the same way
# compile methods do, ie adding Instructions etc. In this way code may be generated that
# has no code equivalent.
#
# The Compiler also keeps a list of used registers, from which one may take to use and return to
# when done. The list may be reset.
#
# The Compiler also carries method and class instance variables. The method is where code is
# added to (with add_code). To be more precise, the @current instruction is where code is added
# to, and that may be changed with set_current
# All Statements reset the registers and return nil.
# Expressions use registers and return the register where their value is stored.
# Helper function to create a new compiler and compie the statement(s)
def self.compile statement
compiler = Compiler.new
code = Soml.ast_to_code statement
compiler.process code
end
class Compiler
def initialize( method = nil )
@regs = []
return unless method
@method = method
@clazz = method.for_class
@current = method.instructions
end
attr_reader :clazz , :method
# Dispatches `code` according to it's class name, for class NameExpression
# a method named `on_NameExpression` is invoked with one argument, the `code`
#
# @param [Soml::Code, nil] code
def process(code)
name = code.class.name.split("::").last
# Invoke a specific handler
on_handler = :"on_#{name}"
if respond_to? on_handler
return send on_handler, code
else
raise "No handler on_#{name}(code) #{code.inspect}"
end
end
# {#process}es each code from `codes` and returns an array of
# results.
#
def process_all(codes)
codes.to_a.map do |code|
process code
end
end
def on_Statements(codes)
process_all codes.statements
end
# create the method, do some checks and set it as the current method to be added to
# class_name and method_name are pretty clear, args are given as a ruby array
def create_method( class_name , method_name , args = {})
raise "create_method #{class_name}.#{class_name.class}" unless class_name.is_a? Symbol
clazz = Register.machine.space.get_class_by_name class_name
raise "No such class #{class_name}" unless clazz
create_method_for( clazz , method_name , args)
end
# create a method for the given class ( Parfait class object)
# method_name is a Symbol
# args a ruby array
# the created method is set as the current and the given class too
# return the compiler (for chaining)
def create_method_for clazz , method_name , args
@clazz = clazz
raise "create_method #{method_name}.#{method_name.class}" unless method_name.is_a? Symbol
arguments = []
if( args.is_a? Array)
arguments = args
else
args.each do | name , type |
arguments << Parfait::Variable.new( type , name.to_sym)
end
end
@method = clazz.create_instance_method( method_name , Parfait.new_list(arguments))
self
end
# add method entry and exit code. Mainly save_return for the enter and
# message shuffle and FunctionReturn for the return
# return self for chaining
def init_method
source = "_init_method"
name = "#{method.for_class.name}.#{method.name}"
@method.instructions = Register::Label.new(source, name)
@current = enter = method.instructions
add_code Register::Label.new( source, "return #{name}")
#load the return address into pc, affecting return. (other cpus have commands for this, but not arm)
add_code Register::FunctionReturn.new( source , Register.message_reg , Register.resolve_index(:message , :return_address) )
@current = enter
self
end
# set the insertion point (where code is added with add_code)
def set_current c
@current = c
end
# add an instruction after the current (insertion point)
# the added instruction will become the new insertion point
def add_code instruction
unless instruction.is_a?(Register::Instruction)
raise instruction.to_s
end
@current.insert(instruction) #insert after current
@current = instruction
self
end
# require a (temporary) register. code must give this back with release_reg
def use_reg type , value = nil
if @regs.empty?
reg = Register.tmp_reg(type , value)
else
reg = @regs.last.next_reg_use(type , value)
end
@regs << reg
return reg
end
def copy reg , source
copied = use_reg reg.type
add_code Reister.transfer source , reg , copied
copied
end
# releasing a register (accuired by use_reg) makes it available for use again
# thus avoiding possibly using too many registers
def release_reg reg
last = @regs.pop
raise "released register in wrong order, expect #{last} but was #{reg}" if reg != last
end
# reset the registers to be used. Start at r4 for next usage.
# Every statement starts with this, meaning each statement may use all registers, but none
# get saved. Statements have affect on objects.
def reset_regs
@regs.clear
end
# ensure the name given is not space and raise exception otherwise
# return the name for chaining
def no_space name
raise "space is a reserved name" if name == :space
name
end
# load the soml files from parfait directory
# Need to remove this and put it back into ruby code
def self.load_parfait
["word","class","type","message" ,"integer", "object"].each do |o|
str = File.open(File.expand_path("parfait/#{o}.soml", File.dirname(__FILE__))).read
syntax = Parser::Salama.new.parse_with_debug(str, reporter: Parslet::ErrorReporter::Deepest.new)
parts = Parser::Transform.new.apply(syntax)
code = Soml.ast_to_code parts
self.new.process( code )
end
end
end
end
require_relative "ast_helper"
require_relative "compiler/assignment"
require_relative "compiler/basic_values"
require_relative "compiler/call_site"
require_relative "compiler/class_field"
require_relative "compiler/class_statement"
require_relative "compiler/collections"
require_relative "compiler/field_def"
require_relative "compiler/field_access"
require_relative "compiler/function_definition"
require_relative "compiler/if_statement"
require_relative "compiler/name_expression"
require_relative "compiler/operator_value"
require_relative "compiler/return_statement"
require_relative "compiler/statement_list"
require_relative "compiler/while_statement"

View File

@ -0,0 +1,69 @@
### Compiling
The Ast (abstract syntax tree) is created by [salama-reader](https://github.com/salama/salama-reader)
gem and the classes defined there
The code in this directory compiles the AST to the virtual machine code, and Parfait object structure.
If this were an interpreter, we would just walk the tree and do what it says.
Since it's not things are a little more difficult, especially in time.
When compiling we deal with two times, compile-time and run-time.
All the headache comes from mixing those two up.*
Similarly, the result of compiling is two-fold: a static and a dynamic part.
- the static part are objects like the constants, but also defined classes and their methods
- the dynamic part is the code, which is stored as streams of instructions in the MethodSource
Too make things a little simpler, we create a very high level instruction stream at first and then
run transformation and optimization passes on the stream to improve it.
The compiler has a method for each type for ast, named along on_xxx with xxx as the type
#### Compiler holds scope
The Compiler instance can hold arbitrary scope needed during the compilation. Since we compile Soml
(a static language) things have become more simple.
A class statement sets the current @clazz scope , a method definition the @method.
If either are not set when needed compile errors will follow. So easy, so nice.
All code is encoded as a stream of Instructions in the MethodSource.
Instructions are stored as a list of Blocks, and Blocks are the smallest unit of code,
which is always linear.
Code is added to the method (using add_code), rather than working with the actual instructions.
This is so each compiling method can just do it's bit and be unaware of the larger structure
that is being created.
The general structure of the instructions is a graph
(with if's and whiles and breaks and what), but we build it to have one start and *one* end (return).
#### Messages and frames
Since the machine is virtual, we have to define it, and since it is oo we define it in objects.
Also it is important to define how instructions operate, which is is in a physical machine would
be by changing the contents of registers or some stack.
Our machine is not a register machine, but an object machine: it operates directly on objects and
also has no separate stack, only objects. There is only one object which is accessible,
basically meaning pinned to a register, the Message.
One can think of the Message as an oo replacement of the stack.
When a Method needs to make a call, it creates a NewMessage object.
Messages contain return addresses (yes, plural) and arguments.
The important thing here is that Messages and Frames are normal objects.
### Distinctly future proof
Soml is designed to be used as an implementation language for a higher oo language. Some, or
even many, features may not make sense on their own. But these features, like several return
addresses, are important to implement the higher language.
In fact, Soml's main purpose is not even to be written. The main purpose is to have a language to
compile ruby to. In the same way that the assembler layer in salama is not designed to be written,
we just need it to create our layers.

View File

@ -0,0 +1,33 @@
module Typed
Compiler.class_eval do
def on_Assignment statement
reset_regs # statements reset registers, ie have all at their disposal
#puts statement.inspect
# name , value = *statement
name_s = no_space statement.name
v = process(statement.value)
raise "Not register #{v}" unless v.is_a?(Register::RegisterValue)
code = nil
if( index = @method.has_arg(name_s.name))
# TODO, check type @method.arguments[index].type
code = Register.set_slot(statement , v , :message , Parfait::Message.get_indexed(index) )
else # or a local so it is in the frame
index = @method.has_local( name_s.name )
if(index)
# TODO, check type @method.locals[index].type
frame = use_reg(:Frame)
add_code Register.get_slot(statement , :message , :frame , frame )
code = Register.set_slot(statement , v , frame , Parfait::Frame.get_indexed(index) )
end
end
if( code )
#puts "addin code #{code}"
add_code code
else
raise "must define variable #{name} before using it in #{@method.inspect}"
end
end
end
end

View File

@ -0,0 +1,56 @@
module Typed
# collection of the simple ones, int and strings and such
Compiler.class_eval do
# Constant expressions can by definition be evaluated at compile time.
# But that does not solve their storage, ie they need to be accessible at runtime from _somewhere_
# So expressions move the data into a Register.
# All expressions return registers
# But in the future (in the one that holds great things) we optimize those unneccesay moves away
def on_IntegerExpression expression
int = expression.value
reg = use_reg :Integer , int
add_code Register::LoadConstant.new( expression, int , reg )
return reg
end
def on_TrueExpression expression
reg = use_reg :Boolean
add_code Register::LoadConstant.new( expression, true , reg )
return reg
end
def on_FalseExpression expression
reg = use_reg :Boolean
add_code Register::LoadConstant.new( expression, false , reg )
return reg
end
def on_NilExpression expression
reg = use_reg :NilClass
add_code Register::LoadConstant.new( expression, nil , reg )
return reg
end
def on_StringExpression expression
value = Parfait.new_word expression.value.to_sym
reg = use_reg :Word
Register.machine.constants << value
add_code Register::LoadConstant.new( expression, value , reg )
return reg
end
def on_ClassExpression expression
name = expression.value
clazz = Parfait::Space.object_space.get_class_by_name! name
raise "No such class #{name}" unless clazz
reg = use_reg :MetaClass , clazz
add_code Register::LoadConstant.new( expression, clazz , reg )
return reg
end
end
end

View File

@ -0,0 +1,71 @@
module Typed
Compiler.class_eval do
def on_CallSite statement
#puts statement
# name_s , arguments , receiver = *statement
raise "not inside method " unless @method
reset_regs
#move the new message (that we need to populate to make a call) to std register
new_message = Register.resolve_to_register(:new_message)
add_code Register.get_slot(statement, :message , :next_message , new_message )
if statement.receiver
me = process( statement.receiver )
else
me = use_reg @method.for_class.name
add_code Register.get_slot(statement, :message , :receiver , me )
end
if(me.type == :MetaClass)
clazz = me.value.meta
else
# now we have to resolve the method name (+ receiver) into a callable method
clazz = Register.machine.space.get_class_by_name(me.type)
end
# move our receiver there
add_code Register.set_slot( statement , me , :new_message , :receiver)
set_message_details(statement , statement.arguments)
set_arguments(statement.arguments)
ret = use_reg( :Integer ) #TODO real return type
do_call(clazz , statement)
# the effect of the method is that the NewMessage Return slot will be filled, return it
# but move it into a register too
add_code Register.get_slot(statement, :new_message , :return_value , ret )
ret
end
private
def do_call clazz , statement
name = statement.name
#puts "clazz #{clazz.name}"
raise "No such class" unless clazz
method = clazz.resolve_method(name)
#puts Register.machine.space.get_class_by_name(:Integer).method_names.to_a
raise "Method not implemented #{clazz.name}.#{name}" unless method
Register.issue_call( self , method )
end
def set_message_details name_s , arguments
name = name_s.name
# load method name and set to new message (for exceptions/debug)
name_tmp = use_reg(:Word)
add_code Register::LoadConstant.new(name_s, name , name_tmp)
add_code Register.set_slot( name_s , name_tmp , :new_message , :name)
# next arguments. first length then args
len_tmp = use_reg(:Integer , arguments.to_a.length )
add_code Register::LoadConstant.new(name_s, arguments.to_a.length , len_tmp)
add_code Register.set_slot( name_s , len_tmp , :new_message , :indexed_length)
end
def set_arguments arguments
# reset tmp regs for each and load result into new_message
arguments.to_a.each_with_index do |arg , i|
reset_regs
# processing should return the register with the value
val = process( arg)
raise "Not register #{val}" unless val.is_a?(Register::RegisterValue)
# which we load int the new_message at the argument's index (the one comes from c index)
set = Register.set_slot( arg , val , :new_message , Parfait::Message.get_indexed(i+1))
add_code set
end
end
end
end

View File

@ -0,0 +1,18 @@
module Typed
Compiler.class_eval do
def on_ClassField statement
#puts statement.inspect
#type , name , value = *statement
for_class = @clazz
raise "no class" unless for_class
index = for_class.instance_type.variable_index(statement.name)
#raise "class field already defined:#{name} for class #{for_class.name}" if index
#puts "Define field #{name} on class #{for_class.name}"
for_class.instance_type.add_instance_variable( statement.name , statement.type )
return nil # statements don't reurn values, only expressions
end
end
end

View File

@ -0,0 +1,14 @@
module Typed
Compiler.class_eval do
def on_ClassStatement statement
#puts statement.inspect
raise "classes dont yet play babushka, get coding #{name}" if @clazz
@clazz = Parfait::Space.object_space.get_class_by_name! statement.name
#puts "Compiling class #{@clazz.name.inspect}"
statement_value = process(statement.statements).last
@clazz = nil
return statement_value
end
end
end

View File

@ -0,0 +1,13 @@
module Typed
Compiler.class_eval do
# attr_reader :values
def on_array statement, context
end
# attr_reader :key , :value
def on_association context
end
def on_hash context
end
end
end

View File

@ -0,0 +1,26 @@
module Typed
Compiler.class_eval do
def on_FieldAccess statement
# receiver_ast , field_ast = *statement
receiver = process(statement.receiver)
clazz = Register.machine.space.get_class_by_name receiver.type
field_name = statement.field.name
index = clazz.instance_type.variable_index(field_name)
raise "field access, but no such field:#{field_name} for class #{clazz.name}" unless index
value = use_reg(clazz.instance_type.type_at(index))
add_code Register.get_slot(statement , receiver , index, value)
value
end
def on_receiver expression
process expression.first
end
end
end

View File

@ -0,0 +1,16 @@
module Typed
Compiler.class_eval do
include AST::Sexp
def on_FieldDef statement
reset_regs # field_def is a statement, no return and all regs
#puts statement.inspect
# type , name , value = *statement
name_s = no_space( statement.name.value )
@method.ensure_local( name_s, statement.type ) unless( @method.has_arg(name_s))
# if there is a value assigned, process it as am assignemnt statement (kind of call on_assign)
process( Soml::Assignment.new(statement.name , statement.value ) ) if statement.value
return nil
end
end
end

View File

@ -0,0 +1,43 @@
module Typed
Compiler.class_eval do
def on_FunctionStatement statement
#puts statement.inspect
# return_type , name , parameters, kids , receiver = *statement
name = statement.name
raise "Already in method #{@method}" if @method
args = statement.parameters.collect do |p|
Parfait::Variable.new( *p )
end
class_method = nil
if(statement.receiver )
if( statement.receiver.first == :self) #class method
class_method = @clazz
@clazz = @clazz.meta
else
raise "Not covered #{statement.receiver}"
end
end
@method = @clazz.get_instance_method( name )
if(@method)
#puts "Warning, redefining method #{name}" unless name == :main
#TODO check args / type compatibility
init_method
else
create_method_for(@clazz, name , args ).init_method
end
@method.source = statement
#puts "compile method #{@method.name}"
process(statement.statements)
@clazz = class_method if class_method
@method = nil
# function definition is a statement, does not return any value
return nil
end
end
end

View File

@ -0,0 +1,35 @@
module Typed
Compiler.class_eval do
# an if evaluates the condition and jumps to the true block if true
# so the else block is automatically after that.
# But then the else needs to jump over the true block unconditionally.
def on_IfStatement statement
# branch_type , condition , if_true , if_false = *statement
# condition = condition.first
reset_regs
process(statement.condition)
branch_class = Object.const_get "Register::Is#{statement.branch_type.capitalize}"
true_block = Register::Label.new(statement, "if_true")
add_code branch_class.new( statement.condition , true_block )
# compile the false block
reset_regs
process(statement.if_false) if statement.if_false.statements
merge = Register::Label.new(statement , "if_merge")
add_code Register::Branch.new(statement.if_false, merge )
# compile the true block
add_code true_block
reset_regs
process(statement.if_true)
#puts "compiled if: end"
add_code merge
nil # statements don't return anything
end
end
end

View File

@ -0,0 +1,45 @@
module Typed
Compiler.class_eval do
# attr_reader :name
# compiling name needs to check if it's a local variable
# or an argument
# whichever way this goes the result is stored in the return slot (as all compiles)
def on_NameExpression statement
name = statement.name
if( name == :self)
ret = use_reg @clazz.name
add_code Register.get_slot(statement , :message , :receiver , ret )
return ret
end
if(name == :space)
space = Parfait::Space.object_space
reg = use_reg :Space , space
add_code Register::LoadConstant.new( statement, space , reg )
return reg
end
if(name == :message)
reg = use_reg :Message
add_code Register::RegisterTransfer.new( statement, Register.message_reg , reg )
return reg
end
# either an argument, so it's stored in message
if( index = @method.has_arg(name))
ret = use_reg @method.arguments[index].value_type
add_code Register.get_slot(statement , :message , Parfait::Message.get_indexed(index), ret )
return ret
else # or a local so it is in the frame
index = @method.has_local( name )
if(index)
frame = use_reg :Frame
add_code Register.get_slot(statement , :message , :frame , frame )
ret = use_reg @method.locals[index].value_type
add_code Register.get_slot(statement , frame , Parfait::Frame.get_indexed(index), ret )
return ret
end
end
raise "must define variable '#{name}' before using it"
end
end #module
end

View File

@ -0,0 +1,18 @@
module Typed
Compiler.class_eval do
def on_OperatorExpression statement
#puts "operator #{statement.inspect}"
# operator , left_e , right_e = *statement
# left and right must be expressions. Expressions return a register when compiled
left_reg = process(statement.left_expression)
right_reg = process(statement.right_expression)
raise "Not register #{left_reg}" unless left_reg.is_a?(Register::RegisterValue)
raise "Not register #{right_reg}" unless right_reg.is_a?(Register::RegisterValue)
#puts "left #{left_reg}"
#puts "right #{right_reg}"
add_code Register::OperatorInstruction.new(statement,statement.operator,left_reg,right_reg)
return left_reg # though this has wrong value attached
end
end
end

View File

@ -0,0 +1,10 @@
module Typed
Compiler.class_eval do
def on_ReturnStatement statement
reg = process(statement.return_value)
add_code Register.set_slot( statement, reg , :message , :return_value)
nil # statements don't return
end
end
end

View File

@ -0,0 +1,9 @@
module Typed
Compiler.class_eval do
def on_statements statement
process_all( statement.children )
end
end
end

View File

@ -0,0 +1,28 @@
module Typed
Compiler.class_eval do
def on_WhileStatement statement
#puts statement.inspect
#branch_type , condition , statements = *statement
condition_label = Register::Label.new(statement.condition , "condition_label")
# unconditionally branch to the condition upon entering the loop
add_code Register::Branch.new(statement.condition,condition_label)
add_code start = Register::Label.new(statement , "while_start" )
reset_regs
process(statement.statements)
# This is where the loop starts, though in subsequent iterations it's in the middle
add_code condition_label
reset_regs
process(statement.condition)
branch_class = Object.const_get "Register::Is#{statement.branch_type.capitalize}"
# this is where the while ends and both branches meet
add_code branch_class.new( statement.condition , start )
nil # statements don't return anything
end
end
end