Skip to content

feat!: require create_job's context to cover the template's extensions - #407

Merged
mwiebe merged 8 commits into
OpenJobDescription:mainfrom
mwiebe:fix/create-job-profile-contract
Sep 24, 2026
Merged

mwiebe merged 8 commits into
OpenJobDescription:mainfrom
mwiebe:fix/create-job-profile-contract

Conversation

@mwiebe

@mwiebe mwiebe commented Sep 23, 2026

Copy link
Copy Markdown
Contributor

What was the problem/requirement? (What/Why)

Background: templates are validated against a ValidationContext —
the spec revision, the enabled extensions (like EXPR, the expression
language), and any caller limits. create_job takes the same context
when it instantiates a job from a validated template. Since #404 it
also re-checks the template's carried-forward strings (action
command/args, environment variables, embedded-file data) with
the real parameter values bound, catching value-dependent failures at
submission instead of on every worker.

create_job documented that passing a context whose extensions
differ from what the template declares is supported policy — e.g.
"strip EXPR even if the template requests it". That one allowance
poisoned the new checks: with a mismatched context, an evaluation
error could mean either "the template is broken" or "the context
turned a feature off", so the checks had to silently skip all
evaluation errors except budget exceedances. Silent skips are
unobservable — a field that errored looks identical to a field that
passed — and the special-casing this required was itself buggy.

What was the solution? (How)

Remove the allowance and enforce the coherent contract: the context's
revision must match the template's, and its extensions must cover
everything the template declares (more is fine; fewer is a
Compatibility error). An application that doesn't support an
extension already rejects such templates at decode via
supported_extensions — no production caller passes a mismatched
context.

With mismatch impossible, every evaluation error at job creation is a
real defect, so the checks now report them all, and the skip machinery
is deleted. That strictness immediately surfaced two latent bugs it
had been masking, both fixed here:

  • Budgets bypassed inside conditionals. For x if test else y
    with a test only a worker can resolve (Session.*), the evaluator
    runs both branches and absorbs a single failing branch (run time may
    never take it). It also absorbed memory/operation budget errors —
    but those are spent in this evaluation no matter which branch run
    time takes, so a caller's lowered budget silently stopped applying.
    Budget errors now propagate; value errors are still absorbed.
  • Path-format mismatch on Windows. create_job resolves
    everything (including let bindings) with POSIX-format paths, but
    the re-checks evaluated under the host format — so a path value
    flowing from a let binding into an arg drew a "Path format
    mismatch" error on Windows. The checks now evaluate under POSIX to
    match the values they read.

Also, the symbol-table seeding for these checks no longer discards
set failures (let _ =) — they propagate as ModelError, never a
panic, so language bindings surface them as exceptions.

What is the impact of this change?

Callers passing a context derived from the template (the norm,
via default_validation_context() or equivalent) see no change.
Evaluation errors that depend on parameter values now fail
create_job instead of every session that runs the job. Lowered
evaluation budgets are enforced inside unresolved conditionals, the
idiomatic construction.

How was this change tested?

  • cargo clippy --all-features --all-targets --workspace -- -D warnings clean.
  • cargo test for expr/model/cli green, including new tests: contract
    rejection message, value-dependent evaluation errors reported at
    create_job, budget propagation out of unresolved conditionals
    (pinned at both the expr and model level), value-error absorption
    control, and path-format agreement.
  • Conformance suite: 1139/1139.

Was this change documented?

Yes — create_job and FsEval docs, specs/model/job-creation.md
(error policy, ctx contract), specs/model/public-api.md, and
specs/expr/evaluator.md (IfExp absorption and its budget exemption).

Is this a breaking change?

Yes. create_job returns a Compatibility error when the context's
revision differs from the template's or its extensions don't cover the
template's declared ones. Callers deliberately stripping extensions at
job creation must instead reject the template at decode via
supported_extensions.

Does this change impact security?

No new files or permissions. Enforcement-wise it strengthens the
caller-limits story: evaluation budgets can no longer be bypassed via
unresolved-test conditionals.


By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.

@mwiebe
mwiebe requested a review from a team as a code owner September 23, 2026 00:38
Comment thread crates/openjd-model/src/template/validate_v2023_09/format_strings.rs Outdated
Comment thread crates/openjd-expr/src/eval/evaluator.rs
@wyongzhi

Copy link
Copy Markdown
Contributor

