Push stack codegen to function for cleaner binaries. Some refactoring in allocators.

This commit is contained in:
Christoffer Lerno
2023-06-30 01:01:58 +02:00
parent 57c8b5fc75
commit f74e294dc2
15 changed files with 171 additions and 111 deletions

View File

@@ -30,16 +30,20 @@ fn void ArenaAllocator.reset(ArenaAllocator* this)
this.used = 0;
}
module std::core::mem::allocator @private;
struct ArenaAllocatorHeader
{
usz size;
char[*] data;
}
/**
* @require !alignment || math::is_power_of_2(alignment)
* @require data `unexpectedly missing the allocator`
*/
fn void*! arena_allocator_function(Allocator* data, usz size, usz alignment, usz offset, void* old_pointer, AllocationKind kind) @private
fn void*! arena_allocator_function(Allocator* data, usz size, usz alignment, usz offset, void* old_pointer, AllocationKind kind)
{
ArenaAllocator* arena = (ArenaAllocator*)data;
bool clear = false;
@@ -93,7 +97,7 @@ fn void*! arena_allocator_function(Allocator* data, usz size, usz alignment, usz
* @require mem::aligned_offset(offset, ArenaAllocatorHeader.alignof) == offset
* @require this != null
**/
fn void*! ArenaAllocator._alloc(ArenaAllocator* this, usz size, usz alignment, usz offset) @private
fn void*! ArenaAllocator._alloc(ArenaAllocator* this, usz size, usz alignment, usz offset)
{
usz total_len = this.data.len;
if (size > total_len) return AllocationFailure.CHUNK_TOO_LARGE?;
@@ -119,7 +123,7 @@ fn void*! ArenaAllocator._alloc(ArenaAllocator* this, usz size, usz alignment, u
* @require mem::aligned_offset(offset, ArenaAllocatorHeader.alignof) == offset
* @require this != null
**/
fn void*! ArenaAllocator._realloc(ArenaAllocator* this, void *old_pointer, usz size, usz alignment, usz offset) @private
fn void*! ArenaAllocator._realloc(ArenaAllocator* this, void *old_pointer, usz size, usz alignment, usz offset)
{
assert(old_pointer >= this.data.ptr, "Pointer originates from a different allocator.");
usz total_len = this.data.len;

View File

@@ -1,7 +1,6 @@
// Copyright (c) 2021 Christoffer Lerno. All rights reserved.
// Use of this source code is governed by the MIT license
// a copy of which can be found in the LICENSE_STDLIB file.
module std::core::mem::allocator;
struct DynamicArenaAllocator
@@ -27,7 +26,7 @@ fn void DynamicArenaAllocator.init(DynamicArenaAllocator* this, usz page_size, A
}
/**
* @require this != null
* @param [&inout] this "The allocator to free"
**/
fn void DynamicArenaAllocator.free(DynamicArenaAllocator* this)
{
@@ -49,13 +48,13 @@ fn void DynamicArenaAllocator.free(DynamicArenaAllocator* this)
this.unused_page = null;
}
struct DynamicArenaPage
struct DynamicArenaPage @local
{
void* memory;
void* prev_arena;
usz total;
usz used;
void* last_ptr;
void* current_stack_ptr;
}
struct DynamicArenaChunk @local
@@ -67,14 +66,14 @@ struct DynamicArenaChunk @local
* @require ptr && this
* @require this.page `tried to free pointer on invalid allocator`
*/
fn void DynamicArenaAllocator.free_ptr(DynamicArenaAllocator* this, void* ptr) @private
fn void DynamicArenaAllocator.free_ptr(DynamicArenaAllocator* this, void* ptr) @local
{
DynamicArenaPage* current_page = this.page;
if (ptr == current_page.last_ptr)
if (ptr == current_page.current_stack_ptr)
{
current_page.used = (usz)((ptr - DEFAULT_SIZE_PREFIX) - current_page.memory);
}
current_page.last_ptr = null;
current_page.current_stack_ptr = null;
}
/**
@@ -91,13 +90,13 @@ fn void*! DynamicArenaAllocator._realloc(DynamicArenaAllocator* this, void* old_
if (old_size >= size && mem::ptr_is_aligned(old_pointer, alignment))
{
*old_size_ptr = size;
if (current_page.last_ptr == old_pointer)
if (current_page.current_stack_ptr == old_pointer)
{
current_page.used = (usz)((old_pointer - DEFAULT_SIZE_PREFIX) - current_page.memory);
}
return old_pointer;
}
if REUSE: (current_page.last_ptr == old_pointer && mem::ptr_is_aligned(old_pointer, alignment))
if REUSE: (current_page.current_stack_ptr == old_pointer && mem::ptr_is_aligned(old_pointer, alignment))
{
assert(size > old_size);
usz add_size = size - old_size;
@@ -153,7 +152,7 @@ fn void*! DynamicArenaAllocator._alloc_new(DynamicArenaAllocator* this, usz size
page.total = page_size;
page.used = mem_start + size - page.memory;
this.page = page;
page.last_ptr = mem_start;
page.current_stack_ptr = mem_start;
return mem_start;
}

View File

@@ -4,7 +4,6 @@
module std::core::mem::allocator;
def MemoryAllocFn = fn char[]!(usz);
struct SimpleHeapAllocator
{
@@ -147,7 +146,7 @@ fn void! SimpleHeapAllocator.add_block(SimpleHeapAllocator* this, usz aligned_by
}
fn void SimpleHeapAllocator._free(SimpleHeapAllocator* this, void* ptr)
fn void SimpleHeapAllocator._free(SimpleHeapAllocator* this, void* ptr) @local
{
// Empty ptr -> do nothing.
if (!ptr) return;
@@ -214,7 +213,7 @@ fn void SimpleHeapAllocator._free(SimpleHeapAllocator* this, void* ptr)
}
}
union Header @private
union Header @local
{
struct
{

View File

@@ -9,26 +9,6 @@ struct OnStackAllocator
OnStackAllocatorExtraChunk* chunk;
}
macro void @stack_mem(usz $size; @body(Allocator* mem)) @builtin
{
char[$size] buffer;
OnStackAllocator allocator;
allocator.init(&buffer, mem::heap());
defer allocator.free();
@body(&allocator);
}
macro void @stack_pool(usz $size; @body) @builtin
{
char[$size] buffer;
OnStackAllocator allocator;
allocator.init(&buffer, mem::heap());
defer allocator.free();
mem::@scoped(&allocator)
{
@body();
};
}
struct OnStackAllocatorExtraChunk @local
{

View File

@@ -30,12 +30,44 @@ fn void TrackingAllocator.init(TrackingAllocator* this, Allocator* using)
this.map.init(.using = using);
}
/**
* Free this tracking allocator.
* @param [&inout] this "The allocator to modify"
**/
fn void TrackingAllocator.free(TrackingAllocator* this)
{
this.map.free();
*this = {};
}
/**
* @return "the total allocated memory not yet freed."
**/
fn usz TrackingAllocator.allocated(TrackingAllocator* this)
{
usz allocated = 0;
@pool()
{
foreach (usz allocation : this.map.value_tlist()) allocated += allocation;
};
return allocated;
}
/**
* @return "the total memory allocated (freed or not)."
**/
fn usz TrackingAllocator.total_allocated(TrackingAllocator* this) => this.mem_total;
/**
* @return "the total number of allocations (freed or not)."
**/
fn usz TrackingAllocator.total_allocation_count(TrackingAllocator* this) => this.allocs_total;
/**
* @return "the number of non-freed allocations."
**/
fn usz TrackingAllocator.allocation_count(TrackingAllocator* this) => this.map.count;
/**
* @param [inout] data
* @require !alignment || math::is_power_of_2(alignment)
@@ -75,31 +107,3 @@ fn void*! tracking_allocator_fn(Allocator* data, usz size, usz alignment, usz of
}
unreachable();
}
fn usz TrackingAllocator.allocated(TrackingAllocator* this)
{
usz allocated = 0;
@pool()
{
foreach (usz allocation : this.map.value_tlist())
{
allocated += allocation;
}
};
return allocated;
}
fn usz TrackingAllocator.total_allocated(TrackingAllocator* this)
{
return this.mem_total;
}
fn usz TrackingAllocator.total_allocation_count(TrackingAllocator* this)
{
return this.allocs_total;
}
fn usz TrackingAllocator.allocation_count(TrackingAllocator* this)
{
return this.map.count;
}

View File

@@ -368,6 +368,27 @@ fn void* trealloc(void* ptr, usz size, usz alignment = mem::DEFAULT_MEM_ALIGNMEN
return temp().realloc_aligned(ptr, size, alignment)!!;
}
macro void @stack_mem(usz $size; @body(Allocator* mem)) @builtin
{
char[$size] buffer;
OnStackAllocator allocator;
allocator.init(&buffer, mem::heap());
defer allocator.free();
@body(&allocator);
}
macro void @stack_pool(usz $size; @body) @builtin
{
char[$size] buffer;
OnStackAllocator allocator;
allocator.init(&buffer, mem::heap());
defer allocator.free();
mem::@scoped(&allocator)
{
@body();
};
}
macro void @pool(;@body) @builtin
{
TempAllocator* allocator = temp();

View File

@@ -7,6 +7,7 @@ const Allocator* NULL_ALLOCATOR = &_NULL_ALLOCATOR;
const Allocator* LIBC_ALLOCATOR = &_SYSTEM_ALLOCATOR;
def AllocatorFunction = fn void*!(Allocator* allocator, usz new_size, usz alignment, usz offset, void* old_pointer, AllocationKind kind);
def MemoryAllocFn = fn char[]!(usz);
macro bool is_allocator($Type)
{

View File

@@ -3,6 +3,14 @@
// a copy of which can be found in the LICENSE_STDLIB file.
module std::core::runtime;
struct StackTrace
{
StackTrace* prev;
String file;
String function;
uint line;
}
struct VirtualAny
{
void* ptr;

View File

@@ -5233,9 +5233,9 @@ void llvm_emit_raw_call(GenContext *c, BEValue *result_value, FunctionPrototype
BEValue no_err;
// Emit the current stack into the thread local or things will get messed up.
if (c->debug.last_ptr)
if (c->debug.current_stack_ptr)
llvm_store_to_ptr_raw_aligned(c,
c->debug.last_ptr,
c->debug.current_stack_ptr,
c->debug.stack_slot,
type_alloca_alignment(type_voidptr));
@@ -5274,9 +5274,9 @@ void llvm_emit_raw_call(GenContext *c, BEValue *result_value, FunctionPrototype
}
// Emit the current stack into the thread local or things will get messed up.
if (c->debug.last_ptr)
if (c->debug.current_stack_ptr)
llvm_store_to_ptr_raw_aligned(c,
c->debug.last_ptr,
c->debug.current_stack_ptr,
c->debug.stack_slot,
type_alloca_alignment(type_voidptr));
@@ -5652,10 +5652,10 @@ static void llvm_emit_call_expr(GenContext *c, BEValue *result_value, Expr *expr
llvm_emit_raw_call(c, result_value, prototype, func_type, func, arg_values, arg_count, inline_flag, error_var, sret_return, &synthetic_return_param);
// Emit the current stack into the thread local or things will get messed up.
if (c->debug.last_ptr)
if (c->debug.current_stack_ptr)
{
llvm_store_to_ptr_raw_aligned(c,
c->debug.last_ptr,
c->debug.current_stack_ptr,
c->debug.stack_slot,
type_alloca_alignment(type_voidptr));
}

View File

@@ -417,6 +417,49 @@ void llvm_emit_function_body(GenContext *c, Decl *decl)
astptr(decl->func_decl.body));
}
void llvm_emit_stacktrace_definitions(GenContext *c)
{
const char *name = ".stacktrace_current";
LLVMValueRef current_stack = c->debug.current_stack_ptr = llvm_add_global_raw(c, name, c->ptr_type, 0);
LLVMSetThreadLocal(current_stack, true);
LLVMSetInitializer(current_stack, llvm_get_zero_raw(c->ptr_type));
llvm_set_weak(c, current_stack);
LLVMTypeRef args[5] = { c->ptr_type, c->ptr_type, c->size_type, c->ptr_type, c->size_type };
LLVMTypeRef func_type = c->debug.stack_init_fn_type = LLVMFunctionType(LLVMVoidTypeInContext(c->context), args, 5, false);
LLVMValueRef func = c->debug.stack_init_fn = LLVMAddFunction(c->module, ".stacktrace_init", func_type);
llvm_set_weak(c, func);
LLVMBuilderRef builder = LLVMCreateBuilderInContext(c->context);
LLVMBasicBlockRef entry = LLVMAppendBasicBlockInContext(c->context, func, "entry");
LLVMPositionBuilderAtEnd(builder, entry);
c->builder = builder;
LLVMValueRef stacktrace = LLVMGetParam(func, 0);
AlignSize align_to_use;
LLVMTypeRef slot_type = c->debug.stack_type;
AlignSize alignment = llvm_abi_alignment(c, slot_type);
LLVMValueRef prev_ptr = llvm_emit_struct_gep_raw(c, stacktrace, slot_type, 0, alignment, &align_to_use);
llvm_store_to_ptr_raw_aligned(c, prev_ptr,
LLVMBuildLoad2(c->builder, c->ptr_type, c->debug.current_stack_ptr, ""),
align_to_use);
LLVMValueRef func_name = llvm_emit_struct_gep_raw(c, stacktrace, slot_type, 1, alignment, &align_to_use);
LLVMValueRef func_name_ptr = llvm_emit_struct_gep_raw(c, func_name, c->chars_type, 0, align_to_use, &align_to_use);
llvm_store_to_ptr_raw_aligned(c, func_name_ptr, LLVMGetParam(func, 1), align_to_use);
LLVMValueRef func_name_sz = llvm_emit_struct_gep_raw(c, func_name, c->chars_type, 1, align_to_use, &align_to_use);
llvm_store_to_ptr_raw_aligned(c, func_name_sz, LLVMGetParam(func, 2), align_to_use);
LLVMValueRef file_name = llvm_emit_struct_gep_raw(c, stacktrace, slot_type, 2, alignment, &align_to_use);
LLVMValueRef file_name_ptr = llvm_emit_struct_gep_raw(c, file_name, c->chars_type, 0, align_to_use, &align_to_use);
llvm_store_to_ptr_raw_aligned(c, file_name_ptr, LLVMGetParam(func, 3), align_to_use);
LLVMValueRef file_name_sz = llvm_emit_struct_gep_raw(c, file_name, c->chars_type, 1, align_to_use, &align_to_use);
llvm_store_to_ptr_raw_aligned(c, file_name_sz, LLVMGetParam(func, 4), align_to_use);
llvm_store_to_ptr_raw_aligned(c,
c->debug.current_stack_ptr,
stacktrace,
type_alloca_alignment(type_voidptr));
LLVMBuildRetVoid(c->builder);
LLVMDisposeBuilder(c->builder);
c->builder = NULL;
}
void llvm_emit_body(GenContext *c, LLVMValueRef function, const char *module_name, const char *function_name,
FileId file_id, FunctionPrototype *prototype, Signature *signature, Ast *body)
{
@@ -424,24 +467,33 @@ void llvm_emit_body(GenContext *c, LLVMValueRef function, const char *module_nam
LLVMValueRef prev_function = c->function;
LLVMBuilderRef prev_builder = c->builder;
bool use_stacktrace = emit_debug && c->debug.enable_stacktrace;
if (use_stacktrace && !c->debug.stack_init_fn)
{
llvm_emit_stacktrace_definitions(c);
c->builder = prev_builder;
}
c->opt_var = NULL;
c->catch_block = NULL;
c->function = function;
if (!function_name) function_name = "anonymous function";
size_t func_name_len = 0;
size_t file_name_len = 0;
if (emit_debug)
{
c->debug.function = LLVMGetSubprogram(function);
if (c->debug.enable_stacktrace)
if (use_stacktrace)
{
scratch_buffer_clear();
scratch_buffer_append(module_name);
scratch_buffer_append("::");
scratch_buffer_append(function_name);
c->debug.func_name = llvm_emit_string_const(c, scratch_buffer_to_string(), ".funcname");
c->debug.func_name = llvm_emit_zstring_named(c, scratch_buffer_to_string(), ".funcname");
func_name_len = scratch_buffer.len;
File *file = source_file_by_id(file_id);
c->debug.file_name = llvm_emit_string_const(c, file->name, ".filename");
file_name_len = strlen(file->name);
c->debug.file_name = llvm_emit_zstring_named(c, file->name, ".filename");
}
}
@@ -462,43 +514,22 @@ void llvm_emit_body(GenContext *c, LLVMValueRef function, const char *module_nam
{
llvm_debug_scope_push(c, c->debug.function);
EMIT_LOC(c, body);
if (c->debug.enable_stacktrace)
if (use_stacktrace)
{
LLVMTypeRef slot_type = c->debug.stack_type;
if (!c->debug.last_ptr)
{
const char *name = ".$last_stack";
LLVMValueRef last_stack = c->debug.last_ptr = llvm_add_global_raw(c, name, c->ptr_type, 0);
LLVMSetThreadLocal(last_stack, true);
LLVMSetInitializer(last_stack, llvm_get_zero_raw(c->ptr_type));
llvm_set_weak(c, last_stack);
}
assert(c->debug.current_stack_ptr && c->debug.stack_init_fn);
AlignSize alignment = llvm_abi_alignment(c, slot_type);
c->debug.stack_slot = llvm_emit_alloca(c, slot_type, alignment, ".$stackslot");
AlignSize align_to_use;
LLVMValueRef prev_ptr = llvm_emit_struct_gep_raw(c, c->debug.stack_slot, slot_type, 0, alignment, &align_to_use);
llvm_store_to_ptr_raw_aligned(c,
prev_ptr,
LLVMBuildLoad2(c->builder, c->ptr_type, c->debug.last_ptr, ""),
align_to_use);
LLVMValueRef func_name = llvm_emit_struct_gep_raw(c, c->debug.stack_slot, slot_type, 1, alignment, &align_to_use);
llvm_store_to_ptr_raw_aligned(c, func_name, c->debug.func_name, align_to_use);
LLVMValueRef file_name = llvm_emit_struct_gep_raw(c, c->debug.stack_slot, slot_type, 2, alignment, &align_to_use);
llvm_store_to_ptr_raw_aligned(c, file_name, c->debug.file_name, align_to_use);
c->debug.stack_slot_row = llvm_emit_struct_gep_raw(c, c->debug.stack_slot, slot_type, 3, alignment, &align_to_use);
LLVMValueRef last_ptr = NULL;
if (function_name != kw_main && function_name != kw_mainstub)
LLVMValueRef stacktrace = c->debug.stack_slot = llvm_emit_alloca(c, slot_type, alignment, ".$stacktrace");
LLVMValueRef args[] = { stacktrace, c->debug.func_name, llvm_const_int(c, type_usz, func_name_len),
c->debug.file_name, llvm_const_int(c, type_usz, file_name_len) };
LLVMBuildCall2(c->builder, c->debug.stack_init_fn_type, c->debug.stack_init_fn, args, 5, "");
c->debug.stack_slot_row = LLVMBuildStructGEP2(c->builder, slot_type, c->debug.stack_slot, 3, ".$row");
if (function_name == kw_main || function_name == kw_mainstub)
{
last_ptr = c->debug.last_ptr;
AlignSize align_size;
LLVMValueRef last = llvm_emit_struct_gep_raw(c, c->debug.stack_slot, slot_type, 0, alignment, &align_size);
llvm_store_to_ptr_raw_aligned(c, last, LLVMConstNull(c->ptr_type), align_size);
}
else
{
last_ptr = prev_ptr;
}
llvm_store_to_ptr_raw_aligned(c,
last_ptr,
c->debug.stack_slot,
type_alloca_alignment(type_voidptr));
}
}

View File

@@ -61,7 +61,9 @@ typedef struct
LLVMMetadataRef inlined_at;
LLVMValueRef func_name;
LLVMValueRef file_name;
LLVMValueRef last_ptr;
LLVMValueRef current_stack_ptr;
LLVMValueRef stack_init_fn;
LLVMTypeRef stack_init_fn_type;
LLVMTypeRef stack_type;
LLVMValueRef stack_slot;
LLVMValueRef stack_slot_row;

View File

@@ -144,7 +144,7 @@ void gencontext_begin_module(GenContext *c)
c->chars_type,
llvm_get_type(c, type_uint) };
LLVMStructSetBody(c->debug.stack_type, types, 4, false);
c->debug.last_ptr = NULL;
c->debug.current_stack_ptr = NULL;
c->debug.enable_stacktrace = true;
}
}

View File

@@ -145,7 +145,7 @@ void symtab_init(uint32_t capacity)
kw_incr = KW_DEF("incr");
kw_inout = KW_DEF("inout");
kw_libc = KW_DEF("libc");
kw_mainstub = KW_DEF("_$start");
kw_mainstub = KW_DEF("_$main");
kw_main = KW_DEF("main");
kw_nameof = KW_DEF("nameof");
kw_noinline = KW_DEF("noinline");

View File

@@ -1 +1 @@
#define COMPILER_VERSION "0.4.544"
#define COMPILER_VERSION "0.4.545"

View File

@@ -173,6 +173,7 @@ entry:
%not_err = icmp eq i64 %26, 0
%27 = call i1 @llvm.expect.i1(i1 %not_err, i1 true)
br i1 %27, label %after_check, label %after_check12
after_check: ; preds = %entry
%28 = getelementptr inbounds %Foo, ptr %retparam10, i32 0, i32 0
%29 = insertvalue %any undef, ptr %28, 0
@@ -183,6 +184,7 @@ after_check: ; preds = %entry
%not_err11 = icmp eq i64 %32, 0
%33 = call i1 @llvm.expect.i1(i1 %not_err11, i1 true)
br i1 %33, label %after_check12, label %after_check12
after_check12: ; preds = %entry, %after_check, %after_check
%34 = call i8 @"std.collections.map$int$test.Foo$.HashMap.has_key"(ptr %map, i32 1)
store i8 %34, ptr %taddr, align 1
@@ -253,18 +255,22 @@ after_check12: ; preds = %entry, %after_check
%80 = load ptr, ptr @std.core.mem.thread_temp_allocator, align 8
%not = icmp eq ptr %80, null
br i1 %not, label %if.then, label %if.exit
if.then: ; preds = %after_check12
%81 = load ptr, ptr @std.core.mem.thread_allocator, align 8
%82 = call i64 @std.core.mem.allocator.new_temp(ptr %retparam49, i64 262144, ptr %81)
%not_err50 = icmp eq i64 %82, 0
%83 = call i1 @llvm.expect.i1(i1 %not_err50, i1 true)
br i1 %83, label %after_check51, label %assign_optional
assign_optional: ; preds = %if.then
store i64 %82, ptr %error_var, align 8
br label %panic_block
after_check51: ; preds = %if.then
%84 = load ptr, ptr %retparam49, align 8
br label %noerr_block
panic_block: ; preds = %assign_optional
%85 = insertvalue %any undef, ptr %error_var, 0
%86 = insertvalue %any %85, i64 ptrtoint (ptr @"$ct.anyfault" to i64), 1
@@ -273,11 +279,13 @@ panic_block: ; preds = %assign_optional
%88 = insertvalue %"any[]" undef, ptr %varargslots52, 0
%89 = insertvalue %"any[]" %88, i64 1, 1
store %"any[]" %89, ptr %indirectarg, align 8
call void @std.core.builtin.panicf(ptr @.panic_msg, i64 36, ptr @.file, i64 6, ptr @.func, i64 4, i32 399, ptr byval(%"any[]") align 8 %indirectarg)
call void @std.core.builtin.panicf(ptr @.panic_msg, i64 36, ptr @.file, i64 6, ptr @.func, i64 4, i32 420, ptr byval(%"any[]") align 8 %indirectarg)
unreachable
noerr_block: ; preds = %after_check51
store ptr %84, ptr @std.core.mem.thread_temp_allocator, align 8
br label %if.exit
if.exit: ; preds = %noerr_block, %after_check12
%90 = load ptr, ptr @std.core.mem.thread_temp_allocator, align 8
store ptr %90, ptr %allocator, align 8
@@ -314,14 +322,17 @@ if.exit: ; preds = %noerr_block, %after
define internal void @.static_initialize.0() {
entry:
br label %dtable_check
dtable_check: ; preds = %dtable_next, %entry
%dtable_ref = phi ptr [ getelementptr inbounds (%.introspect, ptr @"$ct.test.Foo", i32 0, i32 1), %entry ], [ %next_dtable_ref, %dtable_next ]
%dtable_ptr = load ptr, ptr %dtable_ref, align 8
%0 = icmp eq ptr %dtable_ptr, null
br i1 %0, label %dtable_found, label %dtable_next
dtable_next: ; preds = %dtable_check
%next_dtable_ref = getelementptr inbounds { ptr, ptr, ptr }, ptr %dtable_ptr, i32 0, i32 2
br label %dtable_check
dtable_found: ; preds = %dtable_check
store ptr @"$ct.dyn.test.Foo.to_string", ptr %dtable_ref, align 8
ret void