From e0b6f6049c3265eb1d9d254aec06c3a9dddd3c0a Mon Sep 17 00:00:00 2001 From: hl-valdemar Date: Thu, 25 Jun 2026 17:53:04 +0200 Subject: [PATCH] c bool support and c-integer coercion --- compiler/checker/checker.odin | 14 +++++++++ compiler/cimport/cimport.odin | 1 + compiler/cimport/libclang.odin | 2 ++ compiler/hir/hir.odin | 1 + compiler/ir/ir.odin | 1 + compiler/llvm/llvm.odin | 48 +++++++++++++++++++++-------- compiler/loader/loader.odin | 1 + compiler/lower/lower.odin | 3 +- compiler/translatec/translatec.odin | 8 +++++ compiler/types/types.odin | 14 +++++++++ 10 files changed, 80 insertions(+), 13 deletions(-) diff --git a/compiler/checker/checker.odin b/compiler/checker/checker.odin index 2e4b80b..dddb2de 100644 --- a/compiler/checker/checker.odin +++ b/compiler/checker/checker.odin @@ -1882,6 +1882,20 @@ coerce_expr :: proc( }, ) } + if types.can_coerce_c_integer(actual, expected) { + return add_hir_expr( + checker, + hir.Expr { + kind = .C_Coerce, + span = span, + type = expected, + left = expr_id, + target = hir.INVALID_REF, + right = hir.INVALID_EXPR, + diagnostic = source.INVALID_DIAGNOSTIC, + }, + ) + } id := source.addf( checker.diagnostics, span, diff --git a/compiler/cimport/cimport.odin b/compiler/cimport/cimport.odin index 0478f3e..5276310 100644 --- a/compiler/cimport/cimport.odin +++ b/compiler/cimport/cimport.odin @@ -9,6 +9,7 @@ INVALID_TYPE :: Type_Id(0xffff_ffff) Type_Kind :: enum u8 { Invalid, Void, + C_Bool, C_Char, C_Schar, C_Uchar, diff --git a/compiler/cimport/libclang.odin b/compiler/cimport/libclang.odin index 46044d2..3841643 100644 --- a/compiler/cimport/libclang.odin +++ b/compiler/cimport/libclang.odin @@ -132,6 +132,7 @@ CXTLS_None :: i32(0) CXType_Invalid :: i32(0) CXType_Unexposed :: i32(1) CXType_Void :: i32(2) +CXType_Bool :: i32(3) CXType_Char_U :: i32(4) CXType_UChar :: i32(5) CXType_UShort :: i32(8) @@ -435,6 +436,7 @@ translate_type :: proc(ctx: ^Context, value: CXType, preferred_record_name := "" } switch value.kind { case CXType_Void: return add_type(ctx, Type{kind=.Void, child=INVALID_TYPE}) + case CXType_Bool: return add_type(ctx, Type{kind=.C_Bool, child=INVALID_TYPE}) case CXType_Char_U: return add_type(ctx, Type{kind=.C_Char, child=INVALID_TYPE}) case CXType_Char_S: return add_type(ctx, Type{kind=.C_Char, child=INVALID_TYPE}) case CXType_SChar: return add_type(ctx, Type{kind=.C_Schar, child=INVALID_TYPE}) diff --git a/compiler/hir/hir.odin b/compiler/hir/hir.odin index 040bc18..13fda6c 100644 --- a/compiler/hir/hir.odin +++ b/compiler/hir/hir.odin @@ -95,6 +95,7 @@ Expr_Kind :: enum u8 { Unwrap, Orelse, Widen, + C_Coerce, C_Vararg_Promote, Retype, Weaken_Pointer, diff --git a/compiler/ir/ir.odin b/compiler/ir/ir.odin index 0d011a5..dd33173 100644 --- a/compiler/ir/ir.odin +++ b/compiler/ir/ir.odin @@ -92,6 +92,7 @@ Opcode :: enum u8 { Orelse_Begin, Orelse, Widen, + C_Coerce, C_Vararg_Promote, Retype, Weaken_Pointer, diff --git a/compiler/llvm/llvm.odin b/compiler/llvm/llvm.odin index 3b6aadb..68a6f9e 100644 --- a/compiler/llvm/llvm.odin +++ b/compiler/llvm/llvm.odin @@ -191,6 +191,10 @@ function_result_type :: proc(function: ir.Function, store: ^types.Store) -> stri c_abi_extension :: proc(value: types.Type, store: ^types.Store) -> string { resolved := types.runtime_representation(value, store) + if types.is_bool(resolved) { + // clang lowers C `_Bool` as `zeroext i1` across the ABI boundary. + return "zeroext" + } if !types.is_concrete_integer(resolved) { return "" } @@ -252,7 +256,7 @@ valid_value :: proc( .Load_Global, .Function_Address, .Address_Of, .Load, .Slice, .Length, .Slice_Ptr, .Extract, .Select, .Unwrap, .Optional_Is_Some, .Optional_Value, .Orelse, - .Widen, .C_Vararg_Promote, .Retype, .Weaken_Pointer, .Weaken_Slice, .Decay_Array_Pointer, + .Widen, .C_Coerce, .C_Vararg_Promote, .Retype, .Weaken_Pointer, .Weaken_Slice, .Decay_Array_Pointer, .Neg_Checked, .Add_Checked, .Sub_Checked, .Mul_Checked, .Div_Checked, .Pointer_Add, .Not, .Compare, .Call: return true case .Address_Global, .Alloca, .Index_Address, .Field_Address, .Orelse_Begin, @@ -297,19 +301,15 @@ write_constant :: proc(builder: ^strings.Builder, value: i64, value_type: types. } selected := store.selected if store != nil else target.DEFAULT if types.is_float(resolved, selected) { - text := "" + // LLVM rejects decimal float literals that don't round-trip exactly, so + // emit the IEEE-754 double bit pattern as a hex literal (`0x...`), which + // always parses. For `float` we widen the f32 to f64 first — exact, and + // LLVM requires the value be representable as float, which it is. + number := transmute(f64)value if types.bits(resolved, selected) == 32 { - bits := u32(value) - number := transmute(f32)bits - text = fmt.tprintf("%.9g", number) - } else { - number := transmute(f64)value - text = fmt.tprintf("%.17g", number) - } - strings.write_string(builder, text) - if !strings.contains(text, ".") && !strings.contains(text, "e") && !strings.contains(text, "E") { - strings.write_string(builder, ".0") + number = f64(transmute(f32)u32(value)) } + fmt.sbprintf(builder, "0x%016X", transmute(u64)number) return } fmt.sbprintf(builder, "%d", value) @@ -1198,6 +1198,30 @@ 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_Coerce: + if !valid_instruction(instructions, instruction.a) || + !types.can_coerce_c_integer(instructions[instruction.a].type, instruction.type) { + emit_recovery_value(emitter, instruction_index, instruction, "invalid C integer coercion operand") + continue + } + from_type := instructions[instruction.a].type + from_bits := types.bits(from_type, emitter.module.target) + to_bits := types.bits(instruction.type, emitter.module.target) + if from_bits == to_bits { + // Same-width signedness change: c_uint and c_int both lower to the + // identical `iN`, so this is a pure reinterpret (no-op `select`). + 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 := "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 .C_Vararg_Promote: if !valid_instruction(instructions, instruction.a) { emit_recovery_value(emitter, instruction_index, instruction, "invalid C variadic promotion operand") diff --git a/compiler/loader/loader.odin b/compiler/loader/loader.odin index cce81c5..7b86439 100644 --- a/compiler/loader/loader.odin +++ b/compiler/loader/loader.odin @@ -149,6 +149,7 @@ translate_c_type :: proc( switch item.kind { case .Invalid: translated = types.INVALID case .Void: translated = types.VOID + case .C_Bool: translated = types.BOOL case .C_Char: translated = types.C_CHAR case .C_Schar: translated = types.C_SCHAR case .C_Uchar: translated = types.C_UCHAR diff --git a/compiler/lower/lower.odin b/compiler/lower/lower.odin index 38912e5..575693e 100644 --- a/compiler/lower/lower.odin +++ b/compiler/lower/lower.odin @@ -490,7 +490,7 @@ lower_expr :: proc(state: ^State, expr_id: hir.Expr_Id) -> ir.Instruction_Id { }) } _ = pop(&stack) - case .Widen, .C_Vararg_Promote, .Retype, .Weaken_Pointer, .Weaken_Slice, .Decay_Array_Pointer: + case .Widen, .C_Coerce, .C_Vararg_Promote, .Retype, .Weaken_Pointer, .Weaken_Slice, .Decay_Array_Pointer: stack[frame_index].stage = 1 append(&stack, Lower_Expr_Frame{expr=expr.left}) case .Negate: @@ -547,6 +547,7 @@ lower_expr :: proc(state: ^State, expr_id: hir.Expr_Id) -> ir.Instruction_Id { case .Weaken_Pointer: op = .Weaken_Pointer case .Weaken_Slice: op = .Weaken_Slice case .Decay_Array_Pointer: op = .Decay_Array_Pointer + case .C_Coerce: op = .C_Coerce case .C_Vararg_Promote: op = .C_Vararg_Promote case .Retype: op = .Retype case: op = .Widen diff --git a/compiler/translatec/translatec.odin b/compiler/translatec/translatec.odin index f1a6e6f..cf22fb6 100644 --- a/compiler/translatec/translatec.odin +++ b/compiler/translatec/translatec.odin @@ -77,6 +77,7 @@ render_type :: proc(b: ^strings.Builder, result: ^cimport.Result, id: cimport.Ty switch item.kind { case .Invalid: strings.write_string(b, "void") case .Void: strings.write_string(b, "void") + case .C_Bool: strings.write_string(b, "bool") case .C_Char: strings.write_string(b, "c_char") case .C_Schar: strings.write_string(b, "c_schar") case .C_Uchar: strings.write_string(b, "c_uchar") @@ -210,6 +211,13 @@ emit_macros :: proc(b: ^strings.Builder, result: ^cimport.Result, record_names: wrote = true continue } + // A macro whose name collides with a brolang keyword (e.g. `true`/`false` + // from ) can't be a binding name; emitting it is a parse error. + if lexer.keyword_kind(macro.name) != .Identifier { + fmt.sbprintf(b, "# unsupported in bindings: macro '%s' — name is a brolang keyword\n", macro.name) + wrote = true + continue + } if macro.aggregate { emit_aggregate_macro(b, result, macro, record_names) wrote = true diff --git a/compiler/types/types.odin b/compiler/types/types.odin index d7e013f..9a5e2e6 100644 --- a/compiler/types/types.odin +++ b/compiler/types/types.odin @@ -1094,6 +1094,20 @@ can_widen :: proc(from, to: Type) -> bool { bits(from) < bits(to) } +// can_coerce_c_integer reports whether `from` may implicitly convert to `to` +// under C's integer conversion rules. Brolang keeps its own exact-width scalars +// strict (`u32 -> i32` is rejected), but C interop types deliberately follow C: +// virtually every C library relies on it — e.g. an unsigned-backed enum constant +// (`c_uint`) passed to an `int` (`c_int`) parameter — so disallowing it would +// make C interop cumbersome. Scope: widening (sext/zext) and same-width +// signedness changes (no-op reinterpret); narrowing is intentionally excluded so +// lossy conversions stay an error, matching brolang's trap-on-narrow philosophy. +can_coerce_c_integer :: proc(from, to: Type) -> bool { + return from != to && is_c(from) && is_c(to) && + is_concrete_integer(from) && is_concrete_integer(to) && + bits(from) <= bits(to) +} + widest :: proc(a, b: Type) -> Type { if equal(a, b) && is_concrete_scalar(a) { return a