This PR makes create_job stricter: if you hand it a context that enables fewer extensions than the template itself declares, it now refuses with a Compatibility error instead of quietly running. That matters because #404 taught create_job to re-validate commands, env vars and embedded files after the real parameter values are plugged in, and that re-validation only makes sense if the context is at least as wide as what the template asked for. Every caller we could find already builds its context from the template (CLI, JS bindings, the Python bindings' default, and Deadline Cloud's DataPlane which passes none), so nothing on our side breaks.

wyongzhi
wyongzhi previously approved these changes Sep 23, 2026
@leongdl

leongdl commented Sep 23, 2026 •

Copy link
Copy Markdown
Contributor

Regression: strict eval errors at create_job reject a template that openjd check passes and that runs

The contract change and the two bug fixes look right to me, and the POSIX switch matches what
openjd-model-for-python's _let_bindings.py does for the same stated reason. One problem with
making the re-checks strict: Unresolved propagation is not complete, so a valid template is now
refused at submission.

Repro

specificationVersion: jobtemplate-2023-09
extensions: [EXPR]
name: LcFilter
parameterDefinitions:
  - name: Files
    type: STRING
steps:
  - name: Render
    parameterSpace:
      taskParameterDefinitions:
        - name: Skip
          type: STRING
          range: ["b"]
    script:
      actions:
        onRun:
          command: echo
          args: ["{{ [f for f in Param.Files.split(',') if f != Task.Param.Skip] }}"]

Built openjd-cli at 70cd40b and at f0c5601, same template, same inputs:

base 70cd40b this PR f0c5601
openjd check passes passes
openjd summary -p Files=a,b,c (runs create_job) succeeds fails
openjd run -p Files=a,b,c --step Render succeeds, prints a c blocked by create_job
ERROR: Model validation error: 1 validation error for JobTemplate
steps[0] -> script -> actions -> onRun -> args[0]:
	Failed to parse interpolation expression at [0, 65]. List comprehension filter must be a boolean, got unresolved[bool]
  [f for f in Param.Files.split(',') if f != Task.Param.Skip]
                                        ^~~~~~~~~~~~~~~~~~~~

Mechanism

eval_listcomp has two paths and they disagree about Unresolved filters.

The unresolved-iterable path accepts one explicitly — is_bool_compatible tests
cond_inner.code() == TypeCode::Unresolved alongside BOOL, Any and a BOOL-containing
union. The concrete-iterable path does not: it requires ExprValue::Bool and errors otherwise.

Which path runs depends on the iterable, and the iterable's concreteness changes per stage:

  • pass 8: Param.Files is Unresolved, so the iterable is unresolved, tolerant path, Ok.
  • create_job: Param.Files is bound and Task.Param.Skip is not, so the iterable is concrete
    and the filter is unresolved[bool]. Strict path, Err.
  • run time: everything is bound, filter is a concrete bool, Ok.

So the failure is specific to a symbol-table state that exists at no other stage — Param.*
concrete, Task.*/Session.* unresolved. The error kind is Other(..), which is why the
report_eval_errors early-return this PR deletes was swallowing it. The defect predates the PR;
this PR makes it reachable.

eval_boolop already takes the position I think eval_listcomp should take here, and
specs/expr/evaluator.md states it: "Errors in operands past the unresolved one are suppressed,
since a runtime short-circuit could make them unreachable." eval_ifexp says the same thing one
section down. eval_listcomp's concrete-iterable filter is the one place an unresolved value
produces a hard error instead of an Unresolved result.

This falsifies one of the new spec sentences

specs/model/job-creation.md, under Error policy:

so an evaluation error at this stage cannot be a context artifact: it is either a template
defect pass 8 missed or a deterministic value-dependent failure that every session resolving
the field would hit.

The repro above is a third thing. Not a template defect — pass 8 accepts it, correctly, because at
run time the filter is a concrete bool. Not a failure every session would hit — every session
resolves it fine. It is an artifact of partial resolution, an axis the context contract does not
address.

I would not fix this by rewording the sentence alone. That leaves a template that decodes, would
run, and is rejected at submission. The narrow code fix: on the concrete-iterable path, when the
filter evaluates to Unresolved, abandon the accumulated result and return
unresolved(list[body_type]) — per-element inclusion is undecidable, so the whole comprehension is
unknown, which is what the unresolved-iterable path already concludes via
unresolved_list_from_elements. Around ten lines, plus keeping absorb_counters and the
regex_cache restore on the new exit path. Then the sentence holds, with the caveat that it is
asserting an exhaustive disjunction and a future partial-resolution asymmetry would falsify it
again.

