Skip to content

feat(aot): generate native-entry Go frame from main/-main (#425 Item 1) - #628

Merged
mparrett merged 13 commits into
mainfrom
wt/425-entry
Aug 11, 2026
Merged

feat(aot): generate native-entry Go frame from main/-main (#425 Item 1)#628
mparrett merged 13 commits into
mainfrom
wt/425-entry

Conversation

@mparrett

@mparrett mparrett commented Jul 24, 2026

Copy link
Copy Markdown
Collaborator

Summary

Ownership (Gloat)

  • Let-Go owns explicitly requested standalone frames (--entry-frame).
  • Gloat keeps owning template/lg-main.go / shared-lib templates (different MainChunk / override-drain semantics).
  • Earlier heads-up: comment — corrected now that emission is opt-in.

Test plan

  • go test ./pkg/rt/ -run 'TestLoadProgramNamespaces|TestRunProgramMainChunk|TestRunExecUnit|TestInvokeProgramEntry'
  • go test ./test/ -run TestRunner/entry_frame
  • go test ./test/e2e/ -run TestLgCompileEntryFrameOptIn (no flag / flag / package parity)
  • ./examples/aot/native-entry/build.shfib.native prints 55 / 5702887 for fib(10)/fib(34)
  • CI green (lint fix pushed in e21014a7; re-run pending)
  • Ingy ack on ownership boundary

@mparrett

mparrett commented Jul 24, 2026

Copy link
Copy Markdown
Collaborator Author

@ingydotnet A note on gloat's let-go backend, prompted by a recent let-go change:

#628 in let-go adds a runtime-only AOT entry frame: boot core, load the program's namespaces, then enter -main natively or fall back to the VM. While checking it wouldn't step on gloat, I saw your two let-go templates template/lg-main.go and template/lg-lib-main.go both carry the same ~20-line boot loop:

decode program.lgb, replay NSOrder skipping MainChunk, and drain rt.ApplyGoOverrides after each ns. And both boot through compiler.NewCompiler + the resolver, which links the whole let-go compiler into the binary.

This PR pulls that loop into three public rt helpers:

Both templates could drop to a couple of lines and shed the compiler/resolver imports, which should shrink those binaries. One gotcha if you adopt it: LoadProgramNamespaces deliberately does not skip MainChunk the way your loop does (a single-ns program needs those vars installed before the native call), so the fallback goes through InvokeProgramEntry rather than a bare NewFrame(unit.MainChunk).

No urgency (the two don't share a code path today and nothing's broken).

  1. lg-compile now emits its own standalone main.go frame. If you ever point it at a gloat build dir, gate one of the two frames so you don't get two func main().
  2. I'm could take a stab at the gloat PR moving both let-go templates onto these helpers if you like.

Let us know your thoughts. Thanks!

@nnunley nnunley left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewed (multi-agent + verification). Implements #425 Item 1; sits at the Go executable shell, no new IR ops, parity unaffected. One item to fix pre-merge:

Undeduplicated blank imports can emit invalid Go. In the entry-frame generation the blanks vector is built by keeping :go-pkg per lowered spec with no distinct. If two inputs resolve to the same Go package (multi-file same-namespace, or a duplicate input), the generated import block gets _ "pkg" twice → duplicate-import compile failure. Wrapping the collected packages in distinct before emitting fixes it.

Robustness nits (safe today — both fall back conservatively): go-func-needs-error? (entry_frame.lg ~L260) defaults to true when a signature spans multiple lines or has non-gofmt spacing (it parses only the first line); go-src-has-func? (~L256) uses exact func <name>( matching, so non-standard formatting forces a VM fallback instead of the direct native call. Both are fine for gofmt'd output.

mparrett added a commit that referenced this pull request Jul 31, 2026
Callers collect one :go-pkg per lowered spec, so a namespace spanning
several files (or an input passed twice) hands the same package back more
than once and the frame emitted a repeated '_ "pkg"' — which Go rejects,
so the generated main.go would not compile.

Dedupe at the emission site rather than in lg-compile: :blank-imports is
a documented public opt, and a repeated import is wrong whoever passed
it, so the invariant belongs with the code that writes the import block.

Reported by nnunley in review of #628.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@mparrett

Copy link
Copy Markdown
Collaborator Author

Fixed the import bug, rebased, and added the lane that would have caught it. Now at 96b64ef9.

Duplicate blank imports (c6ddf549). Confirmed at scripts/lg-compile:200-204: blanks is a keep over every spec collecting (:go-pkg s), and entry_frame.lg emitted one _ "pkg" line per element. I put the distinct at the emission site rather than in lg-compile:blank-imports is a documented opt, and a repeated import is wrong whoever passed it, so the invariant belongs with the code writing the import block. entry_frame_test.lg covers a plain repeat and a repeated entry-import on the not-lowered path, where it is kept rather than filtered. Both fail on the unfixed tree; I checked by reverting the source and re-running.

Nothing compiled a generated frame (96b64ef9). This turned out to be the bigger hole. entry_frame_test.lg asserts on the emitted string, and examples/aot/native-entry/build.sh was referenced by no workflow and no make target. A duplicate import is a go build failure, so no lane could have caught it, and the same gap covers every other way the frame emits well-formed-looking but invalid Go.

The example now runs in the same job as the expensive lowering e2e: lg-compile, embed the .lgb, link, then assert both documented outputs (55 and 5702887). About a second of wall clock from a clean out/, which seemed proportionate next to the determinism harness already in that job. Happy to move it to its own job or a narrower trigger if you'd rather.

The two parser nits — I left both. Your read of the failure modes matches mine, and they're the tolerable ones: an unmatched func <name>( falls back to the VM entry (slower, correct), and an unparseable result list defaults to needs-error? true, which shows up as a Go compile error at the call site rather than wrong runtime behavior. With the new lane, both now fail loudly.

The coupling bothers me more than the parsing. gogen knows the name and result list structurally, renders them to text, and entry-frame re-derives them with includes? and index-of; tightening the string matching would preserve that. Filed #657 to have the lowering hand the entry's shape back as data, keeping the string path as a fallback, which takes both predicates off the critical path. I'd default to doing that when someone's next in gogen's emit path, but happy to pick it up before this merges if you'd prefer.

@nnunley nnunley left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

So, it looks like this should lean on the code I wrote for gogen - there's a lot of direct string generation that could be handled by it, rather than hand-rolling another copy. The gogen code already has name munging support, too, so that might remove some of the duplication.

Comment thread .github/workflows/go.yml Outdated
Comment thread examples/aot/native-entry/build.sh Outdated
Comment thread pkg/rt/core/ir/passes/entry_frame.lg
Comment thread pkg/rt/core/ir/passes/entry_frame.lg
Comment thread pkg/rt/core/ir/passes/entry_frame.lg Outdated
@mparrett

mparrett commented Aug 3, 2026

Copy link
Copy Markdown
Collaborator Author

Thanks for all the great feedback, I will take a look.

mparrett added a commit that referenced this pull request Aug 4, 2026
Callers collect one :go-pkg per lowered spec, so a namespace spanning
several files (or an input passed twice) hands the same package back more
than once and the frame emitted a repeated '_ "pkg"' — which Go rejects,
so the generated main.go would not compile.

Dedupe at the emission site rather than in lg-compile: :blank-imports is
a documented public opt, and a repeated import is wrong whoever passed
it, so the invariant belongs with the code that writes the import block.

Reported by nnunley in review of #628.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@mparrett

mparrett commented Aug 4, 2026

Copy link
Copy Markdown
Collaborator Author

Phase 0 checkpoint — rebased onto current main.

SHA
base (main) 1f281df4
head 1143337a

@mparrett

mparrett commented Aug 4, 2026

Copy link
Copy Markdown
Collaborator Author

Phase 1 checkpoint — lowering metadata is now the source of truth (#657).

Head: 87e8b7b0 (on base 1f281df4)

  • Added lower-ns-to-go-result / lower-all-ns-to-go-result; string-returning wrappers preserved.
  • lg-compile selects the entry by source name from :fns and passes {:fn …} to analyze-entry.
  • Text scanning (go-src-has-func? / go-func-needs-error?) is compat-only — off the critical path.
  • Confirmed: main + -main → frame calls prog.Main__main (summary: Main__main ✓).
  • Tests cover collision, identical-arity, and metadata-over-text.

Next: Phase 2 (gogen AST frame) or Phase 3 (arity rejection / args order / ns-qualified fallback).

@mparrett

mparrett commented Aug 4, 2026

Copy link
Copy Markdown
Collaborator Author

Phase 3 checkpoint — entry/runtime semantics corrected.

Head: e6eedb70

  • 3.1 Accept only [], [argv], [& args]. [first & rest] fails lg-compile with a clear diagnostic before writing output.
  • 3.2 SetCommandLineArgs runs immediately after BootCore, before decode/load.
  • 3.3 InvokeProgramEntry(ec, namespace, name, argv) resolves via LookupNS(ns).LookupLocal(name) only.
  • 3.4 Frame owns defer rt.ShutdownAllPods() (documented; matches lg / lg-runtime).

Verified: entry_frame + InvokeProgramEntry tests, fib.native 55/5702887, arity-reject exit 1 with no output dir.

Next: Phase 2 (gogen AST frame) or Phase 5 (generic E2E matrix). Phase 4 still needs the Gloat ownership decision.

@mparrett

mparrett commented Aug 4, 2026

Copy link
Copy Markdown
Collaborator Author

Follow-up on Phase 1 test hardening + return-hinted entry gap.

Head: 5a9e95c4

  • find-program-entry now unwraps (with-meta …) name/arity via the same ir.build return-hint helpers the lowering pipeline uses. Both (defn -main ^long [] …) and (defn ^long -main [] …) emit a frame (Main ✓).
  • Added lower-ns-to-go-result-contract: promised keys, Main__main collision record, and lower-ns-to-go = (:source result).

Ready for Phase 5 (E2E matrix should include the return-hint cases).

@mparrett

mparrett commented Aug 4, 2026

Copy link
Copy Markdown
Collaborator Author

Phase 4 (partial) — frame emission is opt-in.

Head: b05e7cd0

./lg scripts/lg-compile [--entry-frame] <out-dir> <import-prefix> <file.lg>...
  • Without --entry-frame: packages only (historical behavior). Summary still prints.
  • With --entry-frame: exactly one main.go frame (+ private bridge when needed).
  • Example/CI pass the flag. TestLgCompileEntryFrameOptIn covers no-flag / flag / package parity.
  • Unsupported entry arities are rejected only when --entry-frame is set.

Correction to the earlier Gloat note (comment): gloat does not need a new gate to avoid a duplicate main. Omit --entry-frame and lg-compile will not emit one. Gloat keeps owning its executable and shared-library templates; let-go owns only explicitly requested standalone frames.

Draft note for Ingy is staged locally for a quick prose pass before posting.

@mparrett
mparrett marked this pull request as draft August 4, 2026 02:28
@mparrett

mparrett commented Aug 4, 2026

Copy link
Copy Markdown
Collaborator Author

Status checkpoint on wt/425-entry. nnunley's central ask — replace string emission with gogen — is still open; re-review waits on that.

Since the last round:

  • --entry-frame opt-in: without the flag, lg-compile writes packages only (no main.go, no private bridge). The summary line still reports when a native entry is available.
  • Bridge contract e2e: no flag + private → no frame/bridge; flag + public → one main.go; flag + private → frame + bridge; package output identical excluding the intentional bridge file.
  • Native-entry matrix in CI (TestLgCompileEntryFrameOptIn / TestNativeEntryMatrix): return-hint spellings, typed/no-error call shape, unsupported-arity rejection (flag-only), argv / variadic, collision, namespace-qualified VM fallback, fib spike inputs.

Next on this branch:

  1. Phase 2 — rewrite frame emission onto gogen AST, with the matrix as the regression fence.
  2. Phase 6 — full verification / generated-artifact checks.
  3. Phase 7 — reply on each review thread, update AOT entry frame: recover the lowered signature from gogen as data, not by string-matching rendered Go #657 bookkeeping, then mark ready and ask for re-review.

@mparrett
mparrett marked this pull request as ready for review August 4, 2026 03:38
@mparrett
mparrett requested a review from nnunley August 4, 2026 03:38
@mparrett

mparrett commented Aug 4, 2026

Copy link
Copy Markdown
Collaborator Author

Phase 6/7 complete — ready for re-review.

Phase 6 (local): make build / check-generated / gogen-diff green; go test ./pkg/rt/ ./test/ ./test/e2e/ and go test ./... green; fib example prints 55 / 5702887 once each. Gloat RUN_SLOW_TESTS=1 prove -v test/lg-native-bin.t green against this tip. test/lg-shared-lib.t still fails building the shared lib (rt.LoadProgramNamespaces undefined in Gloat's go.mod pin) — same on sibling let-go main; tracked on gloathub/gloat#7, not a #628 regression.

Phase 7: replied + resolved the five review threads; closed #657 as done here. Lint CI failure on 957652e5 fixed in e21014a7 (staticcheck on the new error string).

@nnunley — requesting re-review.

@mparrett

mparrett commented Aug 4, 2026

Copy link
Copy Markdown
Collaborator Author

CI update on e21014a7: lint is green. The remaining tinygo-wasi-build failure is wasmtime: command not found on the runner (exit 127) — unrelated to this PR; same workflow is green on main.

@mparrett

mparrett commented Aug 4, 2026

Copy link
Copy Markdown
Collaborator Author

@ingydotnet follow-up on the earlier note about #628 and gloat's templates.

Short version of the boundary I think we want: Gloat owns Gloat's frames; Let-Go owns explicitly requested standalone frames.

lg-compile can emit a standalone native-entry main.go, but emission is now explicit opt-in:

./lg scripts/lg-compile --entry-frame <out-dir> <import-prefix> <file.lg>...

Without --entry-frame, lg-compile writes only lowered packages — the historical behavior. The Item 3 summary line still prints either way.

So:

  • Standalone let-go example / CI passes --entry-frame; let-go owns that frame.
  • Gloat omits the flag and keeps owning template/lg-main.go and the shared-library template. Those frames have different semantics (skip the driver MainChunk while draining native overrides; shared-lib support), so they should not be forced onto let-go's standalone shape.

Earlier I suggested gating one of the two frames if gloat pointed lg-compile at its build dir. With opt-in, that concern goes away: gloat needs no new flag to keep today's package-only path — just don't pass --entry-frame.

Does that match how you want the boundary drawn for now? Longer term I'm curious whether gloat would ever want to consume a structured let-go manifest/frame, or prefer to keep its specialized drivers.

mparrett added a commit that referenced this pull request Aug 5, 2026
installHttpNS is registered from an init(), so net/http — and crypto/tls and
crypto/x509 behind it — is reachable in every binary linking pkg/rt, whether or
not the program ever opens a socket. The linker has no way to see it is unused,
which is part of why an AOT hello-world and an AOT fib(35) differ by 16 bytes.

Build with -tags lg_no_http to leave it out. On darwin/arm64, cmd/lg-runtime
goes 17,594,402 -> 12,175,106 bytes, and 12,131,874 -> 8,382,754 with -s -w:
about 30% either way, and `go tool nm` finds zero net/http or crypto symbols in
the tagged binary. That is more than the ~1.9 MB the issue estimated from symbol
sizes, because the symbol table undercounts what a package drags in.

TinyGo's net/http does not compile, so that lane has always built without the
namespace and gains nothing new here — but it already had exactly the stub this
tag needs, so http_tinygo.go becomes http_stub.go and serves both lanes off
`tinygo || lg_no_http`. One stub, two reasons.

This is the http half of #652 only. The other tagged tree named there,
go/parser and go/printer via pkg/rt/gogen.go, is left alone because #628 and
#657 are both in flight on gogen.

The no-http-build CI job exists because nothing in the default build compiles
http_stub.go; without it a signature change in http.go would break the tagged
lane silently. It also asserts net/http stayed out, so the size win cannot
regress unnoticed.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>

@nooga nooga left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

LGTM. This is a well-executed, well-tested feature — checked out the branch and ran it for real: built and ran examples/aot/native-entry end to end (fib.native → 55, fib.native x → 5702887), inspected the generated main.go (clean, correct — boot → args → decode → load namespaces → argv-box → prog.Main → error-checked exit). test/e2e/native_entry_matrix_test.go is genuine E2E across 17 cases (public/private -main/main, argv/variadic adaptation, arity rejection, main+-main collision, error propagation and exit codes, both return-hint spellings) — actually builds and executes each binary, not just structural checks on generated source.

Verified the structural concern from nnunley's review is properly addressed: frame emission now goes through gogen AST constructors + go/format, not string templates, and entry selection is driven by structured :fns metadata rather than text-scanning; the old text-scan predicates are an explicit off-critical-path fallback.

On the tinygo-wasi-build CI failure: pulled the actual job log rather than taking the "unrelated infra" claim on faith — confirmed it's a wasmtime-installer flake (curl choking on an unresolved {} version placeholder in the download URL), and confirmed the identical step succeeded cleanly on the next main CI run the same day. Genuinely unrelated to this PR.

Two minor, non-blocking notes:

  • Generated main.go's import block isn't goimports-grouped (stdlib/third-party/local all together) — compiles fine, cosmetic.
  • A multi-arity (defn -main ([] …) ([x] …)) silently isn't recognized as an entry at all (falls through with no diagnostic) rather than erroring — probably fine given multi-arity -main is unusual, but worth a doc note if anyone hits it.

One mechanical thing before merge: mergeable: CONFLICTING is, same as the other PRs in this batch, just the generated.sums digest — main has moved ~9 commits since the last rebase. Zero real conflicts locally via the merge=sums driver; needs a git rebase origin/main && make generate push to clear it on GitHub's side.

Approving.

mparrett added a commit that referenced this pull request Aug 5, 2026
Callers collect one :go-pkg per lowered spec, so a namespace spanning
several files (or an input passed twice) hands the same package back more
than once and the frame emitted a repeated '_ "pkg"' — which Go rejects,
so the generated main.go would not compile.

Dedupe at the emission site rather than in lg-compile: :blank-imports is
a documented public opt, and a repeated import is wrong whoever passed
it, so the invariant belongs with the code that writes the import block.

Reported by nnunley in review of #628.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@mparrett

mparrett commented Aug 6, 2026

Copy link
Copy Markdown
Collaborator Author

@nooga Thanks for the approval. FYI: addressed the two non-blocking suggestions in #692

mparrett and others added 13 commits August 8, 2026 17:24
lg-compile now emits a BootCore-based main.go when a recognized entry
lowers, so typed AOT binaries reach native code instead of silently
running at VM speed. Includes LoadProgramNamespaces drain, fib demo,
and the Item 3 summary line.

Co-authored-by: Cursor <cursoragent@cursor.com>
…e, RunExecUnit guard

Review response on the #425 native-entry frame:

- entry-frame no longer reimplements the Go-name munge: entry-go-name now
  calls the (newly public) lower-go/go-name for private entries, so the
  munge — including reserved-word handling — stays single-sourced with the
  lowered emitter. Drops the duplicate private-go-name.
- emit-vm-fallback shapes argv to the entry's arity: [] invokes with no
  args, [argv] passes a single string vector, [& args] spreads os.Args.
  Previously it always spread, mis-invoking non-variadic entries.
- emit-entry-frame-go keeps the entry package blank-imported when the entry
  itself did not lower, so sibling lowered fns still register init()
  overrides. Also drops the now-dead `_ = ec` (fallback uses ec).
- RunProgramMainChunk simplified; InvokeProgramEntry's registry fallback now
  iterates namespaces in sorted order for deterministic resolution.
- count-lowered-funcs actually excludes __gogen* wrappers and init() now
  (the summary count was inflated); fixed the docstring and a stray tab.
- New tests: TestRunExecUnitReplayOrderAndMainOnce pins the replay-order and
  main-runs-exactly-once contract (incl. the MainChunk-aliases-a-namespace
  case) that the refactor onto LoadProgramNamespaces relies on; .lg tests
  cover the arity-shaped fallback and retained blank import.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Callers collect one :go-pkg per lowered spec, so a namespace spanning
several files (or an input passed twice) hands the same package back more
than once and the frame emitted a repeated '_ "pkg"' — which Go rejects,
so the generated main.go would not compile.

Dedupe at the emission site rather than in lg-compile: :blank-imports is
a documented public opt, and a repeated import is wrong whoever passed
it, so the invariant belongs with the code that writes the import block.

Reported by nnunley in review of #628.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Nothing compiled a generated entry frame. test/entry_frame_test.lg
asserts on the emitted string, and examples/aot/native-entry/build.sh
was referenced by no workflow and no make target — so a frame that
emitted invalid Go passed every gate and shipped green. The duplicate
blank import fixed in the previous commit is exactly that class: a
go build failure that no lane ran.

Runs lg-compile, embeds the .lgb, links the binary, and asserts both
documented outputs (fib 10 = 55, fib 34 = 5702887). About a second of
wall clock from a clean out/, next to the ~60-120s determinism harness
in the same job.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The sums merge driver wrote a stale digest during rebase; make build /
lgbgen recomputed the correct source digest for the rebased tree.

Co-authored-by: Cursor <cursoragent@cursor.com>
Entry-frame was recovering go-name / needs-error? by scanning rendered
Go, which misses collision-resolved names (main + -main → Main vs
Main__main) and couples the frame to gogen formatting. lower-ns-to-go-
result now hands back per-fn records; lg-compile selects the entry by
source name and passes that record to analyze-entry. Text scanning
remains only as a compat fallback (#657).

Co-authored-by: Cursor <cursoragent@cursor.com>
Publish *command-line-args* immediately after BootCore so top-level
forms see the real argv; reject [fixed & rest] with a source diagnostic
instead of emitting uncompilable Go; resolve VM fallback via the
selected namespace only; and own defer ShutdownAllPods in the standalone
frame (same contract as lg / lg-runtime).

Co-authored-by: Cursor <cursoragent@cursor.com>
find-program-entry missed `(defn -main ^long [] …)` and
`(defn ^long -main [] …)` because it required a bare symbol name and
vector? binding — the reader reifies those as with-meta forms. Unwrap
with the same ir.build return-hint helpers the lowering pipeline uses
(#598), and add a direct lower-ns-to-go-result contract test for shape,
Main__main collision metadata, and the string-wrapper equality.

Co-authored-by: Cursor <cursoragent@cursor.com>
lg-compile historically emitted only lowered packages. Emitting main.go
by default would collide with orchestrators such as Gloat that own their
own executable and shared-library frames (and that skip MainChunk while
draining overrides). Frame emission is now explicit via --entry-frame;
package-only mode and the summary line remain the default. Example/CI
pass the flag; e2e covers no-flag / with-flag / package parity.

Co-authored-by: Cursor <cursoragent@cursor.com>
Pin private/public bridge emission and package parity under --entry-frame,
omit unused pkg/vm imports for typed no-error [] frames, and replace the
fib-only CI lane with a full go-build/run matrix (return hints, arity
rejection, argv, collision, fallback).

Co-authored-by: Cursor <cursoragent@cursor.com>
Replace hand-built Go string templates in entry_frame with gogen
constructors (plus defer/slice/variadic-call/range/go-directive helpers)
so the frame and private bridge render through go/format. Matrix and
constructor unit tests fence the rewrite.

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
mparrett added a commit that referenced this pull request Aug 9, 2026
Two non-blocking notes from the #628 review.

A multi-arity `(defn -main ([] …) ([x] …))` had no single binding vector to
classify, so parse-entry-form returned nil — which reads as "no entry in this
namespace" and quietly emitted the VM fallback frame. It now parses as an
entry with an :unsupported shape, so the existing entry-arity-error path
reports it and lg-compile exits non-zero, same as [first & rest].

The frame's import block was one undifferentiated group. gogen renders through
a FileSet whose positions are densed so output stays a pure function of the AST
(renderFset), and that pass also drops any blank line the printer would emit —
so the goimports group break is restored on the rendered text instead, local to
this frame rather than by loosening the renderer's determinism invariant.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@mparrett
mparrett merged commit f3ca5f9 into main Aug 11, 2026
19 checks passed
@mparrett
mparrett deleted the wt/425-entry branch August 11, 2026 14:50
mparrett added a commit that referenced this pull request Aug 11, 2026
Two non-blocking notes from the #628 review.

A multi-arity `(defn -main ([] …) ([x] …))` had no single binding vector to
classify, so parse-entry-form returned nil — which reads as "no entry in this
namespace" and quietly emitted the VM fallback frame. It now parses as an
entry with an :unsupported shape, so the existing entry-arity-error path
reports it and lg-compile exits non-zero, same as [first & rest].

The frame's import block was one undifferentiated group. gogen renders through
a FileSet whose positions are densed so output stays a pure function of the AST
(renderFset), and that pass also drops any blank line the printer would emit —
so the goimports group break is restored on the rendered text instead, local to
this frame rather than by loosening the renderer's determinism invariant.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
mparrett added a commit that referenced this pull request Aug 11, 2026
Two non-blocking notes from the #628 review.

A multi-arity `(defn -main ([] …) ([x] …))` had no single binding vector to
classify, so parse-entry-form returned nil — which reads as "no entry in this
namespace" and quietly emitted the VM fallback frame. It now parses as an
entry with an :unsupported shape, so the existing entry-arity-error path
reports it and lg-compile exits non-zero, same as [first & rest].

The frame's import block was one undifferentiated group. gogen renders through
a FileSet whose positions are densed so output stays a pure function of the AST
(renderFset), and that pass also drops any blank line the printer would emit —
so the goimports group break is restored on the rendered text instead, local to
this frame rather than by loosening the renderer's determinism invariant.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
mparrett added a commit that referenced this pull request Aug 13, 2026
Two non-blocking notes from the #628 review.

A multi-arity `(defn -main ([] …) ([x] …))` had no single binding vector to
classify, so parse-entry-form returned nil — which reads as "no entry in this
namespace" and quietly emitted the VM fallback frame. It now parses as an
entry with an :unsupported shape, so the existing entry-arity-error path
reports it and lg-compile exits non-zero, same as [first & rest].

The frame's import block was one undifferentiated group. gogen renders through
a FileSet whose positions are densed so output stays a pure function of the AST
(renderFset), and that pass also drops any blank line the printer would emit —
so the goimports group break is restored on the rendered text instead, local to
this frame rather than by loosening the renderer's determinism invariant.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
mparrett added a commit that referenced this pull request Aug 13, 2026
Two non-blocking notes from the #628 review.

A multi-arity `(defn -main ([] …) ([x] …))` had no single binding vector to
classify, so parse-entry-form returned nil — which reads as "no entry in this
namespace" and quietly emitted the VM fallback frame. It now parses as an
entry with an :unsupported shape, so the existing entry-arity-error path
reports it and lg-compile exits non-zero, same as [first & rest].

The frame's import block was one undifferentiated group. gogen renders through
a FileSet whose positions are densed so output stays a pure function of the AST
(renderFset), and that pass also drops any blank line the printer would emit —
so the goimports group break is restored on the rendered text instead, local to
this frame rather than by loosening the renderer's determinism invariant.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
mparrett added a commit that referenced this pull request Aug 17, 2026
Two non-blocking notes from the #628 review.

A multi-arity `(defn -main ([] …) ([x] …))` had no single binding vector to
classify, so parse-entry-form returned nil — which reads as "no entry in this
namespace" and quietly emitted the VM fallback frame. It now parses as an
entry with an :unsupported shape, so the existing entry-arity-error path
reports it and lg-compile exits non-zero, same as [first & rest].

The frame's import block was one undifferentiated group. gogen renders through
a FileSet whose positions are densed so output stays a pure function of the AST
(renderFset), and that pass also drops any blank line the printer would emit —
so the goimports group break is restored on the rendered text instead, local to
this frame rather than by loosening the renderer's determinism invariant.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.

AOT entry frame: recover the lowered signature from gogen as data, not by string-matching rendered Go

3 participants