diff --git a/docs/plans/2026-05-10-23.3-ord-class-and-primitive-instances.md b/docs/plans/2026-05-10-23.3-ord-class-and-primitive-instances.md new file mode 100644 index 0000000..e21e35e --- /dev/null +++ b/docs/plans/2026-05-10-23.3-ord-class-and-primitive-instances.md @@ -0,0 +1,1308 @@ +# 23.3 — Ord Class + Primitive Instances — Implementation Plan + +> **Parent spec:** `docs/specs/2026-05-10-23-eq-ord-prelude.md` (Components row 23.3) +> +> **For agentic workers:** REQUIRED SUB-SKILL: use `skills/implement` +> to run this plan. Steps use `- [ ]` checkboxes for tracking. + +**Goal:** Ship `class Ord a extends Eq where compare : (a borrow, a +borrow) -> Ordering` in the auto-loaded prelude with three primitive +instances (Ord Int, Ord Bool, Ord Str), backed by a new +`ail_str_compare` in `runtime/str.c`. After this iter, a user +program can call `compare x y` on Int / Bool / Str and the +monomorphiser will synthesise `compare__Int` / `compare__Bool` / +`compare__Str` top-level fns in the prelude module that codegen +lowers as three-way branch ladders constructing the `Ordering` ADT +values (LT / EQ / GT). + +**Architecture:** All three `compare__T` symbols are hand-rolled via +the codegen intercept established in iter 23.2 — `compare__Bool` +cannot ride the existing primitive `<` dispatch because +`builtin_binop_typed` (`crates/ailang-codegen/src/synth.rs:260-294`) +has Int and Float arms only, and bringing Bool into that table is a +language-wide decision that does not belong here. Hand-rolling all +three keeps the family consistent: the IR shape is identical (a +type-specific LT-test branch + an EQ-test branch + a GT default +branch, each constructing the matching Ordering ctor via +`lower_ctor` for alloc-strategy correctness). The new +`ail_str_compare` in `runtime/str.c` normalises libc strcmp's signed +result to exactly -1 / 0 / +1 so the codegen branch ladder can +compare against constant 0 cleanly. The intercept doc comment from +iter 23.2.2 is corrected (it speculated that compare__Int/Bool would +ride natural lowering; the language facts dictate otherwise). + +Decision 11's single-superclass closure (`Ord` extends `Eq` ⇒ +every `instance Ord T` requires `instance Eq T` to exist in the +workspace) is automatically satisfied: iter 23.2 shipped the +three Eq instances. No new check-side mechanism needed. + +**Tech Stack:** `runtime/str.c` (one new C function), +`crates/ailang-codegen/src/lib.rs` (IR header decl + intercept +arms + a small block-emission helper), +`crates/ailang-core/src/workspace.rs` (prelude-load test extension), +`examples/prelude.ail.json` (class + 3 instances), +`examples/compare_primitives_smoke.ail.json` (new fixture). + +--- + +**Files this plan creates or modifies:** + +- Modify: `runtime/str.c` — append `ail_str_compare`. +- Modify: `crates/ailang-codegen/src/lib.rs` — header gains + `declare i32 @ail_str_compare(ptr, ptr)`; `try_emit_primitive_instance_body` + gains three arms (`compare__Int`, `compare__Bool`, + `compare__Str`); new private helper `emit_ordering_arm`; doc on + `try_emit_primitive_instance_body` corrected. +- Modify: `examples/prelude.ail.json` — append `class Ord a` + + three instance defs. Existing Eq / Ordering / class machinery + stays. +- Modify: `crates/ailang-core/src/workspace.rs` — extend + `loads_workspace_auto_injects_prelude` to assert `class Ord` + + Ord instances on Int / Bool / Str. +- Create: `examples/compare_primitives_smoke.ail.json` — E2E + fixture calling `compare` on each primitive with LT, EQ, GT + pairs. +- Modify: `crates/ail/tests/e2e.rs` — new E2E test + `compare_primitives_smoke_compiles_and_runs`. +- Modify: `crates/ailang-codegen/src/lib.rs` `mod tests` — three + IR-shape integration tests (one per primitive). + +--- + +## Task 1: `runtime/str.c::ail_str_compare` + IR declaration + +**Files:** +- Modify: `runtime/str.c` — append `ail_str_compare`. +- Modify: `crates/ailang-codegen/src/lib.rs:~482` — add + `declare i32 @ail_str_compare(ptr, ptr)` after the existing + `@ail_str_eq` declaration. +- Test: append a codegen unit test asserting the new IR header + line is present. + +- [ ] **Step 1: Write the failing test** + +Append to the existing `#[cfg(test)] mod tests` block in +`crates/ailang-codegen/src/lib.rs`: + +```rust +/// 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}" + ); +} +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: `cargo test --workspace --lib ir_header_declares_ail_str_compare` +Expected: FAIL — `header missing @ail_str_compare declaration`. + +- [ ] **Step 3: Append `ail_str_compare` to `runtime/str.c`** + +After the existing `ail_str_eq` function in `runtime/str.c`, append: + +```c +/* 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; +} +``` + +- [ ] **Step 4: Add the `@ail_str_compare` declaration to the IR header** + +In `crates/ailang-codegen/src/lib.rs`, directly after the existing +`out.push_str("declare zeroext i1 @ail_str_eq(ptr, ptr)\n");` +line (~line 482, added in iter 23.2.2), insert: + +```rust +// 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"); +``` + +- [ ] **Step 5: Run the test to verify it passes** + +Run: `cargo test --workspace --lib ir_header_declares_ail_str_compare` +Expected: PASS. + +- [ ] **Step 6: Re-run the existing test suite to confirm no regressions** + +Run: `cargo build --workspace && cargo test --workspace` +Expected: PASS. Snapshot fixtures under `crates/ail/tests/snapshots/` +will gain one new line each (the new `declare` line) — refresh via +`UPDATE_SNAPSHOTS=1` if the snapshot tests fail and the only diff +is the additive `@ail_str_compare` declaration. Include refreshed +snapshots in the commit. + +- [ ] **Step 7: Commit** + +```bash +git add runtime/str.c crates/ailang-codegen/src/lib.rs crates/ail/tests/snapshots +git commit -m "iter 23.3.1: runtime ail_str_compare + IR header declaration" +``` + +(Omit the `crates/ail/tests/snapshots` path from `git add` if no +snapshot files were modified.) + +--- + +## Task 2: Codegen — three `compare__T` arms in `try_emit_primitive_instance_body` + +**Files:** +- Modify: `crates/ailang-codegen/src/lib.rs::try_emit_primitive_instance_body` + (~line 2452): add three new match arms (`compare__Int`, + `compare__Bool`, `compare__Str`); correct the doc comment at + ~line 2437; add a private helper `emit_ordering_arm` directly + below the intercept. +- Modify: `crates/ailang-codegen/src/lib.rs` `mod tests` — three + unit tests on synthetic FnDefs (one per compare__T arm). + +- [ ] **Step 1: Write the failing tests** + +Append to the existing `#[cfg(test)] mod tests` block in +`crates/ailang-codegen/src/lib.rs`: + +```rust +/// Iter 23.3: codegen intercepts a fn named `compare__Int` and +/// emits a three-way branch ladder: `icmp slt i64 a, b` → +/// LT-block; else `icmp eq i64 a, b` → EQ-block; else GT-block. +/// Each block constructs the matching Ordering ctor via the +/// existing `lower_ctor` path. Pinned with a synthetic FnDef so +/// the test does not depend on prelude auto-injection. +#[test] +fn compare_int_mono_symbol_emits_branch_ladder() { + let m = synth_compare_module("compare__Int", Type::int(), "i64"); + let ir = emit_ir(&m).unwrap(); + assert!( + ir.contains("icmp slt i64"), + "compare__Int body must contain `icmp slt i64`; ir:\n{ir}" + ); + assert!( + ir.contains("icmp eq i64"), + "compare__Int body must contain `icmp eq i64`; ir:\n{ir}" + ); +} + +/// Iter 23.3: same shape for `compare__Bool`. The LT-test uses +/// `icmp ult i1` (unsigned: false=0 ult true=1 gives the natural +/// Bool ordering false < true); the EQ-test uses `icmp eq i1`. +#[test] +fn compare_bool_mono_symbol_emits_branch_ladder() { + let m = synth_compare_module("compare__Bool", Type::bool_(), "i1"); + let ir = emit_ir(&m).unwrap(); + assert!( + ir.contains("icmp ult i1"), + "compare__Bool body must contain `icmp ult i1`; ir:\n{ir}" + ); + assert!( + ir.contains("icmp eq i1"), + "compare__Bool body must contain `icmp eq i1`; ir:\n{ir}" + ); +} + +/// Iter 23.3: `compare__Str` calls `@ail_str_compare` to get the +/// normalised {-1, 0, +1}, then branches: slt 0 → LT, eq 0 → EQ, +/// else GT. +#[test] +fn compare_str_mono_symbol_emits_ail_str_compare_call() { + let m = synth_compare_module("compare__Str", Type::str_(), "ptr"); + let ir = emit_ir(&m).unwrap(); + assert!( + ir.contains("call i32 @ail_str_compare("), + "compare__Str body must call @ail_str_compare; ir:\n{ir}" + ); + assert!( + ir.contains("icmp slt i32"), + "compare__Str body must compare result `slt i32` against 0; ir:\n{ir}" + ); + assert!( + ir.contains("icmp eq i32"), + "compare__Str body must compare result `eq i32` against 0; ir:\n{ir}" + ); +} + +/// Test helper: minimal two-module workspace where the "prelude" +/// module declares `data Ordering = LT | EQ | GT` and an +/// instance-fn shell (the intercept overrides the body), and the +/// entry module's `main` is a Unit no-op so `emit_ir` returns a +/// well-formed program. Used by the three `compare_*` tests above. +fn synth_compare_module(fn_name: &str, param_ail_ty: Type, _llvm_param_ty: &str) -> Module { + let ordering = Def::Type(TypeDef { + name: "Ordering".into(), + vars: vec![], + ctors: vec![ + Ctor { name: "LT".into(), fields: vec![] }, + Ctor { name: "EQ".into(), fields: vec![] }, + Ctor { name: "GT".into(), fields: vec![] }, + ], + doc: None, + drop_iterative: false, + }); + // Ordering is `ptr` at the LLVM level (boxed ADT). + let compare = Def::Fn(FnDef { + name: fn_name.into(), + ty: Type::Fn { + params: vec![param_ail_ty.clone(), param_ail_ty.clone()], + ret: Box::new(Type::Con { name: "Ordering".into(), args: vec![] }), + effects: vec![], + param_modes: vec![ParamMode::Borrow, ParamMode::Borrow], + ret_mode: ParamMode::Implicit, + }, + params: vec!["x".into(), "y".into()], + body: Term::Lit { lit: Literal::Unit }, // placeholder; intercept overrides + suppress: vec![], + doc: None, + }); + let main_def = 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, + }); + Module { + schema: SCHEMA.into(), + name: "prelude".into(), + imports: vec![], + defs: vec![ordering, compare, main_def], + } +} +``` + +Note: the exact field set on `TypeDef` may vary (`drop_iterative` is +the iter-18e field). If the field name differs in the current codebase, +inspect `crates/ailang-core/src/ast.rs::TypeDef` and adjust. If `Ctor` +has additional fields, default them as the rest of the codebase does. + +- [ ] **Step 2: Run the three tests to verify they fail** + +Run: +``` +cargo test --workspace --lib compare_int_mono_symbol_emits_branch_ladder compare_bool_mono_symbol_emits_branch_ladder compare_str_mono_symbol_emits_ail_str_compare_call +``` +Expected: ALL THREE FAIL — `compare__T body must contain ...` (intercept arms not yet wired). + +- [ ] **Step 3: Add the private helper `emit_ordering_arm`** + +In `crates/ailang-codegen/src/lib.rs`, directly below +`try_emit_primitive_instance_body` (currently ending ~line 2495), +insert: + +```rust +/// Iter 23.3: emit one arm of the `compare__T` branch ladder. +/// Starts a fresh basic block labelled `label`, constructs the +/// matching `Ordering` ctor (LT / EQ / GT) via `lower_ctor`, and +/// emits a `ret ptr `. Used three times per `compare__T` +/// arm in `try_emit_primitive_instance_body`. The ctor is a +/// zero-field allocation; `lower_ctor` handles alloc-strategy +/// variance (Gc / Bump / Rc) without the intercept duplicating +/// the per-strategy logic. +fn emit_ordering_arm(&mut self, label: &str, ctor: &str) -> Result<()> { + self.start_block(label); + // term_ptr 0: synthetic call site, not present in the + // escape-analysis result; falls back to the conservative + // "escapes → heap allocate" default. Safe for the Ordering + // return value (the caller owns it after `ret`). + let (ssa, _llvm_ty) = self.lower_ctor( + "Ordering", + ctor, + &[], + 0, + )?; + self.body.push_str(&format!(" ret ptr {ssa}\n")); + self.block_terminated = true; + Ok(()) +} +``` + +- [ ] **Step 4: Correct the iter 23.2.2 doc comment** + +In `crates/ailang-codegen/src/lib.rs`, find the doc comment on +`try_emit_primitive_instance_body` (around line 2467 — the line +that reads `/// methods (\`eq__Int\`, \`eq__Bool\`, \`compare__Int\`, \`compare__Bool\`)`). + +Replace the lines describing the future arms with the corrected +shape: + +```rust +/// Currently inhabited arms: `eq__Str` (ships in 23.2.2) and +/// `compare__Int`, `compare__Bool`, `compare__Str` (this iter +/// 23.3). The two eq Int/Bool primitives (`eq__Int`, `eq__Bool`) +/// ride the natural `lower_eq` dispatch via their lambda body +/// `(== x y)` and do not need a hand-rolled body. The three +/// `compare` arms must be hand-rolled because there is no +/// type-polymorphic primitive that returns an Ordering ADT +/// value (Decision: `builtin_binop_typed` covers Int and Float +/// only — Bool is missing — so a surface-level +/// `if x < y { LT } else if x == y { EQ } else { GT }` body +/// would fail at codegen for `compare__Bool`). Hand-rolling all +/// three keeps the family consistent and the IR shape +/// predictable. +``` + +Use Edit with the precise old text in the current file to +substitute. Read the surrounding doc-comment context first to +make the old_string unique. + +- [ ] **Step 5: Add the three compare arms to the match block** + +Inside `try_emit_primitive_instance_body`'s `match fn_name { ... }`, +add three arms next to the existing `"eq__Str"` arm: + +```rust +"compare__Int" => { + if param_tys != ["i64", "i64"] || ret_ty != "ptr" { + return Err(CodegenError::Internal(format!( + "compare__Int body intercept: unexpected signature \ + ({param_tys:?}) -> {ret_ty} (want (i64, i64) -> ptr)" + ))); + } + let n = self.locals.len(); + let a_ssa = self.locals[n - 2].1.clone(); + let b_ssa = self.locals[n - 1].1.clone(); + let lt_test = self.fresh_ssa(); + self.body.push_str(&format!( + " {lt_test} = icmp slt i64 {a_ssa}, {b_ssa}\n" + )); + let id = self.fresh_id(); + let lt_label = format!("cmp_lt_{id}"); + let after_lt_label = format!("cmp_after_lt_{id}"); + self.body.push_str(&format!( + " br i1 {lt_test}, label %{lt_label}, label %{after_lt_label}\n" + )); + self.emit_ordering_arm(<_label, "LT")?; + self.start_block(&after_lt_label); + let eq_test = self.fresh_ssa(); + self.body.push_str(&format!( + " {eq_test} = icmp eq i64 {a_ssa}, {b_ssa}\n" + )); + let eq_label = format!("cmp_eq_{id}"); + let gt_label = format!("cmp_gt_{id}"); + self.body.push_str(&format!( + " br i1 {eq_test}, label %{eq_label}, label %{gt_label}\n" + )); + self.emit_ordering_arm(&eq_label, "EQ")?; + self.emit_ordering_arm(>_label, "GT")?; + self.body.push_str("}\n\n"); + self.block_terminated = true; + Ok(true) +} +"compare__Bool" => { + if param_tys != ["i1", "i1"] || ret_ty != "ptr" { + return Err(CodegenError::Internal(format!( + "compare__Bool body intercept: unexpected signature \ + ({param_tys:?}) -> {ret_ty} (want (i1, i1) -> ptr)" + ))); + } + let n = self.locals.len(); + let a_ssa = self.locals[n - 2].1.clone(); + let b_ssa = self.locals[n - 1].1.clone(); + let lt_test = self.fresh_ssa(); + self.body.push_str(&format!( + " {lt_test} = icmp ult i1 {a_ssa}, {b_ssa}\n" + )); + let id = self.fresh_id(); + let lt_label = format!("cmp_lt_{id}"); + let after_lt_label = format!("cmp_after_lt_{id}"); + self.body.push_str(&format!( + " br i1 {lt_test}, label %{lt_label}, label %{after_lt_label}\n" + )); + self.emit_ordering_arm(<_label, "LT")?; + self.start_block(&after_lt_label); + let eq_test = self.fresh_ssa(); + self.body.push_str(&format!( + " {eq_test} = icmp eq i1 {a_ssa}, {b_ssa}\n" + )); + let eq_label = format!("cmp_eq_{id}"); + let gt_label = format!("cmp_gt_{id}"); + self.body.push_str(&format!( + " br i1 {eq_test}, label %{eq_label}, label %{gt_label}\n" + )); + self.emit_ordering_arm(&eq_label, "EQ")?; + self.emit_ordering_arm(>_label, "GT")?; + self.body.push_str("}\n\n"); + self.block_terminated = true; + Ok(true) +} +"compare__Str" => { + if param_tys != ["ptr", "ptr"] || ret_ty != "ptr" { + return Err(CodegenError::Internal(format!( + "compare__Str body intercept: unexpected signature \ + ({param_tys:?}) -> {ret_ty} (want (ptr, ptr) -> ptr)" + ))); + } + let n = self.locals.len(); + let a_ssa = self.locals[n - 2].1.clone(); + let b_ssa = self.locals[n - 1].1.clone(); + let cmp_res = self.fresh_ssa(); + self.body.push_str(&format!( + " {cmp_res} = call i32 @ail_str_compare(ptr {a_ssa}, ptr {b_ssa})\n" + )); + let lt_test = self.fresh_ssa(); + self.body.push_str(&format!( + " {lt_test} = icmp slt i32 {cmp_res}, 0\n" + )); + let id = self.fresh_id(); + let lt_label = format!("cmp_lt_{id}"); + let after_lt_label = format!("cmp_after_lt_{id}"); + self.body.push_str(&format!( + " br i1 {lt_test}, label %{lt_label}, label %{after_lt_label}\n" + )); + self.emit_ordering_arm(<_label, "LT")?; + self.start_block(&after_lt_label); + let eq_test = self.fresh_ssa(); + self.body.push_str(&format!( + " {eq_test} = icmp eq i32 {cmp_res}, 0\n" + )); + let eq_label = format!("cmp_eq_{id}"); + let gt_label = format!("cmp_gt_{id}"); + self.body.push_str(&format!( + " br i1 {eq_test}, label %{eq_label}, label %{gt_label}\n" + )); + self.emit_ordering_arm(&eq_label, "EQ")?; + self.emit_ordering_arm(>_label, "GT")?; + self.body.push_str("}\n\n"); + self.block_terminated = true; + Ok(true) +} +``` + +- [ ] **Step 6: Run the three unit tests to verify they pass** + +Run: +``` +cargo test --workspace --lib compare_int_mono_symbol_emits_branch_ladder compare_bool_mono_symbol_emits_branch_ladder compare_str_mono_symbol_emits_ail_str_compare_call +``` +Expected: ALL THREE PASS. + +- [ ] **Step 7: Run the full codegen test suite to confirm no regressions** + +Run: `cargo test --workspace -p ailang-codegen` +Expected: PASS. + +- [ ] **Step 8: Commit** + +```bash +git add crates/ailang-codegen/src/lib.rs +git commit -m "iter 23.3.2: codegen — three compare__T intercept arms + emit_ordering_arm helper" +``` + +--- + +## Task 3: Prelude — `class Ord a extends Eq` + three instances + +**Files:** +- Modify: `examples/prelude.ail.json` — append `class Ord` def + + three `instance Ord T` defs after the existing Eq defs. +- Modify: `crates/ailang-core/src/workspace.rs::loads_workspace_auto_injects_prelude` + test — append assertions for `class Ord` + Ord instances. + +- [ ] **Step 1: Write the failing test** + +In `crates/ailang-core/src/workspace.rs::loads_workspace_auto_injects_prelude`, +after the existing Eq-presence + Eq-instances assertion block +(added in iter 23.2.3), append: + +```rust +// Iter 23.3: prelude also ships the `Ord` class plus three +// primitive instances (Ord Int, Ord Bool, Ord Str). Ord +// extends Eq (Decision 11 single-superclass closure), so its +// shape is the same as Eq's instance set; the loader uses the +// superclass walk to verify the Eq instances exist. +assert!( + prelude.defs.iter().any(|d| matches!( + d, + crate::ast::Def::Class(c) if c.name == "Ord" + )), + "prelude must contain Ord class def" +); +let ord_instance_types: Vec<&str> = prelude + .defs + .iter() + .filter_map(|d| match d { + crate::ast::Def::Instance(i) if i.class == "Ord" => { + if let crate::ast::Type::Con { name, .. } = &i.type_ { + Some(name.as_str()) + } else { + None + } + } + _ => None, + }) + .collect(); +assert!( + ord_instance_types.contains(&"Int"), + "prelude must contain `instance Ord Int`; saw: {ord_instance_types:?}" +); +assert!( + ord_instance_types.contains(&"Bool"), + "prelude must contain `instance Ord Bool`; saw: {ord_instance_types:?}" +); +assert!( + ord_instance_types.contains(&"Str"), + "prelude must contain `instance Ord Str`; saw: {ord_instance_types:?}" +); +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: `cargo test --workspace -p ailang-core loads_workspace_auto_injects_prelude` +Expected: FAIL — `prelude must contain Ord class def`. + +- [ ] **Step 3: Extend `examples/prelude.ail.json` with Ord + 3 instances** + +After the last `Eq Str` instance def in `examples/prelude.ail.json` +(before the closing `]` of the outer `defs` array), append (with +a leading comma after the `Eq Str` instance's closing `}`): + +```json +, + { + "kind": "class", + "name": "Ord", + "param": "a", + "superclass": { "class": "Eq", "type": "a" }, + "doc": "Total ordering. Ships in milestone 23 alongside Eq. `compare x y` returns LT, EQ, or GT (the three-ctor Ordering ADT also in the prelude). Decision 11's single-superclass closure requires `instance Eq T` for every `instance Ord T` — the three Ord instances below pair with the three Eq instances shipped in iter 23.2.3.", + "methods": [ + { + "name": "compare", + "type": { + "k": "fn", + "params": [ + { "k": "var", "name": "a" }, + { "k": "var", "name": "a" } + ], + "param_modes": ["borrow", "borrow"], + "ret": { "k": "con", "name": "Ordering" }, + "effects": [] + } + } + ] + }, + { + "kind": "instance", + "class": "Ord", + "type": { "k": "con", "name": "Int" }, + "doc": "Ord Int. The lambda body shape is a placeholder for round-trip stability; the codegen intercept `try_emit_primitive_instance_body::\"compare__Int\"` emits a three-way `icmp slt` / `icmp eq` branch ladder constructing LT / EQ / GT.", + "methods": [ + { + "name": "compare", + "body": { + "t": "lam", + "params": ["x", "y"], + "paramTypes": [ + { "k": "var", "name": "a" }, + { "k": "var", "name": "a" } + ], + "retType": { "k": "con", "name": "Ordering" }, + "body": { + "t": "ctor", + "type": "Ordering", + "ctor": "EQ", + "args": [] + } + } + } + ] + }, + { + "kind": "instance", + "class": "Ord", + "type": { "k": "con", "name": "Bool" }, + "doc": "Ord Bool. Body lowered via `try_emit_primitive_instance_body::\"compare__Bool\"` — `icmp ult i1` LT-test, `icmp eq i1` EQ-test, GT default.", + "methods": [ + { + "name": "compare", + "body": { + "t": "lam", + "params": ["x", "y"], + "paramTypes": [ + { "k": "var", "name": "a" }, + { "k": "var", "name": "a" } + ], + "retType": { "k": "con", "name": "Ordering" }, + "body": { + "t": "ctor", + "type": "Ordering", + "ctor": "EQ", + "args": [] + } + } + } + ] + }, + { + "kind": "instance", + "class": "Ord", + "type": { "k": "con", "name": "Str" }, + "doc": "Ord Str. Body lowered via `try_emit_primitive_instance_body::\"compare__Str\"` — `call i32 @ail_str_compare(ptr, ptr)` then branch on slt-0 / eq-0 against the normalised {-1, 0, +1} return.", + "methods": [ + { + "name": "compare", + "body": { + "t": "lam", + "params": ["x", "y"], + "paramTypes": [ + { "k": "var", "name": "a" }, + { "k": "var", "name": "a" } + ], + "retType": { "k": "con", "name": "Ordering" }, + "body": { + "t": "ctor", + "type": "Ordering", + "ctor": "EQ", + "args": [] + } + } + } + ] + } +``` + +The placeholder body returns `EQ` — codegen never lowers it +(intercept fires first), but the typechecker validates the body +against the declared return type `Ordering`. `EQ` is a zero-arg +Ordering ctor, so the body is well-typed. + +- [ ] **Step 4: Run the prelude-load test to verify it passes** + +Run: `cargo test --workspace -p ailang-core loads_workspace_auto_injects_prelude` +Expected: PASS. + +- [ ] **Step 5: Run the full workspace test suite** + +Run: `cargo build --workspace && cargo test --workspace` +Expected: PASS. + +If any pre-existing test fails because of the new Ord defs in the +prelude (e.g. a fixture that defines its own `class Ord` or method +`compare` and now collides on `MethodNameCollision`), STOP and +report `NEEDS_CONTEXT` with the failing test names — do NOT +modify unrelated tests to make them pass. (The iter 23.2.3-prep +commits already renamed fixture-side `class Eq.eq` and +`class Ord.lt` to `TEq.teq` / `TOrd.tlt`; if any test fixture +still defines `compare` as a method name on its own class, that +would be a new collision.) + +- [ ] **Step 6: Commit** + +```bash +git add examples/prelude.ail.json crates/ailang-core/src/workspace.rs +git commit -m "iter 23.3.3: prelude — class Ord a extends Eq + Ord Int/Bool/Str instances" +``` + +--- + +## Task 4: E2E fixture + IR-shape integration tests + +**Files:** +- Create: `examples/compare_primitives_smoke.ail.json` — fixture + calling `compare` on Int/Bool/Str with LT/EQ/GT pairs, matching + the result against the three Ordering ctors, printing 1/2/3. +- Modify: `crates/ail/tests/e2e.rs` — new test + `compare_primitives_smoke_compiles_and_runs`. +- Modify: `crates/ailang-codegen/src/lib.rs` `mod tests` — three + IR-shape integration tests on the new fixture (mirror the iter + 23.2.4 pattern). + +- [ ] **Step 1: Write the failing E2E test** + +In `crates/ail/tests/e2e.rs`, after the existing +`eq_primitives_smoke_compiles_and_runs` test (added in iter +23.2.4), append: + +```rust +/// Iter 23.3: end-to-end coverage for the auto-loaded `Ord` class +/// + three primitive instances. The fixture calls `compare` on +/// Int / Bool / Str with three pairs each (an LT pair, an EQ +/// pair, a GT pair), matches the result against LT / EQ / GT, +/// and prints 1 / 2 / 3 respectively. The monomorphiser +/// synthesises `compare__Int` / `compare__Bool` / `compare__Str` +/// in the prelude module; codegen's `try_emit_primitive_instance_body` +/// intercept emits the branch ladder; the binary prints nine +/// lines in the order LT (1), EQ (2), GT (3), per type. +#[test] +fn compare_primitives_smoke_compiles_and_runs() { + let stdout = build_and_run("compare_primitives_smoke.ail.json"); + assert_eq!(stdout, "1\n2\n3\n1\n2\n3\n1\n2\n3\n"); +} +``` + +- [ ] **Step 2: Run the test to verify it fails** + +Run: `cargo test --workspace -p ail compare_primitives_smoke_compiles_and_runs` +Expected: FAIL — `examples/compare_primitives_smoke.ail.json` does +not exist; `ail build` exits non-zero. + +- [ ] **Step 3: Write the fixture** + +Create `examples/compare_primitives_smoke.ail.json`: + +```json +{ + "schema": "ailang/v0", + "name": "compare_primitives_smoke", + "imports": [], + "defs": [ + { + "kind": "fn", + "name": "ord_to_int", + "doc": "Map an Ordering to a printable Int. LT → 1, EQ → 2, GT → 3. Used by main to render the result of each `compare` call as a single line of stdout.", + "type": { + "k": "fn", + "params": [{ "k": "con", "name": "Ordering" }], + "ret": { "k": "con", "name": "Int" }, + "effects": [] + }, + "params": ["o"], + "body": { + "t": "match", + "scrutinee": { "t": "var", "name": "o" }, + "arms": [ + { + "pat": { "p": "ctor", "ctor": "LT", "fields": [] }, + "body": { "t": "lit", "lit": { "kind": "int", "value": 1 } } + }, + { + "pat": { "p": "ctor", "ctor": "EQ", "fields": [] }, + "body": { "t": "lit", "lit": { "kind": "int", "value": 2 } } + }, + { + "pat": { "p": "ctor", "ctor": "GT", "fields": [] }, + "body": { "t": "lit", "lit": { "kind": "int", "value": 3 } } + } + ] + } + }, + { + "kind": "fn", + "name": "main", + "type": { + "k": "fn", + "params": [], + "ret": { "k": "con", "name": "Unit" }, + "effects": ["IO"] + }, + "params": [], + "body": { + "t": "seq", + "lhs": { + "t": "do", + "op": "io/print_int", + "args": [{ + "t": "app", + "fn": { "t": "var", "name": "ord_to_int" }, + "args": [{ + "t": "app", + "fn": { "t": "var", "name": "compare" }, + "args": [ + { "t": "lit", "lit": { "kind": "int", "value": 1 } }, + { "t": "lit", "lit": { "kind": "int", "value": 2 } } + ] + }] + }] + }, + "rhs": { + "t": "seq", + "lhs": { + "t": "do", + "op": "io/print_int", + "args": [{ + "t": "app", + "fn": { "t": "var", "name": "ord_to_int" }, + "args": [{ + "t": "app", + "fn": { "t": "var", "name": "compare" }, + "args": [ + { "t": "lit", "lit": { "kind": "int", "value": 5 } }, + { "t": "lit", "lit": { "kind": "int", "value": 5 } } + ] + }] + }] + }, + "rhs": { + "t": "seq", + "lhs": { + "t": "do", + "op": "io/print_int", + "args": [{ + "t": "app", + "fn": { "t": "var", "name": "ord_to_int" }, + "args": [{ + "t": "app", + "fn": { "t": "var", "name": "compare" }, + "args": [ + { "t": "lit", "lit": { "kind": "int", "value": 9 } }, + { "t": "lit", "lit": { "kind": "int", "value": 3 } } + ] + }] + }] + }, + "rhs": { + "t": "seq", + "lhs": { + "t": "do", + "op": "io/print_int", + "args": [{ + "t": "app", + "fn": { "t": "var", "name": "ord_to_int" }, + "args": [{ + "t": "app", + "fn": { "t": "var", "name": "compare" }, + "args": [ + { "t": "lit", "lit": { "kind": "bool", "value": false } }, + { "t": "lit", "lit": { "kind": "bool", "value": true } } + ] + }] + }] + }, + "rhs": { + "t": "seq", + "lhs": { + "t": "do", + "op": "io/print_int", + "args": [{ + "t": "app", + "fn": { "t": "var", "name": "ord_to_int" }, + "args": [{ + "t": "app", + "fn": { "t": "var", "name": "compare" }, + "args": [ + { "t": "lit", "lit": { "kind": "bool", "value": true } }, + { "t": "lit", "lit": { "kind": "bool", "value": true } } + ] + }] + }] + }, + "rhs": { + "t": "seq", + "lhs": { + "t": "do", + "op": "io/print_int", + "args": [{ + "t": "app", + "fn": { "t": "var", "name": "ord_to_int" }, + "args": [{ + "t": "app", + "fn": { "t": "var", "name": "compare" }, + "args": [ + { "t": "lit", "lit": { "kind": "bool", "value": true } }, + { "t": "lit", "lit": { "kind": "bool", "value": false } } + ] + }] + }] + }, + "rhs": { + "t": "seq", + "lhs": { + "t": "do", + "op": "io/print_int", + "args": [{ + "t": "app", + "fn": { "t": "var", "name": "ord_to_int" }, + "args": [{ + "t": "app", + "fn": { "t": "var", "name": "compare" }, + "args": [ + { "t": "lit", "lit": { "kind": "str", "value": "alpha" } }, + { "t": "lit", "lit": { "kind": "str", "value": "beta" } } + ] + }] + }] + }, + "rhs": { + "t": "seq", + "lhs": { + "t": "do", + "op": "io/print_int", + "args": [{ + "t": "app", + "fn": { "t": "var", "name": "ord_to_int" }, + "args": [{ + "t": "app", + "fn": { "t": "var", "name": "compare" }, + "args": [ + { "t": "lit", "lit": { "kind": "str", "value": "same" } }, + { "t": "lit", "lit": { "kind": "str", "value": "same" } } + ] + }] + }] + }, + "rhs": { + "t": "do", + "op": "io/print_int", + "args": [{ + "t": "app", + "fn": { "t": "var", "name": "ord_to_int" }, + "args": [{ + "t": "app", + "fn": { "t": "var", "name": "compare" }, + "args": [ + { "t": "lit", "lit": { "kind": "str", "value": "zulu" } }, + { "t": "lit", "lit": { "kind": "str", "value": "alpha" } } + ] + }] + }] + } + } + } + } + } + } + } + } + } + } + ] +} +``` + +Pair pattern per type: (LT-result, EQ-result, GT-result): +- Int: `(1,2)` → LT, `(5,5)` → EQ, `(9,3)` → GT. +- Bool: `(false, true)` → LT, `(true, true)` → EQ, `(true, false)` → GT. +- Str: `("alpha","beta")` → LT, `("same","same")` → EQ, `("zulu","alpha")` → GT. + +Expected stdout: `"1\n2\n3\n1\n2\n3\n1\n2\n3\n"`. + +- [ ] **Step 4: Run E2E test to verify it passes** + +Run: `cargo test --workspace -p ail compare_primitives_smoke_compiles_and_runs` +Expected: PASS — stdout is `"1\n2\n3\n1\n2\n3\n1\n2\n3\n"`. + +If the test fails with stdout `"2\n2\n2\n2\n2\n2\n2\n2\n2\n"`, +that means the intercept did not fire and the placeholder `EQ` +body in the prelude got lowered instead. Verify in +`try_emit_primitive_instance_body` that the new arms match the +fn names exactly (case-sensitive, double-underscore separator). + +If the test fails with a different stdout pattern, inspect the +emitted IR by running: +``` +ail emit-ir examples/compare_primitives_smoke.ail.json +``` +and grep for `@ail_prelude_compare__T` to verify the branch ladder +shape. Do NOT loosen the assertion; understand the regression first. + +- [ ] **Step 5: Add three codegen IR-shape integration tests** + +Append to the existing `#[cfg(test)] mod tests` block in +`crates/ailang-codegen/src/lib.rs` (after the iter-23.2.4 +integration tests for `eq__T`): + +```rust +/// Iter 23.3: integration with the auto-loaded prelude. Compiling +/// a workspace that calls `compare` on Int produces a +/// mono-synthesised `@ail_prelude_compare__Int` whose body +/// contains the `icmp slt i64` LT-test and the `icmp eq i64` +/// EQ-test characteristic of the intercept's branch ladder. +#[test] +fn compare_int_call_produces_branch_ladder_mono_fn() { + use ailang_core::load_workspace; + use ailang_check::{check_workspace, monomorphise_workspace}; + let manifest = env!("CARGO_MANIFEST_DIR"); + let entry = std::path::Path::new(manifest) + .parent().unwrap().parent().unwrap() + .join("examples").join("compare_primitives_smoke.ail.json"); + let ws = load_workspace(&entry).expect("load workspace"); + let diags = check_workspace(&ws); + assert!( + diags.iter().all(|d| !matches!(d.severity, ailang_check::Severity::Error)), + "unexpected check_workspace errors: {diags:?}" + ); + let ws = monomorphise_workspace(&ws).expect("monomorphise"); + let ir = lower_workspace(&ws).expect("lower"); + assert!( + ir.contains("@ail_prelude_compare__Int"), + "ir must contain mono-synthesised @ail_prelude_compare__Int; ir was:\n{ir}" + ); + assert!( + ir.contains("icmp slt i64"), + "compare__Int body must contain `icmp slt i64`; ir was:\n{ir}" + ); + assert!( + ir.contains("icmp eq i64"), + "compare__Int body must contain `icmp eq i64`; ir was:\n{ir}" + ); +} + +/// Iter 23.3: same shape for `compare__Bool`. `icmp ult i1` +/// LT-test + `icmp eq i1` EQ-test. +#[test] +fn compare_bool_call_produces_branch_ladder_mono_fn() { + use ailang_core::load_workspace; + use ailang_check::{check_workspace, monomorphise_workspace}; + let manifest = env!("CARGO_MANIFEST_DIR"); + let entry = std::path::Path::new(manifest) + .parent().unwrap().parent().unwrap() + .join("examples").join("compare_primitives_smoke.ail.json"); + let ws = load_workspace(&entry).expect("load workspace"); + let diags = check_workspace(&ws); + assert!( + diags.iter().all(|d| !matches!(d.severity, ailang_check::Severity::Error)), + "unexpected check_workspace errors: {diags:?}" + ); + let ws = monomorphise_workspace(&ws).expect("monomorphise"); + let ir = lower_workspace(&ws).expect("lower"); + assert!( + ir.contains("@ail_prelude_compare__Bool"), + "ir must contain mono-synthesised @ail_prelude_compare__Bool; ir was:\n{ir}" + ); + assert!( + ir.contains("icmp ult i1"), + "compare__Bool body must contain `icmp ult i1`; ir was:\n{ir}" + ); + assert!( + ir.contains("icmp eq i1"), + "compare__Bool body must contain `icmp eq i1`; ir was:\n{ir}" + ); +} + +/// Iter 23.3: `compare__Str` calls `@ail_str_compare` (runtime +/// primitive in `runtime/str.c`) and branches on the normalised +/// {-1, 0, +1} result. Asserts the call site and the two branch +/// tests on the result. +#[test] +fn compare_str_call_produces_ail_str_compare_call_mono_fn() { + use ailang_core::load_workspace; + use ailang_check::{check_workspace, monomorphise_workspace}; + let manifest = env!("CARGO_MANIFEST_DIR"); + let entry = std::path::Path::new(manifest) + .parent().unwrap().parent().unwrap() + .join("examples").join("compare_primitives_smoke.ail.json"); + let ws = load_workspace(&entry).expect("load workspace"); + let diags = check_workspace(&ws); + assert!( + diags.iter().all(|d| !matches!(d.severity, ailang_check::Severity::Error)), + "unexpected check_workspace errors: {diags:?}" + ); + let ws = monomorphise_workspace(&ws).expect("monomorphise"); + let ir = lower_workspace(&ws).expect("lower"); + assert!( + ir.contains("@ail_prelude_compare__Str"), + "ir must contain mono-synthesised @ail_prelude_compare__Str; ir was:\n{ir}" + ); + assert!( + ir.contains("call i32 @ail_str_compare("), + "compare__Str body must call @ail_str_compare; ir was:\n{ir}" + ); + assert!( + ir.contains("icmp slt i32"), + "compare__Str body must contain `icmp slt i32` (LT-test against 0); ir was:\n{ir}" + ); + assert!( + ir.contains("icmp eq i32"), + "compare__Str body must contain `icmp eq i32` (EQ-test against 0); ir was:\n{ir}" + ); +} +``` + +- [ ] **Step 6: Run the three integration tests** + +Run: +``` +cargo test --workspace --lib compare_int_call_produces_branch_ladder_mono_fn compare_bool_call_produces_branch_ladder_mono_fn compare_str_call_produces_ail_str_compare_call_mono_fn +``` +Expected: ALL THREE PASS. + +- [ ] **Step 7: Run the full workspace test suite** + +Run: `cargo build --workspace && cargo test --workspace` +Expected: FULL workspace green. + +- [ ] **Step 8: Commit** + +```bash +git add examples/compare_primitives_smoke.ail.json crates/ail/tests/e2e.rs crates/ailang-codegen/src/lib.rs +git commit -m "iter 23.3.4: e2e smoke fixture + ir-shape integration tests for compare__T mono symbols" +``` + +--- + +## Task 5: JOURNAL entry + +**Files:** +- Modify: `docs/JOURNAL.md` — append a new entry at the END + of the file (chronological order with most recent last, + matching iter 23.1 and 23.2 entries). + +- [ ] **Step 1: Append the JOURNAL entry** + +Append to `docs/JOURNAL.md`: + +```markdown + +## 2026-05-10 — Iteration 23.3: Ord class + three primitive instances + +Third iteration of milestone 23. Ships `class Ord a extends Eq +where compare : (a borrow, a borrow) -> Ordering` in the auto- +loaded prelude plus three primitive instances (Ord Int, Ord Bool, +Ord Str). User programs can now call `compare x y` on any of the +three primitives; the monomorphiser synthesises +`compare__Int` / `compare__Bool` / `compare__Str` top-level fns +in the prelude module and codegen's +`try_emit_primitive_instance_body` intercept hand-rolls a +three-way branch ladder for each: + +- `compare__Int` → `icmp slt i64` LT-test + `icmp eq i64` + EQ-test, constructing LT / EQ / GT via `lower_ctor`. +- `compare__Bool` → `icmp ult i1` LT-test + `icmp eq i1` + EQ-test, same ctor construction. +- `compare__Str` → `call i32 @ail_str_compare(...)` followed by + `icmp slt i32 _, 0` LT-test + `icmp eq i32 _, 0` EQ-test. + +All three are hand-rolled, unlike iter 23.2's eq family where +`eq__Int` / `eq__Bool` rode the natural `lower_eq` dispatch. +The reason: `builtin_binop_typed` +(`crates/ailang-codegen/src/synth.rs:260-294`) only handles Int +and Float for the comparison ops `< / <= / > / >=` — Bool is +missing. A surface-level `if x < y { LT } else if x == y { EQ } +else { GT }` body for `compare__Bool` would fail at codegen. Bringing +Bool into the primitive comparison table is a language-wide +design decision that does not belong in this iter; hand-rolling +all three compare arms keeps the family consistent and the IR +shape predictable. The iter-23.2.2 doc comment that speculated +compare__Int/Bool would ride natural lowering is corrected. + +The Str runtime primitive `ail_str_compare` lives in `runtime/str.c` +alongside `ail_str_eq`; it normalises libc strcmp's signed result +to exactly {-1, 0, +1} so the codegen branch ladder can compare +against constant 0 directly. The .o is supplied by the same +unconditional clang compile+link wired up in iter 23.2.1; no +additional build-side changes. + +Decision 11's single-superclass closure (every `instance Ord T` +requires `instance Eq T`) is automatically satisfied — iter 23.2 +shipped the three Eq instances on the same types. No new +check-side machinery. + +Coverage: +- Three codegen unit tests on synthetic FnDefs pin each + compare arm's IR shape (`compare_*_mono_symbol_emits_*`). +- Three IR-shape integration tests on the real + `examples/compare_primitives_smoke.ail.json` workspace + (`compare_*_call_produces_*_mono_fn`) verify the full + typecheck → monomorphise → codegen pipeline. +- One E2E test `compare_primitives_smoke_compiles_and_runs` + exercises the full pipeline through clang + the produced + binary, asserting stdout `"1\n2\n3\n1\n2\n3\n1\n2\n3\n"` + (LT=1, EQ=2, GT=3 per type × 3 types). +- Prelude-load test extended to assert `class Ord` + Ord + instances on Int / Bool / Str. + +Per-task commits: + +- `` iter 23.3.1: runtime ail_str_compare + IR header + declaration +- `` iter 23.3.2: codegen — three compare__T intercept + arms + emit_ordering_arm helper +- `` iter 23.3.3: prelude — class Ord a extends Eq + Ord + Int/Bool/Str instances +- `` iter 23.3.4: e2e smoke fixture + ir-shape integration + tests for compare__T mono symbols +- `` iter 23.3.5: journal entry — Ord class + primitive + instances + +(Replace `` with the actual commit SHAs before committing. +Use `git log --oneline -5` to retrieve them.) + +Out of scope for this iter (covered by later 23.x): +- Free top-level utility functions `ne` / `lt` / `le` / `gt` / + `ge` (23.4). +- Float-NoInstance diagnostic + DESIGN.md amendment (23.5). + +Workspace at iter-23.3 close: full `cargo test --workspace` +green. Cross-language and the three regression scripts not +re-run for this iter; they run at milestone-23 close. +``` + +- [ ] **Step 2: Replace `` placeholders with actual SHAs** + +Run `git log --oneline -5` to retrieve the SHAs of the four prior +task commits (23.3.1 through 23.3.4). Edit `docs/JOURNAL.md` to +substitute the actual SHAs. (Iter 23.3.5 — this commit — gets its +SHA only after the commit lands; leave that one as a placeholder +or trim the bullet.) + +- [ ] **Step 3: Commit** + +```bash +git add docs/JOURNAL.md +git commit -m "iter 23.3.5: journal entry — Ord class + primitive instances" +``` + +--- + +## Self-review + +1. **Spec coverage**: Components row 23.3 names five + deliverables — (a) `class Ord a extends Eq where compare`, + (b) three instances (Ord Int / Bool / Str), (c) extend + `runtime/str.c` with `ail_str_compare`, (d) codegen arms for + `compare__Int` / `compare__Bool` / `compare__Str`, (e) the + match-on-primitive-comparison-result branch ladder. Mapped: + (c) in Task 1; (d)+(e) in Task 2; (a)+(b) in Task 3. + Coverage tests in Task 4. ✓ +2. **Placeholder scan**: no `TBD`, `TODO`, `similar to`, + `implement later`, `add appropriate ...` in the body. The + `` placeholders in Task 5 Step 1 are explicit and + resolved in Step 2. ✓ +3. **Type consistency**: `compare__Int`, `compare__Bool`, + `compare__Str`, `ail_str_compare`, `emit_ordering_arm`, + `try_emit_primitive_instance_body`, `Ordering`, `LT/EQ/GT`, + `compare_primitives_smoke.ail.json`, + `loads_workspace_auto_injects_prelude` — names match across + tasks. ✓ +4. **Step granularity**: every step is one shell command or one + small code insertion; longest insertions (the three intercept + arms in Task 2 Step 5, the fixture JSON in Task 4 Step 3) are + each a single Edit / Write. ✓