Scope, honestly

I looked for how often this triggers and found nothing. Across the repo's fixtures, tests and spec
examples, deadline-cloud-samples, and openjd-specifications, there are nine distinct
comprehension-with-filter forms and all nine filter on the loop variable or on Param.*, both
concrete at job creation. None references Task.* or Session.*. So this is a verified regression
with no template on hand that trips it.

What makes me raise it anyway is the spec sentence rather than the reachability. A documented
invariant that is false is what stops the next reader checking.

The other finding is separate

eval_boolop did not get the budget exemption eval_ifexp got, so the bypass survives behind an or. Split out into its own comment: #407 (comment)

Verified and not verified

Verified at f0c5601: cargo fmt --check clean, cargo clippy --all-features --all-targets --workspace -- -D warnings clean, workspace suite green. Both items above reproduced by running
code, base against tip.

Not verified: anything on Windows or macOS, so I could not exercise the path-format fix — note its
test is a no-op on Linux and macOS, since PathFormat::host() is Posix there. Reverting
path_format to host() leaves that test green on two of the three CI lanes. It does fail on
windows-latest, which the matrix runs, so the fix is pinned in CI but not on any Linux dev
machine. I also did not re-run the conformance suite or the mutation checks.

@leongdl

leongdl commented Sep 23, 2026

Copy link
Copy Markdown
Contributor

eval_boolop did not get the budget exemption that eval_ifexp did

The budget fix landed in eval_ifexp only. eval_boolop suppresses errors on the identical
rationale and still absorbs a budget exceedance, so the bypass this PR closes is reachable by
moving the expensive subexpression behind an or, and wrapping the newly-fixed eval_ifexp in
one or re-absorbs the error it just propagated.

Example

Probed against this branch, Session.Flag seeded as Unresolved(BOOL), memory budget 1 MiB,
'A' * 10000000 costing ~10 MB:

expression result
'A' * 10000000 if Session.Flag else 'B' propagates — the arm this PR tests
'B' if Session.Flag else 'A' * 10000000 propagates — mirror arm, no test
Session.Flag or ('A' * 10000000 == 'x') absorbed, peak_memory 264
Session.Flag and ('A' * 10000000 == 'x') absorbed
Session.Flag or (('A' * 10000000 if Session.Flag else 'B') == 'x') absorbed
int('nope') if Session.Flag else 7 absorbed — correct, the control

Why

eval_boolop's suppression arm is

Err(_) => { /* suppressed — unresolved might short-circuit */ }

and specs/expr/evaluator.md gives the reason: "Errors in operands past the unresolved one are
suppressed, since a runtime short-circuit could make them unreachable." That holds for a value
error and not for a budget, for exactly the reason contains_budget_error's new doc comment gives
for IfExp — the memory and operations were spent in this evaluation no matter which operand run
time would reach. peak_memory reporting 264 bytes after a 10 MB allocation is the same accounting
gap seen from the other side.

Three lines: the same contains_budget_error check in that arm. Worth doing here because the PR
description states budgets can no longer be bypassed via unresolved-test conditionals, and a test
for it would also close the (Ok(b), Err(oe)) gap above — both existing budget tests put the
failure in the if-branch, so deleting that arm's early return leaves the suite green.

Separately in the same area, eval_listcomp returns child.eval_node(..)? before
self.absorb_counters(&child), so a failing comprehension's child-evaluator spend is discarded —
the same "cost already spent" leak, for the case eval_ifexp deliberately absorbs. Not part of
this PR's scope; noting it so it is written down somewhere.

