Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,22 @@ per [semver.org](https://semver.org/#spec-item-4).

## [Unreleased]

### Fixed

- A picked warm unit with 2+ non-self/cls positional args that ISN'T
`.starmap()`'d (e.g. a `.spawn()`-invoked function like AI-Almanac's
`forecasts_app.py`'s `run_forecast_inference(job_id, model_id, config)`)
now refuses loudly instead of silently `NameError`-ing on every synthetic
item (calque#187) — the warm runner only ever bound the FIRST positional
arg outside the `.starmap()` splat path, leaving the rest undefined.
Found while re-verifying calque#79's closing claim that AI-Almanac's
three real scripts run end-to-end: `forecasts_app.py`'s and
`blending_app.py`'s picked units both hit this unannounced. Does NOT
affect `calque real --arg-file`/`--arg-json` (a real, caller-supplied
positional tuple, e.g. `app.py`'s `run_benchmark_local` — calque#178's
real-hardware verification) — that path explicitly bypasses the new
arity guard, since it already supplies real per-position data.

## [0.6.0] - 2026-08-15

22 commits since v0.5.0 (folding in the changes originally staged under an
Expand Down
2 changes: 1 addition & 1 deletion cmd/calque/fleetrun.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ func fleetRun(o realOpts, shards int) (err error) {
shardBody := calexec.ManifestBody{EnterBody: realEnterBody, MethodBody: realMethodBody, MethodArg: "prompt"}
shardHostMode := false
if scriptBody, ok := manifestBodyForUnit(app, unit, rep); ok {
if err := checkInvokeSupport(app.Script, unit.method, rep); err != nil {
if err := checkInvokeSupport(app.Script, unit.method, rep, false); err != nil {
return err
}
// GPU guard parity with dry-run (run.go's swapLegal check, §7): refuse
Expand Down
60 changes: 56 additions & 4 deletions cmd/calque/gate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ func TestPickWarmUnitUnscopedWhenNotAmbiguous(t *testing.T) {
func TestCheckInvokeSupportStarmapRefusesWithoutRealItems(t *testing.T) {
rep := &leak.Report{}
fn := ir.Function{Name: "combine", Invoke: ir.InvokeStarmap}
if err := checkInvokeSupport("script.py", fn, rep); err == nil {
if err := checkInvokeSupport("script.py", fn, rep, false); err == nil {
t.Fatal("expected an error refusing a .starmap'd warm unit with no real Items, got nil")
}
}
Expand All @@ -193,7 +193,7 @@ func TestCheckInvokeSupportStarmapRunsWithRealItems(t *testing.T) {
Name: "combine", Invoke: ir.InvokeStarmap, Line: 12,
Items: []any{[]any{float64(1), float64(2)}, []any{float64(3), float64(4)}},
}
if err := checkInvokeSupport("script.py", fn, rep); err != nil {
if err := checkInvokeSupport("script.py", fn, rep, false); err != nil {
t.Fatalf("checkInvokeSupport(.starmap with real Items) must not error, got: %v", err)
}
found := false
Expand All @@ -214,7 +214,7 @@ func TestCheckInvokeSupportStarmapRunsWithRealItems(t *testing.T) {
func TestCheckInvokeSupportForEachLeaksNotRefuses(t *testing.T) {
rep := &leak.Report{}
fn := ir.Function{Name: "notify", Invoke: ir.InvokeForEach, Line: 7}
if err := checkInvokeSupport("script.py", fn, rep); err != nil {
if err := checkInvokeSupport("script.py", fn, rep, false); err != nil {
t.Fatalf("checkInvokeSupport(.for_each) must not error, got: %v", err)
}
found := false
Expand All @@ -234,7 +234,7 @@ func TestCheckInvokeSupportMapAndRemoteAreFine(t *testing.T) {
rep := &leak.Report{}
for _, kind := range []ir.InvokeKind{ir.InvokeMap, ir.InvokeRemote, ir.InvokeNone} {
fn := ir.Function{Name: "f", Invoke: kind}
if err := checkInvokeSupport("script.py", fn, rep); err != nil {
if err := checkInvokeSupport("script.py", fn, rep, false); err != nil {
t.Errorf("checkInvokeSupport(%q) must not error, got: %v", kind, err)
}
}
Expand All @@ -243,6 +243,58 @@ func TestCheckInvokeSupportMapAndRemoteAreFine(t *testing.T) {
}
}

// TestCheckInvokeSupportMultiArgNonStarmapRefuses (calque#187): a picked
// unit with 2+ non-self/cls positional args that ISN'T .starmap()'d (e.g. a
// .spawn()-invoked function like AI-Almanac's forecasts_app.py's
// `run_forecast_inference(job_id, model_id, config)`) must refuse loudly
// instead of silently NameError'ing — the warm runner only ever binds the
// FIRST positional arg per item outside the .starmap() splat path (both
// dryRunWarm and manifestBodyForUnit), so the rest would be undefined.
// hasRealArgTuple=false: no --arg-file/--arg-json supplied a real tuple.
func TestCheckInvokeSupportMultiArgNonStarmapRefuses(t *testing.T) {
rep := &leak.Report{}
fn := ir.Function{Name: "run_forecast_inference", Invoke: ir.InvokeSpawn, Args: []string{"job_id", "model_id", "config"}}
err := checkInvokeSupport("script.py", fn, rep, false)
if err == nil {
t.Fatal("expected an error refusing a multi-arg non-starmap unit, got nil")
}
for _, want := range []string{"run_forecast_inference", "job_id", "model_id", "config", "3"} {
if !strings.Contains(err.Error(), want) {
t.Errorf("error %q missing expected substring %q", err.Error(), want)
}
}
}

// TestCheckInvokeSupportMultiArgWithRealArgTupleFine (calque#187) is the
// regression guard for the exact case that made a blanket arity refusal
// wrong: AI-Almanac's app.py's run_benchmark_local(job_id, config, bundle,
// runtime_env) takes 4 positional args, is invoked via .remote() (not
// .starmap()), yet calque#178 verified it running for real on real
// hardware — because `calque real --arg-file`/`--arg-json` supplies a REAL
// per-position tuple, bypassing the single-item-arg synthetic path
// entirely. hasRealArgTuple=true must never refuse, regardless of arity.
func TestCheckInvokeSupportMultiArgWithRealArgTupleFine(t *testing.T) {
rep := &leak.Report{}
fn := ir.Function{Name: "run_benchmark_local", Invoke: ir.InvokeRemote, Args: []string{"job_id", "config", "bundle", "runtime_env"}}
if err := checkInvokeSupport("script.py", fn, rep, true); err != nil {
t.Fatalf("checkInvokeSupport(hasRealArgTuple=true) must not refuse a multi-arg unit, got: %v", err)
}
}

// TestCheckInvokeSupportSingleArgNonStarmapFine proves the arity guard only
// fires on 2+ args — the overwhelmingly common single-arg case (every
// existing .map()/.remote()/plain-function corpus script) must stay
// unaffected, byte-for-byte unchanged from before calque#187.
func TestCheckInvokeSupportSingleArgNonStarmapFine(t *testing.T) {
rep := &leak.Report{}
for _, kind := range []ir.InvokeKind{ir.InvokeMap, ir.InvokeRemote, ir.InvokeNone, ir.InvokeSpawn} {
fn := ir.Function{Name: "f", Invoke: kind, Args: []string{"self", "item"}}
if err := checkInvokeSupport("script.py", fn, rep, false); err != nil {
t.Errorf("checkInvokeSupport(%q, 1 non-self arg) must not error, got: %v", kind, err)
}
}
}

// TestResolveEntrypointNoneDefined: a script with no @app.local_entrypoint()
// has nothing to select — not an error, unless one was explicitly requested.
func TestResolveEntrypointNoneDefined(t *testing.T) {
Expand Down
2 changes: 1 addition & 1 deletion cmd/calque/realrun.go
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,7 @@ func realRun(o realOpts) (err error) {
var dockerfileText string
body := calexec.ManifestBody{EnterBody: realEnterBody, MethodBody: realMethodBody, MethodArg: "prompt"}
if scriptBody, ok := manifestBodyForUnit(app, unit, rep); ok {
if err := checkInvokeSupport(app.Script, unit.method, rep); err != nil {
if err := checkInvokeSupport(app.Script, unit.method, rep, forceStarmap); err != nil {
return err
}
// GPU guard parity with dry-run (run.go's swapLegal check, calque#7/§7):
Expand Down
34 changes: 32 additions & 2 deletions cmd/calque/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ func run(o runOpts) error {
// protocol) and always collects+returns a result. .starmap/.for_each are
// classified at the IR layer but were silently run as if .map'd — check
// explicitly rather than let the mismatch surface as a mystery failure.
if err := checkInvokeSupport(app.Script, unit.method, rep); err != nil {
if err := checkInvokeSupport(app.Script, unit.method, rep, false); err != nil {
return err
}

Expand Down Expand Up @@ -556,7 +556,18 @@ func swapLegal(glog *gpu.Log, owner string) bool {
// .starmap'd unit with no statically-resolvable iterable" — the one case
// realOrSyntheticItems (items.go) can't make splat-safe on its own, because
// there is no real per-item shape to consult.
func checkInvokeSupport(script string, fn ir.Function, rep *leak.Report) error {
// hasRealArgTuple is true only at realrun.go's own call site, when the
// caller already resolved a REAL per-position argument tuple via
// --arg-file/--arg-json (forceStarmap) — that path splats every position
// from real, caller-supplied data regardless of what static parsing
// detected, so the generic arity guard below would wrongly refuse an
// already-solved case (e.g. AI-Almanac's app.py: run_benchmark_local takes
// 4 positional args, invoked via .remote() not .starmap(), but calque#178
// verified it running for real on real hardware precisely BECAUSE
// --arg-file/--arg-json supplied the real tuple). Every other caller
// (dry-run, fleetrun, and realrun.go itself when --arg-file/--arg-json
// weren't given) passes false.
func checkInvokeSupport(script string, fn ir.Function, rep *leak.Report, hasRealArgTuple bool) error {
switch fn.Invoke {
case ir.InvokeStarmap:
if len(fn.Items) == 0 {
Expand All @@ -567,6 +578,25 @@ func checkInvokeSupport(script string, fn ir.Function, rep *leak.Report) error {
case ir.InvokeForEach:
rep.Addf(leak.PrimMap, leak.KindSemanticGap, script, fn.Line,
"%s is .for_each()'d (side-effects only, no result collection in real Modal); calque collects+reports a result per item anyway — harmless but not a faithful .for_each", fn.Name)
default:
// calque#187: outside .starmap() (which already knows how to splat a
// real tuple across every positional arg) and outside
// --arg-file/--arg-json's own real-tuple path (hasRealArgTuple), the
// warm runner binds exactly ONE positional value per item
// (unit.method.ItemArg, the first non-self/cls param) —
// dryRunWarm/manifestBodyForUnit never populate the rest. A picked
// unit with 2+ non-self/cls positional args or (e.g. a
// .spawn()-invoked function like
// `run_forecast_inference(job_id, model_id, config)`, real-world
// case: AI-Almanac's forecasts_app.py) silently NameErrors on every
// item instead of refusing with an honest message — found live via
// calque#79's re-verification.
if hasRealArgTuple {
break
}
if args := nonSelfArgs(fn.Args); len(args) > 1 {
return fmt.Errorf("%s takes %d positional args (%s) but isn't .starmap()'d — the warm runner only binds the first (%q) per item; the rest would be undefined. This function's real invocation shape (multiple positional args, likely via .spawn() fan-out) isn't reproduced by calque's single-arg warm-unit model unless driven via --arg-file/--arg-json (a real, caller-supplied tuple) — see calque#187", fn.Name, len(args), strings.Join(args, ", "), args[0])
}
}
return nil
}
Expand Down
2 changes: 1 addition & 1 deletion cmd/calque/starmap_e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ func TestStarmapEndToEndDryRun(t *testing.T) {
}

// checkInvokeSupport must NOT refuse now that real tuple data exists.
if err := checkInvokeSupport(app.Script, unit.method, rep); err != nil {
if err := checkInvokeSupport(app.Script, unit.method, rep, false); err != nil {
t.Fatalf("checkInvokeSupport must not refuse a .starmap unit with real Items, got: %v", err)
}

Expand Down
Loading