Skip to content

Commit

Permalink
Refactor messages to support non-word arguments
Browse files Browse the repository at this point in the history
Messages are no longer allocated using runtime functions, instead the
standard library uses the system allocator (= malloc) to allocate chunks
of the right size. This ensures that sending of data that isn't word
sized between processes works correctly.

See #755 for more details.

Changelog: fixed
  • Loading branch information
yorickpeterse committed Sep 10, 2024
1 parent 55e6c91 commit 33408cb
Show file tree
Hide file tree
Showing 11 changed files with 135 additions and 312 deletions.
70 changes: 27 additions & 43 deletions compiler/src/llvm/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -116,29 +116,6 @@ impl<'ctx> Builder<'ctx> {
}
}

pub(crate) fn load_array_index(
&self,
array_type: ArrayType<'ctx>,
array: PointerValue<'ctx>,
index: usize,
) -> BasicValueEnum<'ctx> {
let ptr = unsafe {
self.inner
.build_gep(
array_type,
array,
&[
self.context.i32_type().const_int(0, false),
self.context.i32_type().const_int(index as _, false),
],
"",
)
.unwrap()
};

self.inner.build_load(array_type.get_element_type(), ptr, "").unwrap()
}

pub(crate) fn store_array_field<V: BasicValue<'ctx>>(
&self,
array_type: ArrayType<'ctx>,
Expand Down Expand Up @@ -284,10 +261,6 @@ impl<'ctx> Builder<'ctx> {
self.context.bool_type().const_int(value as u64, false)
}

pub(crate) fn u8_literal(&self, value: u8) -> IntValue<'ctx> {
self.context.i8_type().const_int(value as u64, false)
}

pub(crate) fn i64_literal(&self, value: i64) -> IntValue<'ctx> {
self.u64_literal(value as u64)
}
Expand Down Expand Up @@ -782,7 +755,7 @@ impl<'ctx> Builder<'ctx> {
self.function.set_subprogram(function);
}

pub(crate) fn allocate<'a, 'b>(
pub(crate) fn allocate_instance<'a, 'b>(
&self,
module: &'a mut Module<'b, 'ctx>,
db: &Database,
Expand All @@ -793,37 +766,48 @@ impl<'ctx> Builder<'ctx> {
let name = &names.classes[&class];
let global = module.add_class(class, name).as_pointer_value();
let class_ptr = self.load_untyped_pointer(global);
let size = module.layouts.size_of_class(class);
let typ = module.layouts.instances[class.0 as usize];
let res = self.malloc(module, typ);
let header = module.layouts.header;

// Atomic values start with a reference count of 1, so atomic decrements
// returns the correct result for a value for which no extra references
// have been created (instead of underflowing).
let refs = self.u32_literal(if atomic { 1 } else { 0 });

self.store_field(header, res, HEADER_CLASS_INDEX, class_ptr);
self.store_field(header, res, HEADER_REFS_INDEX, refs);
res
}

pub(crate) fn malloc<'a, 'b, T: BasicType<'ctx>>(
&self,
module: &'a mut Module<'b, 'ctx>,
typ: T,
) -> PointerValue<'ctx> {
let err_func =
module.runtime_function(RuntimeFunction::AllocationError);
let alloc_func = module.runtime_function(RuntimeFunction::Allocate);
let size = self.u64_literal(size).into();
let res = self.call(alloc_func, &[size]).into_pointer_value();

let size = typ.size_of().unwrap();
let res = self.inner.build_malloc(typ, "").unwrap();
let err_block = self.add_block();
let ok_block = self.add_block();
let is_null = self.pointer_is_null(res);
let header = module.layouts.header;

self.branch(is_null, err_block, ok_block);

// The block to jump to when the allocation failed.
self.switch_to_block(err_block);
self.call_void(err_func, &[class_ptr.into()]);
self.call_void(err_func, &[size.into()]);
self.unreachable();

// The block to jump to when the allocation succeeds.
self.switch_to_block(ok_block);

// Atomic values start with a reference count of 1, so atomic decrements
// returns the correct result for a value for which no extra references
// have been created (instead of underflowing).
let refs = self.u32_literal(if atomic { 1 } else { 0 });

self.store_field(header, res, HEADER_CLASS_INDEX, class_ptr);
self.store_field(header, res, HEADER_REFS_INDEX, refs);
res
}

