Skip to content

WIP: gnodev native loader#5604

Draft
gfanton wants to merge 50 commits intognolang:masterfrom
gfanton:feat/gnodev-native-loader
Draft

WIP: gnodev native loader#5604
gfanton wants to merge 50 commits intognolang:masterfrom
gfanton:feat/gnodev-native-loader

Conversation

@gfanton
Copy link
Copy Markdown
Member

@gfanton gfanton commented Apr 27, 2026

TODO

replace and close #4957

gfanton added 30 commits April 22, 2026 14:59
Implements pkgdownload.PackageFetcher backed by an in-memory map of
MemPackages. Replaces the ad-hoc mock resolvers in downstream tools.

Assisted-By: Claude (claude-opus-4-7)
- Add compile-time interface assertion (matches examplespkgfetcher).
- Doc FetchPackage as implementing PackageFetcher (Effective Go).
- Note duplicate-Path "last wins" semantic on NewInMemoryFetcher.

Assisted-By: Claude (claude-opus-4-7)
Preparation for replacing the resolver-chain loader with a single Loader
type that delegates to gnovm's native loader.

Assisted-By: Claude (claude-opus-4-7)
Temporary name; renamed to Package in Phase D after legacy Package is
removed. Backs filesystem-resolved packages with Dir + Kind and mock/RPC
packages with an embedded MemPackage.

Assisted-By: Claude (claude-opus-4-7)
Walks up from start dir looking for gnowork.toml or gnomod.toml. Keeps
gnodev independent of unexported gnovm workspace-discovery internals.

Assisted-By: Claude (claude-opus-4-7)
Per code-review. Also drops the now-redundant "// path/file.go" header
lines at the top of both files.

Assisted-By: Claude (claude-opus-4-7)
Side-by-side with the legacy Loader interface: new LoaderImpl struct
delegates to gnovm.Load for bulk workspace loading and tracks packages
in an index for future Resolve/Reload methods.

Assisted-By: Claude (claude-opus-4-7)
Adds the fast path: RLock-only lookup in the in-memory index, with a
double-checked write-lock cold path ready to host FS and RPC lookups in
subsequent commits. Introduces ErrPackageNotFound.

Assisted-By: Claude (claude-opus-4-7)
Scans each ExtraRoot (and GNOROOT/examples when Examples=true) for a
gnomod.toml matching the requested import path. Walks each root at most
once and caches the import-path -> dir map on the LoaderImpl.

Assisted-By: Claude (claude-opus-4-7)
After FS walk misses, Resolve falls back to the configured
pkgdownload.PackageFetcher. A successful fetch yields a KindRemote
package that is memoized in the index and added to tracked paths.

Assisted-By: Claude (claude-opus-4-7)
Reload re-runs gnovm.Load for the configured workspace and re-Resolves
every tracked path so packages discovered through ExtraRoots or the
fetcher are refreshed. Dropping tracked entries from the index and
invalidating per-root scan caches ensures on-disk changes are picked up.

Assisted-By: Claude (claude-opus-4-7)
Eagerly loads workspace + every ExtraRoot + GNOROOT/examples.
Extra roots are materialized via per-root index + Resolve to bypass
gnovm.Load's "pattern must be inside workspace" constraint.

Assisted-By: Claude (claude-opus-4-7)
- rpcLookup renamed (was rpcLookupLocked): name no longer implies a
  lock contract since l.fetcher is immutable after NewLoaderImpl.
- scanRoot logs truly unparseable gnomod.toml at debug (still silent on
  "no mod file", which is normal during a walk).
- TestLoader_Resolve_MissReturnsNotFound now passes an empty
  InMemoryFetcher so the RPC fallback fails fast instead of reaching
  real gno.land (previous run: ~0.78s → now sub-millisecond).

Assisted-By: Claude (claude-opus-4-7)
-no-examples skips $GNOROOT/examples entirely. -extra-root is
repeatable; each value is added to packages.Config.ExtraRoots.
Not yet wired to the loader — Phase C later tasks consume these.

