Skip to content

feat(compiler): accept truthy filter predicates - #153

Open
jimmyhmiller wants to merge 6 commits into
mainfrom
split/filter-truthy-predicate
Open

feat(compiler): accept truthy filter predicates#153
jimmyhmiller wants to merge 6 commits into
mainfrom
split/filter-truthy-predicate

Conversation

@jimmyhmiller

Copy link
Copy Markdown
Contributor

Summary

  • accept statically representable non-boolean Array.prototype.filter predicate results and lower them through ToBoolean
  • retain a clear fence for void predicates, including static, island-backed, and checked-dynamic arrays
  • add differential and diagnostic coverage for truthiness and the unsupported void ABI

Validation

  • pnpm exec vitest run tests/harness/diagnostics.test.ts
  • pnpm exec vitest run tests/harness/differential.test.ts --testNamePattern "2683-filter-truthy-predicate"
  • pnpm exec vitest run packages/compiler/test/ts7/order-parity.test.ts
  • final reviewer pass: no blocking findings

jimmyhmiller and others added 6 commits August 14, 2026 10:27
`.filter(fn)` required the callback to return exactly bool. JS applies
ToBoolean to whatever the predicate answers, so `xs.filter((s) => s)` — the
idiomatic drop-the-falsy — was refused for no semantic reason.

The filter loop now wraps the call in the same toBool an `if` statement would
apply. A bool answer is unchanged and emits identical IR. A union answer
routes through its interned per-arm truthy helper, requested at the call site
where a real node exists for the diagnostic. A unit-only answer is constantly
falsy, and the call still runs for its effects.

The kinds with no native ToBoolean keep the fence: void has no value at all,
and dyn/jsval/caught truthiness needs the embedded engine.

Verified against Node: filtering strings, numbers, and a
`(string | undefined)[]` all print identically in both.

Diagnostics 100, filter corpus 9 across both
backends.
@vercel

vercel Bot commented Aug 14, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
scriptc Ready Ready Preview Aug 14, 2026 2:28pm

L.unsupported("SC1090", call, "spread arguments in calls through 'unknown' values");
}
const args = call.arguments.map((a) => L.lowerExprExpecting(a, DYN));
const predicate = name === "filter" ? L.lowerExpr(call.arguments[0]!) : null;

@vercel vercel Bot Aug 14, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.filter() with zero arguments on a checked-dynamic array crashes the compiler (uncaught ICE) instead of emitting a diagnostic, because L.lowerExpr(call.arguments[0]!) is called with undefined.

Fix on Vercel

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Having some agents make sure we thoroughly fix this.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On it — I've kicked off a thorough re-verification of the fix. It's checking three things:

  1. That the one-line guard actually removes the ICE for zero-arg .filter() on a checked-dynamic array in a .js file, and that the resulting empty-arg dynInvoke surfaces the arity error at runtime (matching other DYN_DISPATCH_METHODS) rather than silently producing wrong output.
  2. Sibling paths from this PR: the jsval/island branch (loweredArgs[0]?.type.kind), the static lowerArrayHofCall guard, and non-function / optional-chained (?.filter()) first-argument cases.
  3. Whether any additional guard beyond the single-line dyn-path patch is needed to make this comprehensive.

I'll follow up here with the results.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thorough re-verification is done — the single-line guard is sufficient and comprehensive. Findings:

The fix works. Guarding on call.arguments[0] makes zero-arg filter() yield predicate === null, so the call rides dynInvoke with an empty arg list and the dyn engine surfaces the arity error at runtime — same as every other DYN_DISPATCH_METHODS entry. No silent wrong output. The truthiness check also narrows call.arguments[0] to ts.Expression, so dropping the ! still type-checks.

Sibling paths are all safe — no additional guards needed:

  • jsval/island path (~L157): uses loweredArgs[0]?.type.kind and maps over call.arguments, so zero args is a no-op — no crash.
  • static path lowerArrayHofCall (L647): already guards if (!argNode) L.unsupported("SC1090", call, "this call form").
  • optional-chained ?.filter(): excluded by the existing !call.questionDotToken && !access.questionDotToken condition, so it never reaches this branch.
  • non-function first arg: only skips the void-predicate diagnostic, then coerceInto handles it — no crash.

One caveat: the sub-agent couldn't run tsc in its sandbox (deps not installed), but the narrowing is standard TS control-flow and the change is minimal. The patch applies cleanly. Confident this is the complete fix.

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.

2 participants