pub(crate) fn free(&self, value: PointerValue<'ctx>) {
self.inner.build_free(value).unwrap();
}
}

/// A wrapper around the LLVM types used for building debugging information.
Expand Down
3 changes: 0 additions & 3 deletions compiler/src/llvm/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,6 @@ pub(crate) const CLASS_METHODS_INDEX: u32 = 3;
pub(crate) const METHOD_HASH_INDEX: u32 = 0;
pub(crate) const METHOD_FUNCTION_INDEX: u32 = 1;

pub(crate) const CONTEXT_ARGS_INDEX: u32 = 0;

pub(crate) const MESSAGE_ARGUMENTS_INDEX: u32 = 2;
pub(crate) const DROPPER_INDEX: u32 = 0;
pub(crate) const CLOSURE_CALL_INDEX: u32 = 1;

Expand Down
35 changes: 4 additions & 31 deletions compiler/src/llvm/layouts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,7 @@ use inkwell::types::{
};
use inkwell::AddressSpace;
use types::{
CallConvention, ClassId, BOOL_ID, BYTE_ARRAY_ID, FLOAT_ID, INT_ID, NIL_ID,
STRING_ID,
CallConvention, BOOL_ID, BYTE_ARRAY_ID, FLOAT_ID, INT_ID, NIL_ID, STRING_ID,
};

/// The size of an object header.
Expand Down Expand Up @@ -64,9 +63,6 @@ pub(crate) struct Layouts<'ctx> {
/// The layout of object headers.
pub(crate) header: StructType<'ctx>,

/// The layout of the context type passed to async methods.
pub(crate) context: StructType<'ctx>,

/// The layout to use for the type that stores the built-in type method
/// counts.
pub(crate) method_counts: StructType<'ctx>,
Expand All @@ -76,9 +72,6 @@ pub(crate) struct Layouts<'ctx> {
/// This `Vec` is indexed using `MethodId` values.
pub(crate) methods: Vec<Method<'ctx>>,

/// The layout of messages sent to processes.
pub(crate) message: StructType<'ctx>,

/// The layout of a process' stack data.
pub(crate) process_stack_data: StructType<'ctx>,
}
Expand All @@ -91,7 +84,6 @@ impl<'ctx> Layouts<'ctx> {
target_data: &'ctx TargetData,
) -> Self {
let db = &state.db;
let space = AddressSpace::default();
let empty_struct = context.struct_type(&[]);
let num_classes = db.number_of_classes();

Expand Down Expand Up @@ -124,21 +116,11 @@ impl<'ctx> Layouts<'ctx> {
context.i32_type().into(), // scheduler_epoch
]);

let context_layout = context.struct_type(&[
context.pointer_type().into(), // Arguments pointer
]);

let method_counts_layout = context.struct_type(&[
context.i16_type().into(), // String
context.i16_type().into(), // ByteArray
]);

let message_layout = context.struct_type(&[
context.pointer_type().into(), // Function
context.i8_type().into(), // Length
context.pointer_type().array_type(0).into(), // Arguments
]);

