diff --git a/docs/Memory Model.md b/docs/Memory Model.md index bab18f59..5d2c2114 100644 --- a/docs/Memory Model.md +++ b/docs/Memory Model.md @@ -148,7 +148,76 @@ follows: ### Function Pointers -This section is TBD, and will be filled in as part of the work on function pointers. +Function pointers are in common usage in LLVM, making it crucial that Hieratika is able to support +their usage. To that end, the following section outlines their integration with the memory model. + +The precondition for this design is to first understand that we are not considering function +pointers to be _true pointers_ at this stage. While LLVM insists that they _are_—and offsets from +them can be used to access adjacent data such as +[prefix](https://llvm.org/docs/LangRef.html#prefix-data) or +[prologue](https://llvm.org/docs/LangRef.html#prologue-data) data—these features do not seem to be +used by Rust and hence can be safely ignored for now. + +As a result, Hieratika treats function pointers _specially_, as follows: + +- In LLVM IR, function pointer values are _always_ derived from function pointer constants, + referring directly to the encoded name of some function. +- The Hieratika compiler, at the point of generating function stubs, will generate global constants + for these function pointer constants, derived from the correct `blockaddress` expression, + containing a _relocatable_ reference to a block (`block_ptr`), and information about which module + the block was defined in (`module_id`). +- The compiler then generates _dispatch functions_ (named + `format!("__hieratika_dispatch_local_{function_type}")`). These dispatch functions take the + correct arguments `A...` for their type, as well as the function pointer `ptr` and match on the + provided function pointer's `block_ptr` portion. +- If the `block_ptr` matches, the local dispatch function will call the correct target function (in + the current module) for this function pointer, passing the provided arguments and thereby calling + the correct function for that pointer. + +Consider, by way of example, a dispatch function `__hieratika_dispatch_local_(i8, i8) -> i16`. It +would have an implementation similar to the following pseudocode. + +```rust +fn __hieratika_dispatch_local_i8_i8_i16(function_pointer: ptr, arg1: i8, arg2: i8) -> i16 { + match function_pointer.block_ptr { + 0 => function_zero(arg1, arg2), + 1 => function_one(arg1, arg2), + // ... + _ => panic!("Found function pointer {function_pointer} for function (i8, i8) -> i16 but no such target exists") + } +} +``` + +This results in a raft of dispatch functions for all possible function types in a module, which can +then be used to _locally_ discover the correct function to call through a function pointer. + +This, however, is insufficient for _general-case_ function pointer dispatch. Where LLVM's +traditional [`blockaddress`](https://llvm.org/docs/LangRef.html#addresses-of-basic-blocks) +expressions cannot escape the local module, function pointers _easily_ can be passed between modules +and then called. + +In order to solve this problem, the design for function pointers in Hieratika has a _second_ part, +known as the _meta_-dispatch table. + +- These tables, generated by the _linker_—instead of the single-module hieratika compiler—are + similar to the local dispatch tables above, but instead of dispatching based on the `block_ptr` + portion of the function pointer, it instead dispatched based on the `module_id`. +- If the `module_id` matches, the meta dispatch function will call the correct target _local_ + dispatch function for that module, passing the provided arguments and the function pointer, and + allowing the local dispatch function to select the correct implementation. + +At first glance, this dual-layer dispatch seems quite expensive, essentially amounting to performing +comparisons of the function pointer to all possible targets in a program. However, Hieratika uses a +slightly more sensible search mechanism. + +- Both `block_ptr` and `module_id` are (re-)allocated (at link time) such that they are each in a + contiguous block of integer identifiers. +- The dispatch function can then perform _binary search_ on the `module_id` to resolve to the + correct local dispatch function in $\mathbb{O}(\log_2 n)$ time, instead of $\mathbb{O}(n)$ time. + +If, in the future, we work with languages that do make use of prefix or prologue data, this approach +will be entirely insufficient. To make _that_ work, we would need to re-work our handling of +function pointers to be _true_ pointers. ## Felt-Aligned Addressing - An Alternative Model @@ -172,7 +241,8 @@ as a value of another type under byte-addressing and alignment rules—was rampa As an example, it proved common to see IR that allocated `[4 x i8]` and then wrote an `i16` to the first two bytes and read an `i16` from the other two. As, in the felt-aligned model, the first two -`i8`s would be written to individual felts, reading them back as an `i16` is significantly complex. +`i8`s would be written to individual felts, reading them back as an `i16` is significantly more +complex. To that end, the project was forced to abandon this model in favor of a more-traditional byte-aligned addressing model.