Skip to content

spec(gazprea): consolidated spec-review stack — nested types + audit fixes - #139

Open
Sir-NoChill wants to merge 84 commits into
masterfrom
spec/consolidated-review
Open

spec(gazprea): consolidated spec-review stack — nested types + audit fixes#139
Sir-NoChill wants to merge 84 commits into
masterfrom
spec/consolidated-review

Conversation

@Sir-NoChill

Copy link
Copy Markdown
Collaborator

Consolidated spec-review stack — 20 PRs in one

This collapses the stacked-PR chain (gh stack #121, formerly [stack 1/20][stack 20/20], PRs #110#138) into a single PR against master. The 20 individual PRs are closed; their branches and comment threads stay readable at the links in the manifest below. All 40 commits are preserved (nothing squashed) — use the Files changed tab for the cumulative diff, or the commit list for the logical steps.

Footprint: 35 files, +1576 / −229. Spec: 24 files under gazprea/spec/. Infra: the .agents/ reproducibility scaffold + pyproject.toml/uv.lock (from #110).

Provenance: grew out of the vector-semantics discussion (#106) and the Aug-17 rubric-meeting action item (#132). The final nested-types change closes #106, #82, #71, #101, #86.


⚠️ Cross-PR interactions — review the NET result, not each step

The stack intentionally states a rule and later revises it. Review the final tree, not the intermediate commits:


Open questions flagged for reviewer attention (per former PR)

These are the ambiguities each PR flagged as needing a language-owner decision (verify against gazc / the reference compiler where noted).

#122 — combined struct decl form: does struct S (...) x; accept a qualifier (var struct S (...) x;)? The "combined form is always const" claim is inferred from examples; if gazc accepts var, it's wrong.

#123 — mixed concatenation result: is char || char even legal? (patch's "(or characters)" asserts char||char = char[].) Is the rule "at least one operand is string" or "either operand"? char[] || char[] = char[] is unexemplified.

#124 — tuple member conversions: members "convert by their kind" lists only scalar + array. Nested tuple, string, matrix members are unaddressed — now relevant post-#138. Confirm array-in-tuple casts pad/truncate like standalone array casts.

#125 — uninit const & stride: by is restricted to >= 1 (rules out by -1 reverse iteration — intended?). Uninit const is defined as "holds the zero value permanently" — confirm that vs. treating uninit-const as a type error.

#126 — slice bounds: confirm ..-i selects 1..n-i; confirm the 1-indexed assertion doesn't contradict another chapter; confirm grammar accepts a[i..j] by 2 (vs a[i..j by 2]).

#127 — struct fixed-length field: patch chose character[256] over string[256]. If sized/bounded strings are intended, the fix differs. (See cross-PR note — the nesting ban it obeyed is now reversed.)

#128 — procedure call sites: are casts legal on a procedure-call result? (patch adds "and casts" to the operator whitelist.) Confirm the restatement's disallowed-site list is the exact inverse of the chapter-top allowed list.

#129 — method-call surface (largest feat:): (a) receivers are variables only — does gazc accept x.len() on a literal/expression result? (b) mutation limited to function-local vars vs. "no global/captured mutation" — which framing? (c) append tie-break = single-element wins — matches reference? (d) "method call is the only expression usable as a statement" — reconcile with call statements. (e) concatappend rename is breaking — does the reference still expose concat as an alias?

#114 — range vs array-index: is matrix slicing intended? (matrix.rst:103 still "indices must be integers"). No formal def of "range syntax" vs "range expression"; by-on-slice rule lives in a different chapter and may drift.

#115 — promotion vs cast: type_casting.rst:48 still says "promoted" for an explicit as<> cast; type_casting.rst:95 allows array→array casts across dimensions — confirm 1-D↔2-D explicit cast is intended.

#138 — nested aggregates & n-d arrays (deferred, not in scope): a rank-agnostic shape interface (vs rows/columns, per #82), n-d matrix-multiply, and broadcasting are left to a follow-up. The ragged (vector<vector<T>>) vs rectangular (vector<T[*]>, T[*][*]) split from #101 is implied but not yet spelled out operationally.

Lower-priority / follow-ups noted in-PR: #116 (string.rst:8 loose equivalence); #118 (integer.rst Associativity column duplication); #111 (expressions.rst:54 generator-error class; repo-wide "must emit" sweep); #112 (prose String/Vector capitalization left per maintainer decision). #119, #120, #130 flagged no open questions.


Infra (.agents/ scaffold, from #110) — open feedback digest

PR #110 drew 25 inline comments (@Sir-NoChill). Themes (several already addressed by later commits on the same branch — 223c45d drop identity prescription & generated artifacts, 6e2bbba point .agents/ at the DocsDoc image + rewrite skills, 66ae45f uv lockfile):

  • Don't prescribe identity — signing identity should be user-supplied via the manifest, not hardcoded; delete the vendored agent-pubkey.asc. (→ addressed by 223c45d.)
  • Don't commit generated filesbootstrap.sh/check.sh/healthcheck.sh are rendered from templates; gitignore them and regenerate on setup. (→ addressed by 223c45d.)
  • Prefer a Docker/CI image — make this a dev image in cmput415/ci-utils (optionally a nix flake); point the manifest/README at the container instead of a bash env harness. (→ partly addressed by 6e2bbba.)
  • Dependency management via uvpyproject.toml + uv sync on a uv-managed venv. (→ addressed by 66ae45f + pyproject.)
  • Templating via jinja2 — use the jinja2 package rather than a custom renderer (render.py was later approved: "This is perfect").
  • Rewrite the grammar-consistency skill — focus on English prose quality (spelling, passive voice, subject consistency, technical writing), not the Gazprea EBNF (that's the students' exercise); the spec-review skill should consume it after the rewrite. (→ addressed by 6e2bbba.)
  • Local CI via act rather than a hand-rolled check-ci.sh bash harness; add a .agents/scratch gitignore entry.

Full thread: #110.


Manifest — the 20 collapsed PRs

# Title Branch
#110 chore(agents): scaffold reproducible session bootstrap chore/agents-scaffold
#112 spec: replace undefined syntax in examples fix/undefined-syntax-examples
#113 spec: state the vector element-type set (reversed by #138) fix/vector-element-types
#116 spec: scope the vector-array equivalence claim fix/vector-array-divergence
#114 spec: distinguish range slices from array-valued indices fix/range-index-semantics
#115 spec: scope the promotion-implies-cast claim fix/promotion-cast-claim
#118 spec: single home for operator precedence refactor/precedence-single-home
#119 spec: single home for stream_state semantics refactor/stream-state-home
#120 spec: single home for const-by-default rule refactor/const-default-home
#111 spec: name error classes at anonymous error sites feat/errors-chapter
#122 spec: specify combined struct-plus-instance declaration fix/struct-decl-form
#123 spec: state the mixed concatenation result type fix/string-concat-result
#124 spec: tuple member conversions cover array members fix/tuple-conversion-members
#125 spec: close two silent contract gaps (uninit const, stride) fix/uninit-const-and-stride
#126 spec: unify slice bound rule in one section fix/slice-bounds
#127 spec: struct field / fixed-length field (nesting ban reversed by #138) fix/struct-nesting
#128 spec: reconcile procedure call-site restatement fix/procedure-call-sites
#129 spec: specify the method-call surface (largest feat) feat/method-calls
#130 spec: correct misleading examples (batched) fix/example-corrections
#138 spec: permit nested aggregates and n-d arrays feat/nested-composite-types

Consolidated from gh stack #121. Individual PRs closed in favour of this one; commit history preserved.

Manifest-driven apt/check/healthcheck scripts plus the session's
ephemeral agent signing identity, per the agent-bootstrap protocol.
Volatile paths (key material, baselines, agent-env.sh) are gitignored.

The scaffold is vendor-neutral: the directory is `.agents/`, the default
identity is generic ("Agent"), and the skills-directory path is a
manifest variable rather than hardcoded to any one agent tool's
convention.

Assisted-by: Agent <ai@blobfish.icu>
Applies review feedback from PR #110. Removes the committed rendered
scripts (bootstrap.sh, check.sh, healthcheck.sh) and the committed
public key (agent-pubkey.asc); each session regenerates the scripts
from the .tmpl files via render.py before running bootstrap.

Identity is now opt-in: the manifest's `agent:` block is blank by
default, and bootstrap only mints a GPG signing key (and writes
agent-env.sh) when a user fills it in locally. Users choose their own
signing identity; the repo prescribes none.

Python dependencies move to pyproject.toml under uv: bootstrap installs
uv from astral.sh if missing, then runs `uv sync` to provision a venv
pinning sphinx==6.2.1 alongside jinja2 and PyYAML (both needed by
render.py, which now uses stock Jinja2 with a `{## ##}` comment tag to
avoid colliding with bash's `${#arr[@]}` array-length syntax).

Adds an empty `.agents/skills/` directory as the location for bundled
review skills; `manifest.yaml`'s `skills:` list names entries under it,
and healthcheck asserts each listed skill has a non-empty SKILL.md.

BREAKING CHANGE: `.agents/bootstrap.sh`, `.agents/check.sh`, and
`.agents/healthcheck.sh` are no longer tracked. Existing checkouts
must render them once (`for t in .agents/*.tmpl; do
uv run .agents/render.py .agents/manifest.yaml "$t" >
"${t%.tmpl}"; done`) before running bootstrap. Users who had a signing
identity configured must re-populate the `agent:` block in their local
copy of manifest.yaml.

Assisted-by: Agent (claude) <ai@blobfish.icu>
Populates .agents/skills/ with the two skills the repo will actually use
during specification review, and lists them in manifest.yaml so the
healthcheck notices if either goes missing.

spec-review is the editorial/structural checklist a human maintainer
runs before a spec chapter merges: build integrity (`sphinx-build -W -n`),
heading hierarchy, `:term:`/`:ref:`/`:doc:` cross-reference integrity,
admonition placement, gazc-backed sanity check on `.. code-block::
gazprea` examples, TODO/FIXME residue, and targeted sibling-file
consistency spot-checks.

grammar-consistency catches the cross-file syntactic-surface divergences
that PRs like #116 (vector-vs-array) and #118 (precedence single-home)
exist to fix. It compares the same grammar element (keyword, operator,
named rule, type-form) as it appears in different chapters and reports
disagreements without picking a winner -- consistency is orthogonal to
correctness.

Assisted-by: Agent (claude) <ai@blobfish.icu>
Adds .agents/skills/spec-review/check-ci.sh, which runs the same two
checks CI does before a spec change hits the remote:

  * Sphinx build across every doc subdir in the top-level Makefile
    (setup, generator, lolcode, vcalc, gazprea, info), with -W -n so
    warnings and unresolved cross-references become errors -- stricter
    than the CI deploy step's own `make html`.
  * lychee over the exact file globs and args CI's linkcheck.yml uses,
    self-installing the binary from lycheeverse's installer or cargo
    when it is missing rather than silently skipping the check.

Sub-modes `sphinx` and `links` scope the run to one workflow's worth
of checks. The SKILL.md checklist's "build integrity" step now points
at the script instead of open-coding the sphinx-build invocation.

Assisted-by: Agent (claude) <ai@blobfish.icu>
Address the review on #110:

- manifest.yaml now names ghcr.io/cmput415/docs-dev as the preferred
  environment; native apt/uv path stays as a fallback.
- README.md documents the docker-first flow, keeps the "regenerate on
  session start" note, and explicitly frames signing as opt-in and
  non-prescriptive.
- bootstrap.sh.tmpl trims the block-level commentary the reviewer
  called out; identity handling stays gated on a populated agent block.
- skills/grammar-consistency/SKILL.md is rewritten to audit English
  prose (spelling, passive voice, subject/tense, terminology,
  technical-writing anti-patterns). Deriving the Gazprea grammar from
  the informal spec is a student exercise and is now out of scope.
- skills/spec-review/SKILL.md swaps the hand-rolled check-ci.sh for
  act-based workflow replay (act ships in the DocsDev image) and
  points at grammar-consistency for prose review.
- Removes .agents/skills/spec-review/check-ci.sh (subsumed by act).

Assisted-by: Agent (claude) <ai@blobfish.icu>
The /tree/main/DocsDev path does not exist on cmput415/ci-utils and
tripped linkcheck. The repo root already documents the image.
pyproject.toml was committed without its lockfile, so `uv sync` resolved
freely on every checkout and the session environment was not actually
reproducible. Pin it.

Assisted-by: Agent (claude) <ai@blobfish.icu>
String and free len() do not exist (string, length(), or the .len()
method do), char is not a type (character is), and comma indexing
M[1, 2] is defined nowhere (composite M[1][2] is). Each example now uses
the spelling the spec defines.

Assisted-by: Agent <ai@blobfish.icu>
Follow-up polish on top of "replace undefined syntax in examples" so the
patch's own examples read cleanly:

*   ``procedures.rst`` byvalue/byreference now call ``x.len()`` on the
    ``string`` argument instead of the free ``length(x)``.  ``string`` is
    a sub-type of ``vector<character>`` (types/string.rst:76) and
    ``vector`` defines ``.len()`` (types/vector.rst:65); using the method
    keeps the string-vs-vector surface consistent, which is the shape
    the rest of the spec assumes.

*   ``types/struct.rst`` renames the ``character char`` and ``real float``
    fields on the ``Another`` example to ``character c`` and ``real r``.
    Naming a field after a type from another language reads like a
    keyword clash even though Gazprea's grammar allows it; the new
    names follow the ``s1`` example's ``i``/``r``/``iv`` convention.
    Also fixes the paragraph that referred to the struct by the wrong
    name (``Struct type "s"`` -> ``s1``) and updates the field-name
    list to match.

Assisted-by: Agent (claude) <ai@blobfish.icu>
Human-review follow-up.  The previous rename (fcf3ad8) landed on
`character c, real r, string[256] str, s1 struct_field`, which put
Another's `r` right next to the sibling `s1` example's `r` field two
lines above.  Scoping is fine (each struct owns its field namespace),
but the collision reads as an accidental repeat in a two-line example
whose whole point is to show that field identifiers are chosen freely.

Rename to `character ch, real f, string[256] str, s1 struct_field`,
and update the prose that enumerates them.  Distinct short names,
no shadow of the neighbouring example, and still tracks the `s1`
`i`/`r`/`iv` pattern of using initials-of-the-type.

Assisted-by: Agent (claude) <ai@blobfish.icu>
vector<T> never said which T are legal, leaving vector<vector<T>>,
vector<tuple(...)>, and vector<string> undecidable. Now: base types and
1-D arrays of base types, matching every existing example.

Assisted-by: Agent <ai@blobfish.icu>
Human-review follow-up.  The new `vector<T>` element-type rule
enumerated the base types as `(boolean, character, integer, real)`,
which is inconsistent with the two other places the same set is
enumerated: `types/array.rst:6` and `constexpr.rst:22` both use
`(boolean, integer, real, character)`.  Any drift between these three
enumerations makes a reader wonder if the set itself changed at one
of the sites.

Reorder the vector rule to match.  Same set, no rot; if the set ever
does grow (e.g. `string` promoted to a base type), all three files
will need to change in lockstep and having them start identical makes
that easier.

Assisted-by: Agent (claude) <ai@blobfish.icu>
'Vectors behave exactly like arrays' papered over normative differences
the same file relies on: methods, array-valued binary results, and a
pad-to-first ragged policy that contradicts matrix pad-to-longest for
the same literal. The claim is now an interop list plus an explicit
enumeration of the differences, with a cross-reference at the padding
rule. Also replaces the undefined term 'subroutines'.

Assisted-by: Agent <ai@blobfish.icu>
Follow-up polish on top of "scope the vector-array equivalence claim"
(ab6eeed).

Two lingering over-broad claims in ``types/vector.rst``:

*   The intro's differences list said "binary operations *involving a
    vector* produce array results", but the body (the "Operations"
    subsection just below) only supports that for *mixed* vector+array
    operations.  The vector+vector case is not stated anywhere.  Narrow
    the intro to match the body: "a mixed binary operation between a
    vector and an array".

*   The Operations subsection opened with "Operations on vectors are
    identical syntactically **and semantically** to operations on
    arrays" -- the exact over-broad equivalence the patch is scoping
    out of the intro.  Left in place, it re-introduces the paperover
    a few lines down.  Reword to "use the same syntax as ... and,
    except for the differences enumerated above, share their
    semantics" so the enumeration in the intro remains authoritative.

Assisted-by: Agent (claude) <ai@blobfish.icu>
Human-review follow-up.  Two related over-commits in the intro
paragraph now that the six spec-review PRs are viewed as a group:

*   "interoperate freely with arrays" — overstates the case once the
    vector element-type restriction (companion PR #113) lands.  A
    `vector<S>` where `S` is a struct never exists, so interop is
    "free" only over the element types both sides support.  Add the
    qualifier: "interoperate with arrays for the element types they
    both support".

*   "the differences are normative: [three-item list]" — presented as
    exhaustive.  Once PR #113 merges, a fourth normative difference
    (element-type set is narrower on vectors) is documented directly
    below, and a reader who trusts this enumeration as complete will
    miss it.  Soften to "differences include (non-exhaustively)" so
    the paragraph remains true after the neighbouring PR merges and
    tolerates future additions without re-editing.

Assisted-by: Agent (claude) <ai@blobfish.icu>
'Arrays cannot be indexed with array expressions' collided with ranges
being arrays and with slicing being defined as indexing by a range.
The rule now names the distinction: array values (including ranges bound
to variables) are illegal indices; literal range syntax in an index
position is the slice form.

Assisted-by: Agent <ai@blobfish.icu>
Follow-up polish on top of "distinguish range slices from array-valued
indices" (ebe5f8d).

The patch scoped the general rule in statements.rst but left
`types/array.rst`'s indexing subsection saying only "An array may be
indexed using integers", which continued to contradict the very next
subsection defining slices as indexing by a range.  Extend the rule
here to match: an integer index yields an element, a range at the
index position yields a slice (with a cross-reference to the slices
subsection), and an array *value* (including a range bound to a
variable) is not a legal index.  Same distinction the statements.rst
hunk introduced, phrased for the type chapter's local vocabulary.

Assisted-by: Agent (claude) <ai@blobfish.icu>
…pression

Human-review follow-up.  The original rule at statements.rst:46 said
``v[w]`` is illegal "when ``w`` is an array variable", but the
companion rule at types/array.rst:260 (added on the same branch)
correctly says "an array *value* … is not a legal index".  Different
scopes for the same rule: an expression, a parenthesized value, or a
function call that returns an integer array is caught by the array.rst
wording and slips past the statements.rst one.

Widen statements.rst to "whenever ``w`` evaluates to an array value",
and spell out the class of expressions this covers.  Same rule stated
consistently in both chapters, no more gap on function-returned
ranges.

Assisted-by: Agent (claude) <ai@blobfish.icu>
The universal claim was false for string/character[*] (no as<> form
exists) and glossed the size requirement on scalar-to-array casts. The
vague 'higher dimension' exception is replaced with the concrete rule
(no 1-D to 2-D promotion; scalars broadcast), the square-matrix note is
scoped to ** operands, and the string promotion section now names the
array/vector distinction precisely.

Assisted-by: Agent <ai@blobfish.icu>
Follow-up polish on top of "scope the promotion-implies-cast claim"
(c087971).

*   The scalar-to-matrix promotion note used "a matrix of any shape".
    ``shape`` is a stdlib extension in this project, not part of the
    formal spec (a companion patch consciously scopes it out), and
    reusing the word here as a common noun invites conflation.
    Swap to "of any dimensions".

*   The Character-Array/String section's added tail clause
    ("a ``string`` used where a character array is expected, or
    vice-versa, converts silently") duplicated "implicitly converted"
    from the sentence just above it.  Drop the redundant clause; the
    "of note is between ``string`` and character *arrays*" pointer
    still carries the intended emphasis.

Assisted-by: Agent (claude) <ai@blobfish.icu>
Human-review follow-up.  The first commit introduced a positional
forward reference ("see the final section of this chapter") from the
intro's second caveat down to the Character-Array/String section.
Positional references silently break when a later section is added
to the chapter — the "final section" is no longer the intended
target.

Add an `ssec:typePromotion_string` label on the section heading and
swap the intro's forward reference to `:ref:` against that label.
Same target today, robust against reordering, and readable inline
(Sphinx renders the section title).

Assisted-by: Agent (claude) <ai@blobfish.icu>
The precedence relation existed in three copies (expressions.rst,
integer.rst, boolean.rst); the per-type copies are the divergence trap
since any operator change must land in all three. The per-type pages now
reference the normative table in expressions.rst.

Assisted-by: Agent <ai@blobfish.icu>
… parens note

Two human-review follow-ups on the operator-precedence single-home
refactor:

*   `types/real.rst:55` still routed readers to the integer chapter's
    Operations subsection for "operation and precedence".  Now that
    integer.rst delegates upward instead of hosting the table, this
    is a two-hop indirection where the first hop no longer contains
    what the sentence promises.  Split the sentence: operations
    (semantics — IEEE-754-style behavior, unary rules, C99 remainder)
    still point at `sssec:integer_ops`; precedence and associativity
    point directly at the normative table.

*   The integer chapter's removed precedence paragraph carried a
    useful note that parentheses are absent from the list because
    they override precedence rather than participate in it.  The note
    is normative guidance about the table, not integer-specific, so
    lift it into `expressions.rst` alongside the table itself.

Assisted-by: Agent (claude) <ai@blobfish.icu>
The procedure was fully specified twice: only built_in_functions.rst
had the initial state, only streams.rst had the null-value/position
rule, and the two used different wording. streams.rst error handling is
now normative (codes, initial state, per-type table); built-ins keeps
the signature, marked as notional since input_stream is not a language
type. Also fixes the F 1.0 output that violated the %g rule, the
'characters have no error state' claim contradicted by the state table,
and the undefined 'null value' term.

Assisted-by: Agent <ai@blobfish.icu>
The rule was restated in three files; type_qualifiers.rst is now the
normative home and the misleading 'essentially a no-op' wording is
replaced. The other two sites cross-reference it.

Assisted-by: Agent <ai@blobfish.icu>
…e the last duplicate

Two human-review follow-ups on the const-by-default single-home
refactor:

*   The normative paragraph in `type_qualifiers.rst` now names the
    default-is-const rule explicitly, but the "both spellings are
    legal" corollary was only implicit ("writing ``const`` is
    therefore redundant").  A reader is left to infer that
    ``T x`` and ``const T x`` are exchangeable.  Add a one-sentence
    explicit statement of that equivalence so the normative section
    doesn't rely on the reader's inference.

*   `procedures.rst:9` is the last remaining restatement: "By default
    arguments are ``const`` just like functions."  Its scope
    (parameter-passing) is narrower than the variable-declaration
    rule and it is worth keeping in place for readers landing on the
    procedures chapter, but it should point at the normative home so
    it does not become a fourth divergent copy.  Add a
    `see :ref:sec:typeQualifiers` cross-reference.

Assisted-by: Agent (claude) <ai@blobfish.icu>
The impl chapter (sec:errors) defines the full error taxonomy, but a
dozen spec rules said only 'an error', leaving class and phase to guess.
Each site now names the class from the taxonomy and the first mention in
each file cross-references sec:errors. Also normalizes 'should raise' to
'must raise' at these sites.

Assisted-by: Agent <ai@blobfish.icu>
Two error-class mentions predate the errors-chapter patch but never had
the (see :ref:`sec:errors`) cross-reference the patch introduced at the
first mention in each file. Add the reference and normalize the site's
wording to the "must raise" phrasing used by the patch:

*   typedef.rst: SymbolError for duplicate alias names.  Also fixes a
    literal-role typo (single-backticks would render as an unresolved
    default role) and the missing trailing period.
*   types/array.rst: SizeError for RHS-too-large in an array
    initializer.  This is the first error-class mention in the file
    (line 53); the patch's cross-ref at line 280 was on the second
    mention.

Assisted-by: Agent <ai@blobfish.icu>
Follow-up to the errors-chapter patch.  Three coordinated cleanups, kept
in one commit so the wording change stays local to the sites it touches:

*   Fill omitted error classifications the initial patch missed.  Adds a
    ReturnError classification to the "return reachable by all control
    flows" rule (functions.rst), a GlobalError classification covering
    the non-constexpr / vector-global / non-global-statement bullets
    (globals.rst), a StatementError for a declaration outside the
    leading declaration block of a block statement (declarations.rst),
    a SyntaxError for iterator loops with more than one domain
    (statements.rst iterator loop), and a StatementError for a
    ``continue`` outside a loop (statements.rst continue, mirroring the
    ``break`` rule).

*   Normalize two "should raise" sites in files the patch already
    touched but did not reword: SizeError for matmul dimension mismatch
    (types/matrix.rst) and SizeError for elementwise binop size
    mismatch (types/array.rst).  Also normalizes typedef.rst's inline
    "Should raise a ``SizeError``" callout on the size-mismatch example.

*   Converge every patch insertion (and the two residuals above) on
    "must emit a ``X``" (or the passive "must be emitted") for the
    error-raising rule.  The initial patch used six different verbs
    (issue / raise / yield / cause / is / is to be produced) at
    otherwise identical sites; one verb reads more consistently and
    matches how the errors chapter itself describes the requirement.

Also drops the redundant ``(see :ref:`sec:errors`)`` on
types/array.rst:279 (out-of-bounds indexing) since types/array.rst:53
now carries the first-mention cross-reference for that file.

Assisted-by: Agent (claude) <ai@blobfish.icu>
Four follow-ups on the errors-chapter branch flagged by a human-review
pass across the six spec-review PRs open on this stack:

*   `procedures.rst`: the two "*Gazprea* must emit a ``CallError``"
    sites drifted from the "the compiler must emit …" subject used
    everywhere else the pass touched.  Normalize both to
    "the compiler must emit".

*   `globals.rst`: the summary paragraph re-enumerated the three
    restrictions from the bullet list right above it, and the
    re-enumeration silently rots when the bullets are edited.
    Collapse to one sentence: "Violations of any of the above must
    be reported as a ``GlobalError``." — no drifting second copy.

*   `functions.rst`: the ReturnError classification I added on the
    return-reachable-by-all-paths rule ended with a sentence
    ("Control-flow constructs are assumed to be undecidable, so
    both branches of every conditional are considered reachable.")
    that `impl/errors.rst:104-106` already states as part of the
    normative ``ReturnError`` definition.  Drop it here; the cross-ref
    carries the rule.

*   `declarations.rst`: the StatementError classification used the
    undefined term "leading declaration block".  Nothing else in the
    spec introduces it, and the paragraph immediately above uses "at
    the start of the block".  Reword to "the declaration prefix at
    the start of its enclosing block statement", and lead with the
    compiler as the subject to match the surrounding rules.

Assisted-by: Agent (claude) <ai@blobfish.icu>
Add spec/flags.rst: the precise semantics of -ffast-math (the integer math faults of integer.rst become undefined behavior; no effect on real, which stays IEEE 754) plus the testing policy -- student tests never exercise UB, the flag is reserved for perf stress-tests of already-validated linear algebra, and every test is run against the non-fast-math compiler first to confirm it is UB-free.

Make the type pages normative for their own math errors: errors.rst now defers its MathError conditions to integer.rst (integer faults) and notes that real never raises a MathError (real.rst). The glossary states normatively that Gazprea has no undefined behavior under standard operation, with -ffast-math the single exception, and points at flags.rst. integer.rst and real.rst link to flags.rst.

Assisted-by: Agent (claude) <ai@blobfish.icu>
A global must always be initialized. Unlike a local, a global is never implicitly zero-initialized, so a global declared without an initializer is ill-formed and the compiler must emit a GlobalError; an intended zero value must be written explicitly. Fix the const-vector wording accordingly: an empty global vector now needs an explicit [] initializer rather than being declared without one.

Assisted-by: Agent (claude) <ai@blobfish.icu>
receiver is an array or a scalar -- the result is an array, exactly as in
the examples above. Nothing else about concatenation changes: at least one
operand must still be composite, and the operands must share a common
element type through implicit casts. This is what keeps a string

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought the thing that would make x a string is that it is a string variable and the assignment will implicit cast. I think with that in mind, this receiver formulation is unnecessary.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The way I have it specified in vectors is that vector-ness is never propagated, since the vector is always used as an array in arithmetic expressions. The reciever formulation allows for strings to print normally if the reciever is a string. I have string specified as a true compiler-provided typealias for a char array and that printing is the only difference.

So under my vector rules, if we wanted to assign a string to a variable, it would require a type definition.

Before this rule

var a = "ab" || ['c', 'd'] || "de";

a -> std_output; // prints "[a b c d e]"
"ab" || ['c', 'd'] || "de" -> std_output; // prints "[a b c d e]"

After type resolution, I'm thinking that RHS is treated like a char array for the sake of assignment and expression resolution since a vector is never part of an expression and implicitly two-way castable to a the equivalent array with size fixed at the vector's current length.

Under this rule it becomes unwieldly to print raw characters/strings to the terminal.

I want to do it this way because the vector is not a true 'primitive type' and so the language can have an escape hatch here.

After this rule

var a = "ab" || ['c', 'd'] || "de";

a -> std_output; // prints "[a b c d e]"
"ab" || ['c', 'd'] || "de" -> std_output; // prints "abcde"

Since the reciever dictates the output formatting, then the string prints nicely.

Alternatives

Vector-ness is Propagated Everywhere

We could change it so that vector-ness is propagated through, but then do vectors downcast to arrays in expressions or do arrays upcast and what becomes the rules on size errors via expressions?

I think that answering the size error questions in the context of vectors becomes significantly harder than answering them via arrays like we currently do.

Assignment propagates the Widest Type

So vectors cross the boundary to the assignment rather than being elided to their underlying array.

I am open to suggestions here, but having any single element being a vector and that being propagated changes the implications of expressions always using their underlying array as the arithmetic type; or at least results in a bit of an arbitrary contradiction.

so is an lvalue only when that array is mutable (declared ``var``):
- **In value position** (an :term:`rvalue`) -- as an initializer, on the right
of an assignment, as an argument bound to a ``const`` parameter, or anywhere
an array value is expected -- a slice produces a **fresh, independent array**

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is wrong according to Ron’s intention for slices

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it is, but slices on the LHS are not really slices, they are a 'splat'.

Building subarrays from indexes I think is a natural extension of using slices to begin with. I have defined the definition of new variables from splats as being a truly 'new' variable, so a copy. If you hate this, I can revert, but I have a further defense in my subsequent comment.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think our understanding of the language semantics is aligned, but we disagree on how it is presented. Assigning the result of a slice to a variable produces a copy because assignment always copies.

I think of it like an extension of indexing or Tuple element selection

var arr = [1, 2, 3];
arr[1..2] = [10, 20]; // LHS is a lvalue, the is an rvalue
const x = arr[2..3]; // LHS is an lvalue, the slice is an lvalue
// semantically the same as if I do
const y = arr; // arr is an lvalue, assignment copies the array

the way you explain it also works with this example, but it is more complex to reason about.

Remove the restriction that declarations may appear only at the start of a block; a declaration may now be interleaved freely with the statements around it, which is what constexpr.rst already assumed. Drop the corresponding StatementError wording here and in statements.rst.

Globals remain the exception: their dependency order is now stated as a hard requirement -- a global may reference only globals defined earlier in the file, and a forward reference to a not-yet-defined global is a SymbolError (the name is not yet in scope). declarations.rst cross-references this.

Assisted-by: Agent (claude) <ai@blobfish.icu>
assignment, or bound to a ``var`` reference parameter -- a slice is a **view**
that writes *through* to its backing array. This is the only situation in
which a slice aliases storage, and it requires the backing array to be mutable
(declared ``var``); a slice of a ``const`` array is never an lvalue. The

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be the same error as assignment to const—AssignError, I believe

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is one that I should have flagged, I have been considering mutability part of the type.

I think my argument is grounded in gazprea, since const and var parameters have different semantics and historically the compiler implementations decide mutability at type check.

I think that I would rather remove the assign error in favour of having this and other examples fail type check instead, but I can revert this if that is the vibe.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As long as it is consistent, either option is good for me.


Because a subscript chain names successive axes rather than re-indexing an
intermediate result, writing more index positions than the array has axes is
*not* how one indexes into a slice's result; for that, bind the slice to a

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The same operator now has two different meanings

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

consequence of n-d arrays I think. We could change it so that n-d arrays are instead written and indexed in the form <array>[axis, axis, axis][index], which I think is cleaner, but requires a big change. Could also go the numpy way and just keep the indexing as positional.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The root of the problem I think is that indexing is a rank reducing operation, while slicing is a rank preserving operation. Therefore, indexing chains naturally to specify a position in the nd array as a series of indices, while composing slices just repeatedly slices.

If we were to add multidimensional slices we should go the numpy way, but I think we should keep them out of the spec until we see how students handle/implement slices this year.

Prototypes (forward declarations) remain legal for functions and procedures, but a prototype must be matched by a definition. State in functions.rst and procedures.rst that a function or procedure that is prototyped but never defined is ill-formed and the compiler must emit a DefinitionError -- the taxonomy's existing error for 'declared but not defined' (impl/errors.rst).

Assisted-by: Agent (claude) <ai@blobfish.icu>
Make explicit that a scalar operand of ** broadcasts to a rank-1 array's length, since a rank-1 array has a single (unambiguous) dimension: [1, 2, 3] ** 4 is the dot product [1, 2, 3] ** [4, 4, 4] == 24. Generalize matrix.rst's scalar-broadcast rule from 'square matrices and equal-extent hypercubes' to 'operands whose extents are all equal', which a rank-1 array trivially satisfies.

Assisted-by: Agent (claude) <ai@blobfish.icu>
:ref:`procedure <sec:procedure>`. In an argument position it follows the same
copy-or-view rule as everywhere else, decided by the *parameter* it binds to:

- A slice bound to a ``const`` parameter is passed **by value** -- the callee

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As discussed, saying it is passed by value is misleading—in most implementations it would be passed by reference as usual, but the compiler assures that it is never written to

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe I clarify this in the next sentence or two, that being 'passed by value' is more of a conceptual way to think about const arrays. Also in MLIR this abstraction works, we do actually pass the full 'tensor' or whatever we are using as an array, rather than as a reference. The bufferization pass handles the reference/value semantics if done correctly. Likewise for vars, in mlir everything is pass by value and functionally returned without side effects and the students can abuse this feature to help with some of their analysis, especially since globals are all immutable.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree, your following explanation is good. I think though that the surrounding lvalue/rvalue view/copy language should be removed.

@novo52

novo52 commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

The slice changes are a step back in my opinion. They were good as they were in the last meeting with the exception of the fact that slices cannot be assigned to a variable and thus are never const or var in themselves. That has been correctly removed, but most of the added stuff is not good.

Multidimensional slices breaks a property of array and vector representation. Previously, arrays, vectors, and slices are all represented by {ptr, len} and all operations (indexing in particular) compose in the obvious way. Now, indexing by ranges gives non-obvious behaviour and slices cannot be represented as {ptr, len}.

The copy vs read distinction is not needed. If we assume a slice is always a view that lasts for the duration of an expression, then all properties fall out naturally. A slice ends up copied as an r value not because the slice copies, but because assignment always copies—uniform with other array assignment semantics.

I would recommend reverting the slices commit and just deleting the Const vs var paragraph that implies slices can be assigned to a variable as views.

@novo52

novo52 commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Rather than having both slices and ranges be half open, they should both be closed—this is the way most existing 1-indexed languages treat ranges, as this formulation is more natural for 1-indexing.

Push back if you disagree.

A declaration may appear at **any** point within a block; *Gazprea* does not
require the declarations of a block to be grouped at its start, so a declaration
may be interleaved freely with the statements around it. For instance, this is
legal even though a declaration follows an ordinary statement:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This language should not assume so strongly that students would assume declarations must be grouped. This is ordinary behaviour for a programming language.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, agreed.

A vector declaration vector<T> v = E is resolved into exactly one of two cases by the rank of E, which are mutually exclusive so no tie-break is needed. Single-element: E is a scalar or a same-rank array cast/broadcast to T (a fixed-size element is padded to T's size -- vector<integer[3]> v = [4,5] is [[4,5,0]]); the vector has one element. Multi-element: E has the rank of T[] (one higher), and each element must be implicitly castable to T. Any other rank is a TypeError.

This removes the flat-literal ambiguity: [1,2] for vector<integer[*]> is one element ([[1,2]]) while [[1,2]] is a one-element multi declaration ([[1,2]]). append/push now use the identical single-vs-multi test.

Assisted-by: Agent (claude) <ai@blobfish.icu>
State that structs are nominal -- a struct's identity is the declaration that introduced it, not its field layout -- so two definitions with identical fields (or the same name in different scopes) are distinct types, while a typealias introduces no new type and transparently carries its target's identity. Type names now live in a lexically scoped type namespace: a struct or typealias defined in a function/procedure is local and shadows any outer name of the same kind, and does not leak to global scope.

Allow typealias (including the typealias struct form) in local scopes, reversing the previous global-only rule; a duplicate in the same scope is still a SymbolError, while an inner redefinition shadows. Add a worked example showing which struct comparisons are legal across a global/local S and its alias Pair. Updates typealias.rst, struct.rst, and namespaces.rst.

Assisted-by: Agent (claude) <ai@blobfish.icu>
@Sir-NoChill

Copy link
Copy Markdown
Collaborator Author

I withdraw my claim of having a well-reasoned argument for extending slice semantics, but if that's the case then are we allowed to slice an n-d array? Or should we remove all n-d array capabilities and go back to rank-2 maximum?

@novo52

novo52 commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

I think slicing an nd array is fine, but it only slices the outermost extent, in the natural way assuming slices are 1d.

in the future I think we can extend to numpy like syntax for nd slicing.

@Sir-NoChill

Copy link
Copy Markdown
Collaborator Author

The slice changes are a step back in my opinion. They were good as they were in the last meeting with the exception of the fact that slices cannot be assigned to a variable and thus are never const or var in themselves. That has been correctly removed, but most of the added stuff is not good.

This resolves the slices being assignable, fundamentally a different operation, more like a 'splat' than a slice. Of course we could add a new 'splat' operator for this use case.

Multidimensional slices breaks a property of array and vector representation. Previously, arrays, vectors, and slices are all represented by {ptr, len} and all operations (indexing in particular) compose in the obvious way. Now, indexing by ranges gives non-obvious behaviour and slices cannot be represented as {ptr, len}.

So we disallow slicing on any non-terminal rank, does that mean we can also not slice vectors containing arrays?

My argument for this was that we can slice them, we can even assign to n-d arrays, and it is all still computable via index operations from the (ptr, len) tuple. It actually works nicely if you implement slicing as an indexing modifier rather than an 'operation' in itself, then it just becomes a mapping.

We do not have the issue of this being a reference to the underlying array since slices are not themselves storable, so I'm not sure I see the issue with slicing the n-d arrays here.

The copy vs read distinction is not needed. If we assume a slice is always a view that lasts for the duration of an expression, then all properties fall out naturally. A slice ends up copied as an r value not because the slice copies, but because assignment always copies—uniform with other array assignment semantics.

If we remove the splatting, then yes. I like splats because it makes it easy to build an array out of a different, potentially modified array or vector where we do not necessarily know the element we want, but if removing them seems like the better option then I will cede.

I would recommend reverting the slices commit and just deleting the Const vs var paragraph that implies slices can be assigned to a variable as views.

This should already be deleted, I have it specified that slices can be assigned via the splat which just coincidentally shares view syntax but on the LHS. Could be confusing, so could make a new operator, but also could wholesale delete any mention of slices on the LHS or in expressions, which is the implication. Under deletion slices can only be an R-value and can never be used in an expression context.

@novo52

novo52 commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Would you like to call to discuss the splat stuff?

@rcunrau

rcunrau commented Aug 25, 2026

Copy link
Copy Markdown
Collaborator

I didn't think we ever allowed multi-dimensional slicing, so sorry if I missed that whole discussion. The prime reason I wanted slicing is to allow Vectors to interoperate with arrays. We have concluded that n-d vectors can never interoperate with n-d arrays, so I think there's not much point in trying to figure out n-d slices.

The only thing that comes to mind is that an n-d array is still <pointer, len=N1N2...Nn>, so a slice could still be mapped onto the array. In that sense a slice is range indexing, and I can't remember if we still have that or not.

@rcunrau

rcunrau commented Aug 25, 2026

Copy link
Copy Markdown
Collaborator

WRT splatting, I'm not at all interested in extending the language at this point.

@Sir-NoChill

Copy link
Copy Markdown
Collaborator Author

consider it dead

statements.rst: reset the column counter in the 3x3-square example so it actually prints a square; note that the single-statement post-predicated loop needs unbounded lookahead; make arrays the normative home for the array-valued-index prohibition and cross-reference it here.

array.rst: state the in-bounds range for negative indices (-n..-1) and that a negative *left* slice bound is an IndexError while a negative right bound resolves from the end (including the two-sided i..-j form); change 'most binary operations' to 'every' with ==/!= called out as the sole collapsing exception; add two missing semicolons.

matrix.rst: reorder the scalar-** broadcast paragraph so it no longer splits the m x n dimension rule; state that an empty matrix is 0x0 (rows and columns both 0).

Assisted-by: Agent (claude) <ai@blobfish.icu>
expressions: note that >=3-iterator generators are a future addition and that the SyntaxError is a legitimate post-parse syntactic check. integer: reword 'mandatory -ffast-math' as required-to-be-supported-but-off-by-default. real: use an IEEE-754-exact % example (5.5 % 2.0 == 1.5) and note that real == is bit-exact. character: a \x escape with no hex digit is a LiteralError. boolean: add the non-short-circuit divide-by-zero trap example.

vector: replace the confusing |type| metasyntax with named placeholders; make push(x)/append(x) notation uniform. tuple/struct: a type with fewer than two members is a TypeError; note the t1.1 real-literal lexer pitfall and that tuple index errors are compile-time. string: note that growth needs a var receiver (const string is fixed). comments: unterminated block comment is a SyntaxError. type_casting: integer->character uses the non-negative mod (as<character>(-1) is 0xFF), and an empty-literal cast is a TypeError. typealias: add the missing return 0 to the example.

Assisted-by: Agent (claude) <ai@blobfish.icu>
procedures: clarify that a procedure call is a single-target assignment/declaration RHS and cannot be the source of a tuple-unpacking assignment (bind to one variable, then unpack); name the SyntaxError for the procedure = <stmt> form; state that AliasingError is always compile-time using the conservative same-backing-array rule; fix the indentation of the 'Legal' call block.

declarations: make the self-initializer rule crisp -- a reference in a declaration's own initializer resolves to an enclosing binding if one exists, and is a SymbolError only when none does. constexpr: name GlobalError for a bad global initializer and note that a non-constexpr typealias size is not strictly exercised by the test battery.

Assisted-by: Agent (claude) <ai@blobfish.icu>
… input

State that an end-of-stream character read yields 0xFF (255), not -1, since characters are unsigned bytes, and that a real 0xFF byte is distinguishable from EOF only via stream_state (the reason it exists); make the two remaining -1 mentions and the state table consistent. Add an array-element lvalue example for input (v[2] <- std_input) and cross-reference expressions. Reword the real-input whitespace rule to mean the sign and digits must be contiguous.

Assisted-by: Agent (claude) <ai@blobfish.icu>
Add spec/errors.rst as the normative Errors chapter (the set of error classes and when each must be emitted) and move the sec:errors anchor there, so the ~110 cross-references throughout the spec now resolve within the specification part rather than into the implementation book. impl/errors.rst is retitled 'Errors (Implementation)', re-anchored to sec:errors_impl, and back-references the taxonomy; it keeps the reporting mechanics (CompileTimeExceptions.h, the ANTLR listener, run_time_errors.h, examples, tester rules).

The new page also blesses raising SyntaxError from a post-parse syntactic-validation pass (for >=3-iterator generators, multi-domain iterator loops, and function-argument qualifiers), and widens IndexError to cover tuple field indices (always compile-time, since a tuple index is a literal).

Assisted-by: Agent (claude) <ai@blobfish.icu>
Rename the chapter to 'Built-in Functions, Procedures and Methods' (it also documents the stream_state procedure). Add a Signatures section giving each built-in an equivalent Gazprea signature using an exposition-only [T] type-parameter notation, with a note that type parameters are not part of the language and may be added later. Add a Vector and String Methods section that cross-references the vector method spec and a small table contrasting length(x) (built-in; arrays/vectors/strings) with x.len() (method; vectors/strings only, TypeError on arrays).

Assisted-by: Agent (claude) <ai@blobfish.icu>
The disclaimer said glossary entries are terminology 'not statements of Gazprea semantics', which invited readers to skip entries that actually carry normative rules. Amend it to call out the load-bearing entries (zero value, initialization, re-initialization, domain, value type) as normative and cross-referenced to the chapter that states them in full, and add the missing cross-reference from the zero-value entry to the declarations chapter.

Assisted-by: Agent (claude) <ai@blobfish.icu>
…rity

State explicitly that arrays of rank 3 or more have no size query (length is rank-1, rows/columns rank-2), so their extents are currently unobservable -- a known limitation pending a future 'shape' built-in. Also note that pad-to-longest-row is a property of the nested literal, applying identically to a matrix, an array variable, or a vector of arrays, and that only incremental push/append pads to the first element instead (worked contrast in the vector chapter).

Assisted-by: Agent (claude) <ai@blobfish.icu>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants