diff --git a/crates/ail/tests/snapshots/hello.ll b/crates/ail/tests/snapshots/hello.ll index 26e1e9d..d2cb677 100644 --- a/crates/ail/tests/snapshots/hello.ll +++ b/crates/ail/tests/snapshots/hello.ll @@ -9,6 +9,7 @@ declare i32 @puts(ptr) declare ptr @GC_malloc(i64) declare i32 @strcmp(ptr, ptr) declare zeroext i1 @ail_str_eq(ptr, ptr) +declare i32 @ail_str_compare(ptr, ptr) declare i64 @llvm.fptosi.sat.i64.f64(double) @ail_hello_main_clos = private unnamed_addr constant { ptr, ptr } { ptr @ail_hello_main_adapter, ptr null } diff --git a/crates/ail/tests/snapshots/list.ll b/crates/ail/tests/snapshots/list.ll index de9700f..dd6eb22 100644 --- a/crates/ail/tests/snapshots/list.ll +++ b/crates/ail/tests/snapshots/list.ll @@ -9,6 +9,7 @@ declare i32 @puts(ptr) declare ptr @GC_malloc(i64) declare i32 @strcmp(ptr, ptr) declare zeroext i1 @ail_str_eq(ptr, ptr) +declare i32 @ail_str_compare(ptr, ptr) declare i64 @llvm.fptosi.sat.i64.f64(double) @ail_list_sum_list_clos = private unnamed_addr constant { ptr, ptr } { ptr @ail_list_sum_list_adapter, ptr null } diff --git a/crates/ail/tests/snapshots/max3.ll b/crates/ail/tests/snapshots/max3.ll index 8364022..43bac1c 100644 --- a/crates/ail/tests/snapshots/max3.ll +++ b/crates/ail/tests/snapshots/max3.ll @@ -9,6 +9,7 @@ declare i32 @puts(ptr) declare ptr @GC_malloc(i64) declare i32 @strcmp(ptr, ptr) declare zeroext i1 @ail_str_eq(ptr, ptr) +declare i32 @ail_str_compare(ptr, ptr) declare i64 @llvm.fptosi.sat.i64.f64(double) @ail_max3_max_clos = private unnamed_addr constant { ptr, ptr } { ptr @ail_max3_max_adapter, ptr null } diff --git a/crates/ail/tests/snapshots/sum.ll b/crates/ail/tests/snapshots/sum.ll index e6920e5..33834fd 100644 --- a/crates/ail/tests/snapshots/sum.ll +++ b/crates/ail/tests/snapshots/sum.ll @@ -9,6 +9,7 @@ declare i32 @puts(ptr) declare ptr @GC_malloc(i64) declare i32 @strcmp(ptr, ptr) declare zeroext i1 @ail_str_eq(ptr, ptr) +declare i32 @ail_str_compare(ptr, ptr) declare i64 @llvm.fptosi.sat.i64.f64(double) @ail_sum_sum_clos = private unnamed_addr constant { ptr, ptr } { ptr @ail_sum_sum_adapter, ptr null } diff --git a/crates/ail/tests/snapshots/ws_main.ll b/crates/ail/tests/snapshots/ws_main.ll index 69d4564..406504c 100644 --- a/crates/ail/tests/snapshots/ws_main.ll +++ b/crates/ail/tests/snapshots/ws_main.ll @@ -9,6 +9,7 @@ declare i32 @puts(ptr) declare ptr @GC_malloc(i64) declare i32 @strcmp(ptr, ptr) declare zeroext i1 @ail_str_eq(ptr, ptr) +declare i32 @ail_str_compare(ptr, ptr) declare i64 @llvm.fptosi.sat.i64.f64(double) @ail_ws_lib_add_clos = private unnamed_addr constant { ptr, ptr } { ptr @ail_ws_lib_add_adapter, ptr null } diff --git a/crates/ailang-codegen/src/lib.rs b/crates/ailang-codegen/src/lib.rs index 8c07f99..35f5b2c 100644 --- a/crates/ailang-codegen/src/lib.rs +++ b/crates/ailang-codegen/src/lib.rs @@ -495,6 +495,15 @@ fn lower_workspace_inner(ws: &Workspace, alloc: AllocStrategy) -> Result // by `ail build` (see `crates/ail/src/main.rs::build_to`). The // `zeroext i1` return matches clang's lowering of C `_Bool`. out.push_str("declare zeroext i1 @ail_str_eq(ptr, ptr)\n"); + // Iter 23.3: `runtime/str.c::ail_str_compare` backs the prelude's + // `compare__Str` mono symbol (see `try_emit_primitive_instance_body` + // `compare__Str` arm below). Declared unconditionally on the same + // rationale as `@ail_str_eq` — the .o is supplied by the + // unconditionally-linked `runtime/str.c`, and clang -O2 dead- + // strips the symbol when no caller exists. Returns i32 normalised + // to {-1, 0, +1} so the branch ladder in the intercept can compare + // against constant 0 directly. + out.push_str("declare i32 @ail_str_compare(ptr, ptr)\n"); // Floats iter 4.4: saturating fp-to-int intrinsic for // float_to_int_truncate. NaN → 0, +Inf → i64::MAX, -Inf → // i64::MIN, finite-out-of-range saturates, finite-in-range @@ -3653,4 +3662,38 @@ mod tests { "eq__Str body must call @ail_str_eq; ir was:\n{ir}" ); } + + /// Iter 23.3: `runtime/str.c::ail_str_compare` backs the prelude's + /// `compare__Str` mono symbol (see `try_emit_primitive_instance_body`). + /// The declaration is unconditional in the IR header alongside + /// `@ail_str_eq`; this test pins the header line so a regression + /// that drops it would fail at link time only on programs that + /// actually call `compare` on a Str. + #[test] + fn ir_header_declares_ail_str_compare() { + let m = Module { + schema: SCHEMA.into(), + name: "t".into(), + imports: vec![], + defs: vec![Def::Fn(FnDef { + name: "main".into(), + ty: Type::Fn { + params: vec![], + ret: Box::new(Type::unit()), + effects: vec![], + param_modes: vec![], + ret_mode: ParamMode::Implicit, + }, + params: vec![], + body: Term::Lit { lit: Literal::Unit }, + suppress: vec![], + doc: None, + })], + }; + let ir = emit_ir(&m).unwrap(); + assert!( + ir.contains("declare i32 @ail_str_compare(ptr, ptr)"), + "header missing @ail_str_compare declaration; ir was:\n{ir}" + ); + } } diff --git a/runtime/str.c b/runtime/str.c index d238d4c..113899b 100644 --- a/runtime/str.c +++ b/runtime/str.c @@ -24,3 +24,17 @@ bool ail_str_eq(const char *a, const char *b) { return strcmp(a, b) == 0; } + +/* Three-way comparison of NUL-terminated strings. Normalises libc + * strcmp's signed-int output to exactly -1 / 0 / +1 so the codegen + * caller can branch on `icmp slt i32 result, 0` and + * `icmp eq i32 result, 0` against constants directly without + * worrying about strcmp implementations that return values outside + * {-1, 0, +1}. Heap-Str ABI milestone will replace the body with + * length-prefixed comparison without changing the signature. */ +int ail_str_compare(const char *a, const char *b) { + int r = strcmp(a, b); + if (r < 0) return -1; + if (r > 0) return 1; + return 0; +}