create_job documented that passing a ValidationContext whose
extensions differ from the template's declared ones is supported
application-level policy (e.g. "strip EXPR even if the template
requests it"). That contract forced the job-creation resolved-value
checks into a lenient error policy: an evaluation error could be a
context artifact rather than a template defect, so all evaluation
errors except budget exceedances were silently skipped - which in turn
required budget-kind special-casing and left "field errored and was
skipped" indistinguishable from "field checked and passed".

The contract is enforced instead: the context's revision must match
the template's, and its extensions must cover every extension the
template declares (enabling more is allowed) - a Compatibility error
otherwise. An application that does not support an extension already
rejects the template at decode via supported_extensions, and no
production caller diverges (CLI, for-js, and the tests all derive the
context from the template). With the ambiguity gone, the job-creation
checks report evaluation errors exactly as template validation's pass 8
does, deleting the lenient policy and the special-casing outright.

The strictness surfaced two latent defects, both fixed:

- The evaluator absorbed a failing branch of an unresolved-test
  conditional into an Unresolved success even when the failure was a
  budget exceedance, so a lowered evaluation budget silently stopped
  applying inside conditionals (e.g. "{{ 'A' * Param.N if
  Session.HasPathMappingRules else 'B' }}" with huge N was accepted).
  Budget errors now propagate out of the absorption; value errors are
  still absorbed, since run time may select the healthy branch, but
  the budget was spent in this evaluation either way.
- The job-creation checks evaluated under the host path format while
  create_job builds its check symtabs under PathFormat::Posix (the
  uniform format for everything it resolves, including the let
  bindings seeded into those symtabs), so Posix path values drew
  "Path format mismatch" errors on Windows - previously masked by the
  lenient policy, exposed by 11 conformance failures under the strict
  one. The checks now evaluate under Posix to match their symtabs.

Also: the check-symtab seeding no longer discards SymbolTable::set
results - a failed seed silently degraded a check to a no-op. Failures
propagate as ModelError (never a panic), matching how create_job
already handles its other symtab.set calls; the seed keys are
uppercase-rooted and let-binding names must start lowercase, so this
only fails if an internal invariant is broken.

Verification: clippy clean; expr/model/cli suites green including new
tests (contract rejection, value-dependent evaluation errors reported
at create_job, budget propagation out of unresolved conditionals at
both expr and model level, path-format agreement); conformance
1139/1139.

BREAKING CHANGE: create_job returns a Compatibility error when the
context's revision differs from the template's or its extensions do
not cover the template's declared extensions. Callers deliberately
stripping extensions at job creation must instead reject the template
at decode via supported_extensions.

Signed-off-by: Mark <399551+mwiebe@users.noreply.github.com>
@mwiebe
mwiebe force-pushed the fix/create-job-profile-contract branch from f0c5601 to 3198c91 Compare September 23, 2026 22:15
mwiebe added a commit to mwiebe/openjd-rs that referenced this pull request Sep 23, 2026
The review's two code findings (eval_listcomp unresolved-filter
regression, eval_boolop budget suppression) were fixed on the PR
branch; these record what remains: the listcomp child-counter
discard on error paths, an audit of the remaining operators for
incomplete Unresolved propagation, and the rule that any future
error-absorption site must carry the budget exemption.
Post-review follow-ups to the create_job profile-contract change:

- The CLI (run, summary) and openjd-for-js each hand-rolled the
  ValidationContext for create_job - re-parsing the template's
  extensions list with a hardcoded 2023-09 revision. Now that
  create_job enforces its context contract, three drift-prone copies
  become three calls to job_template.default_validation_context().

- The CLI decoded templates under common::caller_limits() (the 32K
  resolved-arg cap and eval budgets) but ran create_job with default
  limits - exactly the trap the default_validation_context() docs
  warn about. Both CLI call sites now layer the same limits onto the
  create_job context, so a param-dependent arg blowup fails at job
  creation instead of mid-session. Sessions already enforce the same
  caps at run time, so this moves detection earlier without adding a
  new restriction. specs/cli/run.md's enforcing-stages paragraph now
  covers all three stages.

- value_dependent_evaluation_error_fails_at_create_job asserted only
  the field path; it now pins the full diagnostic (message,
  expression, caret) per the repo's error-message test standard.

Verification: clippy clean (cli, for-js, model); model and cli suites
green; conformance 1139/1139 with the release CLI; openjd-for-js
checks clean on wasm32-unknown-unknown.

Signed-off-by: Mark <399551+mwiebe@users.noreply.github.com>
Everything the model evaluates outside host context runs under
PathFormat::Posix - that was already true of every evaluation
create_job performs, but pass 8 was the outlier: its format strings
evaluated under the host format, and validate_let_bindings under the
evaluator's default (also host), while the step-level let bindings
feeding range expressions hardcoded Posix. The mixture was masked
because each pass-8 site happened to agree with the symtab values it
read, but the agreement was incidental - and the FsEval::path_format
doc justified host() with 'pass 8's symtabs hold no concrete path
values', which is wrong (a static let binding like
out = path('/x') puts a concrete host-format path value in the
pass-8 symtab).

