Refactor various aspects of VM instructions

== Instruction memory layout

VM instructions now have a fixed size of 16 bytes, and no longer make
use of a separate heap-allocated Vec for their arguments. This reduces
memory usage, and should make for more cache-friendly instruction
handling.

Each instruction is limited to six arguments, which is enough for all
existing instructions. Instructions that need a variable number of
arguments, such as SetArray, make use of register ranges. Instead of
specifying all registers, they specify the first one and a length. The
compiler in turn makes sure all argument registers are in a contiguous
order. This approach is also taken by Lua, though unlike Lua we don't
require the arguments to come after the register containing the block
to run.

Some instructions supported optional arguments, such as SetObject. These
instructions have been modified to simply always require an argument.
This simplifies the VM code, and in almost all cases the arguments were
always specified anyway.

For Inko's test suite, these changes reduce peak RSS usage from 27 MB
down to 21 MB.

== MoveResult instruction

Values returned and thrown are handled differently. Instead of each
ExecutionContext storing the register (of the parent frame) to write
their result to, operations that return or throw a value now store the
value in a per-process "result" variable. The MoveResult instruction
moves this value into a register, setting the "result" variable to NULL.
This approach is inspired by the Dalvik VM, and simplifies instructions
such as Return and Throw.

== Single instruction for pinning processes

The instructions ProcessPinThread and ProcessUnpinThread have been
merged into a single ProcessSetPinned instruction. This instruction
behaves similar to ProcessSetBlocking.

== Removed instructions

The following VM instructions have been removed as they were not used:

* SetPrototype
* RemoveAttribute
* BlockSetReceiver

== Refactoring of instruction handlers

The functions used for handling instructions have been refactored,
renamed (after their instructions), and are now always inlined. This
looks a bit funny at the moment, but it should make it easier for a
future JIT to reuse these functions. The renaming also allows one to
import specific functions, without having to worry about generic names
such as "get" conflicting with other functions.

== Bytecode parser cleanup

The bytecode parser has been cleaned up a bit, and now limits various
data sequences to the maximum u16 value; instead of some arbitrarily
determined limit. The u16::MAX limit ensures that registers can address
the values directly.

== Argument changes

The VM no longer supports keyword arguments and rest arguments. Instead,
the compiler takes care of translating these to positional arguments.
Keyword arguments are translated to positional arguments, with
unspecified arguments being passed NULL pointers. The way this works is
straightforward:

1. Create an array with a NULL value/pointer for every _expected_
   argument. This is achieved by reserving register 0 and using that.

2. For every positional argument passed, fill its corresponding cell. So
   argument 1 fills cell 0, argument 2 fills cell 1, etc.

3. For keyword arguments, look up its argument position and set the
   corresponding cell.

4. Pass this array as arguments to the VM instruction.

This is best illustrated with a simple example:

    def foo(a = 1, b = 2, c = 3) {}

    foo(b: 10)

Here the arguments passed would be:

    [NULL, 10, NULL]

The VM then checks if `b` and `c` are set, sees they are NULL, and
assigns them their default values.

