Change Mom to SlotMachine

rather large commit, but essentially a simple rename
Rationale in docs and blogs
This commit is contained in:
2019-10-03 20:55:41 +03:00
parent fd8a3e9cc5
commit c43436f35a
170 changed files with 481 additions and 480 deletions

View File

@ -0,0 +1,58 @@
# SlotMachine , Minimal Object Machine
This layer sits between the language layer (vool) and the risc machine layer.
It is meant to make the transition (between vool and risc) easier to understand.
Previous efforts were doing the transition without an intermediate layer. But while
this was possible, it was more difficult than need be, and so we go to the old saying
that everything in computing can be fixed by another layer :-)
## Recap
A little recap of why the transition was too steep will naturally reveal the design of SlotMachine.
### Structure
Vool has a tree structure. Risc is a linked list, so essentially flat.
### Memory model
Vool has no memory, it has objects and they just are. Risc on the other hand has only registers
and memory. Data can only move to/from/between registers, ie not from memory to memory.
While Risc knows about objects, it deals in machine words.
### Execution model
Vool's implicit execution model would be interpretation, ie tree traversal. Vool has high level
control structures, including send, and no goto, it is a language after all.
Risc is close to a cpu, it has a current instruction (pc), registers (8) and a register based
instruction set. Risc has word comparisons and a jump. Call is not used as the stack is not
used (stacks are messy, not oo)
## Design
The *essential* step from vool to risc, is the one from a language to a machine. From statements
that hang in the air, to an instruction set.
So to put a layer in the middle of those two, SlotMachine will be:
### Linked list
But, very much like Risc, just higher level so it's easier to understand
### Use object memory
object to object transfer
no registers (one could see the current message as the only register)
### Instruction based
So mom is a machine layer, rather than a language.
No control structures, but compare and jump instructions.
No send or call, just objects and jump.
Again in two steps, see below
Machine capabilities (instructions) for basic operations. Use of macros for higher level.

View File

@ -0,0 +1,46 @@
module Mom
# A BlockCompiler is much like a MehtodCompiler, exept for blocks
#
class BlockCompiler < CallableCompiler
attr_reader :block , :mom_instructions
alias :block :callable
def initialize( block , method)
@method = method
super(block)
end
def source_name
"#{@method.self_type.name}.init"
end
def to_risc
risc_compiler = Risc::BlockCompiler.new(@callable , @method , mom_instructions)
instructions_to_risc(risc_compiler)
#recursive blocks not done
risc_compiler
end
# determine how given name need to be accsessed.
# For blocks the options are args or frame
# or then the methods arg or frame
def slot_type_for(name)
if index = @callable.arguments_type.variable_index(name)
slot_def = ["arg#{index}".to_sym]
elsif index = @callable.frame_type.variable_index(name)
slot_def = ["local#{index}".to_sym]
elsif index = @method.arguments_type.variable_index(name)
slot_def = [:caller , :caller , "arg#{index}".to_sym]
elsif index = @method.frame_type.variable_index(name)
slot_def = [:caller ,:caller , "local#{index}".to_sym ]
elsif
raise "no variable #{name} , need to resolve at runtime"
end
return slot_def
end
end
end

View File

@ -0,0 +1,75 @@
module Mom
# CallableCompiler is used to generate mom instructions. It is an abstact base
# class shared by BlockCompiler and MethodCompiler
# - mom_instructions: The sequence of mom level instructions that mom was compiled to
# Instructions derive from class Instruction and form a linked list
class CallableCompiler
include Util::CompilerList
def initialize( callable )
@callable = callable
@constants = []
@mom_instructions = Label.new(source_name, source_name)
@current = start = @mom_instructions
add_code Label.new( source_name, "return_label")
add_code Mom::ReturnSequence.new(source_name)
add_code Label.new( source_name, "unreachable")
@current = start
end
attr_reader :mom_instructions , :constants , :callable , :current
def return_label
@mom_instructions.each do |ins|
next unless ins.is_a?(Label)
return ins if ins.name == "return_label"
end
end
# add a constant (which get created during compilation and need to be linked)
def add_constant(const)
raise "Must be Parfait #{const}" unless const.is_a?(Parfait::Object)
@constants << const
end
# translate to Risc, ie a Risc level CallableCompiler
# abstract functon that needs to be implemented by Method/BlockCompiler
def to_risc
raise "abstract in #{self.class}"
end
# add a risc instruction after the current (insertion point)
# the added instruction will become the new insertion point
def add_code( instruction )
raise "Not an instruction:#{instruction.to_s}:#{instruction.class.name}" unless instruction.is_a?(Mom::Instruction)
new_current = instruction.last #after insertion this point is lost
@current.insert(instruction) #insert after current
@current = new_current
self
end
# return the frame type, ie the blocks self_type
def receiver_type
@callable.self_type
end
private
# convert al instruction to risc
# method is called by Method/BlockCompiler from to_risc
def instructions_to_risc(risc_compiler)
instruction = mom_instructions.next
while( instruction )
raise "whats this a #{instruction}" unless instruction.is_a?(Mom::Instruction)
#puts "adding mom #{instruction.to_s}:#{instruction.next.to_s}"
risc_compiler.reset_regs
instruction.to_risc( risc_compiler )
#puts "adding risc #{risc.to_s}:#{risc.next.to_s}"
instruction = instruction.next
end
end
end
end

View File

@ -0,0 +1,55 @@
module Mom
# Base class for MOM instructions
# At the base class level instructions are a linked list.
#
# Mom::Instructions are created by the Vool level as an intermediate step
# towards the next level down, the Risc level.
# Mom and Risc are both abstract machines (ie have instructions), so both
# share the linked list functionality (In Util::List)
#
# To convert a Mom instruction to it's Risc equivalent to_risc is called
#
class Instruction
include Util::List
def initialize( source , nekst = nil )
@source = source
@next = nekst
return unless source
unless source.is_a?(String) or
source.is_a?(Vool::Statement)
raise "Source must be string or Instruction, not #{source.class}"
end
end
attr_reader :source
# to_risc, like the name says, converts the instruction to it's Risc equivalent.
# The Risc machine is basically a simple register machine (kind of arm).
# In other words Mom is the higher abstraction and so mom instructions convert
# to many (1-10) risc instructions
#
# The argument that is passed is a MethodCompiler, which has the method and some
# state about registers used. (also provides helpers to generate risc instructions)
def to_risc(compiler)
raise self.class.name + "_todo"
end
end
end
require_relative "instruction/label"
require_relative "instruction/check"
require_relative "instruction/basic_values"
require_relative "instruction/simple_call"
require_relative "instruction/dynamic_call"
require_relative "instruction/block_yield"
require_relative "instruction/resolve_method"
require_relative "instruction/truth_check"
require_relative "instruction/not_same_check"
require_relative "instruction/jump"
require_relative "instruction/return_jump"
require_relative "instruction/slot_load"
require_relative "instruction/return_sequence"
require_relative "instruction/message_setup"
require_relative "instruction/argument_transfer"

View File

@ -0,0 +1,51 @@
module Mom
# Transering the arguments from the current frame into the next frame
#
# This could be _done_ at this level, and in fact used to be.
# The instruction was introduced to
# 1. make optimisations easier
# 2. localise the inevitable change
#
# 1. The optimal risc implementation for this loads old and new frames into registers
# and does a whole bunch of transfers
# But if we do individual SlotMoves here, each one has to load the frames,
# thus making advanced analysis/optimisation neccessary to achieve the same effect.
#
# 2. Closures will have to have access to variables after the frame goes out of scope
# and in fact be able to change the parents variables. The current design does not allow
# for this, and so will have to be change in the not so distant future.
#
class ArgumentTransfer < Instruction
attr_reader :receiver , :arguments
# receiver is a slot_definition
# arguments is an array of SlotLoads
def initialize( source , receiver,arguments )
super(source)
@receiver , @arguments = receiver , arguments
raise "Receiver not SlotDefinition #{@receiver}" unless @receiver.is_a?(SlotDefinition)
@arguments.each{|a| raise "args not SlotLoad #{a}" unless a.is_a?(SlotLoad)}
end
def to_s
"ArgumentTransfer " + ([@receiver] + @arguments).join(",")
end
# load receiver and then each arg into the new message
# delegates to SlotLoad for receiver and to the actual args.to_risc
def to_risc(compiler)
transfer = SlotLoad.new(self.source ,[:message , :next_message , :receiver] , @receiver, self).to_risc(compiler)
#TODO transfer the Number of arguments to :arguments_given (to be checked on entry)
compiler.reset_regs
@arguments.each do |arg|
arg.to_risc(compiler)
compiler.reset_regs
end
transfer
end
end
end

View File

@ -0,0 +1,85 @@
module Mom
# just name scoping the same stuff to mom
# so we know we are on the way down, keeping our layers seperated
# and we can put constant adding into the to_risc methods (instead of on vool classes)
class Constant
end
class LambdaConstant < Constant
attr_reader :lambda
def initialize(bl)
@lambda = bl
end
def to_parfait(compiler)
@lambda
end
end
class IntegerConstant < Constant
attr_reader :value
def initialize(value)
@value = value
end
def to_parfait(compiler)
value = Parfait.object_space.get_factory_for(:Integer).get_next_object
value.set_value(@value)
compiler.add_constant(value)
value
end
def ct_type
Parfait.object_space.get_type_by_class_name(:Integer)
end
end
class FloatConstant < Constant
attr_reader :value
def initialize(value)
@value = value
end
def ct_type
true
end
end
class TrueConstant < Constant
def to_parfait(compiler)
Parfait.object_space.true_object
end
def ct_type
Parfait.object_space.get_type_by_class_name(:TrueClass)
end
end
class FalseConstant < Constant
def to_parfait(compiler)
Parfait.object_space.false_object
end
def ct_type
Parfait.object_space.get_type_by_class_name(:FalseClass)
end
end
class NilConstant < Constant
def to_parfait(compiler)
Parfait.object_space.nil_object
end
def ct_type
Parfait.object_space.get_type_by_class_name(:NilClass)
end
end
class StringConstant < Constant
attr_reader :value
def initialize(value)
@value = value
end
def to_parfait(compiler)
value = Parfait.new_word(@value)
compiler.add_constant(value)
value
end
def ct_type
Parfait.object_space.get_type_by_class_name(:Word)
end
end
class SymbolConstant < Constant
def ct_type
Parfait.object_space.get_type_by_class_name(:Word)
end
end
end

View File

@ -0,0 +1,38 @@
module Mom
# A BlockYield calls an argument block. All we need to know is the index
# of the argument, and the rest is almost as simple as a SimpleCall
class BlockYield < Instruction
attr :arg_index
# pass in the source (vool statement) and the index.
# The index is the argument index of the block that we call
def initialize(source , index)
super(source)
@arg_index = index
end
def to_s
"BlockYield[#{arg_index}] "
end
# almost as simple as a SimpleCall, use a dynamic_jump to get there
def to_risc(compiler)
return_label = Risc.label("block_yield", "continue_#{object_id}")
index = arg_index
compiler.build("BlockYield") do
next_message! << message[:next_message]
return_address! << return_label
next_message[:return_address] << return_address
block_reg! << message["arg#{index}".to_sym]
message << message[:next_message]
add_code Risc::DynamicJump.new("block_yield", block_reg )
add_code return_label
end
end
end
end

View File

