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’sconsts 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 onlyfold()is given the registry that says so. EVL has none, so it still flags such a call in a resource’s props even though--foldreduces 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, butfold()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 laziness —
fold()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 tofold(), never the reverse. - Composite step access (chant #1544) —
Checkout({...}).stepand the same shape from any other single-actionComposite()wrapper (a call immediately narrowed to.step) is the documented idiom for embedding one inline in aJob’ssteps: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.
EVL001: Non-Literal Expression
Section titled “EVL001: Non-Literal Expression”Resource property values must be statically evaluable. Function calls, method calls, and other dynamic expressions are not allowed.
// ❌ Triggers EVL001export const bucket = new _.Resource({ bucketName: getName(), tags: Object.assign({}, baseTags),});// ✅ Fixedconst 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" })],});EVL002: Control Flow Wrapping Resources
Section titled “EVL002: Control Flow Wrapping Resources”Resource instantiation cannot appear inside control flow blocks (if, for, while, switch, try). Resources must be declared at the top level.
// ❌ Triggers EVL002if (env === "prod") { export const bucket = new _.Resource({ bucketName: "prod-data", });}// ✅ Fixedexport const bucket = new _.Resource({ bucketName: "my-data",});EVL003: Dynamic Property Access
Section titled “EVL003: Dynamic Property Access”Computed property access with a non-literal key cannot be evaluated statically.
// ❌ Triggers EVL003const region = config[key];// ✅ Fixedconst region = config["production"];EVL004: Spread From Non-Const Source
Section titled “EVL004: Spread From Non-Const Source”The spread operator requires the source to be traceable to a const declaration.
// ❌ Triggers EVL004export const bucket = new _.Resource({ ...getDefaults(),});// ✅ Fixedconst defaults = { versioning: true, encryption: "AES256" };
export const bucket = new _.Resource({ ...defaults,});EVL005: Block Body in resource() Callback
Section titled “EVL005: Block Body in resource() Callback”Callbacks passed to resource() must use expression body syntax. Block bodies with return statements cannot be statically evaluated.
// ❌ Triggers EVL005resource(_.Resource, (props) => { const name = props.name + "-data"; return { bucketName: name };});// ✅ Fixed — use expression bodyresource(_.Resource, (props) => ({ bucketName: props.name + "-data",}));EVL007: Invalid Siblings Access
Section titled “EVL007: Invalid Siblings Access”In composite resources, accessing siblings.key.attr where key does not exist in the resource map is an error.
// ❌ Triggers EVL007resource(_.ServiceType, (props, siblings) => ({ role: siblings.typo.arn,}));// ✅ Fixedresource(_.ServiceType, (props, siblings) => ({ role: siblings.executionRole.arn,}));