Validating of argument counts is also removed from the VM, now that
dynamic method calls are no longer supported.
parent a215a691
Pipeline #167356151 passed with stages
in 63 minutes and 42 seconds
......@@ -81,7 +81,6 @@ require 'inkoc/pass/define_type'
require 'inkoc/pass/define_type_signatures'
require 'inkoc/pass/validate_throw'
require 'inkoc/pass/implement_traits'
require 'inkoc/pass/optimize_keyword_arguments'
require 'inkoc/pass/generate_tir'
require 'inkoc/pass/define_module_type'
require 'inkoc/pass/code_generation'
......@@ -100,8 +99,8 @@ require 'inkoc/tir/instruction/ternary'
require 'inkoc/tir/instruction/quaternary'
require 'inkoc/tir/instruction/quinary'
require 'inkoc/tir/instruction/copy_blocks'
require 'inkoc/tir/instruction/drop'
require 'inkoc/tir/instruction/unary'
require 'inkoc/tir/instruction/simple'
require 'inkoc/tir/instruction/nullary'
require 'inkoc/tir/instruction/panic'
require 'inkoc/tir/instruction/exit'
......
......@@ -3,13 +3,12 @@
module Inkoc
module Codegen
class CatchEntry
attr_reader :start, :stop, :jump_to, :register
attr_reader :start, :stop, :jump_to
def initialize(start, stop, jump_to, register)
def initialize(start, stop, jump_to)
@start = start
@stop = stop
@jump_to = jump_to
@register = register
end
end
end
......
......@@ -7,14 +7,13 @@ module Inkoc
attr_reader :name, :instructions, :literals, :code_objects
attr_accessor :arguments, :required_arguments, :rest_argument, :locals,
attr_accessor :arguments, :required_arguments, :locals,
:registers, :captures, :catch_table
def initialize(name, location)
@name = name
@location = location
@arguments = []
@required_arguments = 0
@rest_argument = false
@locals = 0
@registers = 0
......
......@@ -68,7 +68,6 @@ module Inkoc
ModuleLoad
SetAttribute
GetAttribute
SetPrototype
GetPrototype
LocalExists
ProcessSpawn
......@@ -80,7 +79,6 @@ module Inkoc
ObjectEquals
GetNil
AttributeExists
RemoveAttribute
GetAttributeNames
TimeMonotonic
GetGlobal
......@@ -99,8 +97,8 @@ module Inkoc
FloatFloor
FloatCeil
FloatRound
Drop
SetBlocking
DropValue
ProcessSetBlocking
StdoutFlush
StderrFlush
FileRemove
......@@ -142,23 +140,21 @@ module Inkoc
EnvArguments
EnvRemove
BlockGetReceiver
BlockSetReceiver
RunBlockWithReceiver
ProcessSetPanicHandler
ProcessAddDeferToCaller
SetDefaultPanicHandler
ProcessPinThread
ProcessUnpinThread
LibraryOpen
FunctionAttach
FunctionCall
PointerAttach
PointerRead
PointerWrite
PointerFromAddress
PointerAddress
ForeignTypeSize
ForeignTypeAlignment
ProcessSetPinned
FFILibraryOpen
FFIFunctionAttach
FFIFunctionCall
FFIPointerAttach
FFIPointerRead
FFIPointerWrite
FFIPointerFromAddress
FFIPointerAddress
FFITypeSize
FFITypeAlignment
StringToInteger
StringToFloat
FloatToBits
......@@ -185,6 +181,7 @@ module Inkoc
ModuleGet
ModuleInfo
GetAttributeInSelf
MoveResult
]
.each_with_index
.each_with_object({}) { |(value, index), hash| hash[value] = index }
......
......@@ -4,7 +4,7 @@ module Inkoc
module Codegen
class Serializer
SIGNATURE = 'inko'.bytes
VERSION = 3
VERSION = 1
INTEGER_LITERAL = 0
FLOAT_LITERAL = 1
......@@ -90,16 +90,19 @@ module Inkoc
end
def instruction(ins)
u8(ins.index) +
array(ins.arguments, :u16) +
u16(ins.line)
output = u8(ins.index) + u8(ins.arguments.length)
ins.arguments.each do |arg|
output += u16(arg)
end
output + u16(ins.line)
end
def catch_entry(entry)
u16(entry.start) +
u16(entry.stop) +
u16(entry.jump_to) +
u16(entry.register)
u16(entry.jump_to)
end
def integer_literal(value)
......@@ -142,7 +145,6 @@ module Inkoc
u16(code.line) +
array(code.arguments, :literal) +
u8(code.required_arguments) +
boolean(code.rest_argument) +
u16(code.locals) +
u16(code.registers) +
boolean(code.captures) +
......
......@@ -21,7 +21,6 @@ module Inkoc
Pass::ImplementTraits,
Pass::DefineType,
Pass::ValidateThrow,
Pass::OptimizeKeywordArguments,
Pass::GenerateTir,
Pass::TailCallElimination,
Pass::CodeGeneration,
......
......@@ -35,7 +35,6 @@ module Inkoc
def assign_compiled_code_metadata(compiled_code, code_object)
compiled_code.arguments = code_object.argument_names
compiled_code.required_arguments = code_object.required_arguments_count
compiled_code.rest_argument = code_object.rest_argument?
compiled_code.locals = code_object.local_variables_count
compiled_code.registers = code_object.registers_count
compiled_code.captures = code_object.captures?
......@@ -51,7 +50,7 @@ module Inkoc
stop = entry.try_block.instruction_end
jump_to = entry.else_block.instruction_offset
Codegen::CatchEntry.new(start, stop, jump_to, entry.register.id)
Codegen::CatchEntry.new(start, stop, jump_to)
end
end
......@@ -128,56 +127,39 @@ module Inkoc
end
def on_run_block(tir_ins, compiled_code, *)
register = tir_ins.register.id
block = tir_ins.block.id
args = tir_ins.arguments.map(&:id)
kwargs = tir_ins.keyword_arguments.map(&:id)
ins_args = [
register,
block,
args.length,
kwargs.length / 2,
*args,
*kwargs
]
start = tir_ins.start.id
amount = tir_ins.amount
compiled_code.instruct(:RunBlock, ins_args, tir_ins.location)
compiled_code
.instruct(:RunBlock, [block, start, amount], tir_ins.location)
end
def on_run_block_with_receiver(tir_ins, compiled_code, *)
register = tir_ins.register.id
block = tir_ins.block.id
receiver = tir_ins.receiver.id
args = tir_ins.arguments.map(&:id)
kwargs = tir_ins.keyword_arguments.map(&:id)
ins_args = [
register,
block,
receiver,
args.length,
kwargs.length / 2,
*args,
*kwargs
]
rec = tir_ins.receiver.id
start = tir_ins.start.id
amount = tir_ins.amount
loc = tir_ins.location
compiled_code
.instruct(:RunBlockWithReceiver, ins_args, tir_ins.location)
.instruct(:RunBlockWithReceiver, [block, rec, start, amount], loc)
end
def on_tail_call(tir_ins, compiled_code, *)
args = tir_ins.arguments.map(&:id)
kwargs = tir_ins.keyword_arguments.map(&:id)
ins_args = [args.length, kwargs.length / 2, *args, *kwargs]
start = tir_ins.start.id
amount = tir_ins.amount
compiled_code
.instruct(:TailCall, ins_args, tir_ins.location)
compiled_code.instruct(:TailCall, [start, amount], tir_ins.location)
end
def on_set_array(tir_ins, compiled_code, *)
register = tir_ins.register.id
values = tir_ins.values.map(&:id)
start = tir_ins.start.id
len = tir_ins.length
compiled_code.instruct(:SetArray, [register, *values], tir_ins.location)
compiled_code
.instruct(:SetArray, [register, start, len], tir_ins.location)
end
def on_set_attribute(tir_ins, compiled_code, *)
......@@ -191,15 +173,13 @@ module Inkoc
end
def on_set_block(tir_ins, compiled_code, *)
register = tir_ins.register.id
block_code = process_node(tir_ins.code_object)
code_index = compiled_code.code_objects.add(block_code)
arguments = [register, code_index]
arguments << tir_ins.receiver.id if tir_ins.receiver
reg = tir_ins.register.id
code = process_node(tir_ins.code_object)
index = compiled_code.code_objects.add(code)
receiver = tir_ins.receiver.id
compiled_code
.instruct(:SetBlock, arguments, tir_ins.location)
.instruct(:SetBlock, [reg, index, receiver], tir_ins.location)
end
def on_set_literal(tir_ins, compiled_code, *)
......@@ -237,6 +217,10 @@ module Inkoc
compiled_code.instruct(:SetGlobal, [reg, var, val], tir_ins.location)
end
def on_simple(tir_ins, compiled_code, *)
compiled_code.instruct(tir_ins.name, [], tir_ins.location)
end
def on_nullary(tir_ins, compiled_code, *)
reg = tir_ins.register.id
......@@ -322,12 +306,6 @@ module Inkoc
compiled_code.instruct(:CopyBlocks, [to, from], tir_ins.location)
end
def on_drop(tir_ins, compiled_code, *)
object = tir_ins.object.id
compiled_code.instruct(:Drop, [object], tir_ins.location)
end
def on_panic(tir_ins, compiled_code, *)
message = tir_ins.message.id
......
......@@ -891,10 +891,6 @@ module Inkoc
end
end
def on_raw_set_prototype(node, _)
node.arguments.fetch(1).type
end
def on_raw_set_attribute(node, *)
node.arguments.fetch(2).type
end
......@@ -1220,17 +1216,6 @@ module Inkoc
TypeSystem::Never.new
end
def on_raw_remove_attribute(node, _)
object = node.arguments.fetch(0).type
name = node.arguments.fetch(1)
if name.string?
object.lookup_attribute(name.value).type
else
new_any_type
end
end
def on_raw_get_prototype(*)
typedb.object_type.new_instance
end
......@@ -1303,11 +1288,11 @@ module Inkoc
typedb.new_array_of_type(typedb.string_type.new_instance)
end
def on_raw_drop(*)
def on_raw_drop_value(*)
typedb.nil_type.new_instance
end
def on_raw_set_blocking(*)
def on_raw_process_set_blocking(*)
typedb.boolean_type.new_instance
end
......@@ -1453,55 +1438,51 @@ module Inkoc
TypeSystem::Block.lambda(typedb.block_type, return_type: new_any_type)
end
def on_raw_process_pin_thread(*)
def on_raw_process_set_pinned(*)
typedb.boolean_type.new_instance
end
def on_raw_process_unpin_thread(*)
typedb.nil_type.new_instance
end
def on_raw_process_identifier(*)
typedb.integer_type.new_instance
end
def on_raw_library_open(node, _)
def on_raw_ffi_library_open(node, _)
node.arguments.fetch(0).type.new_instance
end
def on_raw_function_attach(node, _)
def on_raw_ffi_function_attach(node, _)
node.arguments.fetch(0).type.new_instance
end
def on_raw_function_call(*)
def on_raw_ffi_function_call(*)
new_any_type
end
def on_raw_pointer_attach(node, _)
def on_raw_ffi_pointer_attach(node, _)
node.arguments.fetch(0).type.new_instance
end
def on_raw_pointer_read(*)
def on_raw_ffi_pointer_read(*)
new_any_type
end
def on_raw_pointer_write(*)
def on_raw_ffi_pointer_write(*)
new_any_type
end
def on_raw_pointer_from_address(node, _)
def on_raw_ffi_pointer_from_address(node, _)
node.arguments.fetch(0).type.new_instance
end
def on_raw_pointer_address(*)
def on_raw_ffi_pointer_address(*)
typedb.integer_type.new_instance
end
def on_raw_foreign_type_size(*)
def on_raw_ffi_type_size(*)
typedb.integer_type.new_instance
end
def on_raw_foreign_type_alignment(*)
def on_raw_ffi_type_alignment(*)
typedb.integer_type.new_instance
end
......
This diff is collapsed.
# frozen_string_literal: true
module Inkoc
module Pass
# Pass that replaces keyword arguments with position arguments when passed
# in order.
#
# Consider this method:
#
# def register(name: String, address: String) { }
#
# When called like this:
#
# register(name: 'Elmo', address: 'Sesame Street')
#
# This pass will turn the call into this:
#
# register('Elmo', 'Sesame Street')
class OptimizeKeywordArguments
include VisitorMethods
def initialize(mod, state)
@module = mod
@state = state
end
def run(node)
process_nodes(node.expressions)
[node]
end
def on_body(node)
process_nodes(node.expressions)
end
def on_block(node)
process_nodes(node.body.expressions)
end
alias on_lambda on_block
def on_node_with_body(node)
process_node(node.body)
end
alias on_object on_node_with_body
alias on_trait on_node_with_body
alias on_trait_implementation on_node_with_body
alias on_reopen_object on_node_with_body
alias on_method on_node_with_body
def on_try(node)
process_node(node.expression)
process_node(node.else_body) if node.else_body
end
def on_node_with_value(node)
process_node(node.value) if node.value
end
alias on_throw on_node_with_value
alias on_return on_node_with_value
alias on_define_variable on_node_with_value
alias on_define_variable_with_explicit_type on_node_with_value
alias on_reassign_variable on_node_with_value
def on_type_cast(node)
process_node(node.expression)
end
def on_send(node)
process_node(node.receiver) if node.receiver
node.arguments.map!.with_index do |arg, index|
if arg.keyword_argument?
if node.block_type
on_keyword_argument(arg, index, node.block_type)
else
process_node(arg.value)
arg
end
else
process_node(arg)
arg
end
end
end
def on_keyword_argument(node, position, block_type)
symbol = block_type.arguments[node.name]
if symbol.index == position
process_node(node.value)
node.value
else
node
end
end
def on_dereference(node)
process_node(node.expression)
end
end
end
end
......@@ -25,13 +25,19 @@ module Inkoc
def on_basic_block(code, block)
# The last instruction is always a Return instruction, so we check the
# instruction that preceeds it.
ins = block.instructions[-2]
index = -2
ins = block.instructions[index]
return unless ins
if ins.move_result?
ins = block.instructions[index = -3]
end
return unless tail_call?(code, ins)
block.instructions[-2] = TIR::Instruction::TailCall
.new(ins.arguments, ins.keyword_arguments, ins.location)
block.instructions[index] =
TIR::Instruction::TailCall.new(ins.start, ins.amount, ins.location)
end
def diagnostics
......
......@@ -3,16 +3,11 @@
module Inkoc
module TIR
class CatchEntry
attr_reader :try_block, :else_block, :register
attr_reader :try_block, :else_block
def initialize(try_block, else_block, register)
def initialize(try_block, else_block)
@try_block = try_block
@else_block = else_block
@register = register
end
def inspect
"CatchEntry(register: #{register.inspect}, ...)"
end
end
end
......
......@@ -18,6 +18,10 @@ module Inkoc
def visitor_method
:on_nullary
end
def move_result?
@name == :MoveResult
end
end
end
end
......
......@@ -11,6 +11,10 @@ module Inkoc
def run_block?