@ -0,0 +1,21 @@
module Mom
# A base class for conditions in MOM
# Checks (if in code, compare in assm) jump or not, depending
# The logic we choose is closer to the code logic (the asm is reversed)
# When we write an if, the true is the next code, so the Check logic is
# that if the check passes, no jump happens
# This means you need to pass the false label, where to jump to if the
# check does not pass
# Note: In assembler a branch on 0 does just that, it branches if the condition
# is met. This means that the asm implementation is somewhat the reverse
# of the Mom names. But it's easier to understand (imho)
class Check < Instruction
attr_reader :false_jump
def initialize(false_jump)
@false_jump = false_jump
raise "Jump target must be a label #{false_jump}" unless false_jump.is_a?(Label)
end
end
end

View File

@ -0,0 +1,50 @@
module Mom
# A dynamic call calls a method at runtime. This off course implies that we don't know the
# method at compile time and so must "find" it. Resolving, or finding the method, is a
# a seperate instruction though, and here we assume that we know this Method instance.
#
# Both (to be called) Method instance and the type of receiver are stored as
# variables here. The type is used to check before calling.
#
# Setting up the method is not part of this instructions scope. That setup
# includes the type check and any necccessay method resolution.
# See vool send statement
#
class DynamicCall < Instruction
attr :cache_entry
def initialize(type = nil, method = nil)
@cache_entry = Parfait::CacheEntry.new(type, method)
end
def to_s
str = "DynamicCall "
str += cache_entry.cached_method&.name if cache_entry
str
end
# One could almost think that one can resolve this to a Risc::FunctionCall
# (which btw resolves to a simple jump), alas, the FunctionCall, like all other
# jumping, resolves the address at compile time.
#
# Instead we need a DynamicJump instruction that explicitly takes a register as
# a target (not a label)
def to_risc(compiler)
entry = @cache_entry
compiler.add_constant( entry )
return_label = Risc.label(self, "continue_#{object_id}")
compiler.build("DynamicCall") do
return_address! << return_label
next_message! << message[:next_message]
next_message[:return_address] << return_address
message << message[:next_message]
cache_entry! << entry
cache_entry << cache_entry[:cached_method]
add_code Risc::DynamicJump.new("DynamicCall", cache_entry )
add_code return_label
end
end
end
end

View File

@ -0,0 +1,20 @@
module Mom
# Branch jump to the Label given
# Eg used at the end of while or end of if_true branch
#
# Risc equivalent is the same really, called Branch there.
#
class Jump < Instruction
attr_reader :label
def initialize(label)
@label = label
end
def to_risc(compiler)
compiler.add_code Risc::Branch.new(self , @label.risc_label(compiler))
end
end
end

View File

@ -0,0 +1,48 @@
module Mom
# A Label is the only legal target for a branch (in Mom, in Risc a BinaryCode is ok too)
#
# In the dynamic view (runtime) where the instructions form a graph,
# branches fan out, Labels collect. In other words a branch is the place where
# several roads lead off, and a Label where several roads arrive.
#
# A Label has a name which is mainly used for debugging.
#
# A Mom::Label converts one2one to a Risc::Label. So in a way it could not be more
# simple.
# Alas, since almost by definition several roads lead to this label, all those
# several converted instructions must also point to the identical label on the
# risc level.
#
# This is achieved by caching the created Risc::Label in an instance variable.
# All branches that lead to this label can thus safely call the to_risc and
# whoever calls first triggers the labels creation, but all get the same label.
#
# Off course some specific place still has to be responsible for actually
# adding the label to the instruction list (usually an if/while)
class Label < Instruction
attr_reader :name
def initialize(source , name)
super(source)
@name = name
end
def to_s
"Label: #{name}"
end
# generate the risc label lazily
def risc_label(compiler)
@risc_label ||= Risc.label(self,name)
compiler.add_constant(@risc_label.address)
@risc_label
end
# add the risc_label to the compiler (instruction flow)
# should only be called once
def to_risc(compiler)
compiler.add_code( risc_label(compiler) )
end
end
end

View File

@ -0,0 +1,73 @@
module Mom
# As reminder: a statically resolved call (the simplest one) becomes three Mom Instructions.
# Ie: MessageSetup,ArgumentTransfer,SimpleCall
#
# MessageSetup does Setup before a call can be made, acquiring and filling the message
# basically. Only after MessageSetup is the next_message safe to use.
#
# The Factory (instane kept by Space) keeps a linked list of Messages,
# from which we take and currenty also return.
#
# Message setup set the name to the called method's name, and also set the arg and local
# types on the new message, currently for debugging but later for dynamic checking
#
# The only difference between the setup of a static call and a dynamic one is where
# the method comes from. A static, or simple call, passes the method, but a dynamic
# call passes the cache_entry that holds the resolved method.
#
# In either case, the method is loaded and name,frame and args set
#
class MessageSetup < Instruction
attr_reader :method_source
def initialize(method_source)
raise "no nil source" unless method_source
@method_source = method_source
end
# Move method name, frame and arguemnt types from the method to the next_message
# Get the message from Space and link it.
def to_risc(compiler)
build_with(compiler.builder(self))
end
# directly called by to_risc
# but also used directly in __init
def build_with(builder)
case from = method_source
when Parfait::CallableMethod
builder.build { callable! << from }
when Parfait::CacheEntry
builder.build do
cache_entry! << from
callable! << cache_entry[:cached_method]
end
when Integer
builder.build do
callable! << message[ "arg#{from}".to_sym ]
end
else
raise "unknown source #{method_source.class}:#{method_source}"
end
build_message_data(builder)
return builder.built
end
private
def source
"method setup "
end
# set the method into the message
def build_message_data( builder )
if(reg = builder.names["next_message"])
raise "NEXT = #{reg}"
end
builder.build do
next_message! << message[:next_message]
next_message[:method] << callable
end
end
end
end

View File

@ -0,0 +1,32 @@
module Mom
# Mom internal check, as the name says to see if two values are not the same
# In other words, we this checks identity, bit-values, pointers
#
# The values that are compared are defined as SlotDefinitions, ie can be anything
# available to the machine through frame message or self
#
# Acording to Mom::Check logic, we jump to the given label is the values are the same
#
class NotSameCheck < Check
attr_reader :left , :right
def initialize(left, right , label)
super(label)
@left , @right = left , right
end
def to_s
"NotSameCheck: #{left}:#{right}"
end
# basically move both left and right values into register
# subtract them and see if IsZero comparison
def to_risc(compiler)
l_reg = left.to_register(compiler, self)
r_reg = right.to_register(compiler, self)
compiler.add_code Risc.op( self , :- , l_reg , r_reg)
compiler.add_code Risc::IsZero.new( self, false_jump.risc_label(compiler))
end
end
end

View File

@ -0,0 +1,73 @@
module Mom
# Dynamic method resolution is at the heart of a dynamic language, and here
# is the Mom level instruction to do it.
#
# When the static type can not be determined a CacheEntry is used to store
# type and method of the resolved method. The CacheEntry is shared with
# DynamicCall instruction who is responsible for calling the method in the entry.
#
# This instruction resolves the method, in case the types don't match (and
# at least on first encouter)
#
# This used to be a method, but we don't really need the method setup etc
#
class ResolveMethod < Instruction
attr :cache_entry , :name
# pass in source (VoolStatement)
# name of the method (don't knwow the actaual method)
# and the cache_entry
def initialize(source , name , cache_entry)
super(source)
@name = name
@cache_entry = cache_entry
end
def to_s
"ResolveMethod #{name}"
end
# When the method is resolved, a cache_entry is used to hold the result.
# That cache_entry (holding type and method) is checked before, and
# needs to be updated by this instruction.
#
# We use the type stored in the cache_entry to check the methods if any of it's
# names are the same as the given @name
#
# currently a fail results in sys exit
def to_risc( compiler )
name_ = @name
cache_entry_ = @cache_entry
builder = compiler.builder(self)
builder.build do
word! << name_
cache_entry! << cache_entry_
type! << cache_entry[:cached_type]
callable_method! << type[:methods]
add_code while_start_label
object! << Parfait.object_space.nil_object
object - callable_method
if_zero exit_label
name! << callable_method[:name]
name - word
if_zero ok_label
callable_method << callable_method[:next_callable]
branch while_start_label
add_code exit_label
MethodMissing.new(compiler.source_name , word.symbol).to_risc(compiler)
add_code ok_label
cache_entry[:cached_method] << callable_method
end
end
end
end

View File

@ -0,0 +1,32 @@
module Mom
# the return jump jumps to the return label
# the method setup is such that there is exactly one return_label in a method
# This is so the actual code that executes the return can be quite complicated
# and big, and won't be repeated
#
class ReturnJump < Instruction
attr_reader :return_label
# pass in the source_name (string/vool_instruction) for accounting purposes
# and the return_label, where we actually jump to. This is set up by the
# method_compiler, so it is easy to find (see return_label in compiler)
def initialize( source , label )
super(source)
@return_label = label
end
# the jump quite simple resolves to an uncondition risc Branch
# we use the label that is passed in at creation
def to_risc(compiler)
compiler.add_code Risc::Branch.new(self , return_label.risc_label(compiler))
end
def to_s
"ReturnJump: #{return_label}"
end
end
end

View File

@ -0,0 +1,43 @@
module Mom
# The ReturnSequence models the return from a method.
#
# This involves the jump to the return address stored in the message, and
# the reinstantiation of the previous message.
#
# The machine (mom) only ever "knows" one message, the current message.
# Messages are a double linked list, calling involves going forward,
# returning means going back.
#
# The return value of the current message is transferred into the return value of the
# callers return value during the swap of messages, and just before the jump.
#
# The callers perspective of a call is the magical apperance of a return_value
# in it's message at the instruction after the call.
#
# The instruction is not parameterized as it translates to a constant
# set of lower level instructions.
#
class ReturnSequence < Instruction
def to_risc(compiler)
compiler.reset_regs
builder = compiler.builder(self)
builder.build do
object! << message[:return_value]
caller_reg! << message[:caller]
caller_reg[:return_value] << object
end
builder.build do
return_address! << message[:return_address]
return_address << return_address[ Parfait::Integer.integer_index]
message << message[:caller]
add_code Risc.function_return("return #{compiler.callable.name}", return_address)
end
end
def to_s
"ReturnSequence"
end
end
end

View File

@ -0,0 +1,41 @@
module Mom
# A SimpleCall is just that, a simple call. This could be called a function call too,
# meaning we managed to resolve the function at compile time and all we have to do is
# actually call it.
#
# As the call setup is done beforehand (for both simple and cached call), the
# calling really means mostly jumping to the address. Simple.
#
class SimpleCall < Instruction
attr_reader :method
def initialize(method)
@method = method
end
def to_s
"SimpleCall #{@method.name}"
end
# Calling a Method is basically jumping to the Binary (+ offset).
# We just swap in the new message and go.
#
# For returning, we add a label after the call, and load it's address into the
# return_address of the next_message, for the ReturnSequence to pick it up.
def to_risc(compiler)
method = @method
return_label = Risc.label(self,"continue_#{object_id}")
compiler.build("SimpleCall") do
return_address! << return_label
next_message! << message[:next_message]
next_message[:return_address] << return_address
message << message[:next_message]
add_code Risc::FunctionCall.new("SimpleCall", method )
add_code return_label
end
end
end
end

View File

@ -0,0 +1,110 @@
module Mom
# A SlotDefinition defines a slot. A bit like a variable name but for objects.
#
# PS: for the interested: A "developement" of Smalltalk was the
# prototype based language (read: JavaScript equivalent)
# called Self https://en.wikipedia.org/wiki/Self_(programming_language)
#
# SlotDefinitions are the instance names of objects. But since the language is dynamic
# what is it that we can say about instance names at runtime?
# Start with a known object like the Message (in register one), we know all it's
# variables. But there is a Message in there, and for that we know the instances
# too. And off course for _all_ objects we know where the type is.
#
# The definiion is an array of symbols that we can resolve to SlotLoad
# Instructions. Or in the case of constants to ConstantLoad
#
class SlotDefinition
attr_reader :known_object , :slots
# is an array of symbols, that specifies the first the object, and then the Slot.
# The first element is either a known type name (Capitalized symbol of the class name) ,
# or the symbol :message
# And subsequent symbols must be instance variables on the previous type.
# Examples: [:message , :receiver] or [:Space , :next_message]
def initialize( object , slots)
raise "No slots #{object}" unless slots
slots = [slots] unless slots.is_a?(Array)
@known_object , @slots = object , slots
raise "Not known #{slots}" unless object
end
def to_s
names = [known_name] + @slots
"[#{names.join(', ')}]"
end
def known_name
case known_object
when Constant , Parfait::Object
known_object.class.short_name
when Risc::Label
known_object.to_s
when Symbol
known_object
else
"unknown"
end
end
# load the slots into a register
# the code is added to compiler
# the register returned
def to_register(compiler, source)
if known_object.respond_to?(:ct_type)
type = known_object.ct_type
elsif(known_object.respond_to?(:get_type))
type = known_object.get_type
else
type = :Object
end
right = compiler.use_reg( type )
case known_object
when Constant
parfait = known_object.to_parfait(compiler)
const = Risc.load_constant(source, parfait , right)
compiler.add_code const
if slots.length == 1
raise "only type allowed for constants, not #{slots[0]}" unless slots[0] == :type
compiler.add_code Risc::SlotToReg.new( source , right , Parfait::TYPE_INDEX, right)
end
raise "Can't have slots into Constants #{slots}" if slots.length > 1
when Parfait::Object , Risc::Label
const = const = Risc.load_constant(source, known_object , right)
compiler.add_code const
if slots.length > 0
# desctructively replace the existing value to be loaded if more slots
compiler.add_code Risc.slot_to_reg( source , right ,slots[0], right)
end
when Symbol
return sym_to_risc(compiler , source)
else
raise "We have a #{self} #{known_object}"
end
if slots.length > 1
# desctructively replace the existing value to be loaded if more slots
index = Risc.resolve_to_index(slots[0] , slots[1] ,compiler)
compiler.add_code Risc::SlotToReg.new( source , right ,index, right)
if slots.length > 2
raise "3 slots only for type #{slots}" unless slots[2] == :type
compiler.add_code Risc::SlotToReg.new( source , right , Parfait::TYPE_INDEX, right)
end
end
return const.register
end
# resolve the slots one by one to slot_to_reg instructions using the
# type information inferred from their names / type hierachy
def sym_to_risc(compiler , source)
slots = @slots.dup
raise "Not Message #{@known_object}" unless @known_object == :message
left = Risc.message_reg
left = left.resolve_and_add( slots.shift , compiler)
reg = compiler.current.register
while( !slots.empty? )
left = left.resolve_and_add( slots.shift , compiler)
end
return reg
end
end
end

View File

@ -0,0 +1,83 @@
module Mom
# SlotLoad is for moving data into a slot, either from another slot, or constant
# A Slot is basically an instance variable, but it must be of known type
#
# The value loaded (the right hand side) can be a constant (Mom::Constant) or come from
# another Slot (SlotDefinition)
#
# The Slot on the left hand side is always a SlotDefinition.
# The only known object (*) for the left side is the current message, which is a bit like
# the oo version of a Stack (Stack Register, Frame Pointer, ..)
# (* off course all class objects are global, and so they are allowed too)
#
# A maybe not immediately obvious corrolar of this design is the total absence of
# general external instance variable accessors. Ie only inside an object's functions
# can a method access instance variables, because only inside the method is the type
# guaranteed.
# From the outside a send is neccessary, both for get and set, (which goes through the method
# resolution and guarantees the correct method for a type), in other words perfect data hiding.
#
# @left: A SlotDefinition, or an array that can be passed to the constructor of the
# SlotDefinition (see there)
#
# @right: A SlotDefinition with slots or a Mom::Constant
# original_source: optinally another mom instruction that will be passed down to created
# risc instructions. (Because SlotLoad is often used internally in mom)
class SlotLoad < Instruction
attr_reader :left , :right , :original_source
def initialize(source , left , right, original_source = nil)
super(source)
@left , @right = left , right
@left = SlotDefinition.new(@left.shift , @left) if @left.is_a? Array
@right = SlotDefinition.new(@right.shift , @right) if @right.is_a? Array
raise "right not Mom, #{@right.to_s}" unless @right.is_a?( SlotDefinition )
@original_source = original_source || self
end
def to_s
"SlotLoad #{right} -> #{left}"
end
# resolve the SlotLoad to the respective risc Instructions.
# calls sym_to_risc for most (symbols), and ConstantLoad for CacheEntry
# after loading the right into register
def to_risc(compiler)
const_reg = @right.to_register(compiler , original_source)
left_slots = @left.slots
case @left.known_object
when Symbol
sym_to_risc(compiler , const_reg)
when Parfait::CacheEntry
left = compiler.use_reg( :CacheEntry )
compiler.add_code Risc.load_constant(original_source, @left.known_object , left)
compiler.add_code Risc.reg_to_slot(original_source, const_reg , left, left_slots.first)
else
raise "We have left #{@left.known_object}"
end
compiler.reset_regs
end
# load the data in const_reg into the slot that is named by left symbols
# left may usually be only 3 long, as the first is known, then the second is loaded
# with type known type (as it comes from message)
#
# actual lifting is done by RegisterValue resolve_and_add
def sym_to_risc(compiler , const_reg)
left_slots = @left.slots.dup
raise "Not Message #{object}" unless @left.known_object == :message
left = Risc.message_reg
slot = left_slots.shift
while( !left_slots.empty? )
left = left.resolve_and_add( slot , compiler)
slot = left_slots.shift
end
compiler.add_code Risc.reg_to_slot(original_source, const_reg , left, slot)
end
end
end
require_relative "slot_definition"

View File

@ -0,0 +1,36 @@
module Mom
# The funny thing about the ruby truth is that it is anything but false or nil
#
# To implement the normal ruby logic, we check for false or nil and jump
# to the false branch. true_block follows implicitly
#
class TruthCheck < Check
attr_reader :condition
def initialize(condition , false_jump)
super(false_jump)
@condition = condition
raise "condition must be slot_definition #{condition}" unless condition.is_a?(SlotDefinition)
end
def to_s
"TruthCheck #{@condition} -> #{false_jump}"
end
def to_risc(compiler)
false_label = @false_jump.risc_label(compiler)
builder = compiler.builder("TruthCheck")
condition_reg = @condition.to_register(compiler,self)
builder.build do
object! << Parfait.object_space.false_object
object.op :- , condition_reg
if_zero false_label
object << Parfait.object_space.nil_object
object.op :- , condition_reg
if_zero false_label
end
end
end
end

View File

@ -0,0 +1,19 @@
## Builtin module
The Builtin module contains functions that can not be coded in ruby.
It is the other side of the parfait coin, part of the runtime.
The functions are organised by their respective classes and get loaded in boot_classes! ,
right at the start. (see register/boot.rb)
These functions return their code, ie a Parfait::CallableMethod with a MethodSource object,
which can then be called by ruby code as if it were a "normal" function.
A normal ruby function is one that is parsed and transformed to code. But not all
functionality can be written in ruby, one of those chicken and egg things.
C uses Assembler in this situation, we use Builtin functions.
Slightly more here : http://ruby-x.org/2014/06/10/more-clarity.html (then still called Kernel)
The Builtin module is scattered into several files, but that is just so the file
doesn't get too long.

View File

@ -0,0 +1,30 @@
module Mom
class Comparison < Macro
attr_reader :operator
def initialize(name , operator)
super(name)
@operator = operator.value
end
def to_risc(compiler)
builder = compiler.builder(compiler.source)
operator = @operator # make accessible in block
builder.build do
integer! << message[:receiver]
integer.reduce_int
integer_reg! << message[:arg1] #"other"
integer_reg.reduce_int
swap_names(:integer , :integer_reg) if(operator.to_s.start_with?('<') )
integer.op :- , integer_reg
if_minus false_label
if_zero( false_label ) if operator.to_s.length == 1
object! << Parfait.object_space.true_object
branch merge_label
add_code false_label
object << Parfait.object_space.false_object
add_code merge_label
message[:return_value] << object
end
return compiler
end
end
end

View File

@ -0,0 +1,63 @@
module Mom
class Div10 < Macro
def to_risc(compiler)
s = "div_10 "
builder = compiler.builder(compiler.source)
integer_tmp = builder.allocate_int
builder.build do
integer_self! << message[:receiver]
integer_self.reduce_int
integer_1! << integer_self
integer_reg! << integer_self
integer_const! << 1
integer_1.op :>> , integer_const
integer_const << 2
integer_reg.op :>> , integer_const
integer_reg.op :+ , integer_1
integer_const << 4
integer_1 << integer_reg
integer_reg.op :>> , integer_1
integer_reg.op :+ , integer_1
integer_const << 8
integer_1 << integer_reg
integer_1.op :>> , integer_const
integer_reg.op :+ , integer_1
integer_const << 16
integer_1 << integer_reg
integer_1.op :>> , integer_const
integer_reg.op :+ , integer_1
integer_const << 3
integer_reg.op :>> , integer_const
integer_const << 10
integer_1 << integer_reg
integer_1.op :* , integer_const
integer_self.op :- , integer_1
integer_1 << integer_self
integer_const << 6
integer_1.op :+ , integer_const
integer_const << 4
integer_1.op :>> , integer_const
integer_reg.op :+ , integer_1
integer_tmp[Parfait::Integer.integer_index] << integer_reg
message[:return_value] << integer_tmp
end
return compiler
end
end
end

View File

@ -0,0 +1,17 @@
module Mom
class Div4 < Macro
def to_risc(compiler)
builder = compiler.builder(compiler.source)
integer_tmp = builder.allocate_int
builder.build do
integer_self! << message[:receiver]
integer_self.reduce_int
integer_1! << 2
integer_self.op :>> , integer_1
integer_tmp[Parfait::Integer.integer_index] << integer_self
message[:return_value] << integer_tmp
end
return compiler
end
end
end

View File

@ -0,0 +1,10 @@
module Mom
class Exit < Macro
def to_risc(compiler)
builder = compiler.builder(compiler.source)
builder.prepare_int_return # makes integer_tmp variable as return
Macro.exit_sequence(builder)
return compiler
end
end
end

View File

@ -0,0 +1,17 @@
module Mom
class GetInternalByte < Macro
def to_risc(compiler)
builder = compiler.builder(compiler.source)
integer_tmp = builder.allocate_int
builder.build do
object! << message[:receiver]
integer! << message[:arg1] #"at"
integer.reduce_int
object <= object[integer]
integer_tmp[Parfait::Integer.integer_index] << object
message[:return_value] << integer_tmp
end
return compiler
end
end
end

View File

@ -0,0 +1,13 @@
module Mom
class GetInternalWord < Macro
def to_risc(compiler)
compiler.builder(compiler.source).build do
object! << message[:receiver]
integer! << message[:arg1] #"at" is at index 0
integer.reduce_int
object << object[integer]
message[:return_value] << object
end
end
end
end

View File

@ -0,0 +1,43 @@
module Mom
# Init "method" is the first thing that happens in the machine
# There is an inital jump to it, but that's it, no setup, no nothing
#
# The method is in quotes, because it is not really a method, it does not return!!
# This is common to all double underscore "methods", but __init also does not
# rely on the message. In fact it's job is to set up the first message
# and to call the main (possibly later _init_ , single undescrore)
#
class Init < Macro
def to_risc(compiler)
builder = compiler.builder(compiler.source)
main = Parfait.object_space.get_method!(:Space, :main)
# Set up the first message, but advance one, so main has somewhere to return to
builder.build do
factory! << Parfait.object_space.get_factory_for(:Message)
message << factory[:next_object]
next_message! << message[:next_message]
factory[:next_object] << next_message
end
builder.reset_names
# Set up the call to main, with space as receiver
Mom::MessageSetup.new(main).build_with( builder )
builder.build do
message << message[:next_message]
space? << Parfait.object_space
message[:receiver] << space
end
# set up return address and jump to main
exit_label = Risc.label(compiler.source , "#{compiler.receiver_type.object_class.name}.#{compiler.source.name}" )
ret_tmp = compiler.use_reg(:Label).set_builder(builder)
builder.build do
ret_tmp << exit_label
message[:return_address] << ret_tmp
add_code Risc.function_call( "__init__ issue call" , main)
add_code exit_label
end
compiler.reset_regs
Macro.exit_sequence(builder) # exit will use mains return_value as exit_code
return compiler
end
end
end

View File

@ -0,0 +1,67 @@
module Mom
class Macro < Instruction
def to_s
self.class.name.split("::").last
end
# emit the syscall with given name
# there is a Syscall instruction, but the message has to be saved and restored
def self.emit_syscall( builder , name )
save_message( builder )
builder.add_code Risc::Syscall.new("emit_syscall(#{name})", name )
restore_message(builder)
return unless (@clazz and @method)
builder.add_code Risc.label( "#{@clazz.name}.#{@message.name}" , "return_syscall" )
end
# a sort of inline version of exit method.
# Used by exit and __init__ (so it doesn't have to call it)
# Assumes int return value and extracts the fixnum for process exit code
def self.exit_sequence(builder)
save_message( builder )
builder.build do
message << message[:return_value]
message.reduce_int
add_code Risc::Syscall.new("emit_syscall(exit)", :exit )
end
end
# save the current message, as the syscall destroys all context
#
# This relies on linux to save and restore all registers
#
def self.save_message(builder)
r8 = Risc::RegisterValue.new( :r8 , :Message).set_builder(builder)
builder.build {r8 << message}
end
# restore the message that we save in r8
# before th restore, the syscall return, a fixnum, is saved
# The caller of this method is assumed to caal prepare_int_return
# so that the return value already has an integer instance
# This instance is filled with os return value
def self.restore_message(builder)
r8 = Risc::RegisterValue.new( :r8 , :Message)
builder.build do
integer_reg! << message
message << r8
integer_2! << message[:return_value]
integer_2[Parfait::Integer.integer_index] << integer_reg
end
end
end
end
require_relative "comparison"
require_relative "exit"
require_relative "init"
require_relative "putstring"
require_relative "set_internal_word"
require_relative "div10"
require_relative "get_internal_byte"
require_relative "method_missing"
require_relative "div4"
require_relative "get_internal_word"
require_relative "operator"
require_relative "set_internal_byte"

View File

@ -0,0 +1,21 @@
module Mom
class MethodMissing < Macro
attr_reader :name
def initialize( source , name )
super(source)
name = name.value if name.is_a?(Vool::SymbolConstant)
raise "No reg #{name.class}" unless name.class == Symbol
@name = name
end
def to_risc(compiler)
builder = compiler.builder(compiler.source_name)
from = Risc::RegisterValue.new(@name , :Word)
to = Risc::RegisterValue.new(:r1 , :Word)
builder.add_code Risc::Transfer.new(self , from , to)
builder.add_code Risc::Syscall.new(self, :died )
return compiler
end
end
end

View File

@ -0,0 +1,3 @@
module Mom
end

View File

@ -0,0 +1,25 @@
module Mom
class IntOperator < Macro
attr_reader :operator
def initialize(name , operator)
super(name)
@operator = operator.value
end
def to_risc(compiler)
builder = compiler.builder(compiler.source)
integer_tmp = builder.allocate_int
operator = @operator # make accessible in block
builder.build do
integer! << message[:receiver]
integer.reduce_int
integer_reg! << message[:arg1] #"other"
integer_reg.reduce_int
integer.op operator , integer_reg
integer_tmp[Parfait::Integer.integer_index] << integer
message[:return_value] << integer_tmp
end
return compiler
end
end
end

View File

@ -0,0 +1,14 @@
module Mom
class Putstring < Macro
def to_risc(compiler)
builder = compiler.builder(compiler.source)
builder.prepare_int_return # makes integer_tmp variable as return
builder.build do
word! << message[:receiver]
integer! << word[Parfait::Word.get_length_index]
end
Mom::Macro.emit_syscall( builder , :putstring )
compiler
end
end
end

View File

@ -0,0 +1,16 @@
module Mom
class SetInternalByte < Macro
def to_risc(compiler)
compiler.builder(compiler.source).build do
word! << message[:receiver]
integer_reg! << message[:arg2] #VALUE
message[:return_value] << integer_reg
integer! << message[:arg1] #"index"
integer.reduce_int
integer_reg.reduce_int
word[integer] <= integer_reg
end
return compiler
end
end
end

View File

