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:
parent
e010c2cecc
commit
1900f9ca80
7 changed files with 19750 additions and 19403 deletions
|
|
@ -1011,7 +1011,8 @@ are **`& | ^ << >> ~`** (`>>` is a logical/unsigned shift).
|
|||
**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
|
||||
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
|
||||
`` `text {expr} text` `` embeds any expression in `{…}` — numbers, bools and
|
||||
|
|
|
|||
10
changes/reference-identity.md
Normal file
10
changes/reference-identity.md
Normal 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.
|
||||
|
|
@ -161,19 +161,31 @@ function emit_bin(e: Node) -> Val {
|
|||
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
|
||||
# and compound assignment (`x += y` is exactly `x = x + y`), so both agree on
|
||||
# string concatenation, Q16.16 multiply/divide, and int->long promotion.
|
||||
# `isnull` marks a comparison against the literal null (pointer identity, not
|
||||
# string content).
|
||||
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,
|
||||
# and `==`/`!=` between pointers is content comparison — except `x == null`,
|
||||
# which is a pointer-identity test and falls through to the icmp below.
|
||||
# strings are pointer-typed, so any `+` with a pointer operand is concatenation.
|
||||
# `==`/`!=` compares by content only when BOTH sides are text (`string`, or an
|
||||
# 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")
|
||||
if ptrish {
|
||||
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")
|
||||
# a 64-bit operand (and no fixed/ptr involved) promotes the whole expression to
|
||||
|
|
|
|||
19549
selfhost/ludicc.seed.ll
19549
selfhost/ludicc.seed.ll
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
21
selfhost/tests/identity.ludic
Normal file
21
selfhost/tests/identity.ludic
Normal 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
|
||||
}
|
||||
}
|
||||
|
|
@ -82,6 +82,7 @@ function cmd_selfhost_test() -> int {
|
|||
|
||||
print("== the self-host compiler is correct ==")
|
||||
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("sort", "0 39 20 1 2 3 3 1 30 10 20 40 2 8")
|
||||
sh_case("control", "55 4 15 1")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue