fix(compiler): == / != on references is identity; only text compares by content

emit_bin_vals lowered every pointer-typed ==/!= to @lp_str_eq, so two
distinct records compared their bytes up to the first zero byte. Content
compare now needs both sides to be text (string, or the untyped
pointer/ptr runtime code carries text in); other references use
icmp eq ptr, and string vs a non-text reference is a compile error.
Adds selfhost/tests/identity.ludic; both seeds regenerated.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-16 16:06:31 +03:00
parent e010c2cecc
commit 1900f9ca80
7 changed files with 19750 additions and 19403 deletions

View file

@ -1011,7 +1011,8 @@ are **`& | ^ << >> ~`** (`>>` is a logical/unsigned shift).
**Strings are values.** `a + b` concatenates two strings, and `a == b` / `a != b` **Strings are values.** `a + b` concatenates two strings, and `a == b` / `a != b`
compare them **by content** (not by pointer). `"go" + dir == "goleft"` works as compare them **by content** (not by pointer). `"go" + dir == "goleft"` works as
written. (Under the hood these call a small emitted string runtime; a `==`/`!=` written. (Under the hood these call a small emitted string runtime; a `==`/`!=`
against `null` is still a pointer test.) against `null` is still a pointer test. Every other reference — records, slices,
enums — compares by identity, and comparing a string with one is a compile error.)
**Interpolation is the readable way to build them.** A backtick string **Interpolation is the readable way to build them.** A backtick string
`` `text {expr} text` `` embeds any expression in `{…}` — numbers, bools and `` `text {expr} text` `` embeds any expression in `{…}` — numbers, bools and

View file

@ -0,0 +1,10 @@
bump: patch
type: fix
**`==` / `!=` on records is identity again** — only two strings compare by content.
Every pointer-typed comparison used to be lowered to a C-string content compare,
so two distinct records (properties, slices, enums) compared their bytes up to
the first zero byte: `a == b` could be true for different objects that shared a
leading field, depending on layout. References now compare by identity
(`icmp eq ptr`); `string` (and untyped `pointer`/`pointers` text) still compares by content,
and comparing a `string` with a non-string reference is a compile error.

View file

@ -161,19 +161,31 @@ function emit_bin(e: Node) -> Val {
return r return r
} }
# text types: `==` on two of these compares content, not the pointers. Besides
# `string`, that is the untyped raw pointer — `pointer`, and `ptr`, the element of
# a `pointers` buffer — which runtime code uses to carry text.
function is_textish(t: pointer) -> bool { return (t == "string") or (t == "pointer") or (t == "ptr") }
# lower `a <op> b` on two already-evaluated operands. Shared by binary expressions # lower `a <op> b` on two already-evaluated operands. Shared by binary expressions
# and compound assignment (`x += y` is exactly `x = x + y`), so both agree on # and compound assignment (`x += y` is exactly `x = x + y`), so both agree on
# string concatenation, Q16.16 multiply/divide, and int->long promotion. # string concatenation, Q16.16 multiply/divide, and int->long promotion.
# `isnull` marks a comparison against the literal null (pointer identity, not # `isnull` marks a comparison against the literal null (pointer identity, not
# string content). # string content).
function emit_bin_vals(op: pointer, a: Val, b: Val, isnull: bool) -> Val { function emit_bin_vals(op: pointer, a: Val, b: Val, isnull: bool) -> Val {
# strings are pointer-typed, so any `+` with a pointer operand is concatenation, # strings are pointer-typed, so any `+` with a pointer operand is concatenation.
# and `==`/`!=` between pointers is content comparison — except `x == null`, # `==`/`!=` compares by content only when BOTH sides are text (`string`, or an
# which is a pointer-identity test and falls through to the icmp below. # untyped raw pointer, see is_textish); every other reference — records,
# slices, enums, buffers — compares by identity in the icmp below, as does
# `x == null`. A string against a non-text reference is a type error.
let ptrish = (llty(a.ty) == "ptr") or (llty(b.ty) == "ptr") let ptrish = (llty(a.ty) == "ptr") or (llty(b.ty) == "ptr")
if ptrish { if ptrish {
if (op == ("+")) { return emit_str_op("+", a, b) } if (op == ("+")) { return emit_str_op("+", a, b) }
if ((op == ("==")) or (op == ("!="))) and not isnull { return emit_str_op(op, a, b) } if ((op == ("==")) or (op == ("!="))) and not isnull {
if is_textish(a.ty) and is_textish(b.ty) { return emit_str_op(op, a, b) }
if ((a.ty == "string") and (llty(b.ty) == "ptr")) or ((b.ty == "string") and (llty(a.ty) == "ptr")) {
perr(`cannot compare {a.ty} with {b.ty} using {op}: strings compare by content, other references by identity`)
}
}
} }
let fx = (a.ty == "fixed") or (b.ty == "fixed") let fx = (a.ty == "fixed") or (b.ty == "fixed")
# a 64-bit operand (and no fixed/ptr involved) promotes the whole expression to # a 64-bit operand (and no fixed/ptr involved) promotes the whole expression to

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,21 @@
program T {
# `==` / `!=` on two references is identity; only strings compare by content.
# The records below start with a zero/equal field, which a byte compare would
# have called equal.
property Thing { kind: int = 0, name: string = "" }
entry {
let a = new Thing
let b = new Thing
print(a == b) # 0: distinct records, same (zero) leading field
print(a == a) # 1: the same record
a.kind = 3
b.kind = 3
print(a != b) # 1: equal fields, still distinct
let c = a
print(c == a) # 1: an alias is the same record
let xs: []Thing = [a, b]
print(xs[1] == b) # 1: identity through a slice
let s = "ab"
print(s + "c" == "abc") # 1: strings still compare by content
}
}

View file

@ -82,6 +82,7 @@ function cmd_selfhost_test() -> int {
print("== the self-host compiler is correct ==") print("== the self-host compiler is correct ==")
sh_case("structs", "7 9 109 2 42") sh_case("structs", "7 9 109 2 42")
sh_case("identity", "0 1 1 1 1 1")
sh_case("slices", "0 20 361 777") sh_case("slices", "0 20 361 777")
sh_case("sort", "0 39 20 1 2 3 3 1 30 10 20 40 2 8") sh_case("sort", "0 39 20 1 2 3 3 1 30 10 20 40 2 8")
sh_case("control", "55 4 15 1") sh_case("control", "55 4 15 1")