Pass 8 now evaluates under Posix everywhere: FsEval applies it in
both options() (format strings) and budgeted() (let bindings), which
also makes the range-let-binding site's explicit override redundant.
With the strict-error-policy split gone (d2e09ea) the path format was
the last difference between FsEval::new and FsEval::for_job_creation,
so the field and the second constructor are deleted outright - pass 8
and the job-creation re-checks now construct identically.

Besides consistency with job creation, this makes validation outcomes
independent of the OS running them: a template accepted by check on
Linux is accepted on Windows and vice versa. Documented as a Path
Format section in specs/model/validation.md.

Verification: clippy clean; model suites green; conformance 1139/1139
on Windows - the platform where host and POSIX formats differ.

Signed-off-by: Mark <399551+mwiebe@users.noreply.github.com>
…gation

Two review follow-ups from the create_job profile-contract change,
both comments only:

- create_job's revision-mismatch arm cannot fire while V2023_09 is
  the only SpecificationRevision variant, and no test can pin its
  error message until a second one exists. Say so at the check, so
  revision coverage doesn't silently go missing when one is added.

- contains_budget_error propagates a compound error whole when a
  budget exceedance sits among value sub-errors, rather than trimming
  to just the budget error. Document that as deliberate - the caller
  gets the failing branch's full diagnostic context.

Signed-off-by: Mark <399551+mwiebe@users.noreply.github.com>
…lement

