Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat!: Grain implementation of memory allocator #530

Merged
merged 3 commits into from
Mar 7, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
239 changes: 194 additions & 45 deletions compiler/src/codegen/compcore.re
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,14 @@ let module_runtime_id = Ident.create_persistent("moduleRuntimeId");
let reloc_base = Ident.create_persistent("relocBase");
let table_size = Ident.create_persistent("GRAIN$TABLE_SIZE");
let runtime_mod = Ident.create_persistent("grainRuntime");
let malloc_mod = Ident.create_persistent("GRAIN$MODULE$runtime/malloc");
let stdlib_external_runtime_mod =
Ident.create_persistent("stdlib-external/runtime");
let console_mod = Ident.create_persistent("console");
let check_memory_ident = Ident.create_persistent("checkMemory");
let throw_error_ident = Ident.create_persistent("throwError");
let malloc_ident = Ident.create_persistent("malloc");
let malloc_closure_ident = Ident.create_persistent("GRAIN$EXPORT$malloc");
let incref_ident = Ident.create_persistent("incRef");
let new_rational_ident = Ident.create_persistent("newRational");
let new_float32_ident = Ident.create_persistent("newFloat32");
Expand Down Expand Up @@ -266,23 +268,36 @@ let traced_imports =
];
};

let runtime_global_imports = [
let required_global_imports = [
{
mimp_mod: runtime_mod,
mimp_name: reloc_base,
mimp_type: MGlobalImport(I32Type),
mimp_type: MGlobalImport(I32Type, false),
mimp_kind: MImportWasm,
mimp_setup: MSetupNone,
},
{
mimp_mod: runtime_mod,
mimp_name: module_runtime_id,
mimp_type: MGlobalImport(I32Type),
mimp_type: MGlobalImport(I32Type, false),
mimp_kind: MImportWasm,
mimp_setup: MSetupNone,
},
];

let grain_runtime_imports = [
{
mimp_mod: malloc_mod,
mimp_name: malloc_closure_ident,
mimp_type: MGlobalImport(I32Type, true),
mimp_kind: MImportWasm,
mimp_setup: MSetupNone,
},
];

let runtime_global_imports =
List.append(required_global_imports, grain_runtime_imports);

let runtime_function_imports =
List.append(
[
Expand All @@ -294,9 +309,9 @@ let runtime_function_imports =
mimp_setup: MSetupNone,
},
{
mimp_mod: runtime_mod,
mimp_mod: malloc_mod,
mimp_name: malloc_ident,
mimp_type: MFuncImport([I32Type], [I32Type]),
mimp_type: MFuncImport([I32Type, I32Type], [I32Type]),
mimp_kind: MImportWasm,
mimp_setup: MSetupNone,
},
Expand Down Expand Up @@ -481,13 +496,22 @@ let call_runtime_throw_error = (wasm_mod, env, args) =>
Type.none,
);

let call_malloc = (wasm_mod, env, args) =>
let call_malloc = (wasm_mod, env, args) => {
let args = [
Expression.global_get(
wasm_mod,
get_imported_name(malloc_mod, malloc_closure_ident),
Type.int32,
),
...args,
];
Expression.call(
wasm_mod,
get_imported_name(runtime_mod, malloc_ident),
get_imported_name(malloc_mod, malloc_ident),
args,
Type.int32,
);
};
let call_incref = (wasm_mod, env, arg) =>
if (Config.no_gc^) {
arg;
Expand Down Expand Up @@ -1844,18 +1868,96 @@ let compile_record_op = (wasm_mod, env, rec_imm, op) => {

/** Heap allocations. */

let round_up = (num: int, multiple: int): int => num + num mod multiple;
// Heap pointer used by runtime before memory allocation is available
let runtime_heap_ptr_name = "_RUNTIME_HEAP_PTR";
let runtime_heap_ptr = MGlobalBind(runtime_heap_ptr_name, I32Type, false);

/** Rounds the given number of words to be aligned correctly */
let round_to_even = num_words =>
if (num_words mod 2 == 0) {
num_words;
} else {
num_words + 1;
};

let round_allocation_size = (num_words: int): int => round_up(num_words, 4);
let heap_allocate = (wasm_mod, env, num_words: int) =>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I kind of wish we used an ADT here which was something like this:

type allocation_size =
  | AllocWords(int)
  | AllocBytes(int)

Even if we only have AllocWords as the only variant, I feel like it'd be more expressive/readable/"safe":tm:, since it will be clear at the use sites that the number we're passing into heap_allocate represents words rather than bytes.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could be a good future change, yeah.

if (Env.is_runtime_mode()) {
let addition =
Expression.binary(
wasm_mod,
Op.add_int32,
compile_bind(wasm_mod, env, ~action=BindGet, runtime_heap_ptr),
Expression.const(
wasm_mod,
const_int32(round_to_even(num_words) * 4),
),
);
Expression.tuple_extract(
wasm_mod,
Expression.tuple_make(
wasm_mod,
[
compile_bind(wasm_mod, env, ~action=BindGet, runtime_heap_ptr),
compile_bind(
wasm_mod,
env,
~action=BindTee(addition),
runtime_heap_ptr,
),
],
),
0,
);
} else {
call_malloc(
wasm_mod,
env,
[Expression.const(wasm_mod, const_int32(4 * num_words))],
);
};

let heap_allocate = (wasm_mod, env, num_words: int) => {
let words_to_allocate = round_allocation_size(num_words);
call_malloc(
let heap_runtime_allocate_imm =
(~additional_words=0, wasm_mod, env, num_words: immediate) => {
let num_words = () =>
untag_number(wasm_mod, compile_imm(wasm_mod, env, num_words));
let addition =
Expression.binary(
wasm_mod,
Op.add_int32,
compile_bind(wasm_mod, env, ~action=BindGet, runtime_heap_ptr),
Expression.binary(
wasm_mod,
Op.mul_int32,
Expression.binary(
wasm_mod,
Op.and_int32,
Expression.binary(
wasm_mod,
Op.add_int32,
num_words(),
phated marked this conversation as resolved.
Show resolved Hide resolved
// Add 1 extra and clear final bit to round up to an even number of words
Expression.const(wasm_mod, const_int32(1 + additional_words)),
phated marked this conversation as resolved.
Show resolved Hide resolved
),
Expression.const(wasm_mod, const_int32(0xfffffffe)),
),
Expression.const(wasm_mod, const_int32(4)),
),
);
Expression.tuple_extract(
wasm_mod,
env,
[Expression.const(wasm_mod, const_int32(4 * words_to_allocate))],
Expression.tuple_make(
wasm_mod,
[
compile_bind(wasm_mod, env, ~action=BindGet, runtime_heap_ptr),
compile_bind(
wasm_mod,
env,
~action=BindTee(addition),
runtime_heap_ptr,
),
],
),
0,
);
};

Expand Down Expand Up @@ -1897,6 +1999,14 @@ let heap_allocate_imm =
};
};

let heap_allocate_imm =
(~additional_words=0, wasm_mod, env, num_words: immediate) =>
if (Env.is_runtime_mode()) {
heap_runtime_allocate_imm(~additional_words, wasm_mod, env, num_words);
} else {
heap_allocate_imm(~additional_words, wasm_mod, env, num_words);
};

let buf_to_ints = (buf: Buffer.t): list(int64) => {
let num_bytes = Buffer.length(buf);
let num_ints = num_bytes / 8;
Expand Down Expand Up @@ -1939,16 +2049,7 @@ let allocate_string = (wasm_mod, env, str) => {
~offset=0,
wasm_mod,
tee_swap(
call_malloc(
wasm_mod,
env,
[
Expression.const(
wasm_mod,
const_int32 @@ 4 * (2 + 2 * List.length(ints_to_push)),
),
],
),
heap_allocate(wasm_mod, env, 2 + 2 * List.length(ints_to_push)),
),
Expression.const(
wasm_mod,
Expand Down Expand Up @@ -1998,13 +2099,7 @@ let allocate_char = (wasm_mod, env, char) => {
store(
~offset=0,
wasm_mod,
tee_swap(
call_malloc(
wasm_mod,
env,
[Expression.const(wasm_mod, const_int32(8))],
),
),
tee_swap(heap_allocate(wasm_mod, env, 2)),
Expression.const(
wasm_mod,
const_int32(tag_val_of_heap_tag_type(CharType)),
Expand Down Expand Up @@ -2061,10 +2156,7 @@ let allocate_closure =
wasm_mod,
Op.sub_int32,
get_swap(),
Expression.const(
wasm_mod,
const_int32 @@ 4 * round_allocation_size(closure_size),
),
Expression.const(wasm_mod, const_int32 @@ 4 * closure_size),
),
lambda,
);
Expand Down Expand Up @@ -3345,19 +3437,27 @@ let compile_imports = (wasm_mod, env, {imports}) => {
| MImportWasm => Ident.name(name)
| MImportGrain => "GRAIN$EXPORT$" ++ Ident.name(name);

// HACK: The malloc module should have no Grain imports, though it does
// depend on the low-level wasm libraries. All of the wasm instructions are
// already inlined, so this dependency is unnecessary. Binaryen has an
// optimization pass which removes those imports, but Binaryen optimizations
// are disabled because of #196. For now, we just omit all Grain imports.
let malloc_mode = Env.is_malloc_mode();

let compile_import = ({mimp_mod, mimp_name, mimp_type, mimp_kind}) => {
let module_name = compile_module_name(mimp_mod, mimp_kind);
let item_name = compile_import_name(mimp_name, mimp_kind);
let internal_name = get_imported_name(mimp_mod, mimp_name);
switch (mimp_kind, mimp_type) {
| (MImportGrain, MGlobalImport(ty)) =>
| (MImportGrain, _) when malloc_mode => ()
| (MImportGrain, MGlobalImport(ty, mut)) =>
Import.add_global_import(
wasm_mod,
internal_name,
module_name,
item_name,
wasm_type(ty),
true,
mut,
)
| (_, MFuncImport(args, ret)) =>
let proc_list = l =>
Expand All @@ -3370,15 +3470,15 @@ let compile_imports = (wasm_mod, env, {imports}) => {
proc_list(args),
proc_list(ret),
);
| (_, MGlobalImport(typ)) =>
| (_, MGlobalImport(typ, mut)) =>
let typ = compile_asm_type(typ);
Import.add_global_import(
wasm_mod,
internal_name,
module_name,
item_name,
typ,
false,
mut,
);
};
};
Expand Down Expand Up @@ -3582,7 +3682,12 @@ let prepare = (env, {imports} as prog) => {
let import_offset = List.length(runtime_imports);
let import_global_offset = import_offset + List.length(imports);

let new_imports = List.append(runtime_imports, imports);
let new_imports =
if (Env.is_runtime_mode()) {
List.append(required_global_imports, imports);
phated marked this conversation as resolved.
Show resolved Hide resolved
} else {
List.append(runtime_imports, imports);
};
let new_env =
List_utils.fold_lefti(
process_import(~is_runtime_import=true),
Expand All @@ -3608,6 +3713,37 @@ let prepare = (env, {imports} as prog) => {
);
};

let setup_runtime_compilation = (wasm_mod, {exports}) =>
if (Env.is_malloc_mode()) {
// Disallow 0 as a valid Grain pointer
ignore @@
phated marked this conversation as resolved.
Show resolved Hide resolved
Global.add_global(
wasm_mod,
runtime_heap_ptr_name,
Type.int32,
true,
// TODO: We still have some AssemblyScript modules which use the data
// segment, so we offset where the runtime begins. Once those data
// segments are removed, this should be changed to 8.
Expression.const(wasm_mod, const_int32(256)),
);
ignore @@
phated marked this conversation as resolved.
Show resolved Hide resolved
Export.add_global_export(
wasm_mod,
runtime_heap_ptr_name,
runtime_heap_ptr_name,
);
} else {
Import.add_global_import(
wasm_mod,
runtime_heap_ptr_name,
Ident.name(malloc_mod),
runtime_heap_ptr_name,
Type.int32,
true,
);
};

let compile_wasm_module = (~env=?, ~name=?, prog) => {
let env =
switch (env) {
Expand Down Expand Up @@ -3637,11 +3773,24 @@ let compile_wasm_module = (~env=?, ~name=?, prog) => {
);
let _ =
Memory.set_memory(wasm_mod, 0, Memory.unlimited, "memory", [], false);
let () = ignore @@ compile_functions(wasm_mod, env, prog);
let () = ignore @@ compile_imports(wasm_mod, env, prog);
let () = ignore @@ compile_exports(wasm_mod, env, prog);
let () = ignore @@ compile_globals(wasm_mod, env, prog);
let () = ignore @@ compile_tables(wasm_mod, env, prog);

let compile_all = () => {
ignore @@ compile_functions(wasm_mod, env, prog);
ignore @@ compile_imports(wasm_mod, env, prog);
ignore @@ compile_exports(wasm_mod, env, prog);
ignore @@ compile_globals(wasm_mod, env, prog);
ignore @@ compile_tables(wasm_mod, env, prog);
};

if (Env.is_runtime_mode()) {
setup_runtime_compilation(wasm_mod, prog);
Config.preserve_config(() => {
Config.no_gc := true;
compile_all();
});
} else {
compile_all();
};

let serialized_cmi = Cmi_format.serialize_cmi(prog.signature);
Module.add_custom_section(
Expand Down
2 changes: 1 addition & 1 deletion compiler/src/codegen/mashtree.re
Original file line number Diff line number Diff line change
Expand Up @@ -429,7 +429,7 @@ and block = list(instr);
[@deriving sexp]
type import_type =
| MFuncImport(list(asmtype), list(asmtype))
| MGlobalImport(asmtype);
| MGlobalImport(asmtype, bool);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should consider documenting these fields. Perhaps we should try to add documentation to an ADT definition whenever we update it?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In general, I suppose so, yeah. This one sorta makes sense because globals went from always being immutable to optionally being mutable. I think in general we could prefer the record-type variants since those are self-documenting.


[@deriving sexp]
type import_kind =
Expand Down
Loading