diff --git a/lib/typecheck.ml b/lib/typecheck.ml index 676f29a..1ed29a4 100644 --- a/lib/typecheck.ml +++ b/lib/typecheck.ml @@ -1024,6 +1024,12 @@ let rec synth (ctx : context) (expr : expr) : ty result = | TCon "Float" -> let* () = check ctx rhs ty_float in Ok ty_bool + | TCon "String" -> + (* String relational ops are byte-wise lexicographic, matching + JS / Lua / Rust convention. Surfaced by #458 (was the + rate-limiter for several TS→AS ports). *) + let* () = check ctx rhs ty_string in + Ok ty_bool | _ -> let (lhs_ty', rhs_ty, result_ty) = type_of_binop op in let* () = unify_or_err lhs_ty lhs_ty' in diff --git a/tests/codegen-deno/string_lex_cmp.affine b/tests/codegen-deno/string_lex_cmp.affine new file mode 100644 index 0000000..99491fa --- /dev/null +++ b/tests/codegen-deno/string_lex_cmp.affine @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: MPL-2.0 +// issue #458 — String relational ops (<, >, <=, >=) must be built-in +// for lexicographic comparison. Pre-fix, `a < b` for `String` raised +// `TypeMismatch (String, Int)` because the comparison branch in +// Typecheck dispatched only on Float / Int. Now String dispatches +// alongside, lowering to JS `<` (which already does lex compare). + +pub fn lt(a: String, b: String) -> Bool { return a < b; } +pub fn gt(a: String, b: String) -> Bool { return a > b; } +pub fn le(a: String, b: String) -> Bool { return a <= b; } +pub fn ge(a: String, b: String) -> Bool { return a >= b; } + +// Common derived: equal-length lex compare, single-character pivot, +// empty-string handling. +pub fn first_lt() -> Bool { return "abc" < "abd"; } +pub fn first_gt() -> Bool { return "z" > "a"; } +pub fn equal_strings_le() -> Bool { return "x" <= "x"; } +pub fn equal_strings_ge() -> Bool { return "x" >= "x"; } +pub fn equal_strings_lt() -> Bool { return "x" < "x"; } +pub fn empty_lt() -> Bool { return "" < "a"; } +pub fn empty_le() -> Bool { return "" <= ""; } +pub fn prefix_lt() -> Bool { return "abc" < "abcd"; } diff --git a/tests/codegen-deno/string_lex_cmp.harness.mjs b/tests/codegen-deno/string_lex_cmp.harness.mjs new file mode 100644 index 0000000..3f28454 --- /dev/null +++ b/tests/codegen-deno/string_lex_cmp.harness.mjs @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: MPL-2.0 +// issue #458 — exercises String relational ops via the Deno-ESM +// backend. JS's <, >, <=, >= on strings are lex compare natively, so +// the typecheck fix is enough — codegen needs no special case. +import assert from "node:assert/strict"; +import { + lt, gt, le, ge, + first_lt, first_gt, + equal_strings_le, equal_strings_ge, equal_strings_lt, + empty_lt, empty_le, prefix_lt, +} from "./string_lex_cmp.deno.js"; + +assert.equal(lt("abc", "abd"), true, "abc < abd"); +assert.equal(lt("abd", "abc"), false, "abd not < abc"); +assert.equal(gt("z", "a"), true, "z > a"); +assert.equal(gt("a", "z"), false, "a not > z"); +assert.equal(le("x", "x"), true, "x <= x (equal)"); +assert.equal(le("x", "y"), true, "x <= y"); +assert.equal(le("y", "x"), false, "y not <= x"); +assert.equal(ge("x", "x"), true, "x >= x (equal)"); +assert.equal(ge("y", "x"), true, "y >= x"); +assert.equal(ge("x", "y"), false, "x not >= y"); + +assert.equal(first_lt(), true, "lit: abc < abd"); +assert.equal(first_gt(), true, "lit: z > a"); +assert.equal(equal_strings_le(), true, "equal le"); +assert.equal(equal_strings_ge(), true, "equal ge"); +assert.equal(equal_strings_lt(), false, "equal lt is false"); +assert.equal(empty_lt(), true, "empty < non-empty"); +assert.equal(empty_le(), true, "empty <= empty"); +assert.equal(prefix_lt(), true, "prefix < longer"); + +console.log("string_lex_cmp.harness.mjs OK");