Assisted-By: Claude (claude-opus-4-7)
node.go no longer imports packages.Loader. Caller supplies the initial
pkg list plus a reload function. generateTxs and ListPkgs migrated to
*NewPackage; pkg.MemPackage.Path → pkg.ImportPath.

Build is intentionally broken at this commit — callers are fixed in
the next two commits.

Assisted-By: Claude (claude-opus-4-7)
Drops the -resolver chain in favor of workspace detection +
-no-examples/-extra-root flags. Workspace-absent case warns and
continues in discovery mode unless -no-examples + no roots leaves
nothing to load, which is fatal.

Lazy mode hydrates only the workspace at startup and relies on the
proxy + Reload to fill in the rest; eager mode (-lazy-loader=false)
calls LoadAll for both the initial pkg set and subsequent reloads,
which is the staging subcommand's default.

guessPath moves out of setup_loader.go (slated for deletion in
Phase D) into a standalone paths.go so the helper survives.

Assisted-By: Claude (claude-opus-4-7)
Staging eager-loads workspace + examples + extra-roots via LoadAll.
The eager path is selected in app.Setup by lazyLoader=false, which
is already the default for staging. Drops the legacy resolver-chain
seeding that no longer has a consumer.

Assisted-By: Claude (claude-opus-4-7)
Dir (was Location), KindFS (was PackageKindFS).

Legacy pkg/dev tests (node_test.go, node_state_test.go) gated behind
the phase_e_tests build tag: they depend on the removed cfg.Loader
field and the MockResolver/BaseLoader chain. Phase E rewrites them
against NodeConfig.InitialPkgs + a stub Reload.