let stack_data_layout = context.struct_type(&[
context.pointer_type().into(), // Process
context.pointer_type().into(), // Thread
Expand Down Expand Up @@ -194,10 +176,8 @@ impl<'ctx> Layouts<'ctx> {
instances,
state: state_layout,
header,
context: context_layout,
method_counts: method_counts_layout,
methods: vec![dummy_method; num_methods],
message: message_layout,
process_stack_data: stack_data_layout,
};

Expand Down Expand Up @@ -350,10 +330,9 @@ impl<'ctx> Layouts<'ctx> {
// table slots.
for &method in &mir_class.methods {
let typ = if method.is_async(db) {
context.void_type().fn_type(
&[context_layout.ptr_type(space).into()],
false,
)
context
.void_type()
.fn_type(&[context.pointer_type().into()], false)
} else {
let mut args: Vec<BasicMetadataTypeEnum> = Vec::new();

Expand Down Expand Up @@ -460,10 +439,4 @@ impl<'ctx> Layouts<'ctx> {

layouts
}

pub(crate) fn size_of_class(&self, class: ClassId) -> u64 {
let layout = &self.instances[class.0 as usize];

size_of_type(self.target_data, layout)
}
}
102 changes: 49 additions & 53 deletions compiler/src/llvm/passes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,9 @@ use crate::llvm::builder::Builder;
use crate::llvm::constants::{
ARRAY_BUF_INDEX, ARRAY_CAPA_INDEX, ARRAY_LENGTH_INDEX,
CLASS_METHODS_COUNT_INDEX, CLASS_METHODS_INDEX, CLOSURE_CALL_INDEX,
CONTEXT_ARGS_INDEX, DROPPER_INDEX, FIELD_OFFSET, HEADER_CLASS_INDEX,
HEADER_REFS_INDEX, MESSAGE_ARGUMENTS_INDEX, METHOD_FUNCTION_INDEX,
METHOD_HASH_INDEX, PROCESS_FIELD_OFFSET, STACK_DATA_EPOCH_INDEX,
STACK_DATA_PROCESS_INDEX, STATE_EPOCH_INDEX,
DROPPER_INDEX, FIELD_OFFSET, HEADER_CLASS_INDEX, HEADER_REFS_INDEX,
METHOD_FUNCTION_INDEX, METHOD_HASH_INDEX, PROCESS_FIELD_OFFSET,
STACK_DATA_EPOCH_INDEX, STACK_DATA_PROCESS_INDEX, STATE_EPOCH_INDEX,
};
use crate::llvm::context::Context;
use crate::llvm::layouts::Layouts;
Expand Down Expand Up @@ -931,7 +930,7 @@ impl<'shared, 'module, 'ctx> LowerModule<'shared, 'module, 'ctx> {
let class_id = ClassId::array()
.specializations(&self.shared.state.db)[&vec![shape]];
let layout = self.layouts.instances[class_id.0 as usize];
let array = builder.allocate(
let array = builder.allocate_instance(
self.module,
&self.shared.state.db,
self.shared.names,
Expand Down Expand Up @@ -1080,28 +1079,20 @@ impl<'shared, 'module, 'ctx> LowerMethod<'shared, 'module, 'ctx> {
let entry_block = self.builder.add_block();

self.builder.switch_to_block(entry_block);
self.define_register_variables();

let space = AddressSpace::default();
let num_args = self.method.arguments.len() as u32;
let args_type =
self.builder.context.pointer_type().array_type(num_args);
let arg_types = self
.method
.arguments
.iter()
.skip(1)
.map(|r| self.variable_types[r])
.collect::<Vec<_>>();
let args_type = self.builder.context.struct_type(&arg_types);
let args_var = self.builder.new_stack_slot(args_type.ptr_type(space));
let ctx_var =
self.builder.new_stack_slot(self.layouts.context.ptr_type(space));

self.define_register_variables();

// Destructure the context into its components. This is necessary as the
// context only lives until the first yield.
self.builder.store(ctx_var, self.builder.argument(0));

let ctx = self.builder.load_pointer(self.layouts.context, ctx_var);
let args = self
.builder
.load_field(self.layouts.context, ctx, CONTEXT_ARGS_INDEX)
.into_pointer_value();

self.builder.store(args_var, args);
self.builder.store(args_var, self.builder.argument(0));

// For async methods we don't include the receiver in the message, as
// we can instead just read the process from the private stack data.
Expand All @@ -1115,14 +1106,19 @@ impl<'shared, 'module, 'ctx> LowerMethod<'shared, 'module, 'ctx> {
for (index, reg) in self.method.arguments.iter().skip(1).enumerate() {
let var = self.variables[reg];
let args = self.builder.load_pointer(args_type, args_var);
let val = self
.builder
.load_array_index(args_type, args, index)
.into_pointer_value();
let val = self.builder.load_field(args_type, args, index as _);

self.builder.store(var, val);
}

// Now that the arguments are unpacked, we can deallocate the heap
// structure passed as part of the message.
//
// If no arguments are passed, the data pointer is NULL.
if !arg_types.is_empty() {
self.builder.free(self.builder.load_pointer(args_type, args_var));
}

let (line, _) =
self.shared.mir.location(self.method.location).line_column();
let debug_func = self.module.debug_builder.new_function(
Expand Down Expand Up @@ -2163,42 +2159,42 @@ impl<'shared, 'module, 'ctx> LowerMethod<'shared, 'module, 'ctx> {
.as_global_value()
.as_pointer_value()
.into();
let len =
self.builder.u8_literal(ins.arguments.len() as u8).into();
let message_new =
self.module.runtime_function(RuntimeFunction::MessageNew);
let send_message = self
.module
.runtime_function(RuntimeFunction::ProcessSendMessage);
let message = self
.builder
.call(message_new, &[method, len])
.into_pointer_value();
let arg_types = ins
.arguments
.iter()
.map(|r| self.variable_types[r])
.collect::<Vec<_>>();
let args = if arg_types.is_empty() {
self.builder.context.pointer_type().const_null()
} else {
let args_type =
self.builder.context.struct_type(&arg_types);
let args = self.builder.malloc(self.module, args_type);

// The receiver doesn't need to be stored in the message, as
// each async method sets `self` to the process running it.
for (index, reg) in ins.arguments.iter().enumerate() {
let typ = self.variable_types[reg];
let var = self.variables[reg];
let val = self.builder.load(typ, var);
let slot = self.builder.u32_literal(index as u32);
let addr = self.builder.array_field_index_address(
self.layouts.message,
message,
MESSAGE_ARGUMENTS_INDEX,
slot,
);
// The receiver doesn't need to be stored in the message, as
// each async method sets `self` to the process running it.
for (index, reg) in ins.arguments.iter().enumerate() {
let typ = self.variable_types[reg];
let var = self.variables[reg];
let val = self.builder.load(typ, var);

self.builder.store(addr, val);
}
self.builder
.store_field(args_type, args, index as _, val);
}

args
};

let state = self.load_state();
let sender = self.load_process().into();
let rec = self.builder.load(rec_typ, rec_var).into();

self.builder.call_void(
send_message,
&[state.into(), sender, rec, message.into()],
&[state.into(), sender, rec, method, args.into()],
);
}
Instruction::GetField(ins)
Expand Down Expand Up @@ -2771,7 +2767,7 @@ impl<'shared, 'module, 'ctx> LowerMethod<'shared, 'module, 'ctx> {
}

fn allocate(&mut self, class: ClassId) -> PointerValue<'ctx> {
self.builder.allocate(
self.builder.allocate_instance(
self.module,
&self.shared.state.db,
self.shared.names,
Expand Down Expand Up @@ -2922,7 +2918,7 @@ impl<'a, 'ctx> GenerateMain<'a, 'ctx> {
.add_function(
&self.names.methods[&main_method_id],
self.module.context.void_type().fn_type(
&[self.layouts.context.ptr_type(space).into()],
&[self.module.context.pointer_type().into()],
false,
),
None,
Expand Down
Loading

0 comments on commit 33408cb

Please sign in to comment.