Skip to content

fix: make parentheses open a nested indentation context - #283

Merged
konard merged 10 commits into
mainfrom
issue-282-c154325b4497
Aug 20, 2026
Merged

fix: make parentheses open a nested indentation context#283
konard merged 10 commits into
mainfrom
issue-282-c154325b4497

Conversation

@konard

@konard konard commented Aug 20, 2026

Copy link
Copy Markdown
Member

Fixes #282.

Problem

Indentation was structural at the root but ignored inside ( ), so a
parenthesised group collapsed into one flat list of references and could not
express nested records.

Reproduction (before this PR):

$ node experiments/issue-282/repro.js
--- INPUT ---
array (
  a
    b
  c
    d
)
--- OUTPUT ---
  (array (a b c d))        # the four root links collapsed into one flat list

--- INPUT ---
value (
  id "1"
  label "one"
)
--- OUTPUT ---
  (value (id 1 label one)) # the record lost its boundaries

At the root the same four lines yield four links — (a), (a b), (c),
(c d) — so the two contexts disagreed.

Solution

( now opens a nested context: the indentation stack and base indentation
are saved and reset, the body is parsed by the same document/links production
the root uses, and ) restores the enclosing context. Inside a group the
closing parenthesis ends the last line, just as a line break does at the root.

The body is flattened the same way the root document is. A body that yields a
single link collapses to that link, so (a b c) and (a: b c) are unchanged.
The one exception is a body that is itself a single parenthesised group, which
keeps ((a b)) distinct from (a b).

Blank lines no longer end a block, at the root and inside parentheses alike.

After this PR:

array (          ─▶  (array ((a) ((a) (b)) (c) ((c) (d))))
  a
    b
  c
    d
)

value (          ─▶  (value ((id 1) (label one)))
  id "1"
  label "one"
)

value (                       ─▶  (value ((id 1 label one) (id 2 label two)))
  id "1" label "one"
  id "2" label "two"
)

Changes

All six implementations were changed the same way:

Language Files
JavaScript js/src/grammar.pegjs, js/src/parser-generated.js, js/src/Parser.js
Python python/links_notation/parser.py
Go go/parser.go
Java java/src/main/java/io/github/linkfoundation/linksnotation/Parser.java
C# csharp/Link.Foundation.Links.Notation/Parser.peg, LinksGroup.cs, ILinksGroupListExtensions.cs
Rust rust/links-notation/src/parser.rs, rust/links-notation/src/lib.rs

