rename phisol to soml

This commit is contained in:
Torsten Ruger
2015-10-23 14:22:55 +03:00
parent 991cc0519f
commit e0c5bc4c11
31 changed files with 60 additions and 97 deletions

23
lib/soml/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

77
lib/soml/compiler.rb Normal file
View File

@ -0,0 +1,77 @@
module Soml
class Compiler < AST::Processor
def initialize()
@regs = []
end
def handler_missing node
raise "No handler on_#{node.type}(node)"
end
# Compiling is the conversion of the AST into 2 things:
# - code (ie sequences of Instructions inside Blocks) carried by MethodSource
# - an object graph containing all the Methods, their classes and Constants
#
# Some compile methods just add code, some may add structure (ie Blocks) 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.
#
def self.compile statement
compiler = Compiler.new
compiler.process statement
end
# simple helper to add the given code to the current method (instance variable)
def add_code code
@method.source.add_code code
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
# 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
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/collections"
require_relative "compiler/statement_list"
require_relative "compiler/field_def"
require_relative "compiler/field_access"
require_relative "compiler/function_definition"
require_relative "compiler/if_statement"
require_relative "compiler/class_statement"
require_relative "compiler/name_expression"
require_relative "compiler/operator_value"
require_relative "compiler/return_statement"
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 Soml
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 = name.to_a.first
v = process(value)
raise "Not register #{v}" unless v.is_a?(Register::RegisterValue)
code = nil
if( index = @method.has_arg(name))
# TODO, check type @method.arguments[index].type
code = Register.set_slot(statement , v , :message , index + Parfait::Message.offset )
else # or a local so it is in the frame
index = @method.has_local( 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 , index + Parfait::Frame.offset )
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,47 @@
module Soml
# collection of the simple ones, int and strings and such
Compiler.class_eval do
# Constant statements 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 we view ConstantExpressions like functions that return the value of the constant.
# In other words, their storage is the return slot as it would be for a method
# The current approach moves the constant into a variable before using it
# But in the future (in the one that holds great things) we optimize those unneccesay moves away
def on_int statement
int = statement.first
reg = use_reg :Integer , int
add_code Register::LoadConstant.new( statement, int , reg )
return reg
end
def on_true statement
reg = use_reg :Boolean
add_code Register::LoadConstant.new( statement, true , reg )
return reg
end
def on_false statement
reg = use_reg :Boolean
add_code Register::LoadConstant.new( statement, false , reg )
return reg
end
def on_nil statement
reg = use_reg :NilClass
add_code Register::LoadConstant.new( statement, nil , reg )
return reg
end
def on_string statement
value = statement.first.to_sym
reg = use_reg :Word
@method.source.constants << value
add_code Register::LoadConstant.new( statement, value , reg )
return reg
end
end
end

View File

@ -0,0 +1,50 @@
module Soml
Compiler.class_eval do
def on_call statement
#puts statement
name , arguments , receiver = *statement
name = name.to_a.first
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(@method, :message , :next_message , new_message )
if receiver
me = process( receiver.to_a.first )
else
me = use_reg @method.for_class.name
add_code Register.get_slot(@method, :message , :receiver , me )
end
# move our receiver there
add_code Register.set_slot( statement , me , :new_message , :receiver)
# load method name and set to new message (for exceptions/debug)
name_tmp = use_reg(:Word)
add_code Register::LoadConstant.new(statement, name , name_tmp)
add_code Register.set_slot( statement , name_tmp , :new_message , :name)
# next 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( statement , val , :new_message , i + 1 + Parfait::Message.offset)
add_code set
end
# now we have to resolve the method name (+ receiver) into a callable method
clazz = Register.machine.space.get_class_by_name(me.type)
raise "No such class #{me.type}" unless clazz
method = clazz.get_instance_method(name)
#puts Register.machine.space.get_class_by_name(:Integer).method_names.to_a
raise "Method not implemented #{me.type}.#{name}" unless method
Register.issue_call( @method , method )
ret = use_reg( method.source.return_type )
# 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(@method, :message , :return_value , ret )
ret
end
end
end

View File

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

View File

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

View File

@ -0,0 +1,13 @@
module Soml
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,31 @@
module Soml
Compiler.class_eval do
def on_field_access statement
#puts statement.inspect
receiver_ast , field_ast = *statement
receiver = receiver_ast.first_from(:name)
field_name = field_ast.first_from(:name)
case receiver
when :self
index = @clazz.object_layout.variable_index(field_name)
raise "field access, but no such field:#{field_name} for class #{@clazz.name}" unless index
value = use_reg(@clazz.name) #TODO incorrect, this is the self, but should be the type of variable at index
add_code Register.get_slot(statement , :message , :receiver , value )
# reuse the register for next move
move = Register.get_slot(statement, value , index , value )
add_code move
return value
when :message
#message Slot
raise "message not yet"
else
#arg / frame Slot
raise "frame not implemented"
end
value
end
end
end

View File

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

View File

@ -0,0 +1,51 @@
module Soml
Compiler.class_eval do
def on_function statement
#puts statement.inspect
return_type , name , parameters, kids , receiver = *statement
name = name.to_a.first
args = parameters.to_a.collect do |p|
raise "error, argument must be a identifier, not #{p}" unless p.type == :parameter
Parfait::Variable.new( *p)
end
if receiver
# compiler will always return slot. with known value or not
r = receiver.first
if( r.is_a? Parfait::Class )
class_name = r.value.name
else
if( r != :self)
raise "unimplemented case in function #{r}"
else
r = Register::Self.new()
class_name = method.for_class.name
end
end
else
r = @clazz
class_name = @clazz.name
end
raise "Already in method #{@method}" if @method
@method = @clazz.get_instance_method( name )
if(@method)
#puts "Warning, redefining method #{name}" unless name == :main
#TODO check args / type compatibility
@method.source.init @method
else
@method = Register::MethodSource.create_method(class_name, return_type, name , args )
@method.for_class.add_instance_method @method
end
@method.source.receiver = r
#puts "compile method #{@method.name}"
kids.to_a.each do |ex|
ret = process(ex)
end
@method = nil
# function definition is a statement, does not return any value
return nil
end
end
end

View File

@ -0,0 +1,39 @@
module Soml
Compiler.class_eval do
# if - attr_reader :cond, :if_true, :if_false
def on_if_statement statement
branch_type , condition , if_true , if_false = *statement
condition = condition.first
# to execute the logic as the if states it, the blocks are the other way around
# so we can the jump over the else if true ,
# and the else joins unconditionally after the true_block
merge_block = @method.source.new_block "if_merge" # last one, created first
true_block = @method.source.new_block "if_true" # second, linked in after current, before merge
false_block = @method.source.new_block "if_false" # directly next in order, ie if we don't jump we land here
reset_regs
is = process(condition)
branch_class = Object.const_get "Register::Is#{branch_type.capitalize}"
add_code branch_class.new( condition , true_block )
# compile the true block (as we think of it first, even it is second in sequential order)
@method.source.current true_block
reset_regs
last = process_all(if_true).last
# compile the false block
@method.source.current false_block
reset_regs
last = process_all(if_false).last if if_false
add_code Register::Branch.new(statement, merge_block )
#puts "compiled if: end"
@method.source.current merge_block
#TODO should return the union of the true and false types
last
end
end
end

View File

@ -0,0 +1,34 @@
module Soml
Compiler.class_eval do
# attr_reader :name
# compiling name needs to check if it's a variable and if so resolve it
# otherwise it's a method without args and a send is issued.
# whichever way this goes the result is stored in the return slot (as all compiles)
def on_name statement
name = statement.to_a.first
if( name == :self)
ret = use_reg @clazz.name
add_code Register.get_slot(statement , :message , :receiver , ret )
return ret
end
# either an argument, so it's stored in message
if( index = @method.has_arg(name))
ret = use_reg @method.arguments[index].type
add_code Register.get_slot(statement , :message , index + Parfait::Message.offset , 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].type
add_code Register.get_slot(statement , frame , index + Parfait::Frame.offset , ret )
return ret
end
end
raise "must define variable #{name} before using it"
end
end #module
end

View File

@ -0,0 +1,18 @@
module Soml
Compiler.class_eval do
def on_operator_value 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(left_e)
right_reg = process(right_e)
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,operator,left_reg,right_reg)
return left_reg # though this has wrong value attached
end
end
end

View File

@ -0,0 +1,9 @@
module Soml
Compiler.class_eval do
# return attr_reader :statement
def on_return statement
return process(statement.to_a.first )
end
end
end

View File

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

View File

@ -0,0 +1,30 @@
module Soml
Compiler.class_eval do
def on_while_statement statement
#puts statement.inspect
branch_type , condition , statements = *statement
condition = condition.first
# this is where the while ends and both branches meet
merge = @method.source.new_block("while merge")
# this comes after the current and beofre the merge
start = @method.source.new_block("while_start" )
@method.source.current start
cond = process(condition)
branch_class = Object.const_get "Register::Is#{branch_type.capitalize}"
add_code branch_class.new( condition , merge )
last = process_all(statements).last
# unconditionally branch to the start
add_code Register::Branch.new(statement,start)
# continue execution / compiling at the merge block
@method.source.current merge
last
end
end
end