Skip to content

Evaluability Rules (EVL)

Evaluability rules flag TypeScript patterns that the chant evaluator cannot statically resolve. All evaluability rules have severity error.

These are a diagnostic layer over one specific enforcement mechanism, not a guarantee that holds regardless of it. EVL001 and EVL003 share a single subset definition with fold(), the static AST reducer chant build uses by default (see TypeScript as Data) — so a shape either rule flags is, with a few environment-dependent exceptions below, exactly a shape the folder rejects, at the same line. EVL exists to catch it early — in your editor and in chant lint, before a build — while fold() is what actually enforces it. A file the folder rejects is not a build error: it falls back to being imported and run, and EVL is what keeps that run inside the deterministic subset.

Where EVL and fold() diverge. Both import the same classifier (fold/subset.ts) so they can’t drift on node kinds or operators, but several things are inherently environment-dependent and can’t be decided from shape alone:

  • Identifier resolution — EVL treats any bare name as valid; fold() alone resolves it against the file’s consts and rejects an unbound one. A false negative on EVL’s part, never the reverse.
  • Intrinsic tag registration — EVL has no lexicon registry at lint time, so it can’t tell a registered intrinsic tagged template from a made-up one; it treats any tagged-template interior as opaque. fold() has the registry and rejects an unregistered tag.
  • Intrinsic call-form registration — an intrinsic its lexicon opted into call-form folding (Ref(...), Concat(...)) folds, but only fold() is given the registry that says so. EVL has none, so it still flags such a call in a resource’s props even though --fold reduces it — the one place EVL is stricter than the folder rather than more permissive. The classifier takes the registry as an optional parameter, so a tool that has one gets the exact answer; wiring it into the lint engine is not done today.
  • Spread-source runtime type — EVL (via EVL004) checks that a spread source is traceable to a const; fold() additionally rejects it if that const turns out not to be an object or array at all.
  • A nested new Type(...) used as a property value — shape-valid to EVL either way, but fold() rejects a nested resource constructor (as opposed to the top-level one being folded) as a value, because it can’t be represented without actually constructing it — the file falls back to run.
  • Short-circuit lazinessfold() evaluates &&/||/??/? : lazily, so an unfoldable untaken branch doesn’t reject. EVL has no notion of “taken” and requires every branch to be shape-valid — a false positive relative to fold(), never the reverse.
  • Composite step access (chant #1544) — Checkout({...}).step and the same shape from any other single-action Composite() wrapper (a call immediately narrowed to .step) is the documented idiom for embedding one inline in a Job’s steps: array. EVL001 treats it as shape-valid; fold() still can’t fold it (a call is not a value it can reduce) and falls the file back to the run path, exactly as documented — that fallback was never an error, only EVL001’s flagging of it was.

None of these change what EVL catches in practice; they’re the edge cases where a passing lint doesn’t guarantee a fold, or a stricter EVL flag doesn’t mean fold() would actually have rejected the code.


Resource property values must be statically evaluable. Function calls, method calls, and other dynamic expressions are not allowed.

// ❌ Triggers EVL001
export const bucket = new _.Resource({
bucketName: getName(),
tags: Object.assign({}, baseTags),
});
// ✅ Fixed
const name = "my-data-bucket";
const tags = { ...baseTags, env: "prod" };
export const bucket = new _.Resource({
bucketName: name,
tags: tags,
});
// ✅ Also allowed — a Composite() wrapper's `.step` output embedded inline
// (chant #1544; see "Where EVL and fold() diverge" above)
export const build = new Job({
"runs-on": "ubuntu-latest",
steps: [Checkout({}).step, new Step({ name: "Test", run: "npm test" })],
});

Resource instantiation cannot appear inside control flow blocks (if, for, while, switch, try). Resources must be declared at the top level.

// ❌ Triggers EVL002
if (env === "prod") {
export const bucket = new _.Resource({
bucketName: "prod-data",
});
}
// ✅ Fixed
export const bucket = new _.Resource({
bucketName: "my-data",
});

Computed property access with a non-literal key cannot be evaluated statically.

// ❌ Triggers EVL003
const region = config[key];
// ✅ Fixed
const region = config["production"];

The spread operator requires the source to be traceable to a const declaration.

// ❌ Triggers EVL004
export const bucket = new _.Resource({
...getDefaults(),
});
// ✅ Fixed
const defaults = { versioning: true, encryption: "AES256" };
export const bucket = new _.Resource({
...defaults,
});

Callbacks passed to resource() must use expression body syntax. Block bodies with return statements cannot be statically evaluated.

// ❌ Triggers EVL005
resource(_.Resource, (props) => {
const name = props.name + "-data";
return { bucketName: name };
});
// ✅ Fixed — use expression body
resource(_.Resource, (props) => ({
bucketName: props.name + "-data",
}));

In composite resources, accessing siblings.key.attr where key does not exist in the resource map is an error.

// ❌ Triggers EVL007
resource(_.ServiceType, (props, siblings) => ({
role: siblings.typo.arn,
}));
// ✅ Fixed
resource(_.ServiceType, (props, siblings) => ({
role: siblings.executionRole.arn,
}));