Each implementation marks links that came from parentheses (nested key in
JS/Python/Go/Java, LinksGroup.IsParenthesized in C#, Link.nested in Rust)
so the single-link collapse can spare ((a b)).

Docs updated to match the parsers: multiline_link, multiline_value_link and
multiline_values are replaced by nested_group and nested_group_body, eol
also matches the end of a nested group, and ENTER_NESTED_CONTEXT /
EXIT_NESTED_CONTEXT were added to the indentation semantic actions
(docs/grammar/links-notation.ebnf, GRAMMAR.md, grammar.lino,
syntax-diagrams.md).

Tests

A reproducing test suite was added for every implementation, covering the exact
cases from the issue plus record boundaries, deep nesting, the indented-id
syntax inside parentheses, blank lines inside parentheses, and the unchanged
single-line forms:

  • js/tests/NestedIndentation.test.js
  • python/tests/test_nested_indentation.py
  • go/nested_indentation_test.go
  • java/src/test/java/io/github/linkfoundation/linksnotation/NestedIndentationTest.java
  • csharp/Link.Foundation.Links.Notation.Tests/NestedIndentationTests.cs
  • rust/links-notation/tests/nested_indentation_tests.rs

experiments/issue-282/repro.js reproduces the issue directly.

Full suites, run locally on this branch:

Language Result
JavaScript 195 passed
Python 183 passed, 1 skipped
Go all passed (gofmt, go vet clean)
Java 124 passed (spotless:check clean)
C# 187 passed
Rust 294 passed (cargo fmt --check, cargo clippy -D warnings clean)

Release

Package versions bumped to 0.14.0 (js/package.json, python/pyproject.toml,
rust/links-notation/Cargo.toml, go/VERSION, the C# VersionPrefix) and the
Java artifact from 0.1.0 to 0.2.0, so the release workflows publish the fix.
CHANGELOG.md records the change under [Unreleased].

Known divergence (pre-existing, out of scope)

Rust's single_line_value_link collapses a lone reference, so a document
containing just a formats as a where the other implementations produce
(a); the same shows in (a)a and (((a)))((a)). This predates
this PR — removing the collapse breaks 49 existing Rust tests — so it is
documented in rust/links-notation/tests/nested_indentation_tests.rs rather
than changed here.

Adding .gitkeep for PR creation (default mode).
This file will be removed when the task is complete.

Issue: #282
@konard konard self-assigned this Aug 20, 2026
konard added 8 commits August 20, 2026 03:44
A parenthesised group used to parse its body as a flat, whitespace separated
list of values, so line breaks and indentation inside ( ) were ignored and
records lost their boundaries.

The group now opens a nested context that starts fresh at indentation level
zero and is parsed by the same links rule as the root of the document, so
'array (a\n  b\nc\n  d)' yields the same links the root produces, nested
under 'array'. A group holding a single link keeps that link, unless the link
is itself a parenthesised group, so '(a b c)' and '((a b))' are unchanged.
The content of a parenthesized group is now parsed as a document of its own,
starting fresh at indentation level zero, so line breaks and indentation mean
the same thing inside ( ) as they do at the root.

Quoted strings are skipped with the same multi-quote rules everywhere, which
also makes a colon inside a group stop being mistaken for a top level id
separator and keeps '(a b) (c d)' two groups instead of one.
The content of a parenthesized group is now parsed as a document of its own,
starting fresh at indentation level zero, so line breaks and indentation mean
the same thing inside ( ) as they do at the root.

Quoted strings are skipped with the same multi-quote rules everywhere, which
also makes a colon inside a group stop being mistaken for a top level id
separator and keeps '(a b) (c d)' two groups instead of one.
Parentheses now start a fresh context at indentation level zero and are
parsed by the same document rules as the root, so nested records keep
their boundaries instead of collapsing into one flat list.

Refs #282
Parentheses now start a fresh context at indentation level zero and are
parsed by the same document rules as the root, so nested records keep
their boundaries instead of collapsing into one flat list.

Refs #282
Parentheses now open a nested context that starts fresh at indentation
level zero and is parsed with the same rules as the root document, so
indented records inside `( )` keep their boundaries instead of
collapsing into one flat list.

Blank lines are skipped between lines as well, at the root and inside
parentheses, which keeps documents such as multi-line quoted references
parseable.

Fixes part of #282
Update the grammar specification to match the parsers: parentheses now open
a nested context, so multiline_link, multiline_value_link and multiline_values
are replaced by nested_group and nested_group_body, eol also matches the end
of a nested group, and ENTER_NESTED_CONTEXT/EXIT_NESTED_CONTEXT are added to
the indentation semantic actions.

Bump the package versions so the release workflows publish the fix.
@konard konard changed the title [WIP] Indentation is structural at the root but ignored inside ( ), so parenthesised groups cannot express nested records fix: make parentheses open a nested indentation context Aug 20, 2026
@konard
konard marked this pull request as ready for review August 20, 2026 04:34
@konard

konard commented Aug 20, 2026

Copy link
Copy Markdown
Member Author

Working session summary

The work is complete. PR #283 is ready for review with all CI green.

#283

What was done

Root cause: ( ) was parsed by a flat "multiline values" production that used a newline-spanning whitespace rule, and indentation state was global rather than per-context. So a group collapsed into one flat list: array (\n a\n b\n c\n d\n)(array (a b c d)).

Fix: ( now opens a nested context — the indentation stack and base indentation are saved and reset to zero, the body is parsed by the same links/document production the root uses, and ) restores the enclosing context. Inside a group, ) also ends the last line the way a line break does at the root. Blank lines no longer end a block.

Result: (array ((a) ((a) (b)) (c) ((c) (d)))) — the same four links the root produces, nested under array. Records keep their boundaries: value (\n id "1"\n label "one"\n)(value ((id 1) (label one))).

Single-line forms are unchanged because a body yielding one link collapses to that link — except when the body is itself a parenthesised group, which keeps ((a b)) distinct from (a b).

Scope: all six implementations (JS/Peggy, Python, Go, Java, C#/Pegasus, Rust/nom), each with a reproducing test file, plus experiments/issue-282/repro.js.

Language Tests
JavaScript 195 passed
Python 183 passed, 1 skipped
Go all passed (gofmt, vet clean)
Java 124 passed (spotless clean)
C# 187 passed
Rust 294 passed (fmt, clippy -D warnings clean)

This session finished the release prep: grammar docs rewritten (multiline_link/multiline_value_link/multiline_valuesnested_group/nested_group_body, eol extended, ENTER_NESTED_CONTEXT/EXIT_NESTED_CONTEXT added) across links-notation.ebnf, GRAMMAR.md, grammar.lino and syntax-diagrams.md; CHANGELOG.md updated; versions bumped to 0.14.0 (Java 0.1.0 → 0.2.0, which uses its own scheme); the .gitkeep placeholder removed.

Two things worth flagging:

  • Rust divergence, pre-existing and left alone: single_line_value_link collapses a lone reference, so a document of just a formats as a where the others give (a). Removing the collapse broke 49 existing Rust tests, so it is documented in rust/links-notation/tests/nested_indentation_tests.rs rather than changed.
  • A docs claim I did not fix: GRAMMAR.md says indented nesting and inline nesting "produce equivalent structures", but outer:\n inner:\n a\n b\n c yields (outer: inner c) — grandchildren are dropped. I verified this is identical on main, so it is unrelated to this issue and out of scope; the group body now reproduces it faithfully, which is what Indentation is structural at the root but ignored inside ( ), so parenthesised groups cannot express nested records #282 asked for.

This summary was automatically extracted from the AI working session output.

@konard

konard commented Aug 20, 2026

Copy link
Copy Markdown
Member Author

🤖 Solution Draft Log

This log file contains the complete execution trace of the AI solution draft process.

💰 Cost: $20.177393

📊 Context and tokens usage:

Claude Opus 5: (5 sub-sessions)

  1. 114.2K / 1M (11%) input tokens, 41.8K / 128K (33%) output tokens
  2. 117.0K / 1M (12%) input tokens, 35.3K / 128K (28%) output tokens
  3. 114.5K / 1M (11%) input tokens, 36.1K / 128K (28%) output tokens
  4. 116.7K / 1M (12%) input tokens, 41.3K / 128K (32%) output tokens
  5. 109.6K / 1M (11%) input tokens, 30.5K / 128K (24%) output tokens

Total: (12.6K new + 471.8K cache writes + 19.2M cache reads) input tokens, 232.8K output tokens, $20.177393 cost

🤖 Models used:

  • Tool: Anthropic Claude Code
  • Requested: opus (claude-opus-5)
  • Thinking level: high (~23999 tokens)
  • Model: Claude Opus 5 (claude-opus-5)

📎 Log file uploaded as Gist (6677KB)


Now working session is ended, feel free to review and add any feedback on the solution draft.

@konard

konard commented Aug 20, 2026

Copy link
Copy Markdown
Member Author

✅ Ready to merge

This pull request is now ready to be merged:

  • All CI checks have passed
  • No merge conflicts
  • No pending changes

Monitored by hive-mind with --auto-restart-until-mergeable flag

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.

Indentation is structural at the root but ignored inside ( ), so parenthesised groups cannot express nested records

1 participant