lowering: Option equality over a concrete List type (ArrayList, RingList) is an internal error #381

Closed
opened 2026-10-17 22:51:21 +00:00 by orion · 5 comments
Collaborator

Context

Found while implementing #300 (Buffer as a List). It reproduces on a published toolchain without any change of that issue, with the concrete list types RingList and ArrayList.

Reproduction

use RingList from "std/collections"

const ring: RingList<Int> = RingList.from([2, 3])
const found: RingList<Int>? = Some(ring)
print(found == Some(ring))

torb run of this program (toolchain nightly-20261015, torb 0.1.0) stops with

internal error: t_std_x2f_core_compare_Equals_equals__T_std_x2f_core_option_Option__T_std_x2f_collections_list_RingList__Int64 b4: argument 1 is `Object(List<Int64>)` and %6 is `Record(T_std_x2f_collections_list_RingList__Int64)`

The same happens for Option<ArrayList<Int>>, and for Option<Buffer<Int>> once Buffer is declared a List (extend<Item> Buffer<Item> with List<Item>; it has an Equals of its own before). Option<List<Int>> (the trait type, what a literal is) is not affected.

Acceptance criteria

  • Option<T> == Option<T> (and !=) type checks, lowers and runs for a concrete T (ArrayList, RingList, Buffer) that implements List and whose Equals comes from extend<Item: Equals> List<Item> with Equals: the equality passes the List value to List.equals as the witness expects it.
  • A conformance program pins Option equality over an ArrayList and a RingList, in the VM and natively.

Notes

  • Not seed-touching as far as seen: the failing step is the IR the lowering builds for the generic Equals.equals instance, not new syntax.
  • Workaround at the call site: unwrap first (if const Some(inner) = found { inner == other }).

Refinement

  • Implements: docs/design/COLLECTIONS.md section 6e (Buffer is a List; Equals is the one of List), and the existing conformance concrete-collection-equality (direct == on concrete collections), extended to the same comparison through a generic derived Equals.
  • Where: compiler/src/ir/lower/derive.trb fieldEquality calls traitMemberFunction(.., field.subject, "Equals", "equals") for a non-primitive field of the derived Equals of Option<T>; compiler/src/ir/witness.trb boxedReceiverOf is what makes the direct RingList == RingList call box both operands with the List table. The failing call hands the unboxed Record where the callee wants Object(List<Int64>), so the wrapper is not applied (or not matched) on this path. Start with the IR dump of the reproduction and compare it to ringList == ringList.
  • Seed: not touched (no syntax, no native, no driver flag, no std name looked up by string); a compiler lowering fix only.
  • Size: one PR, two Conventional Commits: fix(lowering): box the operands of a field's Equals through the extend target and test(conformance): Option equality over ArrayList, RingList and Buffer.

Test

New conformance program tests/conformance/option-list-equality.trb (+ .expected, .allocations via sh tools/conformance.sh --update-budgets --filter option-list-equality in CI-compatible form), in the style of concrete-collection-equality.trb:

use ArrayList, RingList from "std/collections"

const ring: RingList<Int> = RingList.from([2, 3])
const found: RingList<Int>? = Some(ring)
print(found == Some(ring))                       // true
print(found != Some(ring))                       // false
print(found == Some(RingList.from([3, 2])))      // false
print(found == None)                             // false

const array: ArrayList<Int> = ArrayList.from([2, 3])
const foundArray: ArrayList<Int>? = Some(array)
print(foundArray == Some(array))                 // true
print(foundArray != Some(ArrayList.from([2])))  // true

plus a Buffer<Int> case in tests/conformance/buffer-equality-show-hash.trb's style if Buffer is a List on main (it is, section 6e). Runs in the VM and native as every conformance program does.

## Context Found while implementing #300 (`Buffer` as a `List`). It reproduces on a published toolchain without any change of that issue, with the concrete list types `RingList` and `ArrayList`. ## Reproduction ```trb use RingList from "std/collections" const ring: RingList<Int> = RingList.from([2, 3]) const found: RingList<Int>? = Some(ring) print(found == Some(ring)) ``` `torb run` of this program (toolchain `nightly-20261015`, `torb 0.1.0`) stops with ```text internal error: t_std_x2f_core_compare_Equals_equals__T_std_x2f_core_option_Option__T_std_x2f_collections_list_RingList__Int64 b4: argument 1 is `Object(List<Int64>)` and %6 is `Record(T_std_x2f_collections_list_RingList__Int64)` ``` The same happens for `Option<ArrayList<Int>>`, and for `Option<Buffer<Int>>` once `Buffer` is declared a `List` (`extend<Item> Buffer<Item> with List<Item>`; it has an `Equals` of its own before). `Option<List<Int>>` (the trait type, what a literal is) is not affected. ## Acceptance criteria - [ ] `Option<T> == Option<T>` (and `!=`) type checks, lowers and runs for a concrete `T` (`ArrayList`, `RingList`, `Buffer`) that implements `List` and whose `Equals` comes from `extend<Item: Equals> List<Item> with Equals`: the equality passes the `List` value to `List.equals` as the witness expects it. - [ ] A conformance program pins `Option` equality over an `ArrayList` and a `RingList`, in the VM and natively. ## Notes - Not seed-touching as far as seen: the failing step is the IR the lowering builds for the generic `Equals.equals` instance, not new syntax. - Workaround at the call site: unwrap first (`if const Some(inner) = found { inner == other }`). ## Refinement - **Implements:** `docs/design/COLLECTIONS.md` section 6e (`Buffer` is a `List`; `Equals` is the one of `List`), and the existing conformance `concrete-collection-equality` (direct `==` on concrete collections), extended to the same comparison through a generic derived `Equals`. - **Where:** `compiler/src/ir/lower/derive.trb` `fieldEquality` calls `traitMemberFunction(.., field.subject, "Equals", "equals")` for a non-primitive field of the derived `Equals` of `Option<T>`; `compiler/src/ir/witness.trb` `boxedReceiverOf` is what makes the direct `RingList == RingList` call box both operands with the `List` table. The failing call hands the unboxed `Record` where the callee wants `Object(List<Int64>)`, so the wrapper is not applied (or not matched) on this path. Start with the IR dump of the reproduction and compare it to `ringList == ringList`. - **Seed:** not touched (no syntax, no native, no driver flag, no std name looked up by string); a compiler lowering fix only. - **Size:** one PR, two Conventional Commits: `fix(lowering): box the operands of a field's Equals through the extend target` and `test(conformance): Option equality over ArrayList, RingList and Buffer`. ### Test New conformance program `tests/conformance/option-list-equality.trb` (+ `.expected`, `.allocations` via `sh tools/conformance.sh --update-budgets --filter option-list-equality` in CI-compatible form), in the style of `concrete-collection-equality.trb`: ```trb use ArrayList, RingList from "std/collections" const ring: RingList<Int> = RingList.from([2, 3]) const found: RingList<Int>? = Some(ring) print(found == Some(ring)) // true print(found != Some(ring)) // false print(found == Some(RingList.from([3, 2]))) // false print(found == None) // false const array: ArrayList<Int> = ArrayList.from([2, 3]) const foundArray: ArrayList<Int>? = Some(array) print(foundArray == Some(array)) // true print(foundArray != Some(ArrayList.from([2]))) // true ``` plus a `Buffer<Int>` case in `tests/conformance/buffer-equality-show-hash.trb`'s style if `Buffer` is a `List` on `main` (it is, section 6e). Runs in the VM and native as every conformance program does.
orion changed title from lowering: Option equality over a List that is not an ArrayList is an internal error (RingList) to lowering: Option equality over a concrete List type (ArrayList, RingList) is an internal error 2026-10-17 22:51:34 +00:00
Author
Collaborator

[loop:torbscript] claim refine by session:loop-torbscript-20261019 until 2026-10-18T24:00Z

[loop:torbscript] claim refine by session:loop-torbscript-20261019 until 2026-10-18T24:00Z
Author
Collaborator

[loop:torbscript] decided by the loop: a compiler lowering fix with a conformance test, not a std workaround. Why: the same comparison written directly (ringList == ringList) works through the box wrapper of boxedReceiverOf, so the generic derived Equals path (fieldEquality in derive.trb) must reach the same wrapper; fixing the lowering covers ArrayList, RingList and Buffer at once. Not seed-touching, one PR of two commits.

[loop:torbscript] refine: acceptance criteria, design anchor (COLLECTIONS 6e), suspected location and the reproduction test are in the issue body; label ready set. Worker: sonnet/medium, about 90k tokens. next: implement

[loop:torbscript] decided by the loop: a compiler lowering fix with a conformance test, not a std workaround. Why: the same comparison written directly (`ringList == ringList`) works through the box wrapper of `boxedReceiverOf`, so the generic derived `Equals` path (`fieldEquality` in `derive.trb`) must reach the same wrapper; fixing the lowering covers ArrayList, RingList and Buffer at once. Not seed-touching, one PR of two commits. [loop:torbscript] refine: acceptance criteria, design anchor (COLLECTIONS 6e), suspected location and the reproduction test are in the issue body; label `ready` set. Worker: sonnet/medium, about 90k tokens. next: implement
Author
Collaborator

[loop:torbscript] claim implement by session:loop-torbscript-20261019 until 2026-10-19T05:00Z

[loop:torbscript] claim implement by session:loop-torbscript-20261019 until 2026-10-19T05:00Z
Author
Collaborator

[loop:torbscript] implement: the lowering fix is already on main. torb ir of the reproduction on nightly-20261015 shows the wrapper for the Equals of List boxing self but not other; 3bde36d0 (Bytes is Buffer, after that nightly) made boxedReceiverOf box every parameter that is Self of the trait, and the derived equals of Option goes through that same path. The first CI run of the new conformance program on main matched in native and VM (only the allocation budget placeholder was red; set to 105). So the PR is one commit, test(conformance): Option equality over ArrayList, RingList and Buffer, instead of the planned two (a lowering commit would be empty). PR #385, CI success (gates, tier A, bootstrap, pack the seed, pdisc), claim released, tokens ~150k, next: review (waiting on merge gate argus)

[loop:torbscript] implement: the lowering fix is already on main. `torb ir` of the reproduction on nightly-20261015 shows the wrapper for the `Equals` of `List` boxing `self` but not `other`; `3bde36d0` (Bytes is Buffer<UInt8>, after that nightly) made `boxedReceiverOf` box every parameter that is `Self` of the trait, and the derived `equals` of `Option` goes through that same path. The first CI run of the new conformance program on main matched in native and VM (only the allocation budget placeholder was red; set to 105). So the PR is one commit, `test(conformance): Option equality over ArrayList, RingList and Buffer`, instead of the planned two (a lowering commit would be empty). PR https://git.torb.dev/torbscript/language/pulls/385, CI success (gates, tier A, bootstrap, pack the seed, pdisc), claim released, tokens ~150k, next: review (waiting on merge gate argus)
argus closed this issue 2026-10-18 23:27:38 +00:00
Author
Collaborator

[loop:torbscript] close: landed as dc077652a0 via #385 (the lowering fix itself came with 3bde36d0). The push CI of main on that commit is red, but only on infrastructure: bootstrap (linux-x64) failed in the runner's checkout cache (shallow.lock: File exists, a stale git lock in the act cache), and gates and pack the seed failed after 0 to 5 s as its dependents; the PR run of the same tree was green. The loop cannot rerun a workflow through the API; the next push to main reruns it. Next: done, main CI waiting (runner cache lock)

[loop:torbscript] close: landed as https://git.torb.dev/torbscript/language/commit/dc077652a00df3ec9244b6a929bd66e390ae1b0c via https://git.torb.dev/torbscript/language/pulls/385 (the lowering fix itself came with 3bde36d0). The push CI of main on that commit is red, but only on infrastructure: `bootstrap (linux-x64)` failed in the runner's checkout cache (`shallow.lock: File exists`, a stale git lock in the act cache), and `gates` and `pack the seed` failed after 0 to 5 s as its dependents; the PR run of the same tree was green. The loop cannot rerun a workflow through the API; the next push to main reruns it. Next: done, main CI waiting (runner cache lock)
Sign in to join this conversation.
No labels
loop
ready
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
torbscript/language#381
No description provided.