From 2d3d0bd266f3b73f00c2067cca47e6210f63752a Mon Sep 17 00:00:00 2001 From: hl-valdemar Date: Sun, 14 Jun 2026 15:53:45 +0200 Subject: [PATCH] c variadic calls --- LANGUAGE.md | 2 +- README.md | 13 ++ TODO.md | 2 +- compiler/ast/ast.odin | 1 + compiler/checker/checker.odin | 78 ++++++++-- compiler/cimport/cimport.odin | 9 +- compiler/cimport/libclang.odin | 4 +- compiler/hir/hir.odin | 2 + compiler/ir/ir.odin | 2 + compiler/lexer/lexer.odin | 7 +- compiler/llvm/llvm.odin | 70 ++++++++- compiler/loader/loader.odin | 7 +- compiler/lower/lower.odin | 6 +- compiler/parser/parser.odin | 24 ++- compiler/token/token.odin | 1 + compiler/types/types.odin | 28 ++++ compiler_tests.odin | 154 +++++++++++++++++++ examples/interop/header/app/main.bro | 7 + examples/interop/header/include/native.h | 2 +- examples/interop/header/native.c | 24 +++ examples/interop/header_unsupported/main.bro | 1 - 21 files changed, 409 insertions(+), 35 deletions(-) diff --git a/LANGUAGE.md b/LANGUAGE.md index d9c813c..c2ed08d 100644 --- a/LANGUAGE.md +++ b/LANGUAGE.md @@ -34,6 +34,7 @@ - file-local relative imports, aliases, and qualified member access - relative `.h` imports as synthetic package namespaces - transitive external C function prototypes, typedef chains, C scalars, and pointers to opaque C records +- bodyless manual and imported C variadic declarations with target-aware default argument promotions - reference-time diagnostics for unsupported imported C declarations ### compiler behavior @@ -50,7 +51,6 @@ ### foreign functions and linking -- c variadic calls with default argument promotions - exporting brolang functions to c ### scalar and compound types diff --git a/README.md b/README.md index c7e744f..b028890 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,18 @@ pointers to opaque records. They never add linker inputs; implementations must still be supplied explicitly with the C-prefixed linking options. Set `BROLANG_LIBCLANG_PATH` when libclang is not installed in a standard location. +Bodyless manual and imported C functions may be variadic: + +```bro +log_values :: c_func(tag c_int, ...) c_int +``` + +Arguments after `...` accept concrete scalars, pointers, and nullable pointers. +Narrow integers are promoted to the target C `int` or `unsigned int`, and +`f32`/`c_float` are promoted to `c_double`. Arrays, slices, structs, and other +compound values must be converted to an explicit C-compatible representation +before the call. + Compilation phases are isolated under `compiler/`: ```text @@ -83,6 +95,7 @@ Current prototype features: - Qualified imported globals and functions with package-aware symbol mangling - Demand-monomorphized Brolang and C-ABI functions - Bodyless concrete C function declarations with exact external symbol names +- Bodyless manual and imported C variadic declarations with default argument promotions - Ordered linking of additional C sources, objects, archives, and libraries - Checked signed addition and unary negation - Static, eager runtime, and deferred problematic globals diff --git a/TODO.md b/TODO.md index bd00fd1..197af41 100644 --- a/TODO.md +++ b/TODO.md @@ -38,7 +38,7 @@ - diagnose unsupported declarations when referenced - dynamically load libclang behind a replaceable c importer boundary -3. c variadic calls +3. c variadic calls (implemented) - represent c variadics as a fixed parameter count plus a variadic flag - apply c default argument promotions at call sites - emit LLVM c-variadic declarations and calls diff --git a/compiler/ast/ast.odin b/compiler/ast/ast.odin index 311153a..ccacfac 100644 --- a/compiler/ast/ast.odin +++ b/compiler/ast/ast.odin @@ -130,6 +130,7 @@ Function :: struct { c_abi: bool, imported: bool, has_body: bool, + variadic: bool, params: []Param, result: Type_Syntax, body: []Stmt_Id, diff --git a/compiler/checker/checker.odin b/compiler/checker/checker.odin index da3f722..99c4cb8 100644 --- a/compiler/checker/checker.odin +++ b/compiler/checker/checker.odin @@ -454,7 +454,7 @@ add_unsupported_type_diagnostic :: proc( } function_signatures_equal :: proc(left, right: ast.Function) -> bool { - if left.result != right.result || len(left.params) != len(right.params) { + if left.result != right.result || left.variadic != right.variadic || len(left.params) != len(right.params) { return false } for param, index in left.params { @@ -465,6 +465,17 @@ function_signatures_equal :: proc(left, right: ast.Function) -> bool { return true } +valid_call_arity :: proc(function: ast.Function, count: int) -> bool { + return count >= len(function.params) if function.variadic else count == len(function.params) +} + +call_arg_expected :: proc(function: ast.Function, index: int) -> types.Type { + if index < 0 || index >= len(function.params) { + return types.INVALID + } + return type_from_syntax(function.params[index].type) +} + contains_name :: proc(names: []symbol.Id, name: symbol.Id) -> bool { for existing in names { if existing == name { @@ -566,6 +577,14 @@ validate_declarations :: proc(checker: ^Checker) { symbol_text(checker, function.name), ) } + if function.variadic && (!function.c_abi || function.has_body) { + checker.template_diagnostics[function_id] = source.addf( + checker.diagnostics, + function.span, + "variadic function '%s' must be a bodyless 'c_func' declaration", + symbol_text(checker, function.name), + ) + } if !function.has_body && function.c_abi { for param in function.params { if add_unsupported_type_diagnostic(checker, param.span, type_from_syntax(param.type)) != @@ -1029,11 +1048,12 @@ infer_expr :: proc( } } function := checker.ast_module.functions[frame.template] - if can_specialize(checker, function, stack[frame_index].args) { + if valid_call_arity(function, len(expr.args)) && + can_specialize(checker, function, stack[frame_index].args) { spec := INVALID_SPEC if demanded == nil { spec = ensure_spec(checker, frame.template, stack[frame_index].args) - } else if len(expr.args) == len(function.params) { + } else { spec = find_spec(checker, frame.template, stack[frame_index].args) mark_spec_demanded(checker, spec, demanded) } @@ -1317,6 +1337,32 @@ coerce_expr :: proc( return invalid_hir_expr(checker, span, id, expected) } +promote_c_vararg_expr :: proc(checker: ^Checker, expr_id: hir.Expr_Id, span: source.Span) -> hir.Expr_Id { + actual := checker.module.exprs[expr_id].type + if !types.is_c_vararg_type(actual, &checker.module.types) { + id := source.addf( + checker.diagnostics, + span, + "C variadic argument must be a concrete scalar or pointer, got %s", + types.name(actual), + ) + return invalid_hir_expr(checker, span, id, types.C_INT) + } + promoted := types.c_vararg_promotion(actual, checker.target) + if types.equal(actual, promoted) { + return expr_id + } + return add_hir_expr(checker, hir.Expr{ + kind=.C_Vararg_Promote, + span=span, + type=promoted, + left=expr_id, + target=hir.INVALID_REF, + right=hir.INVALID_EXPR, + diagnostic=source.INVALID_DIAGNOSTIC, + }) +} + build_constant_expr :: proc( checker: ^Checker, expr: ast.Expr, @@ -1891,13 +1937,16 @@ build_expr :: proc( _ = pop(&stack) continue } - if len(expr.args) != len(checker.ast_module.functions[template].params) { + function := checker.ast_module.functions[template] + if !valid_call_arity(function, len(expr.args)) { + message := "function '%s' expects at least %d arguments, got %d" if function.variadic else + "function '%s' expects %d arguments, got %d" id := source.addf( checker.diagnostics, expr.span, - "function '%s' expects %d arguments, got %d", + message, symbol_text(checker, expr.name), - len(checker.ast_module.functions[template].params), + len(function.params), len(expr.args), ) last = invalid_hir_expr(checker, expr.span, id) @@ -1909,7 +1958,7 @@ build_expr :: proc( stack[frame_index].arg_types = make([]types.Type, len(expr.args), checker.allocator) stack[frame_index].stage = 3 if len(expr.args) > 0 { - arg_expected := type_from_syntax(checker.ast_module.functions[template].params[0].type) + arg_expected := call_arg_expected(function, 0) if !is_runtime_type(checker, arg_expected) { arg_expected = types.INVALID } @@ -1980,7 +2029,7 @@ build_expr :: proc( stack[frame_index].arg_index += 1 if frame.arg_index+1 < len(expr.args) { next := frame.arg_index+1 - arg_expected := type_from_syntax(checker.ast_module.functions[frame.template].params[next].type) + arg_expected := call_arg_expected(checker.ast_module.functions[frame.template], next) if !is_runtime_type(checker, arg_expected) { arg_expected = types.INVALID } @@ -2004,7 +2053,8 @@ build_expr :: proc( _ = pop(&stack) continue } - for _, index in stack[frame_index].built_args { + fixed_count := len(checker.ast_module.functions[frame.template].params) + for index in 0.. i32 { linkage := ctx.api.get_cursor_linkage(cursor) if linkage != CXLinkage_External { reason = "static and non-external C functions are not supported" - } else if ctx.api.cursor_is_variadic(cursor) != 0 { - reason = "C variadic functions are not supported" } + variadic := ctx.api.cursor_is_variadic(cursor) != 0 function_type := ctx.api.get_cursor_type(cursor) result_type := translate_type(ctx, ctx.api.get_result_type(function_type)) if result_type == INVALID_TYPE && len(reason) == 0 { @@ -364,6 +363,7 @@ visit_cursor :: proc "c"(cursor, parent: CXCursor, client_data: rawptr) -> i32 { name=fmt.aprintf("%s", name, allocator=ctx.allocator), params=params[:], result=result_type, + variadic=variadic, reason=fmt.aprintf("%s", reason, allocator=ctx.allocator), }) case CXCursor_TypedefDecl: diff --git a/compiler/hir/hir.odin b/compiler/hir/hir.odin index f10a134..4f34099 100644 --- a/compiler/hir/hir.odin +++ b/compiler/hir/hir.odin @@ -93,6 +93,7 @@ Expr_Kind :: enum u8 { Unwrap, Orelse, Widen, + C_Vararg_Promote, Weaken_Pointer, Negate, Add, @@ -144,6 +145,7 @@ Function :: struct { implementation: Implementation, linkage: Linkage, is_main: bool, + variadic: bool, params: []Local_Id, result: types.Type, locals: []Local, diff --git a/compiler/ir/ir.odin b/compiler/ir/ir.odin index eb2d826..2bf49fe 100644 --- a/compiler/ir/ir.odin +++ b/compiler/ir/ir.odin @@ -87,6 +87,7 @@ Opcode :: enum u8 { Orelse_Begin, Orelse, Widen, + C_Vararg_Promote, Weaken_Pointer, Neg_Checked, Add_Checked, @@ -115,6 +116,7 @@ Function :: struct { implementation: Implementation, linkage: Linkage, is_main: bool, + variadic: bool, param_types: []types.Type, result: types.Type, instructions: []Instruction, diff --git a/compiler/lexer/lexer.odin b/compiler/lexer/lexer.odin index e8e306a..591984f 100644 --- a/compiler/lexer/lexer.odin +++ b/compiler/lexer/lexer.odin @@ -119,7 +119,12 @@ lex :: proc( cursor += 1 if cursor < len(bytes) && bytes[cursor] == '.' { cursor += 1 - append_token(&stream, source_file, .Range, start, cursor) + if cursor < len(bytes) && bytes[cursor] == '.' { + cursor += 1 + append_token(&stream, source_file, .Ellipsis, start, cursor) + } else { + append_token(&stream, source_file, .Range, start, cursor) + } } else { append_token(&stream, source_file, .Dot, start, cursor) } diff --git a/compiler/llvm/llvm.odin b/compiler/llvm/llvm.odin index 9be0972..edeffb0 100644 --- a/compiler/llvm/llvm.odin +++ b/compiler/llvm/llvm.odin @@ -112,7 +112,7 @@ valid_value :: proc( switch instructions[value_id].op { case .Param, .Const, .String, .Aggregate, .None, .Optional_Some, .Load_Global, .Address_Of, .Load, .Slice, .Length, .Slice_Ptr, .Unwrap, .Orelse, - .Widen, .Weaken_Pointer, .Neg_Checked, .Add_Checked, .Pointer_Add, .Call: + .Widen, .C_Vararg_Promote, .Weaken_Pointer, .Neg_Checked, .Add_Checked, .Pointer_Add, .Call: return true case .Address_Global, .Alloca, .Index_Address, .Field_Address, .Orelse_Begin, .Store, .Trap, .Return, .Return_Void: @@ -275,14 +275,16 @@ emit_call_args :: proc( if index > 0 { strings.write_string(builder, ", ") } - fmt.sbprintf(builder, "%s ", llvm_type(param_types[index], store)) - if c_abi { - extension := c_abi_extension(param_types[index], store.selected) + arg_type := param_types[index] if index < len(param_types) && valid_instruction(instructions, arg) else + (instructions[arg].type if valid_instruction(instructions, arg) else types.INVALID) + fmt.sbprintf(builder, "%s ", llvm_type(arg_type, store)) + if c_abi && index < len(param_types) { + extension := c_abi_extension(arg_type, store.selected) if len(extension) > 0 { fmt.sbprintf(builder, "%s ", extension) } } - write_operand(builder, instructions, arg, param_types[index], store) + write_operand(builder, instructions, arg, arg_type, store) } } @@ -707,6 +709,31 @@ emit_instruction_stream :: proc( fmt.sbprintf(&emitter.builder, " %%v%d = %s %s ", instruction_index, operation, llvm_type(from_type, &emitter.module.types)) write_operand(&emitter.builder, instructions, instruction.a, from_type, &emitter.module.types) fmt.sbprintf(&emitter.builder, " to %s\n", llvm_type(instruction.type, &emitter.module.types)) + case .C_Vararg_Promote: + if !valid_instruction(instructions, instruction.a) { + emit_recovery_value(emitter, instruction_index, instruction, "invalid C variadic promotion operand") + continue + } + from_type := instructions[instruction.a].type + if types.equal(from_type, instruction.type) || + !types.equal(types.c_vararg_promotion(from_type, emitter.module.target), instruction.type) { + emit_recovery_value(emitter, instruction_index, instruction, "invalid C variadic promotion operand") + continue + } + if types.bits(from_type, emitter.module.target) == types.bits(instruction.type, emitter.module.target) { + type_name := llvm_type(instruction.type, &emitter.module.types) + fmt.sbprintf(&emitter.builder, " %%v%d = select i1 true, %s ", instruction_index, type_name) + write_operand(&emitter.builder, instructions, instruction.a, from_type, &emitter.module.types) + fmt.sbprintf(&emitter.builder, ", %s ", type_name) + write_operand(&emitter.builder, instructions, instruction.a, from_type, &emitter.module.types) + strings.write_string(&emitter.builder, "\n") + continue + } + operation := "fpext" if types.is_float(from_type, emitter.module.target) else + ("sext" if types.is_signed(from_type, emitter.module.target) else "zext") + fmt.sbprintf(&emitter.builder, " %%v%d = %s %s ", instruction_index, operation, llvm_type(from_type, &emitter.module.types)) + write_operand(&emitter.builder, instructions, instruction.a, from_type, &emitter.module.types) + fmt.sbprintf(&emitter.builder, " to %s\n", llvm_type(instruction.type, &emitter.module.types)) case .Weaken_Pointer: if !valid_instruction(instructions, instruction.a) || !types.can_weaken_pointer(instructions[instruction.a].type, instruction.type, &emitter.module.types) { @@ -806,10 +833,20 @@ emit_instruction_stream :: proc( continue } target := emitter.module.functions[function_id] - valid_args := len(instruction.args) == len(target.param_types) + valid_args := (len(instruction.args) >= len(target.param_types) if target.variadic else + len(instruction.args) == len(target.param_types)) && + (!target.variadic || target.calling_convention == .C) if valid_args { for arg, index in instruction.args { - if !valid_value(instructions, arg, target.param_types[index], &emitter.module.types) { + expected := target.param_types[index] if index < len(target.param_types) && valid_instruction(instructions, arg) else + (instructions[arg].type if valid_instruction(instructions, arg) else types.INVALID) + if index >= len(target.param_types) && + (!types.is_c_vararg_type(expected, &emitter.module.types) || + !types.equal(types.c_vararg_promotion(expected, emitter.module.target), expected)) { + valid_args = false + break + } + if !valid_value(instructions, arg, expected, &emitter.module.types) { valid_args = false break } @@ -833,6 +870,19 @@ emit_instruction_stream :: proc( strings.write_string(&emitter.builder, "fastcc ") } emit_function_result(&emitter.builder, target, &emitter.module.types) + if target.variadic { + strings.write_string(&emitter.builder, " (") + for param_type, index in target.param_types { + if index > 0 { + strings.write_string(&emitter.builder, ", ") + } + strings.write_string(&emitter.builder, llvm_type(param_type, &emitter.module.types)) + } + if len(target.param_types) > 0 { + strings.write_string(&emitter.builder, ", ") + } + strings.write_string(&emitter.builder, "...)") + } fmt.sbprintf(&emitter.builder, " @%s(", target.link_name) emit_call_args( &emitter.builder, instructions, instruction.args, target.param_types, @@ -1037,6 +1087,12 @@ emit_functions :: proc(emitter: ^Emitter) { fmt.sbprintf(&emitter.builder, " %%v%d", index) } } + if function.variadic { + if len(function.param_types) > 0 { + strings.write_string(&emitter.builder, ", ") + } + strings.write_string(&emitter.builder, "...") + } if function.implementation == .Declaration { strings.write_string(&emitter.builder, ")\n\n") continue diff --git a/compiler/loader/loader.odin b/compiler/loader/loader.odin index 20cf65f..1d6b764 100644 --- a/compiler/loader/loader.odin +++ b/compiler/loader/loader.odin @@ -177,8 +177,8 @@ translate_c_type :: proc( return translated } -function_signatures_equal :: proc(left: ast.Function, params: []ast.Param, result: types.Type) -> bool { - if left.result != result || len(left.params) != len(params) { +function_signatures_equal :: proc(left: ast.Function, params: []ast.Param, result: types.Type, variadic: bool) -> bool { + if left.result != result || left.variadic != variadic || len(left.params) != len(params) { return false } for param, index in params { @@ -282,7 +282,7 @@ load_header :: proc(state: ^State, path: string, import_span: source.Span) -> as continue } duplicate = true - if !function_signatures_equal(existing, params, function_result) && len(existing.unsupported_reason) == 0 { + if !function_signatures_equal(existing, params, function_result, function.variadic) && len(existing.unsupported_reason) == 0 { existing.unsupported_reason = fmt.aprintf( "conflicting C declarations for '%s'", function.name, @@ -303,6 +303,7 @@ load_header :: proc(state: ^State, path: string, import_span: source.Span) -> as c_abi=true, imported=true, has_body=false, + variadic=function.variadic, params=params, result=function_result, unsupported_reason=strings.clone(unsupported_reason, state.allocator), diff --git a/compiler/lower/lower.odin b/compiler/lower/lower.odin index 4d8d157..029cee5 100644 --- a/compiler/lower/lower.odin +++ b/compiler/lower/lower.odin @@ -324,7 +324,7 @@ lower_expr :: proc(state: ^State, expr_id: hir.Expr_Id) -> ir.Instruction_Id { }) } _ = pop(&stack) - case .Widen, .Weaken_Pointer: + case .Widen, .C_Vararg_Promote, .Weaken_Pointer: stack[frame_index].stage = 1 append(&stack, Lower_Expr_Frame{expr=expr.left}) case .Negate: @@ -358,7 +358,8 @@ lower_expr :: proc(state: ^State, expr_id: hir.Expr_Id) -> ir.Instruction_Id { } if frame.stage == 1 { last = append_instruction(state, ir.Instruction{ - op=.Weaken_Pointer if expr.kind == .Weaken_Pointer else .Widen, + op=.Weaken_Pointer if expr.kind == .Weaken_Pointer else + (.C_Vararg_Promote if expr.kind == .C_Vararg_Promote else .Widen), span=expr.span, type=expr.type, target=ir.INVALID_REF, a=last, b=ir.INVALID_INSTRUCTION, diagnostic=source.INVALID_DIAGNOSTIC, }) @@ -611,6 +612,7 @@ lower :: proc(hir_module: ^hir.Module, allocator := context.allocator) -> ir.Mod implementation=.Declaration if function.implementation == .Declaration else .Definition, linkage=.External if function.linkage == .External else .Internal, is_main=function.is_main, + variadic=function.variadic, param_types=param_types, result=function.result, instructions=nil if function.implementation == .Declaration else lower_body(hir_module, function, allocator), diff --git a/compiler/parser/parser.odin b/compiler/parser/parser.odin index d3f98d7..526a3dc 100644 --- a/compiler/parser/parser.odin +++ b/compiler/parser/parser.odin @@ -889,11 +889,27 @@ parse_statement :: proc(parser: ^Parser) -> ast.Stmt_Id { return id } -parse_params :: proc(parser: ^Parser) -> []ast.Param { +parse_params :: proc(parser: ^Parser) -> ([]ast.Param, bool) { params: [dynamic]ast.Param params.allocator = parser.module.allocator + variadic := false skip_newlines(parser) for current(parser).kind != .Right_Paren && current(parser).kind != .Eof { + if current(parser).kind == .Ellipsis { + marker := advance(parser) + if variadic { + source.add(parser.diagnostics, marker.span, "duplicate variadic marker") + } + variadic = true + skip_newlines(parser) + if _, ok := allow(parser, .Comma); ok { + skip_newlines(parser) + } + if current(parser).kind != .Right_Paren { + source.add(parser.diagnostics, current(parser).span, "variadic marker must be the final parameter") + } + continue + } names: [dynamic]token.Token names.allocator = parser.module.allocator for { @@ -923,7 +939,7 @@ parse_params :: proc(parser: ^Parser) -> []ast.Param { } break } - return params[:] + return params[:], variadic } parse_function :: proc(parser: ^Parser, name: token.Token, c_abi: bool) { @@ -931,7 +947,7 @@ parse_function :: proc(parser: ^Parser, name: token.Token, c_abi: bool) { if _, ok := allow(parser, .Left_Paren); !ok { source.add(parser.diagnostics, current(parser).span, "expected '(' after 'func'") } - params := parse_params(parser) + params, variadic := parse_params(parser) if _, ok := allow(parser, .Right_Paren); !ok { source.add(parser.diagnostics, current(parser).span, "expected ')' after parameters") } @@ -954,6 +970,7 @@ parse_function :: proc(parser: ^Parser, name: token.Token, c_abi: bool) { file=parser.file, c_abi=c_abi, has_body=false, + variadic=variadic, params=params, result=result, diagnostic=source.INVALID_DIAGNOSTIC, @@ -991,6 +1008,7 @@ parse_function :: proc(parser: ^Parser, name: token.Token, c_abi: bool) { file=parser.file, c_abi=c_abi, has_body=true, + variadic=variadic, params=params, result=result, body=body[:], diff --git a/compiler/token/token.odin b/compiler/token/token.odin index 484a54c..c25b34a 100644 --- a/compiler/token/token.odin +++ b/compiler/token/token.odin @@ -19,6 +19,7 @@ Kind :: enum u8 { Minus, Dot, Range, + Ellipsis, At, Star, Ampersand, diff --git a/compiler/types/types.odin b/compiler/types/types.odin index ee7b500..8ea72ff 100644 --- a/compiler/types/types.odin +++ b/compiler/types/types.odin @@ -432,6 +432,34 @@ is_c_signature_type :: proc(value: Type, store: ^Store, allow_void := false) -> return is_concrete_scalar(value) || is_pointer(value, store) || is_optional_pointer(value, store) } +is_c_integer_promotion_candidate :: proc(value: Type) -> bool { + return value >= C_CHAR && value <= C_USHORT +} + +c_vararg_promotion :: proc(value: Type, selected := target.DEFAULT) -> Type { + if !is_concrete_scalar(value) { + return value + } + if is_float(value, selected) && bits(value, selected) < bits(C_DOUBLE, selected) { + return C_DOUBLE + } + if is_concrete_integer(value) { + value_bits := bits(value, selected) + int_bits := bits(C_INT, selected) + if value_bits < int_bits { + return C_INT + } + if is_c_integer_promotion_candidate(value) && value_bits == int_bits { + return C_INT if is_signed(value, selected) else C_UINT + } + } + return value +} + +is_c_vararg_type :: proc(value: Type, store: ^Store) -> bool { + return is_concrete_scalar(value) || is_pointer(value, store) || is_optional_pointer(value, store) +} + child_type :: proc(value: Type, store: ^Store) -> Type { item, ok := node(store, value) return item.child if ok else INVALID diff --git a/compiler_tests.odin b/compiler_tests.odin index c4b30a2..71293c3 100644 --- a/compiler_tests.odin +++ b/compiler_tests.odin @@ -229,6 +229,38 @@ main :: func() void {} testing.expect(t, module.functions[3].has_body) } +@(test) +parser_accepts_terminal_c_variadic_markers_and_recovers_nonterminal_markers :: proc(t: ^testing.T) { + text := `fixed :: c_func(value c_int, ...) c_int +zero :: c_func(...) void +bad :: c_func(..., value c_int) c_int +main :: func() void {} +` + source_file := source.Source{path="test.bro", text=text} + diagnostics := source.init_diagnostics(&source_file) + defer source.destroy_diagnostics(&diagnostics) + symbols := symbol.init_table() + defer symbol.destroy_table(&symbols) + stream := lexer.lex(&source_file, &diagnostics, &symbols) + defer delete(stream.items) + module := parser.parse(&stream, &source_file, &diagnostics) + defer ast.destroy_module(&module) + + found_ellipsis := false + for tok in stream.items { + found_ellipsis = found_ellipsis || tok.kind == .Ellipsis + } + testing.expect(t, found_ellipsis) + testing.expect(t, module.functions[0].variadic) + testing.expect_value(t, len(module.functions[0].params), 1) + testing.expect(t, module.functions[1].variadic) + testing.expect_value(t, len(module.functions[1].params), 0) + testing.expect(t, module.functions[2].variadic) + testing.expect_value(t, len(module.functions[2].params), 1) + testing.expect_value(t, len(diagnostics.items), 1) + testing.expect(t, strings.contains(diagnostics.items[0].message, "final parameter")) +} + @(test) parser_treats_c_as_contextual_only_before_func :: proc(t: ^testing.T) { text := `c :: 5 @@ -680,6 +712,14 @@ c_primitives_remain_distinct_with_apple_silicon_representations :: proc(t: ^test testing.expect_value(t, types.representation(types.C_FLOAT), types.F32) testing.expect_value(t, types.representation(types.C_DOUBLE), types.F64) testing.expect_value(t, types.representation(types.C_LONGDOUBLE), types.F64) + testing.expect_value(t, types.c_vararg_promotion(types.I8), types.C_INT) + testing.expect_value(t, types.c_vararg_promotion(types.U16), types.C_INT) + testing.expect_value(t, types.c_vararg_promotion(types.C_CHAR), types.C_INT) + testing.expect_value(t, types.c_vararg_promotion(types.C_USHORT), types.C_INT) + testing.expect_value(t, types.c_vararg_promotion(types.F32), types.C_DOUBLE) + testing.expect_value(t, types.c_vararg_promotion(types.C_FLOAT), types.C_DOUBLE) + testing.expect_value(t, types.c_vararg_promotion(types.U32), types.U32) + testing.expect_value(t, types.c_vararg_promotion(types.C_DOUBLE), types.C_DOUBLE) testing.expect_value(t, target.llvm_triple(target.DEFAULT), "arm64-apple-macosx13.0.0") } @@ -744,6 +784,113 @@ main :: func() void { testing.expect(t, strings.contains(llvm_text, "orelse_some")) } +@(test) +c_variadic_calls_promote_extras_and_emit_variadic_llvm :: proc(t: ^testing.T) { +text := `variadic :: c_func(tag c_int, ...) c_int +zero :: c_func(...) void +main :: func() void { + narrow i8 :: -2 + unsigned u16 :: 3 + float_value f32 :: 4.0 + c_float_value c_float :: 5.0 + pointer *u8 :: "ok".ptr + nullable ?*u8 :: pointer + zero(pointer) + _ = variadic(7, narrow, unsigned, float_value, c_float_value, pointer, nullable) +} +` + source_file := source.Source{path="test.bro", text=text} + diagnostics := source.init_diagnostics(&source_file) + defer source.destroy_diagnostics(&diagnostics) + symbols := symbol.init_table() + defer symbol.destroy_table(&symbols) + stream := lexer.lex(&source_file, &diagnostics, &symbols) + defer delete(stream.items) + ast_module := parser.parse(&stream, &source_file, &diagnostics) + defer ast.destroy_module(&ast_module) + hir_module := checker.check(&ast_module, &diagnostics, &symbols) + defer hir.destroy_module(&hir_module) + ir_module := lower.lower(&hir_module) + defer ir.destroy_module(&ir_module) + llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols) + defer delete(llvm_text) + + promotions := 0 + for expr in hir_module.exprs { + if expr.kind == .C_Vararg_Promote { + promotions += 1 + } + } + testing.expect_value(t, len(diagnostics.items), 0) + testing.expect_value(t, promotions, 4) + found_hir_variadic := false + for function in hir_module.functions { + found_hir_variadic = found_hir_variadic || function.variadic + } + found_ir_variadic := false + for function in ir_module.functions { + found_ir_variadic = found_ir_variadic || function.variadic + } + testing.expect(t, found_hir_variadic) + testing.expect(t, found_ir_variadic) + testing.expect(t, strings.contains(llvm_text, "declare i32 @variadic(i32, ...)")) + testing.expect(t, strings.contains(llvm_text, "declare void @zero(...)")) + testing.expect(t, strings.contains(llvm_text, "sext i8")) + testing.expect(t, strings.contains(llvm_text, "zext i16")) + testing.expect(t, strings.contains(llvm_text, "fpext float")) + testing.expect(t, strings.contains(llvm_text, "call void (...) @zero(ptr")) + testing.expect(t, strings.contains(llvm_text, "call i32 (i32, ...) @variadic(i32 7, i32")) + testing.expect(t, strings.contains(llvm_text, "double")) + testing.expect(t, strings.contains(llvm_text, "ptr")) +} + +@(test) +c_variadic_restrictions_and_extra_argument_types_are_diagnosed :: proc(t: ^testing.T) { + text := `foreign :: c_func(...) void +requires :: c_func(value c_int, ...) void +native :: func(...) void +bodyful :: c_func(...) void {} +main :: func() void { + values [1]u8 :: [1] + foreign(values) + requires() +} +` + source_file := source.Source{path="test.bro", text=text} + diagnostics := source.init_diagnostics(&source_file) + defer source.destroy_diagnostics(&diagnostics) + symbols := symbol.init_table() + defer symbol.destroy_table(&symbols) + stream := lexer.lex(&source_file, &diagnostics, &symbols) + defer delete(stream.items) + ast_module := parser.parse(&stream, &source_file, &diagnostics) + defer ast.destroy_module(&ast_module) + hir_module := checker.check(&ast_module, &diagnostics, &symbols) + defer hir.destroy_module(&hir_module) + + restricted := 0 + found_extra := false + found_arity := false + for diagnostic in diagnostics.items { + if strings.contains(diagnostic.message, "must be a bodyless 'c_func' declaration") { + restricted += 1 + } + found_extra = found_extra || strings.contains(diagnostic.message, "C variadic argument must be a concrete scalar or pointer") + found_arity = found_arity || strings.contains(diagnostic.message, "expects at least 1 arguments") + } + testing.expect_value(t, restricted, 2) + testing.expect(t, found_extra) + testing.expect(t, found_arity) +} + +@(test) +variadicness_is_part_of_c_function_signature_compatibility :: proc(t: ^testing.T) { + fixed := ast.Function{result=types.C_INT} + variadic := ast.Function{result=types.C_INT, variadic=true} + testing.expect(t, !checker.function_signatures_equal(fixed, variadic)) + testing.expect(t, !loader.function_signatures_equal(fixed, nil, types.C_INT, true)) +} + @(test) c_structs_are_pointer_only_and_may_be_opaque :: proc(t: ^testing.T) { text := `Defined :: c_struct { @@ -1683,6 +1830,7 @@ fake_cimport_backend :: proc(user_data: rawptr, request: cimport.Request, alloca append(&result.functions, cimport.Function{ name=fmt.aprintf("fake_value", allocator=allocator), result=cimport.Type_Id(0), + variadic=true, reason=fmt.aprintf("", allocator=allocator), }) result.available = true @@ -1700,6 +1848,7 @@ cimport_backend_is_replaceable :: proc(t: ^testing.T) { testing.expect_value(t, state.calls, 1) testing.expect_value(t, len(result.functions), 1) testing.expect_value(t, result.functions[0].name, "fake_value") + testing.expect(t, result.functions[0].variadic) } @(test) @@ -1732,6 +1881,11 @@ loader_injects_and_caches_cimport_backend_per_compilation :: proc(t: ^testing.T) testing.expect_value(t, len(module.imports), 2) testing.expect_value(t, module.imports[0].target, module.imports[1].target) testing.expect_value(t, len(module.functions), 2) + found_variadic := false + for function in module.functions { + found_variadic = found_variadic || function.variadic + } + testing.expect(t, found_variadic) } @(test) diff --git a/examples/interop/header/app/main.bro b/examples/interop/header/app/main.bro index fd7d119..e87f04c 100644 --- a/examples/interop/header/app/main.bro +++ b/examples/interop/header/app/main.bro @@ -10,4 +10,11 @@ main :: func() void { _ = native.child_value(7) _ = native.imported_read(native.imported_handle()?) _ = native.configured_value(9) + signed i8 :: -2 + unsigned u16 :: 3 + float_value f32 :: 4.0 + c_float_value c_float :: 5.0 + pointer *u8 :: "ok".ptr + nullable ?*u8 :: pointer + _ = native.imported_variadic(7, signed, unsigned, float_value, c_float_value, pointer, nullable) } diff --git a/examples/interop/header/include/native.h b/examples/interop/header/include/native.h index 36838d6..8a62fed 100644 --- a/examples/interop/header/include/native.h +++ b/examples/interop/header/include/native.h @@ -35,6 +35,6 @@ int configured_value(int value); #endif #define IMPORTED_MACRO 42 -int imported_variadic(const char *format, ...); +int imported_variadic(int marker, ...); #endif diff --git a/examples/interop/header/native.c b/examples/interop/header/native.c index bce8d6f..ab6942f 100644 --- a/examples/interop/header/native.c +++ b/examples/interop/header/native.c @@ -1,4 +1,6 @@ #include "include/native.h" +#include +#include struct Imported_Handle { int value; @@ -26,6 +28,28 @@ int imported_read(const Imported_Handle *value) { return value->value; } +int imported_variadic(int marker, ...) { + va_list args; + va_start(args, marker); + int narrow_signed = va_arg(args, int); + int narrow_unsigned = va_arg(args, int); + double float_value = va_arg(args, double); + double c_float_value = va_arg(args, double); + const unsigned char *pointer = va_arg(args, const unsigned char *); + const unsigned char *nullable = va_arg(args, const unsigned char *); + va_end(args); + if (!(marker == 7 && + narrow_signed == -2 && + narrow_unsigned == 3 && + float_value == 4.0 && + c_float_value == 5.0 && + pointer[0] == 'o' && + nullable == pointer)) { + abort(); + } + return 0; +} + #ifdef BROLANG_FEATURE int configured_value(int value) { return value; diff --git a/examples/interop/header_unsupported/main.bro b/examples/interop/header_unsupported/main.bro index 3999bd6..5c08c33 100644 --- a/examples/interop/header_unsupported/main.bro +++ b/examples/interop/header_unsupported/main.bro @@ -5,7 +5,6 @@ use_union :: c_func(value native.Imported_Union) void use_enum :: c_func(value native.Imported_Enum) void main :: func() void { - _ = native.imported_variadic("value") _ = native.IMPORTED_MACRO _ = native.imported_by_value() _ = native.imported_volatile()