Review of the strict create_job error policy found a regression: a
comprehension over a concrete iterable whose filter condition
evaluates to an unresolved value hard-errored ("List comprehension
filter must be a boolean, got unresolved[bool]") instead of
propagating Unresolved. The defect predates the strict policy - the
lenient policy's silent skip masked it - but strictness made it
reachable: job creation evaluates under a symbol state no other stage
sees (Param.* concrete, Task.*/Session.* unresolved), so a template
with

    args: ["{{ [f for f in Param.Files.split(',') if f != Task.Param.Skip] }}"]

passed openjd check (iterable unresolved - the tolerant path), ran
cleanly on workers (everything bound), and was rejected at create_job
- the one stage where the iterable is concrete and the filter is not.

eval_listcomp's two paths now agree, via a shared helper: when a
filter condition evaluates unresolved on a concrete element,
per-element inclusion is undecidable, so the accumulated elements are
abandoned (BudgetedVec only pre-checks the budget; the list is
tracked only by make_list_checked, which is never reached) and the
comprehension concludes unresolved(list[body_type]). The body type is
derived under an *unresolved* loop variable: evaluating the body on a
concrete element the runtime filter may exclude could raise a
spurious value error (e.g. [10 // x for x in [0, 2] if x > Task.N]).
A filter whose type can never be a boolean still errors on both
paths.

Also from the same review: eval_boolop suppressed *all* errors in
operands after an unresolved one, including budget exceedances - the
same bypass eval_ifexp had. Budget errors now propagate; value errors
are still suppressed (a runtime short-circuit may skip them).

The job-creation spec's claim that every evaluation error at this
stage is a template defect or a deterministic value-dependent failure
was falsified by the repro; it now states its real dependency - the
evaluator propagating Unresolved without error through every operator.

Verification: clippy clean; expr/model suites green including seven
new tests (unresolved filter over concrete list and range iterables,
body value-error shielding, non-bool unresolved filter still refused,
concrete-then-unresolved short-circuit filters, boolop budget
propagation + suppression control, and the reviewer's template
through create_job); the repro passes check/summary/run end to end;
conformance 1139/1139.

Signed-off-by: Mark <399551+mwiebe@users.noreply.github.com>
The boolop budget fix covered the or-case with one test; the review's
probe matrix identified three untested cases, now pinned:

- The else-branch (Ok, Err) arm of eval_ifexp - both existing budget
  tests put the failure in the if-branch, so deleting that arm's
  early return left the suite green.
- 'and' shares eval_boolop's suppression arm with 'or'.
- A boolop wrapping an unresolved-test conditional must not re-absorb
  the budget error the conditional just propagated - the exemptions
  compose.

Signed-off-by: Mark <399551+mwiebe@users.noreply.github.com>
Range-scope analogue of the job-creation path-format test, from an
earlier review round: step-level let bindings evaluate into the range
symbol table under PathFormat::Posix, and before the uniform-POSIX
change the range format strings reading them evaluated under the host
format - a path-valued binding referenced from a parameterSpace range
drew 'Path format mismatch' on Windows. Verified against the pre-fix
code on Windows: fails there, passes now. A no-op on Linux/macOS
(Posix == host); the Windows CI lane exercises it.

Signed-off-by: Mark <399551+mwiebe@users.noreply.github.com>
…naries

The job-creation error-policy text promised budget propagation only
'from inside an unresolved-test conditional' - written when IfExp was
the only absorption site with the exemption. With eval_boolop carrying
the same guard, state the guarantee in terms of the class (every
construct that absorbs errors under an unresolved operand) and cite
both IfExp and BoolOp, so the next absorption site added without the
exemption contradicts the spec instead of hiding behind it.

Signed-off-by: Mark <399551+mwiebe@users.noreply.github.com>
@mwiebe
mwiebe force-pushed the fix/create-job-profile-contract branch from 3198c91 to f39387d Compare September 24, 2026 00:20
@mwiebe
mwiebe merged commit db23321 into OpenJobDescription:main Sep 24, 2026
22 checks passed
@mwiebe
mwiebe deleted the fix/create-job-profile-contract branch September 24, 2026 01:47
err.kind(),
crate::error::ExpressionErrorKind::MemoryLimitExceeded { .. }
| crate::error::ExpressionErrorKind::OperationLimitExceeded { .. }
) || err.sub_errors().iter().any(contains_budget_error)

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.

Nit, not blocking — ship it.

The sub_errors() recursion here is load-bearing but has no test. I found it by
mutation: deleting the || err.sub_errors().iter().any(contains_budget_error) clause
leaves the entire workspace suite green, 7785 tests, including the five budget
tests added in f0152a5.

It is not dead code, and I want to be precise about that. The one construct that
attaches sub-errors is eval_ifexp's (Err, Err) arm, and a compound from there does
flow through a boolop. Measured, 1 MiB budget, Session.Flag unresolved:

Session.Flag or ('A' * 10000000 if Session.Flag else int('nope')) == 'x'
result
as shipped Err — "Both branches fail in the if/else: …" propagates
recursion clause deleted Ok(unresolved) — absorbed, budget silently bypassed

So the clause is exactly the fix for a real bypass, it just has no falsifying test.
The new boolop_does_not_reabsorb_budget_error_from_nested_conditional does not reach
it: its inner ifexp takes the (Err, Ok) arm, which returns a bare
MemoryLimitExceeded with no sub-errors, so the matches! arm answers first.

Suggestion, whenever convenient: the expression above as a test next to that one. The
risk otherwise is a future refactor deleting the clause with a green suite, which
reopens the bypass for exactly the compound case the new doc comment above describes.

One related gap for the backlog rather than this PR. specs/model/job-creation.md now
says budget errors "propagate out of every construct that absorbs errors under an
unresolved operand". eval_attribute can still defeat that: its dotted-path arm
rewrites a failed base evaluation as UndefinedVariable, and its property arm
discards the original into a fresh error, neither attaching sub-errors — so a budget
error raised inside either is invisible to contains_budget_error and gets absorbed
by an enclosing or. I have not constructed a reaching expression, so treat it as
plausible rather than confirmed. Worth either narrowing the spec sentence or attaching
sub-errors at those two sites.

For the record on the rest of the revision: both of my earlier findings are fixed and
I mutation-checked both. Reverting else if cond.is_unresolved() in eval_listcomp
fails five expr tests plus listcomp_with_task_param_filter_over_bound_param_passes_create_job;
reverting the contains_budget_error early return in eval_boolop fails three
test_memory tests. cargo fmt, clippy -D warnings and cargo test --workspace
(7785 passed, 0 failed) all clean at f39387d2.

The listcomp fix is also better-shaped than what I proposed. Re-deriving the body type
under an unresolved loop variable, rather than mirroring is_bool_compatible in place,
is what keeps [10 // x for x in [0, 2] if x > N] from raising at job creation — my
version would have got that wrong.

@github-actions github-actions Bot mentioned this pull request Sep 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants