Skip to content

Fix #183: interpolated deps trigger interpolated producers + cycle detection - #187

Merged
guybedford merged 1 commit into
fix/185-panic-and-depsfrom
fix/183-wildcard-dep-resolution
Apr 27, 2026
Merged

Fix #183: interpolated deps trigger interpolated producers + cycle detection#187
guybedford merged 1 commit into
fix/185-panic-and-depsfrom
fix/183-wildcard-dep-resolution

Conversation

@guybedford

Copy link
Copy Markdown
Owner

Stacked on #186.

Summary

Three related fixes for interpolated task resolution (#183):

  1. Producer-chain through interpolated dep. When a task's interpolated dep (e.g. dep = 'dst/#/manifest.yml') expands to a path that's also another task's interpolated target, expand_interpolate_match used to fall straight to add_file and the build failed with File ... not found. Now the dep resolution scans interpolate_nodes for a target-pattern match (match_interpolate_target) and recursively drives the producer instead.

  2. Name-only interpolation tasks reachable as plain dep refs. A task with name = 'build:#' and no # in deps wasn't registered in interpolate_nodes (is_interpolate_target only inspected deps), so a plain dep 'build:two' couldn't resolve to it. Broadened is_interpolate_target to also consider name/targets. This also required deduping add_job by interpolated name — without that, the for-target expansion loop in EIM creates a fresh node every visit and recurses unboundedly when the consumer has no targets to register in file_nodes.

  3. Cycle detection. drive_all recurses into job.deps with only a self-edge guard, so indirect cycles (e.g. two tasks claiming the same file as their target) blow the stack at execution time. Added check_acyclic, an iterative tri-colour DFS run after expansion. Surfaces Circular dependency detected: a → b → a instead of a stack overflow.

lookup_target and lookup_task_name are refactored around two shared helpers (match_interpolate_target, interpolate_dep_input) so the matching logic lives in one place. expand_interpolate_match now takes dep_path: Option<&str> to handle name-only interpolation tasks.

Test plan

  • New test8 covers both fixes: a chain where the consumer's interpolated dep IS the producer's interpolated target, reached via plain dep refs (no : prefix). Subtasks namespaced as t8:* to stay out of test:run's :test[0-9]* glob.
  • Cycle reproduction (separately verified): two tasks both claiming output/dist/one/config.yml now errors with a clear cycle message instead of overflowing.
  • Full test suite passes: chomp -c test/chompfile.toml test ends with √ :test.
  • CI: Linux + Windows.

…183)

Three related fixes for interpolated task resolution:

1. **Producer-chain through interpolated dep** — `expand_interpolate_match`
   used to fall straight to `add_file` when a consumer's interpolated dep
   wasn't already in `file_nodes`. If another task's interpolated *target*
   pattern would have produced that file, that producer was never driven and
   the build failed with "File not found". Now the dep lookup also scans
   `interpolate_nodes` for a matching target pattern (`match_interpolate_target`)
   and recursively expands the producer instead.

2. **Name-only interpolation tasks reachable as plain dep refs** — a task
   like `name = 'test:build:#'` with no `#` in deps wasn't registered in
   `interpolate_nodes` (`is_interpolate_target` only inspected deps), so
   `dep = 'test:build:two'` couldn't resolve to it. `is_interpolate_target`
   now also considers `name`/`targets` containing `#`, and `add_job` dedupes
   by interpolated name (returning the existing instance) — without that,
   the for-target expansion loop in EIM creates a fresh node every visit
   and recurses without bound when the consumer has no targets to register
   in `file_nodes`.

3. **Cycle detection** — `drive_all` recursively walks `job.deps` with only
   a self-edge guard, so an indirect cycle (e.g. two tasks claiming the
   same file as their target) blew the stack at execution time. Added
   `check_acyclic`, an iterative tri-colour DFS run after expansion in
   `run()`. Surfaces a clear "Circular dependency detected" error with the
   cycle path instead of a stack overflow.

Refactors `lookup_target` and `lookup_task_name` around two shared helpers
(`match_interpolate_target`, `interpolate_dep_input`) so the matching logic
lives in one place. `expand_interpolate_match` now takes `dep_path: Option<&str>`
to handle interpolation tasks with no `#` in deps.