@ -0,0 +1,15 @@
module Mom
class SetInternalWord < Macro
def to_risc(compiler)
compiler.builder(compiler.source).build do
object! << message[:receiver]
integer! << message[:arg1] # "index"
object_reg! << message[:arg2]#"value"
integer.reduce_int
object[integer] << object_reg
message[:return_value] << object_reg
end
return compiler
end
end
end

View File

@ -0,0 +1,91 @@
module Mom
# MethodCompiler is used to generate Mom instructions for methods
# and to instantiate the methods correctly.
class MethodCompiler < CallableCompiler
def initialize( method )
super(method)
end
def source_name
"#{@callable.self_type.name}.#{@callable.name}"
end
def get_method
@callable
end
# sometimes the method is used as source (tb reviewed)
def source
@callable
end
# drop down to risc by converting this compilers instructions to risc.
# and the doing the same for any block_compilers
def to_risc
risc_compiler = Risc::MethodCompiler.new(@callable , mom_instructions)
instructions_to_risc(risc_compiler)
risc_compiler
end
# helper method for builtin mainly
# the class_name is a symbol, which is resolved to the instance_type of that class
#
# return compiler_for_type with the resolved type
#
def self.compiler_for_class( class_name , method_name , args , frame )
raise "create_method #{class_name}.#{class_name.class}" unless class_name.is_a? Symbol
clazz = Parfait.object_space.get_class_by_name! class_name
compiler_for_type( clazz.instance_type , method_name , args , frame)
end
def add_method_to( target )
target.add_method( @callable )
end
def create_block(arg_type , frame_type)
@callable.create_block(arg_type ,frame_type)
end
# create a method for the given type ( Parfait type object)
# method_name is a Symbol
# args a hash that will be converted to a type
# the created method is set as the current and the given type too
# return the compiler
def self.compiler_for_type( type , method_name , args , frame)
raise "create_method #{type.inspect} is not a Type" unless type.is_a? Parfait::Type
raise "Args must be Type #{args}" unless args.is_a?(Parfait::Type)
raise "create_method #{method_name}.#{method_name.class}" unless method_name.is_a? Symbol
method = type.create_method( method_name , args , frame)
self.new(method)
end
# determine how given name need to be accsessed.
# For methods the options are args or frame
def slot_type_for(name)
if index = @callable.arguments_type.variable_index(name)
return ["arg#{index}".to_sym]
end
index = @callable.frame_type.variable_index(name)
raise "no such local or argument #{name} for #{callable.name}:#{callable.frame_type.hash}" unless index
return ["local#{index}".to_sym]
end
# return true or false if the given name is in scope (arg/local)
def in_scope?(name)
ret = true if @callable.arguments_type.variable_index(name)
ret = @callable.frame_type.variable_index(name) unless ret
ret
end
# Only for init, as init has no return
# kind of private
def _reset_for_init
@mom_instructions = Label.new(source_name, source_name)
@current = @mom_instructions
end
end
end

View File

@ -0,0 +1,82 @@
module SlotMachine
# The Compiler/Collection for the SlotMachine level is a collection of SlotMachine level Method
# compilers These will transform to Risc MethodCompilers on the way down.
#
# As RubyCompiler pools source at the vool level, when several classes are compiled
# from vool to mom, several SlotMachineCompilers get instantiated. They must be merged before
# proceeding with translate. Thus we have a append method.
#
class SlotCollection
attr_reader :method_compilers
# Initialize with an array of risc MethodCompilers
def initialize(compilers = [])
@method_compilers = nil
compilers.each{|c| add_compiler(c)}
end
# lazily instantiate the compiler for __init__ function and __method_missing__
def init_compilers
return if @init_compilers
@init_compilers = true
add_compiler SlotCollection.create_init_compiler
add_compiler SlotCollection.create_mm_compiler
self
end
# Return all compilers, namely the MethodCompilers instanc,
# plus the init_compilers
def compilers
init_compilers
@method_compilers
end
def add_compiler(compiler)
if(@method_compilers)
@method_compilers.add_method_compiler(compiler)
else
@method_compilers = compiler
end
self
end
# Append another SlotMachineCompilers method_compilers to this one.
def append(collection)
@method_compilers.add_method_compiler( collection.method_compilers)
self
end
def to_risc( )
init_compilers
riscs =[]
@method_compilers.each_compiler do | mom_c |
riscs << mom_c.to_risc
end
# to_risc all compilers
# for each suffling constnts and fist label, then all instructions (see below)
# then create risc collection
Risc::RiscCollection.new(riscs)
end
# See Init instruction. We must have an init (ie we need it in code), so it is created in code
def self.create_init_compiler
compiler = compiler_for(:Object,:__init__ ,{})
compiler._reset_for_init # no return, just for init
compiler.add_code Init.new("missing")
return compiler
end
def self.create_mm_compiler
compiler = compiler_for(:Object,:__method_missing__ ,{})
compiler.add_code MethodMissing.new("missing" , :r1)
return compiler
end
def self.compiler_for( clazz_name , method_name , arguments , locals = {})
frame = Parfait::Type.for_hash( locals )
args = Parfait::Type.for_hash( arguments )
MethodCompiler.compiler_for_class(clazz_name , method_name , args, frame )
end
end
end

View File

@ -0,0 +1,20 @@
# The *essential* step from vool to risc, is the one from a language to a machine.
# From vools statements that hang in the air, to an instruction set.
#
# ### List based: Bit like Risc, just no registers
#
# ### Use object memory : object to object transfer + no registers
#
# ### Instruction based
#
# So a machine rather than a language. No control structures, but compare and jump
# instructions. No send or dynamic call, just objects and jump.
# Machine capabilities (instructions) for basic operations.
# Use of macros for higher level.
require_relative "instruction.rb"
require_relative "slot_collection"
require_relative "callable_compiler"
require_relative "method_compiler"
require_relative "block_compiler"
require_relative "macro/macro"