Skip to content

fix(parse): recognize .spawn() on a dict-of-functions Subscript (calque#189) - #190

Merged
scttfrdmn merged 1 commit into
mainfrom
fix/calque-189-spawn-dict-subscript
Aug 15, 2026
Merged

fix(parse): recognize .spawn() on a dict-of-functions Subscript (calque#189)#190
scttfrdmn merged 1 commit into
mainfrom
fix/calque-189-spawn-dict-subscript

Conversation

@scttfrdmn

Copy link
Copy Markdown
Contributor

Summary

  • Found while investigating whether calque spawn-run could drive AI-Almanac's forecasts_app.py for real: its .spawn() call site is SEASON_BUNDLE_FNS[_model_env(model_id)].spawn(...) — a dict subscript, not a plain name. tools/pyast/pyast.py's _attr_chain only walks Name/Attribute nodes, so this call site's target resolved to an empty string, silently invisible to spawn-run's target-keyed driver (internal/exec/spawnshard.go's BuildSpawnManifests).
  • pyast now tracks every function assigned into a module-level dict via NAME[<key>] = some_function (new Collector._callable_dicts), regardless of what <key> is — in this real-world case it's a loop variable, never a literal, so the actual runtime-selected callable genuinely isn't statically resolvable (that limit isn't fixed here, and shouldn't be — it's honest).
  • A .spawn() whose receiver is a Subscript on a known such dict now lists every candidate instead of resolving to nothing. internal/parse.SpawnCallSites/new SpawnCallSitesReport expand one SpawnCallSite per candidate (same call args on each, since the real selection can't be known), with a leak noting the expansion — factored into a pure spawnCallSitesFromInvokeCalls helper so the expansion logic is unit-testable without a pyast subprocess.
  • cmd/calque/spawnrun.go now uses SpawnCallSitesReport so this expansion leak lands in the same report the caller already prints.

Files touched

  • tools/pyast/pyast.pyCollector._callable_dicts, visit_Assign's new NAME[<key>] = func branch, .spawn() handler's candidate lookup.
  • internal/parse/parse.gopyInvokeCall.Candidates, SpawnCallSitesReport, spawnCallSitesFromInvokeCalls.
  • internal/parse/parse_test.go — new tests (subprocess-level + pure-Go expansion logic).
  • cmd/calque/spawnrun.go — switched to SpawnCallSitesReport.
  • testdata/scripts/spawn_dict_dispatch.py — new fixture mirroring forecasts_app.py's real shape exactly.
  • CHANGELOG.md.

Test plan

  • go build ./... && go vet ./... && gofmt -l . && go test -count=1 ./... — all pass
  • golangci-lint run ./... — 0 issues
  • ruff check . — all checks passed
  • Manually confirmed against a fresh clone of forecasts_app.py: the .spawn() call site now emits {'target': '', 'candidates': ['run_season_forecast_bundle'], ...} instead of {'target': '', ...} with no candidates key at all
  • New fixture + 4 new tests: real pyast-subprocess resolution against the fixture, plus pure-Go unit tests for the expansion logic (candidate fanout, and the "no target, no candidates → nothing" regression guard)

…ad of resolving to an empty target (calque#189)

Found while investigating whether `calque spawn-run` could drive
AI-Almanac's forecasts_app.py for real: its .spawn() call site is
`SEASON_BUNDLE_FNS[_model_env(model_id)].spawn(...)` -- _attr_chain only
walks Name/Attribute nodes, so the Subscript in the middle of the chain
made the call site's target resolve to an empty string, invisible to
spawn-run's target-keyed driver.

pyast now tracks every function assigned into a module-level dict via
`NAME[<key>] = some_function` (Collector._callable_dicts), regardless of
what <key> is (a loop variable here, never a literal -- the real
runtime-selected callable genuinely isn't resolvable statically). A
.spawn() whose receiver is a Subscript on a known such dict lists every
candidate instead of going silently empty.

internal/parse.SpawnCallSites/SpawnCallSitesReport expand one
SpawnCallSite per candidate (same call args, since the real selection
can't be known) with a leak noting the expansion, factored into a pure
spawnCallSitesFromInvokeCalls helper for unit testing without a pyast
subprocess.
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