From 6088dc8420a5a23390355d1a1e9c3c5b334d9bb5 Mon Sep 17 00:00:00 2001 From: kalsprite Date: Sun, 23 Aug 2026 22:22:15 -0700 Subject: [PATCH 1/3] Reject a non-constant argument to an template's immediate --- .gitignore | 4 +-- src/check_expr.cpp | 12 +++++++++ tests/issues/run.bat | 1 + tests/issues/run.sh | 10 +++++++ .../test_issue_asm_immediate_constant.odin | 26 +++++++++++++++++++ 5 files changed, 51 insertions(+), 2 deletions(-) create mode 100644 tests/issues/test_issue_asm_immediate_constant.odin diff --git a/.gitignore b/.gitignore index b867398c4..3c1b8cb78 100644 --- a/.gitignore +++ b/.gitignore @@ -277,6 +277,8 @@ odin *.bin demo.bin libLLVM*.so* +*.a +*.o # core:rexcode commits its generated encode/decode tables: each arch's tablegen # emits human-readable Odin, serialized to tables/*.bin, which the library @@ -321,5 +323,3 @@ build/ cmake-build*/ CMakeLists.txt sandbox/ - -vendor/box3d/lib/box3d_wasm.o diff --git a/src/check_expr.cpp b/src/check_expr.cpp index 3562a850c..815c48c55 100644 --- a/src/check_expr.cpp +++ b/src/check_expr.cpp @@ -6996,6 +6996,18 @@ gb_internal CallArgumentError check_call_arguments_internal(CheckerContext *c, A } } + // an `asm` template's `$` parameter is encoded as an immediate, so only a constant can reach it + if (e && e->kind == Entity_Variable && (e->flags & EntityFlag_PolyConst)) { + if (o->mode != Addressing_Constant) { + if (show_error) { + gbString str = expr_to_string(o->expr); + error(o->expr, "Expected a constant value for the '$' immediate '%.*s', got %s", LIT(e->token.string), str); + gb_string_free(str); + } + err = CallArgumentError_NoneConstantParameter; + } + } + if (e && e->kind == Entity_Constant && is_type_proc(e->type)) { bool ok = false; if (o->mode == Addressing_Constant) { diff --git a/tests/issues/run.bat b/tests/issues/run.bat index 4cbb424b4..b559d0245 100644 --- a/tests/issues/run.bat +++ b/tests/issues/run.bat @@ -46,6 +46,7 @@ set COMMON=-define:ODIN_TEST_FANCY=false -file -vet -strict-style -ignore-unused ..\..\..\odin check ..\test_issue_ellipsis_type_call.odin -no-entry-point %COMMON% 2>&1 | find /c "Error:" | findstr /x "10" || exit /b ..\..\..\odin check ..\test_issue_foreign_redeclaration.odin -no-entry-point %COMMON% || exit /b ..\..\..\odin check ..\test_issue_foreign_redeclaration_mismatch.odin -no-entry-point %COMMON% 2>&1 | find /c "Error:" | findstr /x "1" || exit /b +..\..\..\odin check ..\test_issue_asm_immediate_constant.odin -no-entry-point %COMMON% 2>&1 | find /c "Error:" | findstr /x "4" || exit /b ..\..\..\odin build ..\test_issue_7037.odin %COMMON% -o:none || exit /b ..\..\..\odin build ..\test_issue_7188.odin %COMMON% || exit /b clang -c ..\test_issue_sysv_abi.c -o test_issue_sysv_abi_c.o || exit /b diff --git a/tests/issues/run.sh b/tests/issues/run.sh index 98c144d9c..8b9b66e02 100755 --- a/tests/issues/run.sh +++ b/tests/issues/run.sh @@ -110,6 +110,16 @@ else exit 1 fi +# `asm` templates are amd64-only, so this file is empty on every other architecture +if [[ "$(uname -m)" == "x86_64" || "$(uname -m)" == "amd64" ]]; then + if [[ $($ODIN check ../test_issue_asm_immediate_constant.odin -no-entry-point $COMMON_CHECK 2>&1 >/dev/null | grep -c "Error:") -eq 4 ]]; then + echo "SUCCESSFUL 1/1" + else + echo "SUCCESSFUL 0/1" + exit 1 + fi +fi + if [[ $($ODIN build ../test_issue_7108.odin $COMMON 2>&1 >/dev/null | grep -c "Error:") -eq 2 ]]; then echo "SUCCESSFUL 1/1" else diff --git a/tests/issues/test_issue_asm_immediate_constant.odin b/tests/issues/test_issue_asm_immediate_constant.odin new file mode 100644 index 000000000..c72699fd3 --- /dev/null +++ b/tests/issues/test_issue_asm_immediate_constant.odin @@ -0,0 +1,26 @@ +#+build amd64 +// An `asm` template's `$` parameter is encoded as an immediate, but a runtime value passed to +// one was accepted by the checker and only died in the backend, with an unlocated LLVM error. +package test_issues + +shl_imm :: asm(a: i32, $n: i32) -> (r: i32) { mov r, a; shl r, n; } + +opaque :: proc() -> i32 { + return 5 +} + +K :: 3 + +bad :: proc(v: i32) { + x: i32 = 2 + _ = shl_imm(1, x) + _ = shl_imm(1, v) + _ = shl_imm(1, opaque()) + _ = shl_imm(1, x + 1) +} + +good :: proc(a: i32) { + _ = shl_imm(a, 2) + _ = shl_imm(a, K) + _ = shl_imm(a, 1 + 1) +} From 01a6a43065f273b344ad81acfeefed17f9eae7af Mon Sep 17 00:00:00 2001 From: kalsprite Date: Mon, 24 Aug 2026 17:56:10 -0700 Subject: [PATCH 2/3] remove the asm immediate error-count test --- tests/issues/run.bat | 1 - tests/issues/run.sh | 6 ----- .../test_issue_asm_immediate_constant.odin | 26 ------------------- 3 files changed, 33 deletions(-) delete mode 100644 tests/issues/test_issue_asm_immediate_constant.odin diff --git a/tests/issues/run.bat b/tests/issues/run.bat index f3050da6e..d31751a87 100644 --- a/tests/issues/run.bat +++ b/tests/issues/run.bat @@ -47,7 +47,6 @@ set COMMON=-define:ODIN_TEST_FANCY=false -file -vet -strict-style -ignore-unused ..\..\..\odin check ..\test_issue_ellipsis_type_call.odin -no-entry-point %COMMON% 2>&1 | find /c "Error:" | findstr /x "10" || exit /b ..\..\..\odin check ..\test_issue_foreign_redeclaration.odin -no-entry-point %COMMON% || exit /b ..\..\..\odin check ..\test_issue_foreign_redeclaration_mismatch.odin -no-entry-point %COMMON% 2>&1 | find /c "Error:" | findstr /x "1" || exit /b -..\..\..\odin check ..\test_issue_asm_immediate_constant.odin -no-entry-point %COMMON% 2>&1 | find /c "Error:" | findstr /x "4" || exit /b ..\..\..\odin check ..\test_issue_asm_rip_register.odin -no-entry-point %COMMON% 2>&1 | find /c "Error:" | findstr /x "6" || exit /b ..\..\..\odin check ..\test_issue_asm_template_as_value.odin -no-entry-point %COMMON% 2>&1 | find /c "Error:" | findstr /x "10" || exit /b ..\..\..\odin build ..\test_issue_7037.odin %COMMON% -o:none || exit /b diff --git a/tests/issues/run.sh b/tests/issues/run.sh index 7b1bddd54..b936a9e30 100755 --- a/tests/issues/run.sh +++ b/tests/issues/run.sh @@ -122,12 +122,6 @@ fi # `asm` templates are amd64-only, so this file is empty on every other architecture if [[ "$(uname -m)" == "x86_64" || "$(uname -m)" == "amd64" ]]; then - if [[ $($ODIN check ../test_issue_asm_immediate_constant.odin -no-entry-point $COMMON_CHECK 2>&1 >/dev/null | grep -c "Error:") -eq 4 ]]; then - echo "SUCCESSFUL 1/1" - else - echo "SUCCESSFUL 0/1" - exit 1 - fi if [[ $($ODIN check ../test_issue_asm_rip_register.odin -no-entry-point $COMMON_CHECK 2>&1 >/dev/null | grep -c "Error:") -eq 6 ]]; then echo "SUCCESSFUL 1/1" else diff --git a/tests/issues/test_issue_asm_immediate_constant.odin b/tests/issues/test_issue_asm_immediate_constant.odin deleted file mode 100644 index c72699fd3..000000000 --- a/tests/issues/test_issue_asm_immediate_constant.odin +++ /dev/null @@ -1,26 +0,0 @@ -#+build amd64 -// An `asm` template's `$` parameter is encoded as an immediate, but a runtime value passed to -// one was accepted by the checker and only died in the backend, with an unlocated LLVM error. -package test_issues - -shl_imm :: asm(a: i32, $n: i32) -> (r: i32) { mov r, a; shl r, n; } - -opaque :: proc() -> i32 { - return 5 -} - -K :: 3 - -bad :: proc(v: i32) { - x: i32 = 2 - _ = shl_imm(1, x) - _ = shl_imm(1, v) - _ = shl_imm(1, opaque()) - _ = shl_imm(1, x + 1) -} - -good :: proc(a: i32) { - _ = shl_imm(a, 2) - _ = shl_imm(a, K) - _ = shl_imm(a, 1 + 1) -} From 4ecaa7dfd9a06fcc6a7ff73e9c05254564e94ee1 Mon Sep 17 00:00:00 2001 From: kalsprite Date: Mon, 24 Aug 2026 18:34:59 -0700 Subject: [PATCH 3/3] normalize bool cmp --- src/llvm_backend_expr.cpp | 18 ++++++ tests/issues/run.bat | 1 + tests/issues/run.sh | 1 + ...test_issue_bool_comparison_truthiness.odin | 57 +++++++++++++++++++ 4 files changed, 77 insertions(+) create mode 100644 tests/issues/test_issue_bool_comparison_truthiness.odin diff --git a/src/llvm_backend_expr.cpp b/src/llvm_backend_expr.cpp index 74cb46ceb..dd54e7370 100644 --- a/src/llvm_backend_expr.cpp +++ b/src/llvm_backend_expr.cpp @@ -3860,6 +3860,24 @@ gb_internal lbValue lb_emit_comp(lbProcedure *p, TokenKind op_kind, lbValue left rhs = lb_emit_byte_swap(p, {rhs, pt}, pt).value; } + if (is_type_boolean(a) && is_type_boolean(b) && (op_kind == Token_CmpEq || op_kind == Token_NotEq)) { + // anything not 0 is true, which is what control flow already tests for + bool lhs_is_const = LLVMIsAConstantInt(lhs) != nullptr; + bool rhs_is_const = LLVMIsAConstantInt(rhs) != nullptr; + if (lhs_is_const != rhs_is_const) { + // against a literal, the truthiness test is the whole comparison + LLVMValueRef v = rhs_is_const ? lhs : rhs; + LLVMValueRef c = rhs_is_const ? rhs : lhs; + bool is_true = LLVMConstIntGetZExtValue(c) != 0; + pred = ((op_kind == Token_CmpEq) == is_true) ? LLVMIntNE : LLVMIntEQ; + lhs = v; + rhs = LLVMConstNull(LLVMTypeOf(v)); + } else { + lhs = LLVMBuildICmp(p->builder, LLVMIntNE, lhs, LLVMConstNull(LLVMTypeOf(lhs)), ""); + rhs = LLVMBuildICmp(p->builder, LLVMIntNE, rhs, LLVMConstNull(LLVMTypeOf(rhs)), ""); + } + } + res.value = LLVMBuildICmp(p->builder, pred, lhs, rhs, ""); } else if (is_type_float(a)) { LLVMRealPredicate pred = {}; diff --git a/tests/issues/run.bat b/tests/issues/run.bat index 4cbb424b4..f211e153b 100644 --- a/tests/issues/run.bat +++ b/tests/issues/run.bat @@ -43,6 +43,7 @@ set COMMON=-define:ODIN_TEST_FANCY=false -file -vet -strict-style -ignore-unused ..\..\..\odin test ..\test_issue_7008.odin %COMMON% || exit /b ..\..\..\odin check ..\test_issue_7012.odin -no-entry-point %COMMON% || exit /b ..\..\..\odin check ..\test_issue_7260.odin -no-entry-point %COMMON% || exit /b +..\..\..\odin test ..\test_issue_bool_comparison_truthiness.odin %COMMON% || exit /b ..\..\..\odin check ..\test_issue_ellipsis_type_call.odin -no-entry-point %COMMON% 2>&1 | find /c "Error:" | findstr /x "10" || exit /b ..\..\..\odin check ..\test_issue_foreign_redeclaration.odin -no-entry-point %COMMON% || exit /b ..\..\..\odin check ..\test_issue_foreign_redeclaration_mismatch.odin -no-entry-point %COMMON% 2>&1 | find /c "Error:" | findstr /x "1" || exit /b diff --git a/tests/issues/run.sh b/tests/issues/run.sh index f7fcfe4cc..175666682 100755 --- a/tests/issues/run.sh +++ b/tests/issues/run.sh @@ -95,6 +95,7 @@ $ODIN test ../test_issue_7356.odin $COMMON $ODIN build ../test_issue_7167.odin $COMMON $ODIN build ../test_issue_7188.odin $COMMON $ODIN check ../test_issue_7260.odin -no-entry-point $COMMON_CHECK +$ODIN test ../test_issue_bool_comparison_truthiness.odin $COMMON $ODIN check ../test_issue_foreign_redeclaration.odin -no-entry-point $COMMON_CHECK if [[ $($ODIN check ../test_issue_foreign_redeclaration_mismatch.odin -no-entry-point $COMMON_CHECK 2>&1 >/dev/null | grep -c "Error:") -eq 1 ]]; then diff --git a/tests/issues/test_issue_bool_comparison_truthiness.odin b/tests/issues/test_issue_bool_comparison_truthiness.odin new file mode 100644 index 000000000..6b218bdc2 --- /dev/null +++ b/tests/issues/test_issue_bool_comparison_truthiness.odin @@ -0,0 +1,57 @@ +package test_issues + +import "core:testing" + +// A boolean is true when its payload is non-zero, which is what `if`, `!` and `&&` test for. +// `==`, `!=` and `switch` compared the payload against 1 instead, so a boolean decoded from +// bytes -- transmuted, read through a pointer, or returned by a foreign procedure -- was true +// under `if` yet matched neither arm of its own switch. + +@(test) +bool_comparison_uses_truthiness :: proc(t: ^testing.T) { + two: u8 = 2 + one: u8 = 1 + nil_: u8 = 0 + + b := transmute(bool)two + c := transmute(bool)one + f := transmute(bool)nil_ + + testing.expect(t, b, "a non-zero payload is true") + + testing.expect_value(t, b == true, true) + testing.expect_value(t, b != true, false) + testing.expect_value(t, b == false, false) + testing.expect_value(t, b != false, true) + + // both operands are normalised, not just the literal one + testing.expect_value(t, b == c, true) + testing.expect_value(t, b != c, false) + testing.expect_value(t, b == f, false) + testing.expect_value(t, b != f, true) + + arm := 2 + switch b { + case true: arm = 1 + case false: arm = 0 + } + testing.expect_value(t, arm, 1) + + // the wider boolean types share the path + two16: u16 = 2 + two32: u32 = 2 + two64: u64 = 2 + testing.expect_value(t, transmute(b8)two == true, true) + testing.expect_value(t, transmute(b16)two16 == true, true) + testing.expect_value(t, transmute(b32)two32 == true, true) + testing.expect_value(t, transmute(b64)two64 == true, true) + + // the normal 0/1 payloads keep behaving + yes := true + no := false + testing.expect_value(t, yes == true, true) + testing.expect_value(t, yes == false, false) + testing.expect_value(t, no == false, true) + testing.expect_value(t, yes == no, false) + testing.expect_value(t, yes != no, true) +}