Adds `test8` covering both regressions: a chain where the consumer's
interpolated dep is the producer's interpolated target, reached via plain
dep refs (no `:` prefix). Subtasks are namespaced as `t8:*` to stay out of
`test:run`'s `:test[0-9]*` glob.
@guybedford
guybedford merged commit 6de0942 into fix/185-panic-and-deps Apr 27, 2026
@guybedford
guybedford deleted the fix/183-wildcard-dep-resolution branch April 27, 2026 02:32
guybedford added a commit that referenced this pull request Apr 27, 2026
* fix: race in CmdPool::new_exec causing intermittent "no entry found for key" panics (#185)

new_exec inserted into cmd_execs before awaiting fs::create_dir_all and only
later inserted into execs in the engine dispatch. While the await yielded,
another exec_future polled by select_all could observe cmd_execs populated
but execs still empty and panic indexing execs at mod.rs:201.

Hoist all create_dir_all awaits into a pre-pass so cmd_execs.insert and the
engine's execs.insert run synchronously without yielding between them.

* deps: update across the dependency tree

In-range bumps (cargo update): tokio 1.37→1.52, regex 1.10→1.12,
serde 1.0.197→1.0.228, serde_json 1.0.116→1.0.149, uuid 1.8→1.23, and
many transitive crates.

Major bumps with no source impact: base64 0.21→0.22, convert_case 0.6→0.11,
dirs 4→6, directories 4→6, sha2 0.10→0.11, toml 0.5→0.8.

notify 4 → 8 + notify-debouncer-mini 0.7: replaced the removed top-level
watcher() / DebouncedEvent with a debouncer-driven setup. Internal channel
events are now PathBuf-only (WatchEvent type alias) since chomp only ever
acted on the path. Watcher trait sites use &mut dyn Watcher to match the
debouncer's accessor.

hyper 0.14 → 1, hyper-tls 0.5 → 0.6, warp 0.3 → 0.4, plus hyper-util,
http-body-util, bytes: rewrote http_client::fetch_uri_cached on
hyper-util's legacy Client + BodyExt::collect; switched the dev server to
Bytes-bodied responses (warp::Body: From<Bytes>) and read files with
fs::read instead of FramedRead since hyper 1 has no Body::wrap_stream.
Boxed the warp filter chain so the spawned serve future is Send.

Removed the unused optional rustls dep.

v8 + serde_v8 are intentionally left at their current versions — the jump
from 0.89 → 147 spans many breaking API changes (ScriptOrigin, HandleScope,
slot API, callback signatures) and warrants a dedicated PR.

* test: align test5 expectation with current Rollup quote-style

Recent Rollup preserves source quote style instead of normalising to double
quotes, so dep.ts's `'dep'` round-trips as `'dep'` in the bundle. Update
the assert fixture to match.

* fix: interpolated deps trigger interpolated producers; detect cycles (#183) (#187)

Three related fixes for interpolated task resolution:

1. **Producer-chain through interpolated dep** — `expand_interpolate_match`
   used to fall straight to `add_file` when a consumer's interpolated dep
   wasn't already in `file_nodes`. If another task's interpolated *target*
   pattern would have produced that file, that producer was never driven and
   the build failed with "File not found". Now the dep lookup also scans
   `interpolate_nodes` for a matching target pattern (`match_interpolate_target`)
   and recursively expands the producer instead.

2. **Name-only interpolation tasks reachable as plain dep refs** — a task
   like `name = 'test:build:#'` with no `#` in deps wasn't registered in
   `interpolate_nodes` (`is_interpolate_target` only inspected deps), so
   `dep = 'test:build:two'` couldn't resolve to it. `is_interpolate_target`
   now also considers `name`/`targets` containing `#`, and `add_job` dedupes
   by interpolated name (returning the existing instance) — without that,
   the for-target expansion loop in EIM creates a fresh node every visit
   and recurses without bound when the consumer has no targets to register
   in `file_nodes`.

3. **Cycle detection** — `drive_all` recursively walks `job.deps` with only
   a self-edge guard, so an indirect cycle (e.g. two tasks claiming the
   same file as their target) blew the stack at execution time. Added
   `check_acyclic`, an iterative tri-colour DFS run after expansion in
   `run()`. Surfaces a clear "Circular dependency detected" error with the
   cycle path instead of a stack overflow.

Refactors `lookup_target` and `lookup_task_name` around two shared helpers
(`match_interpolate_target`, `interpolate_dep_input`) so the matching logic
lives in one place. `expand_interpolate_match` now takes `dep_path: Option<&str>`
to handle interpolation tasks with no `#` in deps.

Adds `test8` covering both regressions: a chain where the consumer's
interpolated dep is the producer's interpolated target, reached via plain
dep refs (no `:` prefix). Subtasks are namespaced as `t8:*` to stay out of
`test:run`'s `:test[0-9]*` glob.
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.

1 participant