Skip to content

feat: async import loader for JS via static preload (#476)#820

Open
stephenamar-db wants to merge 5 commits intomasterfrom
lazyimporter
Open

feat: async import loader for JS via static preload (#476)#820
stephenamar-db wants to merge 5 commits intomasterfrom
lazyimporter

Conversation

@stephenamar-db
Copy link
Copy Markdown
Collaborator

Adds Preloader + ImportFinder to discover imports from each file's AST and drive caller-controlled (async) loading before synchronous evaluation. Exposes SjsonnetMain.interpretAsync for the JS build, accepting a Promise-returning loader so callers can use fetch / FileReader / etc.

Co-authored-by: Isaac

@stephenamar-db stephenamar-db linked an issue May 1, 2026 that may be closed by this pull request
@stephenamar-db
Copy link
Copy Markdown
Collaborator Author

@He-Pin

Adds Preloader + ImportFinder to discover imports from each file's AST
and drive caller-controlled (async) loading before synchronous evaluation.
Exposes SjsonnetMain.interpretAsync for the JS build, accepting a
Promise-returning loader so callers can use fetch / FileReader / etc.

Co-authored-by: Isaac
Scala 2.13/2.12 promote "private default argument never used" to an
error; the binary-loader and log defaults in the test helpers were
unused at every call site. Inline them since no test exercises binary
imports through the async path.

Co-authored-by: Isaac
Copy link
Copy Markdown
Contributor

@He-Pin He-Pin left a comment

Choose a reason for hiding this comment

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

Focused on the Scala.js async import semantics from #476. The added targeted tests pass, but I found a few cases where interpretAsync diverges from the existing synchronous interpret behavior.

Comment thread sjsonnet/src/sjsonnet/Preloader.scala Outdated
Comment thread sjsonnet/src-js/sjsonnet/SjsonnetMain.scala
Comment thread sjsonnet/src/sjsonnet/Preloader.scala
@He-Pin
Copy link
Copy Markdown
Contributor

He-Pin commented May 2, 2026

Three correctness issues with interpretAsync:

1. Preloader now passes the importing file's parent directory as
   docBase to the resolver, matching Importer.resolveAndReadOrFail.
   Before, a JS resolver doing `wd + "/" + importName` would look up
   nested imports under `dir/a.libsonnet/b.libsonnet` instead of
   `dir/b.libsonnet`.

2. interpretAsync now feeds extVar/tlaVar snippets through the
   preloader so any imports they contain land in the cache. Without
   this, `std.extVar('cfg')` where `cfg` is `import 'lib.libsonnet'`
   failed at evaluation time against the cache-only importer.

3. Parse errors on discovered (non-entry) files no longer abort
   preload. Jsonnet evaluation is lazy, so a parse error in
   `if false then import 'bad' else 1` should not fail evaluation;
   the interpreter surfaces the error only if the branch is forced.
   Matches jrsonnet's preloader behavior.

Co-authored-by: Isaac
@stephenamar-db stephenamar-db requested a review from He-Pin May 7, 2026 17:57
…loading

Adds an optional preParsedAst hook on ResolvedFile and a
PreParsedResolvedFile wrapper. CachedResolver.parse uses the attached
AST when present, skipping fastparse and going straight to the static
optimizer. Preloader now stashes the AST it produced during discovery
on the cache entry, so each file is fastparse'd once total instead of
twice (once for discovery, once for evaluation).

Also expands the interpretAsync and Preloader docstrings to make the
eager front-loading semantics explicit: every reachable import is
loaded up front, including ones inside branches the evaluator will
never force; this is the tradeoff that makes synchronous evaluation
possible. Loader rejection still propagates; parse errors on
discovered (non-entry) files are tolerated.

Co-authored-by: Isaac

private def parseStringMap(label: String, value: js.Any): Map[String, String] =
try {
ujson.WebJson.transform(value, ujson.Value).obj.toMap.map {
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.

seems this will generate much allocation

Comment thread sjsonnet/src/sjsonnet/ImportFinder.scala Outdated
Comment thread sjsonnet/src/sjsonnet/Preloader.scala Outdated
Comment thread sjsonnet/src-js/sjsonnet/SjsonnetMain.scala Outdated
Copy link
Copy Markdown
Contributor

@He-Pin He-Pin left a comment

Choose a reason for hiding this comment

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

Found two async correctness issues and left inline comments: mixed import kinds can overwrite the preload cache for the same resolved path, and entry parse errors bypass the normal interpreter error formatting.

Four issues from the review pass:

1. parseStringMap allocations: stop round-tripping through ujson —
   iterate the JS dictionary directly into a Map.newBuilder. Drops the
   intermediate ujson tree, the .obj.toMap copy, and the trailing .map.

2. Naming: rename ImportFinder.Kind to a top-level ImportKind, matching
   the existing ExternalVariableKind convention in the codebase.

3. Cache key collision for importstr/importbin of the same path. The
   cache was keyed by Path only, so a program like
   `if true then importstr "x" else importbin "x"` had whichever load
   ran last clobber the other; sync interpret returns text, async was
   rejecting with NotImplementedError. Cache is now keyed by
   (Path, binaryData), pending dedup tracks the same key, and a new
   record() helper upgrades a pending Str entry to Code if a later
   reference needs the AST. Added putContent that refuses to downgrade
   a PreParsedResolvedFile back to plain text.

4. Entry parse error formatting: interpretAsync was rejecting with
   err.getMessage, bypassing Error.formatError used by interpret0. Now
   we let the entry's parse error fall through to runInterpret so the
   message has the same shape (root frame, "(memory):line:col") as
   synchronous interpret.

New tests:
  - PreloaderTests.scala: importstr+importbin for the same path keep
    separate cache entries.
  - InterpretAsyncTests.scala: same case end-to-end through the JS API,
    and a test that the entry parse error message contains "(memory)".

Co-authored-by: Isaac
@stephenamar-db stephenamar-db requested a review from He-Pin May 7, 2026 19:13
Copy link
Copy Markdown
Contributor

@He-Pin He-Pin left a comment

Choose a reason for hiding this comment

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

LGTM

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.

JavaScript: Proper way to use an async loader callback

3 participants