# Nexus Spec Holes — Pass 1 Audit

This document is the output of Pass 1 of the spec-tightening methodology
(see `plans/proud-strolling-alpaca.md`). Every entry is a corner of Nexus
that is **undefined**, **ambiguous**, or **specified only by the current
implementation**. Each entry has:

- **Where** — the spec section / NOTE / source file the gap relates to
- **What** — the ambiguity in one sentence
- **Repro** — a minimal `.nxs` snippet that exposes the question
- **Proposed resolution** — a concrete answer to write into the spec

Categories: L (lexical), G (grammar), O (operators), T (types), V (variables),
C (control flow), F (closures), R (references), A (arrays), S (strings),
M (methods), N (network), U (use imports), D (determinism/output), E (errors).

Section IDs are stable; cite them in follow-up PRs.

---

## L — Lexical

### L-1. Number format: leading sign and exponent

- **Where** `README.md` lex grammar: `NUMBER → DIGIT+ ( '.' DIGIT+ )?`
- **What** Grammar forbids leading `.5`, leading `+`/`-` (those are unary
  operators), and scientific notation (`1e10`). NOTE 3 says "basic integer /
  full floating-point only" but doesn't mention what happens for things like
  `1.`, `.5`, `1e10`, `0x1F`, `1_000`. Are these scan errors or unexpected
  tokens?
- **Repro** `let x = 1e10;` / `let y = .5;` / `let z = 1_000;`
- **Proposed resolution** Add NOTE 3b: "Scientific notation, hex/oct/bin
  literals, and underscore separators are not supported; lexer reports
  `UnexpectedToken` on the offending character."

### L-2. Identifier Unicode range

- **Where** NOTE 1: ALPHA = Unicode chapter 4 alphabetic
- **What** "Alphabetic" is well-defined but Unicode normalization is not
  mentioned. Are `café` and `café` (decomposed) the same identifier?
  What about identifier-with-zero-width-joiner?
- **Repro** Two source files where one uses NFC and the other NFD for the
  same name — they should refer to the same binding, or be rejected.
- **Proposed resolution** Either (a) require NFC normalization at scan time
  and document it, or (b) compare identifiers as raw byte sequences (which
  is what Rust does) and document it.

### L-3. String escape sequences — invalid `\` handling — **OBSERVED**

- **Where** `ESCAPE_SEQ → '\\' ['"\\nrtbf0] | '\\x' HEX_DIGIT{2} | ...`
- **What** Behavior of an unrecognized escape (`\q`) is unspecified. Hard
  error? Silent passthrough? What about `\x` followed by fewer than two
  hex digits, or `\u{...}` Rust-style escapes that aren't in the grammar?
- **Repro** `let s = "\q";` / `let s = "\x1";` / `let s = "\u{41}";`
- **Pass-2 observation** (`tests/scanner/fail/string_invalid_escape.nxs`):
  `let s = "\q";` → `ScanError: Invalid escape sequence '\q'`. Hard scan
  error; no passthrough.
- **Proposed resolution** Document: unrecognized escape = scan error
  `InvalidEscape`. `\u{...}` is not supported (use `\uXXXX` for BMP only,
  `\UXXXXXXXX` for full).

### L-4. Longest-match interaction between `..`, `..=`, `.`

- **Where** NOTE 4 ("Longest match rule applies")
- **What** What does `1..2` produce? `[1, '..', 2]` (good) or `[1, '.', '.',
2]`? What about `1..=2`? And `arr..1` — does it lex as `arr . . 1` or
  `arr .. 1`? Clear in practice but spec doesn't show worked examples.
- **Repro** `for i in 0..=10 { }` — three tokens or one range op?
- **Proposed resolution** Add worked examples to NOTE 4: maximal-munch
  greedily picks `..=` over `..` over `.`.

### L-5. Block comment nesting

- **Where** `COMMENT_MULTI → '/*' [^*]* '*'+ ( [^/*] [^*]* '*'+ )* '/'`
- **What** Grammar does **not** allow nested block comments. Rust does.
  Is `/* /* */ */` an error or two comments + dangling `*/`?
- **Repro** `/* outer /* inner */ outer */ let x = 1;`
- **Proposed resolution** Decide and document: either nested (rewrite
  grammar) or non-nested (add NOTE: "block comments are not nestable").

---

## G — Grammar / Parsing

### G-1. Trailing expression at top level

- **Where** `program → decl* ( expr )? EOF`
- **What** A trailing expression at file scope is parsed but its value is
  discarded silently. Is this a semicolon-less REPL accommodation or part of
  the language? What if it's a `Group` value — does it show up in the
  network summary?
- **Repro** `let g = group {}; g` (no semicolon, no `print`)
- **Proposed resolution** Document: trailing expression's value is
  discarded; only `print` or binding produces observable output. Or: lift
  trailing expression into the network summary if it evaluates to a group.

### G-2. `expr_stmt` semicolon — when is it optional?

- **Where** `expr_stmt → expr ( ';' )?` + NOTE 3
- **What** NOTE 3 says semicolon is "valid if the expression serves as the
  _result value_ for a block." That covers when it can be **omitted**.
  But the grammar allows the semicolon to be omitted anywhere; can `let x =
1` (no semicolon) parse? What about `print "hi"` (no semicolon)?
- **Repro** `let x = 1\nlet y = 2;`
- **Proposed resolution** Tighten the grammar: only the last statement of a
  block may omit `;`, and `let`/`const`/`return`/`assert`/`print`/`connect`
  always require `;`. Either fix the EBNF or document the parser's actual
  rule.

### G-3. `if` without `else` (grammar mismatch) — **FIXED**

- **Where** `if → 'if' expr block ( 'else' ( if | block ) )`
- **What** The `else` clause is wrapped in parentheses but **not** marked
  optional with `?` — so the grammar literally requires an `else` on every
  `if`. The implementation surely allows `if expr { }` alone.
- **Repro** `if x > 0 { print "pos"; }`
- **Proposed resolution** Fix grammar to `'if' expr block ( 'else' ( if |
block ) )?`.

### G-4. `print` without expression

- **Where** `print → 'print' expr? ';'`
- **What** Bare `print;` is legal grammatically. What does it print —
  empty line? Nothing? Newline only?
- **Repro** `print;`
- **Proposed resolution** Document: `print;` emits a newline. (Confirm
  against impl.)

### G-5. `return` outside function — **OBSERVED**

- **Where** `return → 'return' expr? ';'`
- **What** Grammar permits `return` at any statement position, but it's only
  meaningful inside a `fn`. Error kind `ReturnSignal` exists. Spec is silent
  on top-level `return` or `return` inside a closure body — is the closure
  treated like a function for `return`'s scope, or does it bubble?
- **Repro** `let f = || { return 1; }; print f();`
- **Pass-2 observation** (`cross_return_in_closure.nxs`): `return`
  inside a closure exits the closure body only; the enclosing named
  function continues and returns its own trailing value. `outer()`
  returns `2`, not `1`. Top-level `return` confirmed as hard error
  (`tests/interpreter/fail/return_at_top_level.nxs`).
- **Proposed resolution** Document: `return` exits the innermost
  enclosing `fn` or closure body. Top-level `return` is a hard error.

### G-6. `port_decl` vs. `member_decl` in top-level `decl` — **FIXED**

- **Where** `decl → fn_decl | ... | port_decl | member_decl | ...`
- **What** Both productions appear in `decl`, but NOTE 5 says they're
  valid **only** inside `group { }` — enforced at parse time. Why are they
  in the top-level `decl` production at all? Either the grammar should
  exclude them from `decl` or the spec should explain that the parser
  intentionally allows them syntactically but errors semantically.
- **Repro** `port foo = bar;` at file top level.
- **Proposed resolution** Move `port_decl` and `member_decl` out of `decl`
  and into a new `group_decl` production used only by `group_expr`.

### G-7. `assignment` LHS overlap with `expr_stmt`

- **Where** `assignment → expr ( '=' | '+=' | ... ) ( expr | ref ) ';'`
- **What** Grammar makes `assignment` indistinguishable from `expr_stmt`
  at LL(1) — it's resolved by lookahead for `=`. The spec doesn't note
  this. Also, the LHS is `expr` but the parser must constrain it to
  identifiers and index-chains (per `InvalidAssignmentTarget`). What
  about `(x) = 1;`? `arr[i].field = 1;`?
- **Repro** `(x) = 1;` — error or accepted?
- **Proposed resolution** Either restrict the LHS production to
  `place_expr` (identifier or chained index) or document the post-parse
  validation step.

### G-8. `closure` body — expression vs block

- **Where** `closure → ( '||' | '|' parameters? '|' ) ( '->' type )? (
expr | block )`
- **What** Ambiguity: `|| { ... }` — is `{ ... }` a block expression (the
  body) or the body-as-expression? Reading the grammar, `block` is a kind
  of `primary`, so it could be either path. Does `|x| x + 1` work
  (expression body), and does `|x| { x + 1 }` work too (block body, same
  result)?
- **Repro** `let f = |x| { x };` vs `let f = |x| x;`
- **Proposed resolution** Document equivalence: an expression body and a
  block body with that expression as its trailing expression are
  semantically identical.

### G-9. `array_type` size expression scope

- **Where** `array_type → '[' type ( ';' expr )? ']'`
- **What** The size `expr` is evaluated when? At declaration site, at parse
  time, lazily? Can it reference let-bindings (`let n = 5; let a: [Number;
n];`)? `const` only?
- **Repro** `let n = 5; let a: [Number; n];` — works? Or only `const N`?
- **Proposed resolution** Document: size expression is evaluated at the
  time the declaration is executed, in the current scope; any value
  visible at that point is legal so long as it's a positive integer.

### G-10. Empty `parameters` in closure: `|| ` vs `| |`

- **Where** NOTE 4 + `closure` production
- **What** `||` is a single token (`DoublePipe` in scanner). Is `| |` (with
  whitespace) a valid alternative empty-param closure header?
- **Repro** `let f = | | 1;`
- **Proposed resolution** Document: empty parameter list is the
  single-token `||` only. Two `|` tokens with whitespace = parse error.

---

## O — Operators

### O-1. `..` / `..=` associativity is stated but undefined for chains — **OBSERVED**

- **Where** Precedence table: range operators are left-associative
- **What** The table says left-to-right, but chained ranges (`0..10..20`)
  don't have a meaningful semantic — `Range..Range`? Is this rejected at
  type-check time, or accepted with surprising semantics?
- **Repro** `for i in 0..10..20 { }`
- **Pass-2 observation** (`tests/interpreter/fail/range_chained.nxs`):
  `for i in 0..10..20 { }` → `ParseError: Expected '{' after for loop iterable`.
  The second `..` is rejected at parse time because the for-loop parser
  expects `{` after the iterable expression; the range expression is
  already complete after `0..10`.
- **Proposed resolution** Document: range operators are non-chainable;
  using more than one in a single expression without parentheses is a
  parse error. (Probably simplest fix: change the production from `( '..'
| '..=' ) logic_or` to require parens for chains.)

### O-2. `->` connect operator: precedence vs. range

- **Where** Connect is a statement (`expr '->' expr ';'`), not an
  expression; NOTE 15c says "chaining (a -> b -> c;) not supported"
- **What** But because `->` is also part of `closure` and `fn_type`
  syntax, the spec doesn't tell us how `let f = |x| x + 1 -> something`
  parses. Is the `->` after `x + 1` part of a connect, a return-type
  annotation, or rejected?
- **Repro** `let f = |x| x + 1 -> y;` — parse what?
- **Proposed resolution** Document: `->` in expression position is only
  valid (a) immediately after `|...|` as a return-type annotation, or
  (b) at statement position between two `PortRef` expressions. Anywhere
  else: parse error.

### O-3. Unary `+` semantics — **FIXED**

- **Where** `unary → ( '!' | '+' | '-' ) unary | postfix`
- **What** Unary `+` is in the grammar but has no documented meaning. On
  numbers, presumably identity; on strings, error? On booleans?
- **Repro** `let x = +"hi";` / `let x = +true;`
- **Pass-2 observation** (`cross_unary_plus.nxs`): unary `+` is identity
  on all types — `Number`, `String`, and `bool` all pass through unchanged.
- **Proposed resolution** Document: unary `+` is identity on all types.
  (Or restrict it to `Number` only and error otherwise — but that would
  require a behaviour change.)

### O-4. Equality on heterogeneous types

- **Where** `Value::eq` in `value.rs` requires same-kind operands
- **What** What's the contract for `1 == "1"`, `[] == ""`, `node Foo ==
node Foo`? `value.rs` returns `InvalidComparison` for these, but
  comparing two equal-valued empty arrays of different element types
  succeeds. Spec doesn't lay this out.
- **Repro** `assert 1 == "1";` / `assert (node Foo) == (node Foo);`
- **Proposed resolution** Document: `==`/`!=` require operands of the same
  type. Two `PortRef`s and two `Node`s are never equal (NOTE 15b already
  says PortRef is not equality-comparable; extend to Node/Group).

### O-5. String + non-string addition rules

- **Where** `Value::add` allows `String + Number`, `Number + String`,
  `String + Boolean`, `Boolean + String` (concat with stringification)
- **What** Convenient but unmotivated. Spec is silent. Is this `to_string`
  via `Display`? Locale-sensitive? What about `String + Array`?
- **Repro** `let s = "x" + [1, 2];` — error or `"x[1, 2]"`?
- **Proposed resolution** Either (a) restrict `+` to `String + String`
  only and require `to_string()` builtin, or (b) document precisely which
  pairs concatenate and that the non-string side is stringified via
  `Display`.

---

## T — Type system

### T-1. `Number` is one type — integer vs float distinction

- **Where** `base_type → 'bool' | 'Group' | 'Node' | 'Number' | 'String'`
- **What** Nexus has only `Number`. Array indices and range bounds must
  be "integers" per `IndexNotInteger` and `RangeBoundNotNumeric`. What's
  the rule — `Number` whose fractional part is zero? Or any `Number`?
- **Repro** `let i = 3.0; let a = [1, 2, 3]; print a[i];` — works?
- **Proposed resolution** Document: array index is any `Number` whose
  `floor()` equals itself and is `≥ 0`; range bounds same.

### T-2. `Array` is a keyword but not a `base_type`

- **Where** Language type keywords list has `Array`, but `base_type`
  excludes it; arrays only appear via `array_type` (`[T; N]`).
- **What** Is `Array` a usable type identifier? `let a: Array;` —
  rejected? Or alias for `[Any]`?
- **Repro** `let a: Array;`
- **Proposed resolution** Remove `Array` from the keyword list or
  document that it's reserved-but-unusable.

### T-3. Function and closure type — return type optional? — **FIXED (impl + documented)**

- **Where** `fn_type → 'fn' '(' ( type ( ',' type )* )? ')' ( '->' type
)?`
- **What** Return type is optional. What is the inferred return type when
  omitted? `()` (Unit)? `Number`?
- **Pass-2 observation** (see
  `tests/interpreter/pass/cross_higher_order_with_node.nxs`): a
  closure with no `-> Type` annotation, e.g. `|x: Node| x`, does **not**
  match a parameter typed as `fn(Node) -> Node` — `TypeMismatch`
  results. The closure's return type is recorded as `None`, and
  `None != Some(Node)` in the structural compare.
- **Repro** `fn apply(f: fn(Number) -> Number) -> Number { f(1) } let
g = |x: Number| x; apply(g);` → TypeMismatch.
- **Proposed resolution** Either (a) infer the closure's return type
  from the body's trailing expression at definition time, or (b)
  document that closures used as arguments to `fn(...) -> T`-typed
  parameters **must** carry an explicit `-> T` annotation. (b) is the
  cheaper path; (a) is friendlier. Pick one and write it down.

### T-4. `Node` vs node-type-name in type position

- **Where** NOTE 14: "The expression-type of a `node ID` expression is
  `Node`"
- **What** Clear that `let x: Node = node Foo;` works. But what about
  `let x: Foo = node Foo;` — error per NOTE 14, but which error kind?
  `TypeMismatch` or a new `SchemaNameNotAType`?
- **Repro** `let x: Foo = node Foo;`
- **Proposed resolution** Reuse `TypeMismatch` with a clear message
  pointing at NOTE 14, or add `SchemaNameNotAType` error.

### T-5. `Group` type — schema vs. base type

- **Where** D18 ("groups carry a schema, checked like nodes")
- **What** `let x: Group = group { ... };` — but what about a hypothetical
  named group type? Since groups don't have type declarations like
  `node type`, there's no schema-name-as-type problem — but the spec
  should explicitly state this. The `Group` base type is structural / by
  the value's schema only.
- **Repro** Two groups with completely different port sets both assigned
  to `let _: Group = ...;` — both accepted.
- **Proposed resolution** Add NOTE: "The `Group` base type matches any
  group value regardless of its schema. Connection-time validation uses
  the value's schema, not the binding's declared type."

---

## V — Variables & shadowing

### V-1. `_` placeholder semantics — **FIXED**

- **Where** NOTE 5: "`_` is special; will be ignored; may occur multiple
  times within a scope"
- **What** `_` as let LHS works. What about `_ = expr;` (assignment to
  `_`), `let _ = node Foo;` (does the node still get registered?),
  `print _;` (read of `_`)?
- **Repro** `let _ = node Foo; let _ = node Foo;` — two registered
  anonymous nodes or de-duped?
- **Fix** Parser now accepts `TokenKind::Underscore` as the binding name
  in `let` declarations. Interpreter `eval_var_decl` short-circuits on
  `name == "_"`: evaluates RHS for side effects, never stores. `_ = ...`
  (bare assignment) remains a parse error (`InvalidAssignmentTarget`).
  Reading `_` gives `UndefinedIdentifier` naturally (nothing was stored).
  Regression tests: `tests/interpreter/pass/underscore_multiple_decl.nxs`,
  `tests/interpreter/fail/underscore_read.nxs`.
- **Proposed resolution** Document:
  - `let _ = ...` evaluates RHS for side effects (registers nodes/groups).
  - `_ = ...` (assignment) is a parse error.
  - Reading `_` is `UndefinedIdentifier`.

### V-2. Deferred initialization — read before write — **FIXED**

- **Where** `Value::Uninitialized` exists; NOTE 2: "Mutable variables may
  be declared without ... initialization value"
- **What** What happens when an uninitialized variable is read? Currently
  presumably a runtime error. Which error?
- **Repro** `let mut x: Number; print x;`
- **Pass-2 observation** (`cross_uninit_read.nxs`): reading an
  uninitialized variable does not error — it silently produces the
  `<uninitialized>` display value. Subsequent assignment works normally.
- **Proposed resolution** Decide: (a) accept the silent behavior and
  document it, or (b) add a dedicated `UninitializedRead` error. Currently
  undocumented in the spec either way.

### V-3. Shadowing across kinds — fn / node type / let

- **Where** NOTE 6 ("shadowing works like Rust") + NOTE 13 ("Types and
  values share a single namespace")
- **What** Tension: NOTE 6 says you can re-declare with `let`. NOTE 13
  says `node type Foo` reserves `Foo`. Can a subsequent `let Foo = 1` in
  a nested scope shadow it? Or only in the same scope?
- **Repro** `node type Foo { } fn main() { let Foo = 1; }` (illustrative
  — Nexus has no `main`).
- **Proposed resolution** Clarify: NOTE 13's reservation is per-scope,
  not global. Shadowing of a node-type-name in an inner scope is legal
  (the inner binding wins for value lookup; node instantiation still
  sees the outer schema only if no inner binding shadows it).

### V-4. `const` redeclaration

- **Where** `const_decl → 'const' ID ':' type '=' expr ';'`
- **What** Can `const` be shadowed? Re-declared in same scope? The error
  `AssignToConstant` only covers assignment, not redeclaration.
- **Repro** `const X: Number = 1; const X: Number = 2;`
- **Proposed resolution** Document: `const` may be shadowed by `let` or
  another `const` in an inner scope (Rust semantics); same-scope
  re-declaration is `DuplicateDeclaration`.

---

## C — Control flow

### C-1. `break expr` type vs. loop expression type — **OBSERVED**

- **Where** `break → 'break' expr?` + `BreakSignal(Value)` carries any value
- **What** No constraint stated on the value type. Can one `break 1` in
  one iteration and `break "x"` in another? What's the value of a loop
  that breaks with mixed types?
- **Repro** `let v = loop { if cond { break 1; } else { break "x"; } };`
- **Pass-2 observation** (`cross_break_mixed_types.nxs`): mixed types
  are silently accepted — no cross-arm type check is performed. The
  loop value's type is whatever the executed `break` arm produces.
- **Proposed resolution** Document: the type of a `loop` expression
  is determined at runtime from the executed `break` value. No static
  or cross-iteration type consistency is enforced.

### C-2. `return expr` type vs. declared return type — **OBSERVED**

- **Where** `function → ID '(' parameters? ')' ( '->' type )? block` +
  `ReturnTypeMismatch` error
- **What** Spec doesn't say when this is checked. At every `return` site
  separately, or only at the implicit trailing expression?
- **Repro** `fn f() -> Number { if true { return "x"; } 1 }`
- **Pass-2 observation** (`tests/interpreter/fail/return_type_mismatch_at_return_site.nxs`):
  `return "x"` inside a `-> Number` function raises `ReturnTypeMismatch`
  at the `return` site, before the trailing `1` is reached. Every
  `return` site is checked independently.
- **Proposed resolution** Document: every `return` site's value type is
  checked against the declared return type at runtime; falling through
  also checks. If no return type is declared, the function returns
  `Unit` and `return expr;` is rejected (use `return;`).

### C-3. `loop` without `break` — parser rejects

- **Where** `LoopWithoutBreak` parse error
- **What** Parser rejects `loop { }` with no `break` (good — prevents
  trivial infinite loops). But what about `loop { if cond { break; } }`
  — analysis at parse time can't know if the condition is reachable.
  What's the actual rule?
- **Repro** `loop { print "hi"; }`
- **Proposed resolution** Document: a `loop` body must contain at least
  one `break` statement (syntactic, not semantic, check). Conditional
  break is sufficient. Document the static escape-hatch (or remove the
  check and rely on iteration limit).

### C-4. `continue` with value

- **Where** `continue → 'continue'`
- **What** Grammar disallows `continue expr` (unlike `break expr`). Is
  this intentional?
- **Repro** `continue 1;`
- **Proposed resolution** Document: `continue` carries no value;
  `continue expr;` is a parse error.

### C-5. Iteration limit — **FIXED (documented)**

- **Where** `LoopIterationLimitExceeded(u64)` exists
- **What** What is the limit? Configurable? Per-loop or total?
- **Repro** `let mut i = 0; loop { i += 1; }` (eventually errors)
- **Proposed resolution** Document: `MAX_LOOP_ITERATIONS = 1_000_000`,
  **cumulative** across all nested loop constructs within one function
  invocation, not per-loop and not configurable. Resets to 0 on every
  function call entry. See NOTE 22.

### C-6. `if` as expression vs. statement — **OBSERVED**

- **Where** `if` is part of `control → ... | if | ...` which is reached
  from `primary` — so `if` is an expression.
- **What** Does `let x = if c { 1 };` (no `else`) produce `Unit` or fail?
  Does `if` without `else` always produce `Unit` when condition is false?
- **Repro** `let x = if false { 1 };`
- **Pass-2 observation** (`cross_if_no_else_true.nxs`,
  `cross_if_no_else_false.nxs`): behavior is runtime-dependent. When
  the condition is true, the block value is produced normally. When
  false, `Unit` is produced — and using `Unit` as an initializer raises
  `ExpectedValue` at runtime. The check is not static.
- **Proposed resolution** Document: `if` without `else` produces `Unit`
  when the condition is false. Binding that `Unit` in a `let` initializer
  is a runtime `ExpectedValue` error. Using `if` without `else` as a
  statement (discarding the value) is always valid.

---

## F — Closures

### F-1. Mutation inside closure of captured-by-value binding — **FIXED**

- **Where** NOTE 8: "captured by value", "mutations to outer not visible
  inside"
- **What** Reverse direction unspecified: inside the closure, is the
  captured copy mutable? If captured from a `let mut x`, does the copy
  inside the closure also act `mut`?
- **Repro** `let mut x = 0; let f = || { x = x + 1; x }; f(); f();`
- **Pass-2 observation** (see
  `tests/interpreter/pass/cross_closure_captures_value_snapshot.nxs`):
  the captured copy resets on every call — `f()` returns `1` both
  times. State does **not** persist across calls. This differs from
  Rust `move` semantics where the closure owns the captured binding.
- **Proposed resolution** Document the current behavior as the spec:
  "Each closure invocation re-binds captured variables from their
  snapshot values; mutations inside the closure body do not persist
  across calls. To accumulate state, return the new value and rebind it
  externally, or use a recursive `fn`." If state-preserving closures
  are wanted, that's a future feature.

### F-2. Closure captures of `Node`/`Group`/`PortRef` — **FIXED**

- **Where** NOTE 15b says PortRef is capturable; NOTE 8 says capture by
  value
- **What** "Capture by value" for a `Node` value — does the cloned
  closure see the **same** `NodeId` (i.e., shared identity), or a new
  one? D5 says "Clone produces a deep copy with fresh `NodeId`s." But
  closure capture isn't `clone`, it's structural sharing of the
  `Value::Node` variant.
- **Repro** `let n = node Foo; let f = || n; f().input -> f().input;`
  — same or two distinct nodes?
- **Proposed resolution** Document: closure capture of `Node`/`Group`
  values shares the identity (same `NodeId`). The "deep copy with fresh
  IDs" rule applies only to explicit copy constructs (none currently
  exist) — and may need to be revised in D5.

### F-3. Recursive closure — **FIXED**

- **Where** NOTE 8: "Recursive closures are not supported"
- **What** Pass-2 finding (see
  `tests/interpreter/pass/cross_recursive_closure.nxs`): recursive
  closures **do** work. The closure body's free identifiers are looked
  up at **call time**, not capture time — by the time the closure is
  invoked, the `let f = ...` binding is in scope. Infinite recursion is
  caught by `CallDepthExceeded`, not `UndefinedIdentifier`.
- **Repro** `let f = |n: Number| -> Number { if n == 0 { 0 } else {
f(n - 1) } }; f(3);` — succeeds, returns `0`.
- **Proposed resolution** Update NOTE 8: remove the claim that
  recursive closures aren't supported. Add: "Identifiers referenced in
  a closure body that are not free at creation time (i.e., not in
  scope yet) are resolved at call time against the then-current
  environment. This allows a closure to call itself by name once the
  binding completes." Update MEMORY.md likewise.

### F-4. Closure body and `return`

- **Where** No mention of `return` semantics inside closures
- **What** Inside a closure, does `return` exit the closure or the
  enclosing function?
- **Repro** `fn outer() -> Number { let f = || { return 1; }; f(); 2 }`
  — does `outer` return 1 or 2?
- **Proposed resolution** Document: `return` always targets the
  innermost enclosing `fn` **or** closure body. The above returns 2.

---

## R — References

### R-1. Orphan trigger conditions — **OBSERVED**

- **Where** NOTE 1 + `OrphanedReference(String)` error
- **What** When exactly is a reference orphaned? When its referent's
  scope ends? When the referent is shadowed?
- **Repro** `let r; { let x = 1; r = &x; } print r;`
- **Pass-2 observation** (`tests/interpreter/fail/ref_orphaned.nxs`):
  `let mut r; { let x = 1; r = &x; } print r;` → `OrphanedReference('x')`.
  The reference is orphaned as soon as the block scope containing `x` ends.
- **Proposed resolution** Document: a reference is orphaned when its
  recorded scope ID is no longer present on the scope stack. Shadowing
  does **not** orphan (NOTE 1 already covers this).

### R-2. Reference to a reference — **OBSERVED**

- **Where** NOTE 1: "auto-dereferences to referent's current value"
- **What** What is `&&x`? `&r` where `r = &x`? Is this a ref-to-ref or
  collapsed?
- **Repro** `let x = 1; let r = &x; let rr = &r; print rr;`
- **Pass-2 observation** (`cross_ref_to_ref.nxs`): `&r` where `r = &x`
  chains through to `x`'s value — reading `rr` yields `7`, not a reference
  value. Arbitrary-depth reference chains auto-deref to the underlying value.
- **Proposed resolution** Document: `&` on an identifier that is itself a
  reference produces a reference chain; reading any link in the chain
  auto-derefs to the underlying non-reference value.

### R-3. Reference target restrictions

- **Where** `InvalidRefTarget` error
- **What** Only "named variables" are valid (NOTE 1). What about `&arr[0]`?
  `&self.foo`? `&node.port`? Each is an lvalue but spec is silent.
- **Repro** `let a = [1, 2]; let r = &a[0]; a[0] = 5; print r;`
- **Proposed resolution** Document: in v1, only bare identifiers may be
  referenced. `&expr.field` and `&arr[i]` are parse errors. (Or extend
  later — but state the current restriction.)

### R-4. Write through `&const` / `&immutable` — **OBSERVED**

- **Where** NOTE 1: "writing through such a reference is a runtime error"
- **What** Which error kind? `AssignToConstant`/`AssignToImmutable` or a
  ref-specific one?
- **Repro** `const X: Number = 1; let r = &X; r = 2;`
- **Pass-2 observation** (`tests/interpreter/fail/ref_write_through_const.nxs`):
  `r = 2` raises `AssignToConstant('X')` — reuses the same error as direct
  assignment to a constant, with the original variable name in the message.
- **Proposed resolution** Document: reuse `AssignToConstant` /
  `AssignToImmutable` with the original variable name in the message.

---

## A — Arrays

### A-1. Zero-length sized array fill is a panic — **FIXED** (actual trigger was untyped empty literal `let a = [];`; now reports `TypeMismatch`)

- **Where** Known bug — `[Type; 0]` panics in `value_to_type_id()` per
  `MEMORY.md`
- **What** `[Number; 0]` is grammatically and semantically valid (an
  empty array). The implementation panics during type inference.
- **Repro** `let a: [Number; 0];`
- **Proposed resolution** Bug, not spec gap; but spec should say:
  zero-length arrays are legal. Fix the panic.

### A-2. Element-type inference for empty literals

- **Where** `array_literal → '[' ( expr ( ',' expr )* )? ']'`
- **What** `let a = [];` — what is the element type? Inferred from
  context? Error?
- **Repro** `let a = [];`
- **Proposed resolution** Document: an empty array literal without a
  type annotation is a `TypeMismatch` error ("cannot infer element
  type"). With annotation (`let a: [Number] = [];`), works.

### A-3. Negative/non-integer index

- **Where** `IndexNotInteger` + `IndexOutOfBounds { index: i64, ... }`
- **What** `IndexOutOfBounds` uses `i64`, suggesting negative indices are
  accepted at parse time but rejected at runtime. What about `arr[-1]`
  for "last element" — supported (Python-style)?
- **Repro** `let a = [1, 2, 3]; print a[-1];`
- **Proposed resolution** Document: negative indices are not supported;
  use `arr[arr.len() - 1]` for last-element access. `arr[-1]` is
  `IndexOutOfBounds`.

### A-4. Mixed-type literals

- **Where** `ArrayElementTypeMismatch` error
- **What** When is element-type checked — at literal construction, at
  assignment, both? `[1, "x"]` — error at the `"x"` or at the let?
- **Repro** `let a = [1, "x"];`
- **Proposed resolution** Document: an array literal must have
  uniform-typed elements; first-element type is the array's element
  type; subsequent mismatch raises `ArrayElementTypeMismatch` at literal
  evaluation.

### A-5. Sized array — size enforcement timing — **FIXED**

- **Where** NOTE 9: "enforced at runtime for fn params, returns, var
  decls, const decls"
- **What** What about plain assignments to a previously-sized binding?
  Per `MEMORY.md`, "Stored binding type_id after `eval_var_decl` has
  size stripped to `None` — assignments don't re-check size." So
  `let a: [Number; 3] = [1, 2, 3]; a = [1, 2];` is legal? That sounds
  surprising.
- **Repro** `let mut a: [Number; 3] = [1, 2, 3]; a = [1];`
- **Pass-2 observation** (`cross_sized_array_reassign.nxs`): size is
  only enforced at the initial declaration — reassigning a shorter or
  longer array to the same binding succeeds silently.
- **Proposed resolution** Document the current behaviour: size
  annotation is a one-shot check at declaration time. Subsequent
  assignments to the binding are unchecked.

### A-6. Iteration and assignment with uninitialized array elements — **FIXED**

- **Where** `for..in` over arrays; variable declarations with array initialisers
- **What** A typed fill array such as `let mut arr: [Group; N];` creates an array
  of `N` uninitialized slots. If iterated with `for x in arr { }` before all slots
  are assigned, the interpreter previously panicked (`unreachable!` in
  `value_to_type_id`). Likewise, `let x = arr;` without a type annotation panicked
  when the type inferencer called `value_to_type_id` on the first (uninitialized)
  element.
- **Repro** `let mut arr: [Group; 3]; for x in arr { }` / `let mut arr: [Group; 3]; let x = arr;`
- **Proposed resolution** (implemented): `for..in` raises `UninitializedRead` on
  the first uninitialized element, consistently with reading an uninitialized scalar
  variable. Assigning an all-uninitialized array to an unannotated binding raises
  `TypeMismatch` with a message directing the user to add a type annotation (same
  pattern as empty-array literals). `value_to_type_id` no longer panics on arrays
  that contain uninitialized elements; it scans for the first typed element and falls
  back gracefully when none exists. Regression tests:
  `tests/interpreter/fail/for_in_uninitialized_array.nxs` and
  `tests/interpreter/fail/assign_uninitialized_array_no_annotation.nxs`.

---

## S — Strings

### S-1. `String.len()` byte vs. char

- **Where** Builtin methods table: "returns the length of the string (in
  bytes)"
- **What** "Bytes" is documented but inviting bugs. `"café".len() == 5`
  (UTF-8) is surprising in a network-DSL context. No `char_count()` /
  `len_chars()` exists.
- **Repro** `assert "café".len() == 4;` — fails (it's 5).
- **Proposed resolution** Keep `len()` as bytes (matches Rust semantics);
  add a `chars()` method or accept this as documented. Add an
  example/note to make it impossible to miss.

### S-2. Empty string concatenation

- **Where** `Value::add` — `String + String`
- **What** `"" + ""` — produces `""`? `"" + 1` — produces `"1"`?
- **Repro** `print "" + 1;`
- **Proposed resolution** Document: empty strings concatenate normally;
  no special cases.

### S-3. String comparison ordering — **FIXED**

- **Where** `Value::gt` / `lt` / `gteq` / `lteq` only allow `Number` pairs
- **What** Spec doesn't say strings can't be ordered. Currently
  `InvalidComparison`. Lexicographic order would be natural.
- **Repro** `assert "a" < "b";` — currently errors.
- **Fix** Added `(Value::String, Value::String)` arms to all four
  comparison methods in `value.rs` using Rust's native `str` ordering
  (byte-order / Unicode code-point). See Operator note in README.

---

## M — Methods

### M-1. Method dispatch table — extensibility

- **Where** Builtin methods table: only `len()` on `Array` and `String`
- **What** Are these the only methods, forever? What if a future library
  wants to add methods? `UndefinedMethod` error implies a fixed set.
- **Repro** `let a = [1, 2, 3]; a.push(4);` — `UndefinedMethod`
- **Proposed resolution** Document: built-in methods are a closed set in
  v1; user-defined methods are not supported. Future extension via
  `impl` blocks is out of scope.

### M-2. Method on `Number`, `bool`, `Node`, `Group`

- **Where** Methods table lists only `Array` and `String`
- **What** What about `(3.14).abs()`, `true.not()`, `node.id()`? Each
  raises `UndefinedMethod`. Is the lack of methods on numeric types
  intentional?
- **Repro** `print (3.14).abs();`
- **Proposed resolution** Document explicitly: in v1, the only types
  with built-in methods are `Array` and `String`. All other method
  calls are `UndefinedMethod`.

---

## N — Network (nodes, groups, ports, connections)

### N-1. Anonymous node naming counter — wraparound and persistence

- **Where** NOTE 15: anonymous node = `node_<N>`, counter shared with
  `NodeId`
- **What** What if the counter wraps `u64::MAX` (theoretical)? Across
  `use`-imported files, does the counter reset? Does each `eval()`
  start at 0?
- **Repro** Multi-file imports + many anonymous nodes.
- **Proposed resolution** Document: counter is per-`Interpreter`
  instance, starts at 0, monotonic, wraps panic (or saturating).
  Imports don't reset.

### N-2. Schema name vs. instance name collision

- **Where** NOTE 13 + NOTE 15
- **What** `node type Foo { } let foo = node Foo;` — `foo` (lowercase)
  and `Foo` (uppercase) coexist. What about `node type Foo { } let Foo