Assisted-By: Claude (claude-opus-4-7)
BREAKING: -resolver and -lazy-loader are removed from the user-facing
flag surface. Use -extra-root to add package roots; lazy mode is the
default (what today's gnodev does). `gnodev staging` continues to
eager-load via an internal staging flag.

Internal AppConfig.lazyLoader renamed to staging with inverse sense:
staging=true eager-loads everything (LoadAll), staging=false lazy-loads
(LoadWorkspace + proxy-driven Reload).

Assisted-By: Claude (claude-opus-4-7)
Legacy resolver system (Local/Remote/Root/Mock + chain/middleware).
Replaced by the native Loader's single-path resolution in Phase B.

Also drop the last live caller of NewLocalResolver in command_local.go:
the resolver was only used for its path-validity side effect, replaced
by a direct gnolang.ReadMemPackage check.

Assisted-By: Claude (claude-opus-4-7)
BaseLoader/GlobLoader replaced by LoaderImpl which delegates to
gnovm.Load for pattern expansion.

Assisted-By: Claude (claude-opus-4-7)
…interface

Final sweep of code replaced by the native Loader + NewPackage. Leaves
contribs/gnodev/pkg/packages/ with only the new Loader/Package types,
Config, FindWorkspace, and their tests.

Assisted-By: Claude (claude-opus-4-7)
Temporary names introduced in Phase B to avoid collision with the
legacy types; those are now deleted. Final rename makes the public
surface match the ADR and allows the new Loader to be imported under
its natural name.

Also updates gnodev's LongHelp to drop the removed -resolver flag
documentation.

Assisted-By: Claude (claude-opus-4-7)
Removes the phase_e_tests build tag. Legacy MockResolver/BaseLoader
seeding replaced with a pkgsFromMem test helper that builds
[]*packages.Package via the new Loader + InMemoryFetcher.

The testing Reload closure reads the node's live paths slice directly
(not via Paths(), which would deadlock under the write lock held by
Reset/rebuildNodeFromState) and rebuilds a fresh Loader on each call so
in-place MemPackage.Files mutations are observed.

Assisted-By: Claude (claude-opus-4-7)
Adds TestGnodev_Workspace_EagerLoad: a workspace root containing a
gnowork.toml and one package, verifies Loader.LoadWorkspace returns
that package.

Assisted-By: Claude (claude-opus-4-7)
Adds TestGnodev_NoWorkspace_DiscoveryMode: with Workspace="" +
ExtraRoots, verifies LoadWorkspace returns nil,nil and that Resolve
still succeeds against the extra root.

Assisted-By: Claude (claude-opus-4-7)
…extra-root

Adds TestGnodev_NoWorkspace_NoExamples_ConfigError: when the cwd is
outside any workspace, -no-examples is set, and no -extra-root is
provided, app.Setup must fail fast with a "nothing to load" error per
the ADR error model.

Assisted-By: Claude (claude-opus-4-7)
Adds TestGnodev_Staging_EagerAll: verifies Loader.LoadAll returns
both the workspace package and the extra-root package when Examples
is disabled (to keep the test hermetic).

Assisted-By: Claude (claude-opus-4-7)
Adds TestGnodev_Reload_AfterProxyHit: simulates a proxy hit by
calling Resolve on an extra-root path after LoadWorkspace, then
verifies Loader.Reload returns both the workspace package and the
tracked extra-root package.

Assisted-By: Claude (claude-opus-4-7)
Architecture Decision Record describing the replacement of gnodev's
custom package loader/resolver subsystem with the gnovm native loader
(PR gnolang#4957). Captures the design, alternatives considered, and
consequences that guided the implementation.

Status flipped to Accepted after all implementation phases
(InMemoryFetcher upstream, new Loader type, wiring, legacy deletion,
integration tests) landed and passed local verification.

Assisted-By: Claude (claude-opus-4-7)
gfanton added 19 commits April 22, 2026 14:59
The field was written at 3 call sites but never read inside pkg/dev.
The watcher seeds from ds.devNode.ListPkgs() (reads n.pkgs populated
by Reset -> Reload), not from InitialPkgs. Startup parse errors still
surface because NewDevNode calls Reset which calls Reload.

Assisted-By: Claude (claude-opus-4-7)
Findings aggregated from code-reuse, code-quality, and efficiency
sub-reviews on the branch:

- Replace stringSliceVar with commands.StringArr (already in the
  repo). Deletes contribs/gnodev/flags.go entirely.
- Replace extractPackageName's inline go/parser call with
  gnolang.PackageNameFromFileBody (same behavior, fewer imports).
- Cache gnomod.ModCachePath() on the Loader instead of re-resolving
  it on every package classification.
- scanRoot now skips hidden dirs (.git, etc.), node_modules, and
  _build — avoids descending into VCS/build trees on user-supplied
  extra roots. Matches files by name (gnomod.toml) instead of
  stat-ing every directory.
- scanRoot now surfaces top-level WalkDir errors at Warn.
- Drop the stray "// path/file.go" header comment left at the top of
  workspace.go.

Assisted-By: Claude (claude-opus-4-7)
…staging help

loadWithPatterns took an RLock to snapshot l.fetcher, but l.fetcher is
set once in New() and never mutated — same treatment as l.cfg, which
is read lock-free throughout the loader. Remove the unnecessary lock
and note the invariant in a comment.

Also update the staging command's LongHelp: "lazy-load" is no longer a
user-facing concept in the native-loader design (ADR removes the
-lazy-loader flag). Replace with the ADR's own phrasing describing what
staging eager-loads.

Assisted-By: Claude (claude-opus-4-7)
Running "gnodev" in a loose directory that has .gno files but no
gnomod.toml/gnowork.toml ancestor previously added the CWD's path to
cfg.paths without adding the directory to the loader's extra roots.
The node then referenced a path it couldn't resolve.

This restores the pre-PR "just run it anywhere" UX: when
ReadMemPackage succeeds on CWD and no workspace is detected,
CWD is appended to cfg.extraRoots so Loader.Resolve can find it.

Assisted-By: Claude (claude-opus-4-7)
Two production bugs uncovered by running gnodev in
examples/gno.land/r/gnoland/boards2/v1:

1. Native-stdlib imports (e.g. "chain", "chain/runtime") left
   PkgList.Sort() unable to find the "dep" because gnovm.Load skips
   native stdlibs during dep traversal but leaves them in each
   package's Imports slice. Fix: new stripStdlibs helper drops stdlib
   packages from the returned list and stdlib imports from each
   remaining pkg's import list, mirroring the convention already used
   by gno.land/pkg/gnoland/genesis.go (via ReadPkgListFromDir).

2. ToMemPackage used MPUserAll, which includes *_test.gno and
   *_filetest.gno files. Chain-side type-check then fails when a test
   file imports a package whose tests reference packages not deployed
   yet (seen with bptree's tree_test.gno importing avl). gnodev is a
   dev-time tool; deployed packages don't need test files. Switch to
   MPUserProd / MPStdlibProd.

Adds TestLoader_LoadWorkspace_WithStdlibImport as a regression test
for (1): a workspace package importing "chain" no longer errors.

Assisted-By: Claude (claude-opus-4-7)
Reload was resetting l.rootIdx on every call, forcing every watcher
event to re-walk every extra root. With large extra-roots this is
multi-second per save. Directories are stable mid-session — picking up
new dirs requires a gnodev restart, which is the documented behavior.

Update ADR root-scan-caching section to match.

Assisted-By: Claude (claude-opus-4-7)
Previously a `var p Package` or forgotten `Package{...}` literal would
silently report Kind == KindFS, letting the watcher try to watch a
nonexistent dir. Adding KindUnknown as the iota=0 sentinel makes such
bugs trip loudly at read time.

Assisted-By: Claude (claude-opus-4-7)
Closes the migration gap left by removing the old -resolver flag. The
override applies only to packages outside the workspace: workspace pkgs
hit fsLookup first via gnovm.Load, only unresolvable paths reach the
rpc fetcher which consumes RemoteOverrides.

Repeatable. Validated at parse time (domain and rpc both required).

Assisted-By: Claude (claude-opus-4-7)
Replaces the buried slog warn with a pre-slog stderr banner. The user
gets a distinct, actionable startup message explaining what discovery
mode is and how to add local packages, instead of a single line lost in
the log stream.

Lazy examples still load in discovery mode (no behavior change there).

Assisted-By: Claude (claude-opus-4-7)
Without this check, running gnodev with -no-examples on a workspace
that imports gno.land/r/demo/* silently boots, then panics at first
query. The new pre-flight scans workspace package imports, attempts to
resolve each gno.land/* path via the loader, and warns (non-fatal —
user may be stubbing) for each unresolvable one with a clear hint.

Assisted-By: Claude (claude-opus-4-7)
Staging mode walks every extra root + examples; on real systems this
takes several seconds with no output, looking hung. Add INFO logs at
the start and end of each root (and the workspace) with package counts.

Assisted-By: Claude (claude-opus-4-7)
All prior loader tests used trivial 'package foo' stubs with zero
imports. Real realms expose code paths that those don't — stripStdlibs
on native imports, MPUserProd on test-file handling. This test loads
boards2/v1 from $GNOROOT/examples and asserts the realm loads, the
realm package is in the result, ToMemPackage succeeds, and no test
files leak through.

Skips cleanly if examples aren't available (running outside monorepo).

Assisted-By: Claude (claude-opus-4-7)
CheckMissingExampleImports previously called l.Resolve, which falls through
to the rpc fetcher on FS miss and writes to the loader's index/tracked sets.
Two consequences:

  - Blocking RPC at startup: with -no-examples and several broken imports,
    gnodev startup synchronously hits https://rpc.<domain>:443 per missing
    import, blocking on TCP connects and timeouts.
  - State pollution: a diagnostic helper must not mutate the system it
    observes; index entries seeded by a startup probe outlive the check.

Add Loader.LookupFS as the read-only sibling of Resolve: walks any not-yet-
cached root, consults rootIdx, never touches the fetcher, never writes to
index or tracked. Switch CheckMissingExampleImports to use it.

Assisted-By: Claude (claude-opus-4-7)
Reload preserves rootIdx across calls (deliberately, so directories don't
get re-walked on every reload). Document that the flip side is staleness:
if an extra-root dir is deleted mid-session, Resolve will keep returning
the cached dir until gnodev restarts.

Assisted-By: Claude (claude-opus-4-7)
The String() method iterated the map directly, producing non-deterministic
output across calls. flag.Value implementations are read by the flag package
to print defaults and may be logged for diagnostics, so the output should
be stable. Sort keys lexicographically before joining and add a regression
test.

Assisted-By: Claude (claude-opus-4-7)
The per-root progress log in LoadAll built its progress field via
fmt.Sprintf("%d/%d", ...), which packs two values into one opaque string.
Slog handlers can't filter or extract the components separately. Emit n
and of as distinct integer fields so log consumers can index on either.

Strengthen the existing test to assert the new structured shape (n=1,
of=1) rather than only the message and root path.

Assisted-By: Claude (claude-opus-4-7)
…rts non-mutation

Lock in the contract that the -no-examples diagnostic helper does not
mutate l.index or l.tracked, and that LookupFS never invokes the rpc
fetcher. Without these, a future revert to l.Resolve(imp) would silently
restore the blocking-RPC + state-pollution bug fixed in 2f21494.

Assisted-By: Claude (claude-opus-4-7)
When run in a directory without gnomod.toml, gnodev derives a fallback
import path of gno.land/r/dev/<basename>. The previous regex preserved
hyphens, uppercase letters, and digit-leading names — all rejected by
gno's Re_name validator — causing ReadMemPackage to panic at startup.

Sanitize the basename to lowercase letters/digits/underscore only and
ensure it starts with a letter. Fall back to "app" for inputs with no
letters.

Assisted-By: Claude (claude-opus-4-7)
Comments that referenced commit SHAs (62e4e32, 2f21494) or
described prior behavior with "would have", "regression for the bug
fixed", etc. are rewritten to state the contract being asserted. A
reader should understand each test without knowing repo history.

Assisted-By: Claude (claude-opus-4-7)
@Gno2D2
Copy link
Copy Markdown
Collaborator

Gno2D2 commented Apr 27, 2026

🛠 PR Checks Summary

All Automated Checks passed. ✅

Manual Checks (for Reviewers):
  • IGNORE the bot requirements for this PR (force green CI check)
Read More

🤖 This bot helps streamline PR reviews by verifying automated checks and providing guidance for contributors and reviewers.

✅ Automated Checks (for Contributors):

🟢 Maintainers must be able to edit this pull request (more info)

☑️ Contributor Actions:
  1. Fix any issues flagged by automated checks.
  2. Follow the Contributor Checklist to ensure your PR is ready for review.
    • Add new tests, or document why they are unnecessary.
    • Provide clear examples/screenshots, if necessary.
    • Update documentation, if required.
    • Ensure no breaking changes, or include BREAKING CHANGE notes.
    • Link related issues/PRs, where applicable.
☑️ Reviewer Actions:
  1. Complete manual checks for the PR, including the guidelines and additional checks if applicable.
📚 Resources:
Debug
Automated Checks
Maintainers must be able to edit this pull request (more info)

If

🟢 Condition met
└── 🟢 And
    ├── 🟢 The base branch matches this pattern: ^master$
    └── 🟢 The pull request was created from a fork (head branch repo: gfanton/gno)

Then

🟢 Requirement satisfied
└── 🟢 Maintainer can modify this pull request

Manual Checks
**IGNORE** the bot requirements for this PR (force green CI check)

If

🟢 Condition met
└── 🟢 On every pull request

Can be checked by

  • Any user with comment edit permission

Replaces the help-text section for -resolver / -lazy-loader (removed in
this PR) with -extra-root, -no-examples, and -remote-override. The
staging mode description is updated to reflect eager workspace +
extra-roots + examples loading.

Produced by `make generate` in contribs/gnodev/.

Assisted-By: Claude (claude-opus-4-7)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🛠️ gnodev 📦 🤖 gnovm Issues or PRs gnovm related

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

2 participants