Change Mom to SlotMachine
rather large commit, but essentially a simple rename Rationale in docs and blogs
This commit is contained in:
58
lib/slot_machine/README.md
Normal file
58
lib/slot_machine/README.md
Normal 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.
|
46
lib/slot_machine/block_compiler.rb
Normal file
46
lib/slot_machine/block_compiler.rb
Normal 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
|
75
lib/slot_machine/callable_compiler.rb
Normal file
75
lib/slot_machine/callable_compiler.rb
Normal 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
|
55
lib/slot_machine/instruction.rb
Normal file
55
lib/slot_machine/instruction.rb
Normal 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"
|
51
lib/slot_machine/instruction/argument_transfer.rb
Normal file
51
lib/slot_machine/instruction/argument_transfer.rb
Normal 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
|
85
lib/slot_machine/instruction/basic_values.rb
Normal file
85
lib/slot_machine/instruction/basic_values.rb
Normal 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
|
38
lib/slot_machine/instruction/block_yield.rb
Normal file
38
lib/slot_machine/instruction/block_yield.rb
Normal 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
|
21
lib/slot_machine/instruction/check.rb
Normal file
21
lib/slot_machine/instruction/check.rb
Normal 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
|
50
lib/slot_machine/instruction/dynamic_call.rb
Normal file
50
lib/slot_machine/instruction/dynamic_call.rb
Normal 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
|
20
lib/slot_machine/instruction/jump.rb
Normal file
20
lib/slot_machine/instruction/jump.rb
Normal 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
|
48
lib/slot_machine/instruction/label.rb
Normal file
48
lib/slot_machine/instruction/label.rb
Normal 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
|
73
lib/slot_machine/instruction/message_setup.rb
Normal file
73
lib/slot_machine/instruction/message_setup.rb
Normal 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
|
32
lib/slot_machine/instruction/not_same_check.rb
Normal file
32
lib/slot_machine/instruction/not_same_check.rb
Normal 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
|
73
lib/slot_machine/instruction/resolve_method.rb
Normal file
73
lib/slot_machine/instruction/resolve_method.rb
Normal 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
|
32
lib/slot_machine/instruction/return_jump.rb
Normal file
32
lib/slot_machine/instruction/return_jump.rb
Normal 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
|
43
lib/slot_machine/instruction/return_sequence.rb
Normal file
43
lib/slot_machine/instruction/return_sequence.rb
Normal 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
|
41
lib/slot_machine/instruction/simple_call.rb
Normal file
41
lib/slot_machine/instruction/simple_call.rb
Normal 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
|
110
lib/slot_machine/instruction/slot_definition.rb
Normal file
110
lib/slot_machine/instruction/slot_definition.rb
Normal 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
|
83
lib/slot_machine/instruction/slot_load.rb
Normal file
83
lib/slot_machine/instruction/slot_load.rb
Normal 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"
|
36
lib/slot_machine/instruction/truth_check.rb
Normal file
36
lib/slot_machine/instruction/truth_check.rb
Normal 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
|
19
lib/slot_machine/macro/README.md
Normal file
19
lib/slot_machine/macro/README.md
Normal 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.
|
30
lib/slot_machine/macro/comparison.rb
Normal file
30
lib/slot_machine/macro/comparison.rb
Normal 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
|
63
lib/slot_machine/macro/div10.rb
Normal file
63
lib/slot_machine/macro/div10.rb
Normal 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
|
17
lib/slot_machine/macro/div4.rb
Normal file
17
lib/slot_machine/macro/div4.rb
Normal 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
|
10
lib/slot_machine/macro/exit.rb
Normal file
10
lib/slot_machine/macro/exit.rb
Normal 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
|
17
lib/slot_machine/macro/get_internal_byte.rb
Normal file
17
lib/slot_machine/macro/get_internal_byte.rb
Normal 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
|
13
lib/slot_machine/macro/get_internal_word.rb
Normal file
13
lib/slot_machine/macro/get_internal_word.rb
Normal 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
|
43
lib/slot_machine/macro/init.rb
Normal file
43
lib/slot_machine/macro/init.rb
Normal 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
|
67
lib/slot_machine/macro/macro.rb
Normal file
67
lib/slot_machine/macro/macro.rb
Normal 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"
|
21
lib/slot_machine/macro/method_missing.rb
Normal file
21
lib/slot_machine/macro/method_missing.rb
Normal 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
|
3
lib/slot_machine/macro/object.rb
Normal file
3
lib/slot_machine/macro/object.rb
Normal file
@ -0,0 +1,3 @@
|
||||
|
||||
module Mom
|
||||
end
|
25
lib/slot_machine/macro/operator.rb
Normal file
25
lib/slot_machine/macro/operator.rb
Normal 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
|
14
lib/slot_machine/macro/putstring.rb
Normal file
14
lib/slot_machine/macro/putstring.rb
Normal 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
|
16
lib/slot_machine/macro/set_internal_byte.rb
Normal file
16
lib/slot_machine/macro/set_internal_byte.rb
Normal 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
|
15
lib/slot_machine/macro/set_internal_word.rb
Normal file
15
lib/slot_machine/macro/set_internal_word.rb
Normal 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
|
91
lib/slot_machine/method_compiler.rb
Normal file
91
lib/slot_machine/method_compiler.rb
Normal 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
|
82
lib/slot_machine/slot_collection.rb
Normal file
82
lib/slot_machine/slot_collection.rb
Normal 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
|
20
lib/slot_machine/slot_machine.rb
Normal file
20
lib/slot_machine/slot_machine.rb
Normal 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"
|
Reference in New Issue
Block a user