= 1;`? NOTE 13 says reserved. But what about `let foo = node Foo;
let Foo = 1;` — second `let` shadows the type, breaking subsequent
  `node Foo` references?
- **Repro** `node type Foo { } let Foo = 1; let bar = node Foo;`
- **Proposed resolution** Document: a `let`/`const` cannot reuse a
  declared node-type name in the same scope (`DuplicateDeclaration`).
  Inner-scope shadowing makes `node Foo` unreachable from inside the
  inner scope (resolves to the let-binding, which isn't a node type →
  `NotANodeType` error).

### N-3. Port direction inference for non-literal RHS — **FIXED**

- **Where** NOTE 16 + the recent generalization to arbitrary expr
- **What** `port input = if cond { a.in } else { b.out };` — direction
  is taken from whichever branch runs. Is this allowed?
- **Pass-2 observation** (see
  `tests/interpreter/pass/cross_port_decl_conditional_diff_dir.nxs`):
  the interpreter accepts it. Direction is inferred from whichever
  branch the runtime evaluates; the other branch is never examined.
  `if true { self.n.i } else { self.n.o }` yields an `in` port; `if
false { self.n.i } else { self.n.o }` yields an `out` port. No error
  in either case — including when the two branches have different
  directions.
- **Proposed resolution** Document: port direction is resolved purely at
  runtime from the evaluated `PortRef`. A conditional expression where
  the two branches have different directions is legal; the resulting
  port direction depends on the runtime value of the condition. No
  static check is performed.

### N-4. Schema port-name collisions

- **Where** `node_type_decl → 'node' 'type' ID '{' port_spec* '}'` +
  `port_spec → ( 'in' | 'out' | 'inout' ) ID ( ',' ID )* ';'`
- **What** `node type Foo { in x; out x; }` — same port name in both
  directions. `DuplicatePortName` covers this for parser? What about
  splitting across `port_spec`s — `in x; out x;` vs `in x, x;`?
- **Repro** `node type Foo { in x; out x; }`
- **Proposed resolution** Document: port names are unique across all
  directions in a node type (and similarly in a group's port set).
  Same-direction duplicates within one `port_spec` or across — both
  raise `DuplicatePortName`.

### N-5. Self-loop semantics in network output

- **Where** NOTE 15c: "Self-loops (a.in -> a.out where both refs are on
  same NodeId) are legal"
- **What** Allowed at validation, but what is the network-output
  representation? Both endpoints share a path prefix; how is the
  connection serialized?
- **Repro** `let n = node Foo; n.out -> n.in;`
- **Proposed resolution** Document: a self-loop is serialized as two
  endpoints with identical node paths but different port names. No
  special encoding.

### N-6. Connection between port refs captured across groups

- **Where** NOTE 15c, NOTE 17
- **What** `let p = self.member.port;` is a local `PortRef`. NOTE 17
  says it's a legal endpoint. But what if it's captured into a
  closure and called from outside the group? Does the connection cross
  the group boundary? Does it violate group encapsulation?
- **Repro** `let f; group { self.n = node Foo; f = || self.n.out; }; let
m = node Bar; f() -> m.in;`
- **Proposed resolution** Document: closure capture of a `PortRef`
  freezes the endpoint identity (NodeId + port name). The connection is
  added to whatever group is currently being evaluated at the
  connection point (root group if at top level). Cross-group
  connections are allowed by the validator (NOTE 15c is schema-agnostic).

### N-7. Duplicate-connection detection scope

- **Where** NOTE 15c: `DuplicateConnection` on identical quadruple
- **What** Per group? Or globally? Two groups each containing the same
  connection — duplicate or distinct?
- **Repro** Two groups, each with `self.a -> self.b;` — but on different
  node instances.
- **Proposed resolution** Document: duplicate detection is per-group
  (since each group has its own connection list per D3/D4).

### N-8. Empty group port-less group as connection endpoint

- **Where** NOTE 17: "Empty groups are legal"
- **What** An empty group has no ports. Can it be a connection endpoint
  via `.something`? Hard `UndefinedPort` immediately, presumably.
- **Repro** `let g = group {}; g.foo -> something;`
- **Proposed resolution** Document: an empty group has an empty port
  schema; every member-access raises `UndefinedPort`.

### N-9. `print` of a network value

- **Where** `print` statement, `Display for Value`
- **What** What does `print node Foo;` show? `print group { ... };`?
  The `Display` impls are well-defined but the user-visible format
  isn't part of the spec.
- **Repro** As above.
- **Proposed resolution** Either spec the `Display` output or document
  that `print` of network values is for debugging only; for stable
  output use the network summary (Step 10).

---

## U — Use imports

### U-1. Circular imports — OBSERVED

- **Where** NOTE 12: "Each file is loaded at most once per program"
- **What** Cycle handling: if `a.nxs` uses `b.nxs` and `b.nxs` uses
  `a.nxs`, the load-once rule prevents infinite recursion, but what's
  the visibility order? Does `a` see `b`'s declarations or not, at the
  point of its own first use-statement?
- **Repro** Two-file cycle.
- **Observed**: `eval_use` inserts the canonical path into `loaded_files`
  _before_ recursing into the child interpreter. The clone passed to the
  child includes this entry, so any `use a;` inside `b.nxs` is a silent
  no-op. Result: `a` never sees `b`'s declarations (the child finishes
  empty), but there is no infinite recursion. Any use of `b`'s names in
  `a`'s body that precedes the `use b;` is `UndefinedIdentifier` as
  expected. This matches the proposed resolution.

### U-2. Path-resolution security — OBSERVED

- **Where** NOTE 12: "loads `libname.nxs` from the directory of the
  importing file"
- **What** What about `use ../../etc/passwd`? Use IDs are limited to
  identifier characters, but a future extension to string paths is
  implied. Document the current restriction.
- **Repro** N/A in v1 (identifier-only).
- **Observed**: `use ID;` appends `<ID>.nxs` to the source directory
  path; `ID` is an identifier token so it cannot contain `/`, `..`, or
  other path-escape characters. Path-escape is impossible by design in
  v1.

### U-3. Top-level statement-execution order across imports — OBSERVED

- **Where** NOTE 12 + NOTE 11 (top-level only for `node type` / `use`)
- **What** Imports merge declarations, but what about top-level
  statements (e.g., `let x = expensive();`) in the imported file? Do
  they run? Once? Twice?
- **Repro** `a.nxs` contains `use b; let x = node Foo;` and `b.nxs`
  contains `let y = node Bar;` — is `y` registered in the network?
- **Observed**: The child interpreter runs the full AST of the imported
  file. All top-level statements execute once. Top-level variables,
  nodes, and groups from the imported file are merged into the importer's
  global scope via `env.iter_global()` + `env.define_global()`. Top-level
  connections are appended to the importer's connection list. So `y` IS
  registered in the network — `use` is non-pure by design.

---

## D — Determinism / Output

### D-1. HashMap iteration order in output — **FIXED**

- **Where** `Value::Group { members: HashMap<...> }`, network summary
  walks members
- **What** Is the output network summary order deterministic? `HashMap`
  iteration order is randomized in Rust by default.
- **Repro** Run example9 twice; member order in the network summary —
  same or different?
- **Proposed resolution** Document: network summary order is **stable**
  (sort members by name, connections by `(from_path, to_path)`,
  globally). Add tests asserting byte-identical output across runs.

### D-2. Anonymous node ordering

- **Where** NOTE 15
- **What** Three anonymous nodes in a row — their `node_0`, `node_1`,
  `node_2` ordering reflects source order? What about anonymous nodes
  inside `for` loops?
- **Repro** `for i in 0..3 { node Foo; }`
- **Proposed resolution** Document: the counter is incremented in
  evaluation order; thus loops produce predictable `node_N` sequences.

### D-3. Network summary path collisions

- **Where** Network summary uses `/`-separated paths
- **What** What if a member name contains a `/` or special character?
  Identifiers can't (alphanumeric only), but `_` is allowed. No
  collision possible — but document the invariant.
- **Repro** N/A.
- **Proposed resolution** Document: paths are composed of identifier
  segments separated by `/`; segments cannot contain `/` because
  identifiers can't.

---

## E — Error / diagnostics

### E-1. Error recovery

- **Where** No spec mention
- **What** Does the parser try to recover and produce multiple errors,
  or fail-fast on first error?
- **Repro** A file with three independent syntax errors — one or three
  diagnostics?
- **Proposed resolution** Document: fail-fast in v1; multi-error
  reporting is future work.

### E-2. Assertion messages — **FIXED**

- **Where** `assert → 'assert' expr ( ',' expr )? ';'` +
  `AssertionFailed(Option<String>)`
- **What** Optional message must be a `String` (per MEMORY.md). What
  about a non-string expression — eager error before condition check?
  Lazy after?
- **Repro** `assert false, 42;`
- **Fix** Message expression is evaluated **only** if the assertion fails
  (lazy). If the result is not a `String`, the interpreter now raises
  `TypeMismatch("assert message")` instead of silently discarding the message.
  `assert true, 42;` succeeds without evaluating the message.
  Regression tests: `tests/interpreter/pass/assert_true_non_string_message.nxs`,
  `tests/interpreter/fail/assert_false_non_string_message.nxs`.
- **Proposed resolution** Document: message expression is evaluated
  **only** if the assertion fails. Non-string result is `TypeMismatch`
  at that point.

---

## Summary — top items by likely impact

If you only fix six of these before Step 9, my recommendation:

1. ~~**G-3** (grammar bug: `if`/`else` missing `?`) — pure spec fix.~~ **FIXED**
2. ~~**G-6** (port/member decls in top-level `decl`) — pure spec fix.~~ **FIXED**
3. ~~**A-1** (zero-length array panic) — known bug, small.~~ **FIXED**
4. ~~**F-1, F-2** (closure mutation + closure capturing Node/Group)~~ **FIXED**
5. ~~**D-1** (deterministic network output ordering)~~ **FIXED** (already implemented; documented in NOTE 18)
6. ~~**N-3** (port direction inference on conditional RHS)~~ **FIXED**

The rest are best closed by Pass 2 (cross-feature matrix) producing
concrete tests that pin behavior.
