feat(reader): support registered raw Go fragments - #768
Conversation
f1b6318 to
d0194ea
Compare
|
Reviewed as the child-only delta against #770 ( The lexer balances correctly in every case I tested: nested braces, braces inside interpreted strings, raw strings, rune literals ( One blocking item, in the skip path rather than the lexer. Blocking: raw readers are not dispatched for a nested
|
| payload in a dead branch | branch top level | nested one level |
|---|---|---|
#go [1] (non-brace) |
error | 7 |
#go{s := `}`} (well-formed) |
7 |
error |
Both rows are the same missing dispatch decision in skipReaderForm: at top level the raw reader runs when it should not, and nested it does not run when it should. The same source text means different things depending on nesting depth, in both directions.
That matters for the fix. Making the skipper recurse on # handles the second row and leaves the first, so a decision about non-brace payloads is still needed. The cleanest pairing is to have the built-in #go claim the tag only when the payload opens with {. Falling back to skipReaderForm when a raw reader errors during skipping also works once recursion is in place, but on its own it makes top-level and nested agree while leaving well-formed nested fragments broken.
Running a raw reader while skipping is unavoidable in general: a raw payload is not a Lisp form, so a generic skipper cannot know where it ends. The design is forced; only the failure mode is a choice. Whichever way it goes, a semantic change to reader conditionals belongs in docs/guide/custom-data-readers.md and the PR body, not only in a test assertion.
Smaller items
The side-effect-free contract is on the wrong symbol. It sits at pkg/compiler/tagged_reader.go:76 on TaggedRawReader, but RegisterRaw at pkg/compiler/tagged_reader.go:129 is what someone registering a reader reads. Repeat it there: a registered raw reader runs for branches the program never selects.
The skip path swallows an error the read path treats as fatal. pkg/compiler/reader.go:1571 discards the resolver error with _, present, _ := r.dataReaderResolver(tag, false). A malformed *data-readers* errors when a form is read and falls through to the default raw #go when a form is skipped.
The summary's "propagate malformed/truncated input" is broader than what landed. skipReaderForm gained an error return, but every r.next() failure inside it still becomes return nil. The only new propagation comes from raw readers. Preserving the existing leniency is defensible; the description claims more than the diff does.
Errors never name the tag. The nested failures above surface as reading reader conditional key, at a line and column inside the Go fragment. Nothing points at #go or the raw reader.
Multi-line fragments carry their newlines. #go{⏎…⏎}, which is how anyone would format a multi-line fragment, yields a leading and trailing \n. Fine for display, a silent mismatch for byte-exact fixtures. State the verbatim-including-newlines rule in the guide.
On the #766 connection
Nothing consumes the payload yet: ReadRawGoFragment appears only at its own definition and the default registration, with no emission path on this branch. I also checked the nearest real consumer — the native-entry AST gate reads its expectations from .goexpect.json through encoding/json, so the let-go reader is never involved, and #go cannot reach it without a fixture-format change this PR does not make.
The capability is there for when a consumer arrives. All six Go fragments in the #767 fixtures round-trip byte-exact through #go{...}, and rebuilding conditional_closure as a .lg spec reproduced its functionFragment byte-identically at 284 bytes. The value is real; the PR body currently reads as though the connection is live. "Step one" would be more accurate.
Direction, for whenever a consumer lands
If the eventual answer to a lowering bug becomes "drop to #go{}", that routes around the miscompiler and around the machinery that catches it. #766 surfaced only because go build rejected a stranded temporary; the sibling function of the same shape compiled clean and was silently wrong. Raw Go fragments bypass the invariants the native-entry gate and the parity harness exist to check.
There is also a contract question. Policy §1 requires the standalone interpreter to work with no Go toolchain, and §3 forbids making the VM depend on the AOT path. Today #go{} is an inert string under both engines, so both hold. Once a consumer makes it live, the same source means different things per engine — the divergence parity-full exists to catch, and per #769 that gate is not in CI yet. Cheaper to shape now than after the consumer exists.
Verification
On the PR head: go build ./... clean, go test ./pkg/compiler -count=1 ok, go test -short -count=1 ./... green, make check-generated OK, make lint 0 issues. The reader-conditional behavior above is measured, not read off the diff.
|
@nnunley — this went
Nothing else collides. Your docs-index row and the
|
|
Holding this until #770 lands — the branch is #770 plus one commit, so the six open points there apply here unchanged. Separately, |
d0194ea to
1330c47
Compare
|
Restacked onto the new #770 head ( Three source files conflicted with the #770 review fix and were merged by intent: Two test reconciliations worth a reviewer's eye:
Evidence on the new head: |
mparrett
left a comment
There was a problem hiding this comment.
Reviewed the child-only delta cc6ab3a..1330c47. Four blocking findings remain:
-
Nested raw tags are not dispatched while skipping an unselected reader-conditional collection. The collection branch of skipReaderForm uses a flat delimiter counter, so delimiters inside Go raw strings, runes, or comments alter Lisp depth. A disposable regression using a nested, well-formed #go fragment with two right-parens inside a Go raw string fails at review.lg:1:24 with reading reader conditional key instead of returning the :default value. The top-level raw-tag skip path works; the nested path needs to recurse/dispatch raw readers too.
-
Truncated built-in #go input loses EOF causality. For #go{if ready {, the reader reports unterminated #go fragment but api.IsIncomplete(err) is false. That prevents REPL/API callers from prompting for the rest of a multiline form and is inconsistent with the incomplete tagged-literal contract fixed in #770. Preserve/wrap io.EOF for truncated raw input and add a public-API regression.
-
The committed quick_wins assertion is red locally and in CI. The reader byte-preserves if ready {return "}"}, while the test expects if ready { return "}" }. Either restore matching fixture whitespace or update the expectation; the current build job fails here.
-
go vet ./pkg/compiler fails because TaggedRawInput.ReadRune() has the well-known method name with a nonstandard signature. It reports both the interface and implementation and expects (rune, int, error). Rename the method (for example NextRune) or conform to io.RuneReader before exporting this API.
The raw lexer cases themselves and the focused compiler/race suites pass, and the worktree is clean. #770s separately reported require-propagation issue is inherited by the stack and is intentionally not counted again here.
Stack
Depends on #770. Review the child-only delta at:
nnunley/let-go@feat/custom-data-readers...feat/raw-go-fragment-reader
Because both heads live on a fork, GitHub's upstream PR view includes #770 until the base PR merges; the branch itself is a two-commit stack.
Summary
TaggedRawInput#go{...}reader, preserving Go source while ignoring braces in strings, runes, raw strings, and comments#gohandlerdocs/guide/custom-data-readers.mdwith the raw Go reader contract and embedding APIValidation
go test ./pkg/compiler -count=1— 77 passedgo test -race— 21 passed.lgreader suites — passedgo test -short -count=1 ./...— 1,813 passedmake lint— 0 issuesmake check-generatedjj pushpre-push suite — passed; exact head receipt finalized after GitHub propagationmake bench-ratchetwas run under the exclusive benchmark baton. It reports the known forward-baseline mismatch on currentmain(BenchmarkInitFromLGB26,331 vs 7,028 allocs/op plus existing IR deltas); the reader stack does not change those benchmarked initialization/lowering paths.