From 0972e41ba6b32f572753247c9ea360282ee7cfc1 Mon Sep 17 00:00:00 2001 From: Alfonso Sastre Date: Tue, 8 Sep 2026 19:11:33 +0200 Subject: [PATCH] prompts: fix the reference's own record/variant construction syntax The canonical quick-start's own Shape example was invalid Lex: type Shape = Circle(Float) | Rect { w :: Float, h :: Float } match shape { Circle(r) => ..., Rect { w, h } => w * h } `lex check` on that exact snippet: "parse error ... expected `import`, `type`, or `fn` at top level, got Some(LBrace)". A record-shaped variant's payload needs parens like any other variant argument -- Rect({ w :: Float, h :: Float }), constructed Rect({ w: 1.0, h: 2.0 }), matched Rect({ w, h }) => ... -- there is no bare `Rect { ... }` form. Every lex-code invocation this whole session opened with this example as the FIRST thing shown for algebraic data types. Found live: a fresh lex-economy repo's identity module build (a different, new-domain dogfood of impl_test_fix_loop_verified) stalled for close to an hour on a closely related mistake -- writing `KeyPair { secret: x, public: y }` for a plain record TYPE ALIAS (not a sum type), which also doesn't parse. Records construct with a BARE literal, `{ field: value, ... }` -- the type name is never written at construction, only in an annotation or return type; `TypeName { ... }` reads as an ADT variant lookup and fails with a confusing "unknown_variant" or "expected RParen" error that doesn't say what's actually wrong. Confirmed all of this with minimal repros against the real compiler before touching anything, including that the fixed forms type-check and the broken forms don't. Fixed the quick-start's Shape example, added a worked Company/ incorporate example showing bare-literal construction, and added two new pitfalls-table rows (`{ field = value }` vs `:`; the TypeName- prefixed construction mistake, both for plain records and for record-shaped ADT variants). Co-Authored-By: Claude Sonnet 5 --- src/prompts/lex_lang.lex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/prompts/lex_lang.lex b/src/prompts/lex_lang.lex index c209843..16e82bb 100644 --- a/src/prompts/lex_lang.lex +++ b/src/prompts/lex_lang.lex @@ -10,7 +10,7 @@ # `lex_stdlib` already made for stdlib signatures. fn reference() -> Str { - "## Lex Language — Core Reference\n\nLex is a typed-effect functional language. Files use `.lex`; the default toolchain is `lex`.\n\n### Syntax quick-start\n\n```lex\n# Function with explicit types (all signatures are explicit)\nfn add(x :: Int, y :: Int) -> Int\n examples {\n add(0, 0) => 0,\n add(2, 3) => 5,\n }\n{ x + y }\n\n# Effectful function — effects come BEFORE the return type\nfn save(path :: Str, data :: Str) -> [fs_write] Result[Unit, Str] {\n fs.write(path, data)\n}\n\n# Pure function — NO effect annotation at all\nfn double(n :: Int) -> Int { n * 2 }\n\n# Let binding — uses `:=`, not `=`\nlet result := add(1, 2)\n\n# Algebraic data type\ntype Shape = Circle(Float) | Rect { w :: Float, h :: Float }\n\n# Match (must be exhaustive)\nmatch shape {\n Circle(r) => 3.14159 * r * r,\n Rect { w, h } => w * h,\n}\n\n# Result / Option idioms\nmatch http.get(url) {\n Ok(resp) => resp.body,\n Err(e) => str.concat(\"error: \", e),\n}\n\n# Import\nimport \"std.list\" as list\nimport \"./util\" as util\n```\n\n**No `else if`** — the single most common mistake. Lex only has `if/else`; nest: `else { if cond { ... } else { ... } }`. Full pitfalls table: `lex_guide(\"pitfalls\")`.\n\n### Effect discipline (most important rule)\n- Declare the **narrowest** effect set that matches the body.\n- Pure functions: no effect annotation — `fn f(x :: T) -> T`.\n- Never add `[io]` \"just in case\"; the checker will reject unused effects.\n- Common effects: `io`, `net`, `llm`, `sql`, `fs_read`, `fs_write`, `env`, `proc`, `time`, `concurrent`.\n- Fine-grained path scopes: `[fs_write(\"/tmp/out/\")]`, `[net(\"api.example.com\")]`.\n\n### examples {} blocks\nPure functions SHOULD have an `examples {}` block — they're folded into the SigId and become free regression tests:\n```lex\nfn clamp(n :: Int, lo :: Int, hi :: Int) -> Int\n examples {\n clamp(5, 0, 10) => 5,\n clamp(-1, 0, 10) => 0,\n clamp(99, 0, 10) => 10,\n }\n{ if n < lo { lo } else { if n > hi { hi } else { n } } }\n```\n\n### Stdlib modules\nlist, str, io, http, sql, fs, env, conc, json, regex, int, float, bytes, crypto, and more — call `lex_stdlib(module)` (e.g. `lex_stdlib(\"bytes\")`) for a module's real function names and signatures, straight from the compiler's own builtin registry. Don't guess a stdlib call's name or return type, and don't probe it with a throwaway file — ask first.\n\n### Iteration loop\n1. Write or edit the `.lex` file — the `write` tool auto-runs `lex check` and returns an error if it fails.\n2. **If `write` returns an error, fix the code and call `write` again immediately.** Keep iterating until write succeeds. Never stop after a write error.\n3. Read the structured error (`rule_tag` + `rule_explanation`) to understand what to fix.\n4. `lex_run fn_name` — run a specific function.\n5. `lex_test` — run the test suite.\n\n### Decide once, then write code\nWhen a spec is ambiguous or a design choice has multiple valid answers, reason about it *once*, pick the interpretation that's consistent with any concrete examples/tests you were given, and move straight to `write`. Do not re-derive or re-verify the same conclusion more than once — if you notice yourself writing \"let me reconsider\" or \"hold on\" a second time about the same question, stop: you already decided, go call `write`. `lex check`'s error messages are the cheap way to find out you were wrong; a long chain of self-doubt in your own response is not — it only spends your output budget without writing anything.\n\nMore detail on demand, never by guessing or probing: `lex_guide(topic)` for \"pitfalls\", \"list-idioms\", or \"anti-patterns\"; `lex_stdlib(module)` for stdlib signatures; `lex_cli_help(command)` for a `lex` CLI subcommand's real arguments and examples. For everything else: call `load_guidelines` tool or run `lex agent-guidelines`." + "## Lex Language — Core Reference\n\nLex is a typed-effect functional language. Files use `.lex`; the default toolchain is `lex`.\n\n### Syntax quick-start\n\n```lex\n# Function with explicit types (all signatures are explicit)\nfn add(x :: Int, y :: Int) -> Int\n examples {\n add(0, 0) => 0,\n add(2, 3) => 5,\n }\n{ x + y }\n\n# Effectful function — effects come BEFORE the return type\nfn save(path :: Str, data :: Str) -> [fs_write] Result[Unit, Str] {\n fs.write(path, data)\n}\n\n# Pure function — NO effect annotation at all\nfn double(n :: Int) -> Int { n * 2 }\n\n# Let binding — uses `:=`, not `=`\nlet result := add(1, 2)\n\n# Algebraic data type — a record-shaped variant's payload is a record\n# type, written like any other single argument: in parens. There is no\n# bare `Rect { ... }` form — every variant is VariantName(payload).\ntype Shape = Circle(Float) | Rect({ w :: Float, h :: Float })\n\n# Match (must be exhaustive) — same rule: ({ ... }) to destructure the\n# record payload, not a bare { ... } after the variant name.\nmatch shape {\n Circle(r) => 3.14159 * r * r,\n Rect({ w, h }) => w * h,\n}\n\n# Records: construct with a BARE literal, never the type name.\n# `Company { name: \"x\" }` does not parse — write `{ name: \"x\" }` and let\n# the return type / annotation tell the checker which record type it is.\ntype Company = { name :: Str, mission :: Str }\nfn incorporate(name :: Str, mission :: Str) -> Company { { name: name, mission: mission } }\n\n# Result / Option idioms\nmatch http.get(url) {\n Ok(resp) => resp.body,\n Err(e) => str.concat(\"error: \", e),\n}\n\n# Import\nimport \"std.list\" as list\nimport \"./util\" as util\n```\n\n**No `else if`** — the single most common mistake. Lex only has `if/else`; nest: `else { if cond { ... } else { ... } }`. Full pitfalls table: `lex_guide(\"pitfalls\")`.\n\n### Effect discipline (most important rule)\n- Declare the **narrowest** effect set that matches the body.\n- Pure functions: no effect annotation — `fn f(x :: T) -> T`.\n- Never add `[io]` \"just in case\"; the checker will reject unused effects.\n- Common effects: `io`, `net`, `llm`, `sql`, `fs_read`, `fs_write`, `env`, `proc`, `time`, `concurrent`.\n- Fine-grained path scopes: `[fs_write(\"/tmp/out/\")]`, `[net(\"api.example.com\")]`.\n\n### examples {} blocks\nPure functions SHOULD have an `examples {}` block — they're folded into the SigId and become free regression tests:\n```lex\nfn clamp(n :: Int, lo :: Int, hi :: Int) -> Int\n examples {\n clamp(5, 0, 10) => 5,\n clamp(-1, 0, 10) => 0,\n clamp(99, 0, 10) => 10,\n }\n{ if n < lo { lo } else { if n > hi { hi } else { n } } }\n```\n\n### Stdlib modules\nlist, str, io, http, sql, fs, env, conc, json, regex, int, float, bytes, crypto, and more — call `lex_stdlib(module)` (e.g. `lex_stdlib(\"bytes\")`) for a module's real function names and signatures, straight from the compiler's own builtin registry. Don't guess a stdlib call's name or return type, and don't probe it with a throwaway file — ask first.\n\n### Iteration loop\n1. Write or edit the `.lex` file — the `write` tool auto-runs `lex check` and returns an error if it fails.\n2. **If `write` returns an error, fix the code and call `write` again immediately.** Keep iterating until write succeeds. Never stop after a write error.\n3. Read the structured error (`rule_tag` + `rule_explanation`) to understand what to fix.\n4. `lex_run fn_name` — run a specific function.\n5. `lex_test` — run the test suite.\n\n### Decide once, then write code\nWhen a spec is ambiguous or a design choice has multiple valid answers, reason about it *once*, pick the interpretation that's consistent with any concrete examples/tests you were given, and move straight to `write`. Do not re-derive or re-verify the same conclusion more than once — if you notice yourself writing \"let me reconsider\" or \"hold on\" a second time about the same question, stop: you already decided, go call `write`. `lex check`'s error messages are the cheap way to find out you were wrong; a long chain of self-doubt in your own response is not — it only spends your output budget without writing anything.\n\nMore detail on demand, never by guessing or probing: `lex_guide(topic)` for \"pitfalls\", \"list-idioms\", or \"anti-patterns\"; `lex_stdlib(module)` for stdlib signatures; `lex_cli_help(command)` for a `lex` CLI subcommand's real arguments and examples. For everything else: call `load_guidelines` tool or run `lex agent-guidelines`." } fn topic_names() -> List[Str] { @@ -34,7 +34,7 @@ fn topic(name :: Str) -> Option[Str] { } fn pitfalls_topic() -> Str { - "### Common syntax pitfalls\n| What you might write | Correct Lex |\n|---|---|\n| `x: Int` | `x :: Int` |\n| `let x = e` | `let x := e` |\n| `-> T` with no effects | `-> T` (omit `[]` entirely for pure fns) |\n| `-> [] T` | invalid — write `-> T` |\n| Exceptions / throw | use `Result[T, E]` — no exceptions in Lex |\n| `else if cond { }` | `else { if cond { } }` — **Lex has NO `else if`**. Always nest: `else { if ... { } else { } }` |\n| `True` / `False` | `true` / `false` — booleans are lowercase (unlike Haskell) |\n| `list.concat(a, b, c)` | `list.concat(a, list.concat(b, c))` — concat takes exactly 2 args |\n| `//` comment | `#` comment — Lex uses `#`, not `//` or `/*` |\n| `list.concat(...)` without import | add `import \"std.list\" as list` — every stdlib module must be explicitly imported |\n| `import \"abi\" as abi` / `import \"abi.lex\" as abi` / `import \"src/abi.lex\" as abi` | `import \"./abi\" as abi` (or `\"../src/abi\"` from a sibling dir) — a **local file** needs an explicit `./` or `../` prefix and NO `.lex` extension. A bare or slash-shaped string with no `./`/`../` prefix is looked up as a **package** dependency (from `lex.toml`), not a path, and fails with \"package ... not found\" or \"unknown identifier\" — it is never inferred from a matching filename |\n| `fn f(x) examples{...} match t {` | function body MUST be `{ }` wrapped: `fn f(x) examples{...} { match t { ... } }` |\n| `type T = \\| A \\| B` | NO leading pipe: `type T = A \\| B` — first variant has no `\\|` prefix |\n| `pair.0` / `pair.1` | tuples have no field access — destructure: `match pair { (a, b) => a }` |\n| `while` / `for` loops | Lex is purely functional — use recursion or `list.fold` |\n| `let x := 1` then `let x := 2` | `let` bindings are immutable — cannot reassign; use a recursive helper or fold |\n| `a && b` / `a \\|\\| b` | use `a and b` / `a or b` — Lex uses keywords, not `&&`/`\\|\\|` |\n| `list.find(...)` | does not exist — use `list.filter` + `list.head`, or `list.fold` |\n| `list.zip(a, b)` | does not exist — use `list.enumerate(a)` to get `List[(Int, T)]`, or compare as strings: `str.join(a, \",\") == str.join(b, \",\")` |\n| `str.to_str(n)` | does not exist on `str` — convert Int to Str with `int.to_str(n)` from `std.int` |\n| `fn (kv :: (A, B)) -> T` in a call | tuple type in lambda param causes parse error; match inside body instead: `fn (kv :: (A, B)) -> T { match kv { (a, b) => ... } }` |\n| `match xs { [] => ..., [h, ..t] => ... }` | list pattern matching is NOT supported — use `if list.is_empty(xs) { ... } else { let h := list.head(xs); let t := list.tail(xs); ... }` |\n| `fn helper(...)` inside a function body | local named functions are NOT allowed — extract every helper to the top level |\n| `list.join(parts, sep)` | does not exist — use `str.join(parts, sep)` from `std.str` |\n| `VBool(bool)` / `x :: bool` | type names are uppercase: `Bool`, `Int`, `Str`, `Float` — never `bool`, `int`, `str` |\n| `let a := x,` (comma after let) | `let` bindings inside a block need NO separator — just newlines. Commas go between match arms only. |\n\n**No `else if` — this is the single most common mistake.** Lex only has `if/else`. Chain conditions by nesting:\n```lex\n# WRONG — will not parse:\nif a { x } else if b { y } else { z }\n\n# CORRECT — always nest else { if ... }:\nif a { x } else { if b { y } else { z } }\n```" + "### Common syntax pitfalls\n| What you might write | Correct Lex |\n|---|---|\n| `x: Int` | `x :: Int` |\n| `let x = e` | `let x := e` |\n| `-> T` with no effects | `-> T` (omit `[]` entirely for pure fns) |\n| `-> [] T` | invalid — write `-> T` |\n| Exceptions / throw | use `Result[T, E]` — no exceptions in Lex |\n| `else if cond { }` | `else { if cond { } }` — **Lex has NO `else if`**. Always nest: `else { if ... { } else { } }` |\n| `True` / `False` | `true` / `false` — booleans are lowercase (unlike Haskell) |\n| `list.concat(a, b, c)` | `list.concat(a, list.concat(b, c))` — concat takes exactly 2 args |\n| `//` comment | `#` comment — Lex uses `#`, not `//` or `/*` |\n| `list.concat(...)` without import | add `import \"std.list\" as list` — every stdlib module must be explicitly imported |\n| `import \"abi\" as abi` / `import \"abi.lex\" as abi` / `import \"src/abi.lex\" as abi` | `import \"./abi\" as abi` (or `\"../src/abi\"` from a sibling dir) — a **local file** needs an explicit `./` or `../` prefix and NO `.lex` extension. A bare or slash-shaped string with no `./`/`../` prefix is looked up as a **package** dependency (from `lex.toml`), not a path, and fails with \"package ... not found\" or \"unknown identifier\" — it is never inferred from a matching filename |\n| `fn f(x) examples{...} match t {` | function body MUST be `{ }` wrapped: `fn f(x) examples{...} { match t { ... } }` |\n| `type T = \\| A \\| B` | NO leading pipe: `type T = A \\| B` — first variant has no `\\|` prefix |\n| `pair.0` / `pair.1` | tuples have no field access — destructure: `match pair { (a, b) => a }` |\n| `{ field = value }` | record fields are separated by `:`, not `=` — `{ field: value }` |\n| `TypeName { field: value }` (e.g. `Company { name: \"x\" }`) | records are constructed as a **bare** literal, `{ field: value, ... }` — the type name is NEVER written at construction, only in a type annotation or return type. `TypeName { ... }` does not parse as record construction (it is read as an ADT variant lookup and fails, often with a confusing \"unknown_variant\" or \"expected RParen\" error) |\n| `Rect { w: 1.0, h: 2.0 }` as an ADT variant (e.g. `type Shape = Circle(Float) \\| Rect { w :: Float, h :: Float }`) | a record-shaped variant's payload still needs parens like any other variant argument: declare `Rect({ w :: Float, h :: Float })`, construct `Rect({ w: 1.0, h: 2.0 })`, match `Rect({ w, h }) => ...` — there is no bare `Rect { ... }` form for a variant either |\n| `while` / `for` loops | Lex is purely functional — use recursion or `list.fold` |\n| `let x := 1` then `let x := 2` | `let` bindings are immutable — cannot reassign; use a recursive helper or fold |\n| `a && b` / `a \\|\\| b` | use `a and b` / `a or b` — Lex uses keywords, not `&&`/`\\|\\|` |\n| `list.find(...)` | does not exist — use `list.filter` + `list.head`, or `list.fold` |\n| `list.zip(a, b)` | does not exist — use `list.enumerate(a)` to get `List[(Int, T)]`, or compare as strings: `str.join(a, \",\") == str.join(b, \",\")` |\n| `str.to_str(n)` | does not exist on `str` — convert Int to Str with `int.to_str(n)` from `std.int` |\n| `fn (kv :: (A, B)) -> T` in a call | tuple type in lambda param causes parse error; match inside body instead: `fn (kv :: (A, B)) -> T { match kv { (a, b) => ... } }` |\n| `match xs { [] => ..., [h, ..t] => ... }` | list pattern matching is NOT supported — use `if list.is_empty(xs) { ... } else { let h := list.head(xs); let t := list.tail(xs); ... }` |\n| `fn helper(...)` inside a function body | local named functions are NOT allowed — extract every helper to the top level |\n| `list.join(parts, sep)` | does not exist — use `str.join(parts, sep)` from `std.str` |\n| `VBool(bool)` / `x :: bool` | type names are uppercase: `Bool`, `Int`, `Str`, `Float` — never `bool`, `int`, `str` |\n| `let a := x,` (comma after let) | `let` bindings inside a block need NO separator — just newlines. Commas go between match arms only. |\n\n**No `else if` — this is the single most common mistake.** Lex only has `if/else`. Chain conditions by nesting:\n```lex\n# WRONG — will not parse:\nif a { x } else if b { y } else { z }\n\n# CORRECT — always nest else { if ... }:\nif a { x } else { if b { y } else { z } }\n```" } fn list_idioms_topic() -> Str {