From 32821e7098ba29c30e458f555f62954ce54dcb1a Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Mon, 18 Jan 2016 16:42:45 -0700 Subject: [PATCH] add function pointer support See #14 --- src/all_types.hpp | 17 ++++++--- src/analyze.cpp | 84 ++++++++++++++++++++++++++++-------------- src/codegen.cpp | 92 +++++++++++++++++++++------------------------- src/zig_llvm.cpp | 15 +++----- src/zig_llvm.hpp | 5 +-- std/bootstrap.zig | 2 +- test/run_tests.cpp | 18 +++++++++ 7 files changed, 136 insertions(+), 97 deletions(-) diff --git a/src/all_types.hpp b/src/all_types.hpp index c804e0763f..f61e944f06 100644 --- a/src/all_types.hpp +++ b/src/all_types.hpp @@ -566,6 +566,7 @@ struct AstNodeSymbolExpr { // populated by semantic analyzer Expr resolved_expr; VariableTableEntry *variable; + FnTableEntry *fn_entry; }; struct AstNodeBoolLiteral { @@ -720,7 +721,12 @@ struct TypeTableEntryEnum { struct TypeTableEntryFn { TypeTableEntry *return_type; TypeTableEntry **param_types; - int param_count; + int src_param_count; + LLVMTypeRef raw_type_ref; + bool is_var_args; + int gen_param_count; + LLVMCallConv calling_convention; + bool is_naked; }; enum TypeTableEntryId { @@ -784,6 +790,7 @@ struct ImportTableEntry { // reminder: hash tables must be initialized before use HashMap fn_table; + HashMap fn_type_table; }; struct LabelTableEntry { @@ -797,17 +804,15 @@ struct FnTableEntry { LLVMValueRef fn_value; AstNode *proto_node; AstNode *fn_def_node; - bool is_extern; - bool internal_linkage; - unsigned calling_convention; ImportTableEntry *import_entry; - bool is_naked; - bool is_inline; // Required to be a pre-order traversal of the AST. (parents must come before children) ZigList all_block_contexts; TypeTableEntry *member_of_struct; Buf symbol_name; TypeTableEntry *type_entry; // function type + bool is_inline; + bool internal_linkage; + bool is_extern; // reminder: hash tables must be initialized before use HashMap label_table; diff --git a/src/analyze.cpp b/src/analyze.cpp index 5ba341b541..17a703c439 100644 --- a/src/analyze.cpp +++ b/src/analyze.cpp @@ -348,6 +348,9 @@ static void resolve_function_proto(CodeGen *g, AstNode *node, FnTableEntry *fn_t assert(node->type == NodeTypeFnProto); AstNodeFnProto *fn_proto = &node->data.fn_proto; + TypeTableEntry *fn_type = new_type_table_entry(TypeTableEntryIdFn); + fn_type->data.fn.calling_convention = fn_table_entry->internal_linkage ? LLVMFastCallConv : LLVMCCallConv; + for (int i = 0; i < fn_proto->directives->length; i += 1) { AstNode *directive_node = fn_proto->directives->at(i); Buf *name = &directive_node->data.directive.name; @@ -356,7 +359,7 @@ static void resolve_function_proto(CodeGen *g, AstNode *node, FnTableEntry *fn_t Buf *attr_name = &directive_node->data.directive.param; if (fn_table_entry->fn_def_node) { if (buf_eql_str(attr_name, "naked")) { - fn_table_entry->is_naked = true; + fn_type->data.fn.is_naked = true; } else if (buf_eql_str(attr_name, "inline")) { fn_table_entry->is_inline = true; } else { @@ -373,20 +376,24 @@ static void resolve_function_proto(CodeGen *g, AstNode *node, FnTableEntry *fn_t } } - int param_count = node->data.fn_proto.params.length; + int src_param_count = node->data.fn_proto.params.length; - TypeTableEntry *fn_type = new_type_table_entry(TypeTableEntryIdFn); - fn_type->data.fn.param_count = param_count; - fn_type->data.fn.param_types = allocate(param_count); + fn_type->size_in_bits = g->pointer_size_bytes * 8; + fn_type->align_in_bits = g->pointer_size_bytes * 8; + fn_type->data.fn.src_param_count = src_param_count; + fn_type->data.fn.param_types = allocate(src_param_count); fn_table_entry->type_entry = fn_type; buf_resize(&fn_type->name, 0); - buf_appendf(&fn_type->name, "fn("); + const char *export_str = fn_table_entry->internal_linkage ? "" : "export "; + const char *inline_str = fn_table_entry->is_inline ? "inline " : ""; + const char *naked_str = fn_type->data.fn.is_naked ? "naked " : ""; + buf_appendf(&fn_type->name, "%s%s%sfn(", export_str, inline_str, naked_str); int gen_param_count = 0; - LLVMTypeRef *gen_param_types = allocate(param_count); - LLVMZigDIType **param_di_types = allocate(1 + param_count); - for (int i = 0; i < param_count; i += 1) { + LLVMTypeRef *gen_param_types = allocate(src_param_count); + LLVMZigDIType **param_di_types = allocate(1 + src_param_count); + for (int i = 0; i < src_param_count; i += 1) { AstNode *child = node->data.fn_proto.params.at(i); assert(child->type == NodeTypeParamDecl); TypeTableEntry *type_entry = analyze_type_expr(g, import, import->block_context, @@ -416,6 +423,13 @@ static void resolve_function_proto(CodeGen *g, AstNode *node, FnTableEntry *fn_t } } + fn_type->data.fn.gen_param_count = gen_param_count; + fn_type->data.fn.is_var_args = fn_proto->is_var_args; + if (fn_proto->is_var_args) { + const char *comma = (gen_param_count == 0) ? "" : ", "; + buf_appendf(&fn_type->name, "%s...", comma); + } + TypeTableEntry *return_type = analyze_type_expr(g, import, import->block_context, node->data.fn_proto.return_type); fn_type->data.fn.return_type = return_type; @@ -432,16 +446,29 @@ static void resolve_function_proto(CodeGen *g, AstNode *node, FnTableEntry *fn_t return; } - fn_type->type_ref = LLVMFunctionType(return_type->type_ref, gen_param_types, gen_param_count, - fn_proto->is_var_args); + auto table_entry = import->fn_type_table.maybe_get(&fn_type->name); + if (table_entry) { + fn_type = table_entry->value; + fn_table_entry->type_entry = fn_type; + } else { + fn_type->data.fn.raw_type_ref = LLVMFunctionType(return_type->type_ref, gen_param_types, gen_param_count, + fn_type->data.fn.is_var_args); + fn_type->type_ref = LLVMPointerType(fn_type->data.fn.raw_type_ref, 0); + param_di_types[0] = return_type->di_type; + fn_type->di_type = LLVMZigCreateSubroutineType(g->dbuilder, import->di_file, + param_di_types, gen_param_count + 1, 0); + + import->fn_type_table.put(&fn_type->name, fn_type); + } + fn_table_entry->fn_value = LLVMAddFunction(g->module, buf_ptr(&fn_table_entry->symbol_name), - fn_table_entry->type_entry->type_ref); + fn_type->data.fn.raw_type_ref); if (fn_table_entry->is_inline) { LLVMAddFunctionAttr(fn_table_entry->fn_value, LLVMAlwaysInlineAttribute); } - if (fn_table_entry->is_naked) { + if (fn_type->data.fn.is_naked) { LLVMAddFunctionAttr(fn_table_entry->fn_value, LLVMNakedAttribute); } @@ -451,15 +478,11 @@ static void resolve_function_proto(CodeGen *g, AstNode *node, FnTableEntry *fn_t if (return_type->id == TypeTableEntryIdUnreachable) { LLVMAddFunctionAttr(fn_table_entry->fn_value, LLVMNoReturnAttribute); } - LLVMSetFunctionCallConv(fn_table_entry->fn_value, fn_table_entry->calling_convention); + LLVMSetFunctionCallConv(fn_table_entry->fn_value, fn_type->data.fn.calling_convention); if (!fn_table_entry->is_extern) { LLVMAddFunctionAttr(fn_table_entry->fn_value, LLVMNoUnwindAttribute); } - param_di_types[0] = return_type->di_type; - LLVMZigDISubroutineType *di_sub_type = LLVMZigCreateSubroutineType(g->dbuilder, import->di_file, - param_di_types, gen_param_count + 1, 0); - // Add debug info. unsigned line_number = node->line + 1; unsigned scope_line = line_number; @@ -469,9 +492,8 @@ static void resolve_function_proto(CodeGen *g, AstNode *node, FnTableEntry *fn_t LLVMZigDISubprogram *subprogram = LLVMZigCreateFunction(g->dbuilder, import->block_context->di_scope, buf_ptr(&fn_table_entry->symbol_name), "", import->di_file, line_number, - di_sub_type, fn_table_entry->internal_linkage, + fn_type->di_type, fn_table_entry->internal_linkage, is_definition, scope_line, flags, is_optimized, fn_table_entry->fn_value); - fn_type->di_type = LLVMZigSubroutineToType(di_sub_type); if (fn_table_entry->fn_def_node) { BlockContext *context = new_block_context(fn_table_entry->fn_def_node, import->block_context); fn_table_entry->fn_def_node->data.fn_def.block_context = context; @@ -802,7 +824,6 @@ static void preview_fn_proto(CodeGen *g, ImportTableEntry *import, fn_table_entry->fn_def_node = fn_def_node; fn_table_entry->internal_linkage = !is_c_compat; fn_table_entry->is_extern = extern_node; - fn_table_entry->calling_convention = is_c_compat ? LLVMCCallConv : LLVMFastCallConv; fn_table_entry->label_table.init(8); fn_table_entry->member_of_struct = struct_type; @@ -1677,6 +1698,7 @@ static TypeTableEntry *analyze_symbol_expr(CodeGen *g, ImportTableEntry *import, auto fn_table_entry = import->fn_table.maybe_get(variable_name); if (fn_table_entry) { + node->data.symbol_expr.fn_entry = fn_table_entry->value; return resolve_expr_const_val_as_fn(g, node, fn_table_entry->value); } @@ -2823,15 +2845,21 @@ static TypeTableEntry *analyze_fn_call_expr(CodeGen *g, ImportTableEntry *import // otherwise we treat this as a function pointer. ConstExprValue *const_val = &get_resolved_expr(fn_ref_expr)->const_val; - if (!const_val->ok) { - add_node_error(g, node, buf_sprintf("function pointers not yet supported")); - return g->builtin_types.entry_invalid; + if (const_val->ok) { + if (invoke_type_entry->id == TypeTableEntryIdMetaType) { + return analyze_cast_expr(g, import, context, node); + } else if (invoke_type_entry->id == TypeTableEntryIdFn) { + return analyze_fn_call_raw(g, import, context, expected_type, node, const_val->data.x_fn, nullptr); + } else { + add_node_error(g, fn_ref_expr, + buf_sprintf("type '%s' not a function", buf_ptr(&invoke_type_entry->name))); + return g->builtin_types.entry_invalid; + } } - if (invoke_type_entry->id == TypeTableEntryIdMetaType) { - return analyze_cast_expr(g, import, context, node); - } else if (invoke_type_entry->id == TypeTableEntryIdFn) { - return analyze_fn_call_raw(g, import, context, expected_type, node, const_val->data.x_fn, nullptr); + // function pointer + if (invoke_type_entry->id == TypeTableEntryIdFn) { + return invoke_type_entry->data.fn.return_type; } else { add_node_error(g, fn_ref_expr, buf_sprintf("type '%s' not a function", buf_ptr(&invoke_type_entry->name))); diff --git a/src/codegen.cpp b/src/codegen.cpp index 8de5fba4e5..b4569640e0 100644 --- a/src/codegen.cpp +++ b/src/codegen.cpp @@ -82,24 +82,11 @@ static TypeTableEntry *get_type_for_type_node(AstNode *node) { return const_val->data.x_type; } -static bool type_is_unreachable(CodeGen *g, AstNode *type_node) { - return get_type_for_type_node(type_node)->id == TypeTableEntryIdUnreachable; -} - static bool is_param_decl_type_void(CodeGen *g, AstNode *param_decl_node) { assert(param_decl_node->type == NodeTypeParamDecl); return get_type_for_type_node(param_decl_node->data.param_decl.type)->size_in_bits == 0; } -static int count_non_void_params(CodeGen *g, ZigList *params) { - int result = 0; - for (int i = 0; i < params->length; i += 1) { - if (!is_param_decl_type_void(g, params->at(i))) - result += 1; - } - return result; -} - static void add_debug_source_node(CodeGen *g, AstNode *node) { if (!g->cur_block_context) return; @@ -446,24 +433,25 @@ static LLVMValueRef gen_fn_call_expr(CodeGen *g, AstNode *node) { } } - assert(fn_table_entry->proto_node->type == NodeTypeFnProto); - AstNodeFnProto *fn_proto_data = &fn_table_entry->proto_node->data.fn_proto; + TypeTableEntry *fn_type; + LLVMValueRef fn_val; + if (fn_table_entry) { + fn_val = fn_table_entry->fn_value; + fn_type = fn_table_entry->type_entry; + } else { + fn_val = gen_expr(g, fn_ref_expr); + fn_type = get_expr_type(fn_ref_expr); + } - int expected_param_count = fn_proto_data->params.length; + int expected_param_count = fn_type->data.fn.src_param_count; int fn_call_param_count = node->data.fn_call_expr.params.length; int actual_param_count = fn_call_param_count + (struct_type ? 1 : 0); - bool is_var_args = fn_proto_data->is_var_args; + bool is_var_args = fn_type->data.fn.is_var_args; assert((is_var_args && actual_param_count >= expected_param_count) || actual_param_count == expected_param_count); // don't really include void values - int gen_param_count; - if (is_var_args) { - gen_param_count = actual_param_count; - } else { - gen_param_count = count_non_void_params(g, &fn_table_entry->proto_node->data.fn_proto.params); - } - LLVMValueRef *gen_param_values = allocate(gen_param_count); + LLVMValueRef *gen_param_values = allocate(actual_param_count); int gen_param_index = 0; if (struct_type) { @@ -474,19 +462,18 @@ static LLVMValueRef gen_fn_call_expr(CodeGen *g, AstNode *node) { for (int i = 0; i < fn_call_param_count; i += 1) { AstNode *expr_node = node->data.fn_call_expr.params.at(i); LLVMValueRef param_value = gen_expr(g, expr_node); - if (is_var_args || - !is_param_decl_type_void(g, fn_table_entry->proto_node->data.fn_proto.params.at(i))) - { + TypeTableEntry *param_type = get_expr_type(expr_node); + if (is_var_args || param_type->size_in_bits > 0) { gen_param_values[gen_param_index] = param_value; gen_param_index += 1; } } add_debug_source_node(g, node); - LLVMValueRef result = LLVMZigBuildCall(g->builder, fn_table_entry->fn_value, - gen_param_values, gen_param_count, fn_table_entry->calling_convention, ""); + LLVMValueRef result = LLVMZigBuildCall(g->builder, fn_val, + gen_param_values, gen_param_index, fn_type->data.fn.calling_convention, ""); - if (type_is_unreachable(g, fn_table_entry->proto_node->data.fn_proto.return_type)) { + if (fn_type->data.fn.return_type->id == TypeTableEntryIdUnreachable) { return LLVMBuildUnreachable(g->builder); } else { return result; @@ -1243,7 +1230,7 @@ static LLVMValueRef gen_unwrap_maybe_expr(CodeGen *g, AstNode *node) { LLVMBasicBlockRef non_null_block = LLVMAppendBasicBlock(g->cur_fn->fn_value, "MaybeNonNull"); LLVMBasicBlockRef null_block = LLVMAppendBasicBlock(g->cur_fn->fn_value, "MaybeNull"); LLVMBasicBlockRef end_block; - + bool non_null_reachable = get_expr_type(op1_node)->id != TypeTableEntryIdUnreachable; bool null_reachable = get_expr_type(op2_node)->id != TypeTableEntryIdUnreachable; bool end_reachable = non_null_reachable || null_reachable; @@ -1951,27 +1938,31 @@ static LLVMValueRef gen_number_literal(CodeGen *g, AstNode *node) { } static LLVMValueRef gen_symbol(CodeGen *g, AstNode *node) { - VariableTableEntry *variable = find_variable( - get_resolved_expr(node)->block_context, - &node->data.symbol_expr.symbol); - assert(variable); - if (variable->type->size_in_bits == 0) { - return nullptr; - } else if (variable->is_ptr) { - assert(variable->value_ref); - if (variable->type->id == TypeTableEntryIdArray) { - return variable->value_ref; - } else if (variable->type->id == TypeTableEntryIdStruct || - variable->type->id == TypeTableEntryIdMaybe) - { - return variable->value_ref; + assert(node->type == NodeTypeSymbol); + VariableTableEntry *variable = node->data.symbol_expr.variable; + if (variable) { + if (variable->type->size_in_bits == 0) { + return nullptr; + } else if (variable->is_ptr) { + assert(variable->value_ref); + if (variable->type->id == TypeTableEntryIdArray) { + return variable->value_ref; + } else if (variable->type->id == TypeTableEntryIdStruct || + variable->type->id == TypeTableEntryIdMaybe) + { + return variable->value_ref; + } else { + add_debug_source_node(g, node); + return LLVMBuildLoad(g->builder, variable->value_ref, ""); + } } else { - add_debug_source_node(g, node); - return LLVMBuildLoad(g->builder, variable->value_ref, ""); + return variable->value_ref; } - } else { - return variable->value_ref; } + + FnTableEntry *fn_entry = node->data.symbol_expr.fn_entry; + assert(fn_entry); + return fn_entry->fn_value; } static LLVMValueRef gen_expr_no_cast(CodeGen *g, AstNode *node) { @@ -2714,6 +2705,7 @@ static ImportTableEntry *codegen_add_code(CodeGen *g, Buf *abs_full_path, import_entry->line_offsets = tokenization.line_offsets; import_entry->path = full_path; import_entry->fn_table.init(32); + import_entry->fn_type_table.init(32); import_entry->root = ast_parse(source_code, tokenization.tokens, import_entry, g->err_color, &g->next_node_index); @@ -3098,7 +3090,7 @@ void codegen_link(CodeGen *g, const char *out_file) { // invoke `ar` // example: // # static link into libfoo.a - // ar rcs libfoo.a foo1.o foo2.o + // ar rcs libfoo.a foo1.o foo2.o zig_panic("TODO invoke ar"); return; } diff --git a/src/zig_llvm.cpp b/src/zig_llvm.cpp index 0bee510fff..a46d591298 100644 --- a/src/zig_llvm.cpp +++ b/src/zig_llvm.cpp @@ -284,7 +284,7 @@ void LLVMZigReplaceDebugArrays(LLVMZigDIBuilder *dibuilder, LLVMZigDIType *type, reinterpret_cast(dibuilder)->getOrCreateArray(fields)); } -LLVMZigDISubroutineType *LLVMZigCreateSubroutineType(LLVMZigDIBuilder *dibuilder_wrapped, +LLVMZigDIType *LLVMZigCreateSubroutineType(LLVMZigDIBuilder *dibuilder_wrapped, LLVMZigDIFile *file, LLVMZigDIType **types_array, int types_array_len, unsigned flags) { SmallVector types; @@ -297,7 +297,8 @@ LLVMZigDISubroutineType *LLVMZigCreateSubroutineType(LLVMZigDIBuilder *dibuilder reinterpret_cast(file), dibuilder->getOrCreateTypeArray(types), flags); - return reinterpret_cast(subroutine_type); + DIType *ditype = subroutine_type; + return reinterpret_cast(ditype); } unsigned LLVMZigEncoding_DW_ATE_unsigned(void) { @@ -388,11 +389,6 @@ LLVMZigDIScope *LLVMZigSubprogramToScope(LLVMZigDISubprogram *subprogram) { return reinterpret_cast(scope); } -LLVMZigDIType *LLVMZigSubroutineToType(LLVMZigDISubroutineType *subrtype) { - DIType *di_type = reinterpret_cast(subrtype); - return reinterpret_cast(di_type); -} - LLVMZigDIScope *LLVMZigTypeToScope(LLVMZigDIType *type) { DIScope *scope = reinterpret_cast(type); return reinterpret_cast(scope); @@ -416,16 +412,17 @@ LLVMZigDIFile *LLVMZigCreateFile(LLVMZigDIBuilder *dibuilder, const char *filena LLVMZigDISubprogram *LLVMZigCreateFunction(LLVMZigDIBuilder *dibuilder, LLVMZigDIScope *scope, const char *name, const char *linkage_name, LLVMZigDIFile *file, unsigned lineno, - LLVMZigDISubroutineType *ty, bool is_local_to_unit, bool is_definition, unsigned scope_line, + LLVMZigDIType *fn_di_type, bool is_local_to_unit, bool is_definition, unsigned scope_line, unsigned flags, bool is_optimized, LLVMValueRef function) { Function *unwrapped_function = reinterpret_cast(unwrap(function)); + DISubroutineType *di_sub_type = static_cast(reinterpret_cast(fn_di_type)); DISubprogram *result = reinterpret_cast(dibuilder)->createFunction( reinterpret_cast(scope), name, linkage_name, reinterpret_cast(file), lineno, - reinterpret_cast(ty), + di_sub_type, is_local_to_unit, is_definition, scope_line, flags, is_optimized, unwrapped_function); return reinterpret_cast(result); } diff --git a/src/zig_llvm.hpp b/src/zig_llvm.hpp index db0452c834..c0e06ae787 100644 --- a/src/zig_llvm.hpp +++ b/src/zig_llvm.hpp @@ -81,7 +81,7 @@ void LLVMZigReplaceTemporary(LLVMZigDIBuilder *dibuilder, LLVMZigDIType *type, void LLVMZigReplaceDebugArrays(LLVMZigDIBuilder *dibuilder, LLVMZigDIType *type, LLVMZigDIType **types_array, int types_array_len); -LLVMZigDISubroutineType *LLVMZigCreateSubroutineType(LLVMZigDIBuilder *dibuilder_wrapped, +LLVMZigDIType *LLVMZigCreateSubroutineType(LLVMZigDIBuilder *dibuilder_wrapped, LLVMZigDIFile *file, LLVMZigDIType **types_array, int types_array_len, unsigned flags); unsigned LLVMZigEncoding_DW_ATE_unsigned(void); @@ -101,7 +101,6 @@ LLVMZigDIScope *LLVMZigCompileUnitToScope(LLVMZigDICompileUnit *compile_unit); LLVMZigDIScope *LLVMZigFileToScope(LLVMZigDIFile *difile); LLVMZigDIScope *LLVMZigSubprogramToScope(LLVMZigDISubprogram *subprogram); LLVMZigDIScope *LLVMZigTypeToScope(LLVMZigDIType *type); -LLVMZigDIType *LLVMZigSubroutineToType(LLVMZigDISubroutineType *subrtype); LLVMZigDILocalVariable *LLVMZigCreateLocalVariable(LLVMZigDIBuilder *dbuilder, unsigned tag, LLVMZigDIScope *scope, const char *name, LLVMZigDIFile *file, unsigned line_no, @@ -119,7 +118,7 @@ LLVMZigDIFile *LLVMZigCreateFile(LLVMZigDIBuilder *dibuilder, const char *filena LLVMZigDISubprogram *LLVMZigCreateFunction(LLVMZigDIBuilder *dibuilder, LLVMZigDIScope *scope, const char *name, const char *linkage_name, LLVMZigDIFile *file, unsigned lineno, - LLVMZigDISubroutineType *ty, bool is_local_to_unit, bool is_definition, unsigned scope_line, + LLVMZigDIType *fn_di_type, bool is_local_to_unit, bool is_definition, unsigned scope_line, unsigned flags, bool is_optimized, LLVMValueRef function); void LLVMZigDIBuilderFinalize(LLVMZigDIBuilder *dibuilder); diff --git a/std/bootstrap.zig b/std/bootstrap.zig index 222b15dae7..b337444671 100644 --- a/std/bootstrap.zig +++ b/std/bootstrap.zig @@ -15,7 +15,7 @@ export fn _start() unreachable => { call_main() } -fn strlen(ptr: &u8) usize => { +fn strlen(ptr: &const u8) usize => { var count: usize = 0; while (ptr[count] != 0) { count += 1; diff --git a/test/run_tests.cpp b/test/run_tests.cpp index c5b816e91d..0e032c64d3 100644 --- a/test/run_tests.cpp +++ b/test/run_tests.cpp @@ -1162,6 +1162,24 @@ pub fn main(args: [][]u8) i32 => { return 0; } )SOURCE", "9\n8\n7\n6\n0\n1\n2\n3\n9\n8\n7\n6\n0\n1\n2\n3\n"); + + add_simple_case("function pointers", R"SOURCE( +import "std.zig"; + +pub fn main(args: [][]u8) i32 => { + const fns = []@typeof(fn1) { fn1, fn2, fn3, fn4, }; + for (f, fns) { + print_u64(f()); + print_str("\n"); + } + return 0; +} + +fn fn1() u32 => {5} +fn fn2() u32 => {6} +fn fn3() u32 => {7} +fn fn4() u32 => {8} + )SOURCE", "5\n6\n7\n8\n"); }