c variadic calls

This commit is contained in:
2026-06-14 15:53:45 +02:00
parent 638ca57f5c
commit 2d3d0bd266
21 changed files with 409 additions and 35 deletions
+1 -1
View File
@@ -34,6 +34,7 @@
- file-local relative imports, aliases, and qualified member access - file-local relative imports, aliases, and qualified member access
- relative `.h` imports as synthetic package namespaces - relative `.h` imports as synthetic package namespaces
- transitive external C function prototypes, typedef chains, C scalars, and pointers to opaque C records - 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 - reference-time diagnostics for unsupported imported C declarations
### compiler behavior ### compiler behavior
@@ -50,7 +51,6 @@
### foreign functions and linking ### foreign functions and linking
- c variadic calls with default argument promotions
- exporting brolang functions to c - exporting brolang functions to c
### scalar and compound types ### scalar and compound types
+13
View File
@@ -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 still be supplied explicitly with the C-prefixed linking options. Set
`BROLANG_LIBCLANG_PATH` when libclang is not installed in a standard location. `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/`: Compilation phases are isolated under `compiler/`:
```text ```text
@@ -83,6 +95,7 @@ Current prototype features:
- Qualified imported globals and functions with package-aware symbol mangling - Qualified imported globals and functions with package-aware symbol mangling
- Demand-monomorphized Brolang and C-ABI functions - Demand-monomorphized Brolang and C-ABI functions
- Bodyless concrete C function declarations with exact external symbol names - 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 - Ordered linking of additional C sources, objects, archives, and libraries
- Checked signed addition and unary negation - Checked signed addition and unary negation
- Static, eager runtime, and deferred problematic globals - Static, eager runtime, and deferred problematic globals
+1 -1
View File
@@ -38,7 +38,7 @@
- diagnose unsupported declarations when referenced - diagnose unsupported declarations when referenced
- dynamically load libclang behind a replaceable c importer boundary - 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 - represent c variadics as a fixed parameter count plus a variadic flag
- apply c default argument promotions at call sites - apply c default argument promotions at call sites
- emit LLVM c-variadic declarations and calls - emit LLVM c-variadic declarations and calls
+1
View File
@@ -130,6 +130,7 @@ Function :: struct {
c_abi: bool, c_abi: bool,
imported: bool, imported: bool,
has_body: bool, has_body: bool,
variadic: bool,
params: []Param, params: []Param,
result: Type_Syntax, result: Type_Syntax,
body: []Stmt_Id, body: []Stmt_Id,
+69 -9
View File
@@ -454,7 +454,7 @@ add_unsupported_type_diagnostic :: proc(
} }
function_signatures_equal :: proc(left, right: ast.Function) -> bool { 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 return false
} }
for param, index in left.params { for param, index in left.params {
@@ -465,6 +465,17 @@ function_signatures_equal :: proc(left, right: ast.Function) -> bool {
return true 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 { contains_name :: proc(names: []symbol.Id, name: symbol.Id) -> bool {
for existing in names { for existing in names {
if existing == name { if existing == name {
@@ -566,6 +577,14 @@ validate_declarations :: proc(checker: ^Checker) {
symbol_text(checker, function.name), 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 { if !function.has_body && function.c_abi {
for param in function.params { for param in function.params {
if add_unsupported_type_diagnostic(checker, param.span, type_from_syntax(param.type)) != 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] 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 spec := INVALID_SPEC
if demanded == nil { if demanded == nil {
spec = ensure_spec(checker, frame.template, stack[frame_index].args) 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) spec = find_spec(checker, frame.template, stack[frame_index].args)
mark_spec_demanded(checker, spec, demanded) mark_spec_demanded(checker, spec, demanded)
} }
@@ -1317,6 +1337,32 @@ coerce_expr :: proc(
return invalid_hir_expr(checker, span, id, expected) 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( build_constant_expr :: proc(
checker: ^Checker, checker: ^Checker,
expr: ast.Expr, expr: ast.Expr,
@@ -1891,13 +1937,16 @@ build_expr :: proc(
_ = pop(&stack) _ = pop(&stack)
continue 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( id := source.addf(
checker.diagnostics, checker.diagnostics,
expr.span, expr.span,
"function '%s' expects %d arguments, got %d", message,
symbol_text(checker, expr.name), symbol_text(checker, expr.name),
len(checker.ast_module.functions[template].params), len(function.params),
len(expr.args), len(expr.args),
) )
last = invalid_hir_expr(checker, expr.span, id) 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].arg_types = make([]types.Type, len(expr.args), checker.allocator)
stack[frame_index].stage = 3 stack[frame_index].stage = 3
if len(expr.args) > 0 { 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) { if !is_runtime_type(checker, arg_expected) {
arg_expected = types.INVALID arg_expected = types.INVALID
} }
@@ -1980,7 +2029,7 @@ build_expr :: proc(
stack[frame_index].arg_index += 1 stack[frame_index].arg_index += 1
if frame.arg_index+1 < len(expr.args) { if frame.arg_index+1 < len(expr.args) {
next := frame.arg_index+1 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) { if !is_runtime_type(checker, arg_expected) {
arg_expected = types.INVALID arg_expected = types.INVALID
} }
@@ -2004,7 +2053,8 @@ build_expr :: proc(
_ = pop(&stack) _ = pop(&stack)
continue continue
} }
for _, index in stack[frame_index].built_args { fixed_count := len(checker.ast_module.functions[frame.template].params)
for index in 0..<fixed_count {
stack[frame_index].built_args[index] = coerce_expr( stack[frame_index].built_args[index] = coerce_expr(
checker, checker,
stack[frame_index].built_args[index], stack[frame_index].built_args[index],
@@ -2012,6 +2062,14 @@ build_expr :: proc(
checker.module.exprs[stack[frame_index].built_args[index]].span, checker.module.exprs[stack[frame_index].built_args[index]].span,
) )
} }
for index in fixed_count..<len(stack[frame_index].built_args) {
arg := stack[frame_index].built_args[index]
stack[frame_index].built_args[index] = promote_c_vararg_expr(
checker,
arg,
checker.module.exprs[arg].span,
)
}
function_id := checker.specs[spec].hir_id function_id := checker.specs[spec].hir_id
assert(function_id != hir.INVALID_FUNCTION) assert(function_id != hir.INVALID_FUNCTION)
add_unique_function(calls, function_id) add_unique_function(calls, function_id)
@@ -2128,6 +2186,7 @@ build_function :: proc(checker: ^Checker, id: Spec_Id) {
implementation = .Declaration, implementation = .Declaration,
linkage = .External if function.c_abi else .Internal, linkage = .External if function.c_abi else .Internal,
is_main = function.pkg == 0 && function.name == checker.main_symbol, is_main = function.pkg == 0 && function.name == checker.main_symbol,
variadic = function.variadic,
params = params[:], params = params[:],
result = spec.result, result = spec.result,
locals = hir_locals[:], locals = hir_locals[:],
@@ -2524,6 +2583,7 @@ build_function :: proc(checker: ^Checker, id: Spec_Id) {
implementation = .Definition, implementation = .Definition,
linkage = .External if function.c_abi || (function.pkg == 0 && function.name == checker.main_symbol) else .Internal, linkage = .External if function.c_abi || (function.pkg == 0 && function.name == checker.main_symbol) else .Internal,
is_main = function.pkg == 0 && function.name == checker.main_symbol, is_main = function.pkg == 0 && function.name == checker.main_symbol,
variadic = function.variadic,
params = params[:], params = params[:],
result = spec.result, result = spec.result,
locals = hir_locals[:], locals = hir_locals[:],
+5 -4
View File
@@ -46,10 +46,11 @@ Alias :: struct {
} }
Function :: struct { Function :: struct {
name: string, name: string,
params: []Type_Id, params: []Type_Id,
result: Type_Id, result: Type_Id,
reason: string, variadic: bool,
reason: string,
} }
Unsupported :: struct { Unsupported :: struct {
+2 -2
View File
@@ -338,9 +338,8 @@ visit_cursor :: proc "c"(cursor, parent: CXCursor, client_data: rawptr) -> i32 {
linkage := ctx.api.get_cursor_linkage(cursor) linkage := ctx.api.get_cursor_linkage(cursor)
if linkage != CXLinkage_External { if linkage != CXLinkage_External {
reason = "static and non-external C functions are not supported" 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) function_type := ctx.api.get_cursor_type(cursor)
result_type := translate_type(ctx, ctx.api.get_result_type(function_type)) result_type := translate_type(ctx, ctx.api.get_result_type(function_type))
if result_type == INVALID_TYPE && len(reason) == 0 { 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), name=fmt.aprintf("%s", name, allocator=ctx.allocator),
params=params[:], params=params[:],
result=result_type, result=result_type,
variadic=variadic,
reason=fmt.aprintf("%s", reason, allocator=ctx.allocator), reason=fmt.aprintf("%s", reason, allocator=ctx.allocator),
}) })
case CXCursor_TypedefDecl: case CXCursor_TypedefDecl:
+2
View File
@@ -93,6 +93,7 @@ Expr_Kind :: enum u8 {
Unwrap, Unwrap,
Orelse, Orelse,
Widen, Widen,
C_Vararg_Promote,
Weaken_Pointer, Weaken_Pointer,
Negate, Negate,
Add, Add,
@@ -144,6 +145,7 @@ Function :: struct {
implementation: Implementation, implementation: Implementation,
linkage: Linkage, linkage: Linkage,
is_main: bool, is_main: bool,
variadic: bool,
params: []Local_Id, params: []Local_Id,
result: types.Type, result: types.Type,
locals: []Local, locals: []Local,
+2
View File
@@ -87,6 +87,7 @@ Opcode :: enum u8 {
Orelse_Begin, Orelse_Begin,
Orelse, Orelse,
Widen, Widen,
C_Vararg_Promote,
Weaken_Pointer, Weaken_Pointer,
Neg_Checked, Neg_Checked,
Add_Checked, Add_Checked,
@@ -115,6 +116,7 @@ Function :: struct {
implementation: Implementation, implementation: Implementation,
linkage: Linkage, linkage: Linkage,
is_main: bool, is_main: bool,
variadic: bool,
param_types: []types.Type, param_types: []types.Type,
result: types.Type, result: types.Type,
instructions: []Instruction, instructions: []Instruction,
+6 -1
View File
@@ -119,7 +119,12 @@ lex :: proc(
cursor += 1 cursor += 1
if cursor < len(bytes) && bytes[cursor] == '.' { if cursor < len(bytes) && bytes[cursor] == '.' {
cursor += 1 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 { } else {
append_token(&stream, source_file, .Dot, start, cursor) append_token(&stream, source_file, .Dot, start, cursor)
} }
+63 -7
View File
@@ -112,7 +112,7 @@ valid_value :: proc(
switch instructions[value_id].op { switch instructions[value_id].op {
case .Param, .Const, .String, .Aggregate, .None, .Optional_Some, case .Param, .Const, .String, .Aggregate, .None, .Optional_Some,
.Load_Global, .Address_Of, .Load, .Slice, .Length, .Slice_Ptr, .Unwrap, .Orelse, .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 return true
case .Address_Global, .Alloca, .Index_Address, .Field_Address, .Orelse_Begin, case .Address_Global, .Alloca, .Index_Address, .Field_Address, .Orelse_Begin,
.Store, .Trap, .Return, .Return_Void: .Store, .Trap, .Return, .Return_Void:
@@ -275,14 +275,16 @@ emit_call_args :: proc(
if index > 0 { if index > 0 {
strings.write_string(builder, ", ") strings.write_string(builder, ", ")
} }
fmt.sbprintf(builder, "%s ", llvm_type(param_types[index], store)) arg_type := param_types[index] if index < len(param_types) && valid_instruction(instructions, arg) else
if c_abi { (instructions[arg].type if valid_instruction(instructions, arg) else types.INVALID)
extension := c_abi_extension(param_types[index], store.selected) 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 { if len(extension) > 0 {
fmt.sbprintf(builder, "%s ", extension) 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)) 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) 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)) 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: case .Weaken_Pointer:
if !valid_instruction(instructions, instruction.a) || if !valid_instruction(instructions, instruction.a) ||
!types.can_weaken_pointer(instructions[instruction.a].type, instruction.type, &emitter.module.types) { !types.can_weaken_pointer(instructions[instruction.a].type, instruction.type, &emitter.module.types) {
@@ -806,10 +833,20 @@ emit_instruction_stream :: proc(
continue continue
} }
target := emitter.module.functions[function_id] 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 { if valid_args {
for arg, index in instruction.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 valid_args = false
break break
} }
@@ -833,6 +870,19 @@ emit_instruction_stream :: proc(
strings.write_string(&emitter.builder, "fastcc ") strings.write_string(&emitter.builder, "fastcc ")
} }
emit_function_result(&emitter.builder, target, &emitter.module.types) 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) fmt.sbprintf(&emitter.builder, " @%s(", target.link_name)
emit_call_args( emit_call_args(
&emitter.builder, instructions, instruction.args, target.param_types, &emitter.builder, instructions, instruction.args, target.param_types,
@@ -1037,6 +1087,12 @@ emit_functions :: proc(emitter: ^Emitter) {
fmt.sbprintf(&emitter.builder, " %%v%d", index) 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 { if function.implementation == .Declaration {
strings.write_string(&emitter.builder, ")\n\n") strings.write_string(&emitter.builder, ")\n\n")
continue continue
+4 -3
View File
@@ -177,8 +177,8 @@ translate_c_type :: proc(
return translated return translated
} }
function_signatures_equal :: proc(left: ast.Function, params: []ast.Param, result: types.Type) -> bool { function_signatures_equal :: proc(left: ast.Function, params: []ast.Param, result: types.Type, variadic: bool) -> bool {
if left.result != result || len(left.params) != len(params) { if left.result != result || left.variadic != variadic || len(left.params) != len(params) {
return false return false
} }
for param, index in params { for param, index in params {
@@ -282,7 +282,7 @@ load_header :: proc(state: ^State, path: string, import_span: source.Span) -> as
continue continue
} }
duplicate = true 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( existing.unsupported_reason = fmt.aprintf(
"conflicting C declarations for '%s'", "conflicting C declarations for '%s'",
function.name, function.name,
@@ -303,6 +303,7 @@ load_header :: proc(state: ^State, path: string, import_span: source.Span) -> as
c_abi=true, c_abi=true,
imported=true, imported=true,
has_body=false, has_body=false,
variadic=function.variadic,
params=params, params=params,
result=function_result, result=function_result,
unsupported_reason=strings.clone(unsupported_reason, state.allocator), unsupported_reason=strings.clone(unsupported_reason, state.allocator),
+4 -2
View File
@@ -324,7 +324,7 @@ lower_expr :: proc(state: ^State, expr_id: hir.Expr_Id) -> ir.Instruction_Id {
}) })
} }
_ = pop(&stack) _ = pop(&stack)
case .Widen, .Weaken_Pointer: case .Widen, .C_Vararg_Promote, .Weaken_Pointer:
stack[frame_index].stage = 1 stack[frame_index].stage = 1
append(&stack, Lower_Expr_Frame{expr=expr.left}) append(&stack, Lower_Expr_Frame{expr=expr.left})
case .Negate: case .Negate:
@@ -358,7 +358,8 @@ lower_expr :: proc(state: ^State, expr_id: hir.Expr_Id) -> ir.Instruction_Id {
} }
if frame.stage == 1 { if frame.stage == 1 {
last = append_instruction(state, ir.Instruction{ 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, span=expr.span, type=expr.type, target=ir.INVALID_REF,
a=last, b=ir.INVALID_INSTRUCTION, diagnostic=source.INVALID_DIAGNOSTIC, 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, implementation=.Declaration if function.implementation == .Declaration else .Definition,
linkage=.External if function.linkage == .External else .Internal, linkage=.External if function.linkage == .External else .Internal,
is_main=function.is_main, is_main=function.is_main,
variadic=function.variadic,
param_types=param_types, param_types=param_types,
result=function.result, result=function.result,
instructions=nil if function.implementation == .Declaration else lower_body(hir_module, function, allocator), instructions=nil if function.implementation == .Declaration else lower_body(hir_module, function, allocator),
+21 -3
View File
@@ -889,11 +889,27 @@ parse_statement :: proc(parser: ^Parser) -> ast.Stmt_Id {
return id return id
} }
parse_params :: proc(parser: ^Parser) -> []ast.Param { parse_params :: proc(parser: ^Parser) -> ([]ast.Param, bool) {
params: [dynamic]ast.Param params: [dynamic]ast.Param
params.allocator = parser.module.allocator params.allocator = parser.module.allocator
variadic := false
skip_newlines(parser) skip_newlines(parser)
for current(parser).kind != .Right_Paren && current(parser).kind != .Eof { 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: [dynamic]token.Token
names.allocator = parser.module.allocator names.allocator = parser.module.allocator
for { for {
@@ -923,7 +939,7 @@ parse_params :: proc(parser: ^Parser) -> []ast.Param {
} }
break break
} }
return params[:] return params[:], variadic
} }
parse_function :: proc(parser: ^Parser, name: token.Token, c_abi: bool) { 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 { if _, ok := allow(parser, .Left_Paren); !ok {
source.add(parser.diagnostics, current(parser).span, "expected '(' after 'func'") 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 { if _, ok := allow(parser, .Right_Paren); !ok {
source.add(parser.diagnostics, current(parser).span, "expected ')' after parameters") 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, file=parser.file,
c_abi=c_abi, c_abi=c_abi,
has_body=false, has_body=false,
variadic=variadic,
params=params, params=params,
result=result, result=result,
diagnostic=source.INVALID_DIAGNOSTIC, diagnostic=source.INVALID_DIAGNOSTIC,
@@ -991,6 +1008,7 @@ parse_function :: proc(parser: ^Parser, name: token.Token, c_abi: bool) {
file=parser.file, file=parser.file,
c_abi=c_abi, c_abi=c_abi,
has_body=true, has_body=true,
variadic=variadic,
params=params, params=params,
result=result, result=result,
body=body[:], body=body[:],
+1
View File
@@ -19,6 +19,7 @@ Kind :: enum u8 {
Minus, Minus,
Dot, Dot,
Range, Range,
Ellipsis,
At, At,
Star, Star,
Ampersand, Ampersand,
+28
View File
@@ -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) 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 { child_type :: proc(value: Type, store: ^Store) -> Type {
item, ok := node(store, value) item, ok := node(store, value)
return item.child if ok else INVALID return item.child if ok else INVALID
+154
View File
@@ -229,6 +229,38 @@ main :: func() void {}
testing.expect(t, module.functions[3].has_body) 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) @(test)
parser_treats_c_as_contextual_only_before_func :: proc(t: ^testing.T) { parser_treats_c_as_contextual_only_before_func :: proc(t: ^testing.T) {
text := `c :: 5 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_FLOAT), types.F32)
testing.expect_value(t, types.representation(types.C_DOUBLE), types.F64) 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.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") 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")) 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) @(test)
c_structs_are_pointer_only_and_may_be_opaque :: proc(t: ^testing.T) { c_structs_are_pointer_only_and_may_be_opaque :: proc(t: ^testing.T) {
text := `Defined :: c_struct { text := `Defined :: c_struct {
@@ -1683,6 +1830,7 @@ fake_cimport_backend :: proc(user_data: rawptr, request: cimport.Request, alloca
append(&result.functions, cimport.Function{ append(&result.functions, cimport.Function{
name=fmt.aprintf("fake_value", allocator=allocator), name=fmt.aprintf("fake_value", allocator=allocator),
result=cimport.Type_Id(0), result=cimport.Type_Id(0),
variadic=true,
reason=fmt.aprintf("", allocator=allocator), reason=fmt.aprintf("", allocator=allocator),
}) })
result.available = true 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, state.calls, 1)
testing.expect_value(t, len(result.functions), 1) testing.expect_value(t, len(result.functions), 1)
testing.expect_value(t, result.functions[0].name, "fake_value") testing.expect_value(t, result.functions[0].name, "fake_value")
testing.expect(t, result.functions[0].variadic)
} }
@(test) @(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, len(module.imports), 2)
testing.expect_value(t, module.imports[0].target, module.imports[1].target) testing.expect_value(t, module.imports[0].target, module.imports[1].target)
testing.expect_value(t, len(module.functions), 2) 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) @(test)
+7
View File
@@ -10,4 +10,11 @@ main :: func() void {
_ = native.child_value(7) _ = native.child_value(7)
_ = native.imported_read(native.imported_handle()?) _ = native.imported_read(native.imported_handle()?)
_ = native.configured_value(9) _ = 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)
} }
+1 -1
View File
@@ -35,6 +35,6 @@ int configured_value(int value);
#endif #endif
#define IMPORTED_MACRO 42 #define IMPORTED_MACRO 42
int imported_variadic(const char *format, ...); int imported_variadic(int marker, ...);
#endif #endif
+24
View File
@@ -1,4 +1,6 @@
#include "include/native.h" #include "include/native.h"
#include <stdarg.h>
#include <stdlib.h>
struct Imported_Handle { struct Imported_Handle {
int value; int value;
@@ -26,6 +28,28 @@ int imported_read(const Imported_Handle *value) {
return value->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 #ifdef BROLANG_FEATURE
int configured_value(int value) { int configured_value(int value) {
return value; return value;
@@ -5,7 +5,6 @@ use_union :: c_func(value native.Imported_Union) void
use_enum :: c_func(value native.Imported_Enum) void use_enum :: c_func(value native.Imported_Enum) void
main :: func() void { main :: func() void {
_ = native.imported_variadic("value")
_ = native.IMPORTED_MACRO _ = native.IMPORTED_MACRO
_ = native.imported_by_value() _ = native.imported_by_value()
_ = native.imported_volatile() _ = native.imported_volatile()