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,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