Skip to content

feat(compiler): SYN025/SYN026 — warn on requestAnimationFrame/requestIdleCallback (scheduling bypass, closes #179)#181

Open
marcelofarias wants to merge 5 commits into
mainfrom
botkowski/syn025-026-raf-ric
Open

feat(compiler): SYN025/SYN026 — warn on requestAnimationFrame/requestIdleCallback (scheduling bypass, closes #179)#181
marcelofarias wants to merge 5 commits into
mainfrom
botkowski/syn025-026-raf-ric

Conversation

@marcelofarias

Copy link
Copy Markdown
Owner

Summary

  • Adds SYN025 to the compiler's syn-check pass: fires when requestAnimationFrame(cb) is called (bare, not preceded by ./?.) inside a ?bs 0.7+ fn body
  • Adds SYN026 for requestIdleCallback(cb) — same detection pattern
  • Both follow the SYN010 (setTimeout/setInterval/queueMicrotask) pattern exactly: callbacks scheduled by these globals run after the fn returns, making any effects in those callbacks invisible to the capability model
  • Detection excludes member calls (obj.requestAnimationFrame(...)), fn/function/function* declarations with those names, and object method shorthands
  • Suppressed inside unsafe {} and unsafe "reason" fn bodies
  • Wired through error-codes, MCP explain, tests, and docs
  • Closes feat(compiler): SYN025/SYN026 — warn on requestAnimationFrame/requestIdleCallback (scheduling bypass, like SYN010) #179

Relation to existing checks

Check Scheduling mechanism
SYN010 setTimeout / setInterval / queueMicrotask
SYN025 requestAnimationFrame (browser-only, pre-repaint)
SYN026 requestIdleCallback (browser-only, idle period)

Test plan

  • Fires on requestAnimationFrame(cb), requestAnimationFrame?.(cb) at ?bs 0.7+
  • Fires on requestIdleCallback(cb), requestIdleCallback?.(cb) at ?bs 0.7+
  • Does NOT fire: below ?bs 0.7, inside unsafe, member calls, fn declarations, generator declarations
  • Warning message includes fn name and detected call form
  • Example in examples/react-app/src/scheduling.bs
  • Full test suite passes (1500 tests across 55 test files)

🤖 Generated with Claude Code

…IdleCallback (scheduling bypass, closes #179)

requestAnimationFrame and requestIdleCallback schedule callbacks to run
after the current fn returns — the same invisible-capability problem as
setTimeout/setInterval/queueMicrotask (SYN010). Any effects inside those
callbacks are invisible to callers: no uses {}, reads {}, writes {}, or
throws {} declaration reflects them.

Detection mirrors SYN010: fires on bare calls not preceded by ./?.,
excludes fn/function/function* declarations, method shorthands, and
unsafe {} blocks. 17 tests across two descriptors. Wired through
error-codes registry, MCP explain, AGENTS.md, README.md, and a react-app
scheduling.bs example showing the safe alternative pattern.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Adds two new structural warnings to the compiler’s syn-check pass to flag browser scheduling APIs (requestAnimationFrame / requestIdleCallback) that defer work outside a function’s declared capability surface, and wires them through the diagnostic registry, MCP explain, tests, and docs.

Changes:

  • Implement SYN025 / SYN026 warnings in packages/compiler/src/passes/syn-check.ts for bare global calls in ?bs 0.7+ (suppressed in unsafe contexts, excludes member calls and declarations).
  • Register both diagnostics in packages/compiler/src/error-codes.ts and add MCP long-form explanations in packages/mcp/src/explanations.ts (+ tests updated).
  • Add compiler tests and a React example file documenting the recommended “extract work; caller schedules” pattern.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
README.md Adds SYN025/SYN026 to the documented MCP explain coverage list.
packages/mcp/tests/server.test.ts Extends the KNOWN_CODES allowlist to include SYN025/SYN026.
packages/mcp/src/explanations.ts Adds long-form explain entries and fails/passes examples for SYN025/SYN026.
packages/compiler/tests/syn025-026-check.test.ts New tests covering triggering + non-triggering scenarios for both warnings.
packages/compiler/tests/error-codes.test.ts Updates the compiler diagnostic allowlist to include SYN025/SYN026.
packages/compiler/src/passes/syn-check.ts Implements the new token-scan detection + warning emission for both globals.
packages/compiler/src/error-codes.ts Adds registry entries (rule/idiom/rewrite/example) for SYN025/SYN026.
examples/react-app/src/scheduling.bs Adds an example file describing the safe scheduling pattern.
AGENTS.md Documents SYN025/SYN026 in the diagnostic code table.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/compiler/src/passes/syn-check.ts Outdated
Comment on lines +28 to +33
it("fires SYN025 on requestAnimationFrame?.(cb)", () => {
const src =
"?bs 0.7\n" +
"fn scheduleRender(frame: number) -> void {\n" +
" requestAnimationFrame?.()\n" +
"}\n";
…g test

- Remove `SCHEDULING_GLOBALS` const (declared but never referenced — the
  `case "requestAnimationFrame": case "requestIdleCallback":` dispatch does
  not use it, making it dead code that could confuse future readers).
- Fix the `requestAnimationFrame?.(cb)` test: the source had no-arg form
  `requestAnimationFrame?.()` which doesn't match the test description;
  updated to use `requestAnimationFrame?.(cb)` with an actual callback arg
  and a typed parameter so the intent is clear.

Addresses Copilot review suggestions on PR #181.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 9 out of 9 changed files in this pull request and generated 3 comments.

Comment on lines +62 to +70
it("does NOT fire SYN025 inside unsafe {} block", () => {
const src =
"?bs 0.7\n" +
"fn scheduleRender() -> void {\n" +
" unsafe \"schedules animation frame\" { requestAnimationFrame(cb) }\n" +
"}\n";
const result = compile(src);
expect(result.warnings.some((w) => w.code === "SYN025")).toBe(false);
});
Comment on lines +176 to +184
it("does NOT fire SYN026 inside unsafe {} block", () => {
const src =
"?bs 0.7\n" +
"fn deferWork() -> void {\n" +
" unsafe \"schedules idle callback\" { requestIdleCallback(cb) }\n" +
"}\n";
const result = compile(src);
expect(result.warnings.some((w) => w.code === "SYN026")).toBe(false);
});
Comment on lines +196 to +202
it("does NOT fire SYN026 on function* requestIdleCallback() generator declaration", () => {
const src =
"?bs 0.7\n" +
"function* requestIdleCallback(cb: any) { yield 0 }\n";
const result = compile(src);
expect(result.warnings.some((w) => w.code === "SYN026")).toBe(false);
});
…regression tests

Copilot re-review flagged three gaps in coverage:
- unsafe "reason" fn suppression path not tested for either SYN025 or SYN026
- SYN026 missing: fn declaration, function declaration, and method-shorthand exclusion tests

Brings SYN025/026 test coverage in line with SYN010 (the precedent for this family).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 9 out of 9 changed files in this pull request and generated no new comments.

…and exclusion test for SYN025, bare-reference exclusion test for SYN026

Copilot flagged two missing coverage paths:
- SYN025 had no test for object method shorthands named requestAnimationFrame
- SYN026 had no test for bare reference (no call) which should not fire

Both SYN025 and SYN026 already had tests for unsafe fn body suppression and all
declaration exclusions (fn/function/function*); the previous-version Copilot
comments about missing those are resolved by existing code.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 9 out of 9 changed files in this pull request and generated 3 comments.

Comment thread packages/compiler/src/error-codes.ts Outdated
"Any effects inside `cb` are invisible to callers: no capability declaration, no `writes {}` label, no `throws {}` entry reflects them. " +
"The fn appears to return nothing; the real work happens asynchronously in a future animation frame.",
idiom:
"pass the work as a return value the caller can schedule, or wrap in " +
Comment thread packages/compiler/src/error-codes.ts Outdated
"fn deferCleanup() uses { fs } -> void {\n" +
" requestIdleCallback(() => fs.delete(\"/tmp/cache\")) // SYN026\n" +
"}\n\n" +
"// after — return the work; caller decides when to schedule it\n" +
Comment on lines +3 to +7
// SYN025/SYN026 — safe scheduling pattern
// requestAnimationFrame and requestIdleCallback schedule callbacks outside
// the capability surface. The fix: extract the side-effectful work into
// a separately declared fn, then let the caller decide when to schedule it.

… wire scheduling.bs into example build

- SYN025 idiom: "pass as return value" → "extract into a separate fn the caller passes to requestAnimationFrame"
- SYN026 rewrite: "return the work" → "extract the work into a separate fn"
- Import scheduling.bs from main.tsx so vite compiles the example end-to-end

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 10 out of 10 changed files in this pull request and generated 4 comments.

Comment on lines +1127 to +1144
rewrite:
"// before — idle callback hides side effects from callers\n" +
"fn deferCleanup() uses { fs } -> void {\n" +
" requestIdleCallback(() => fs.delete(\"/tmp/cache\")) // SYN026\n" +
"}\n\n" +
"// after — extract the work into a separate fn; caller decides when to schedule it\n" +
"fn cleanup() uses { fs } -> void {\n" +
" fs.delete(\"/tmp/cache\")\n" +
"}",
example:
"// SYN026: idle callback hides a filesystem effect from callers\n" +
"fn deferCleanup() uses { fs } -> void {\n" +
" requestIdleCallback(() => fs.delete(\"/tmp/cache\")) // SYN026\n" +
"}\n\n" +
"// fix: extract the work into a separate fn\n" +
"fn cleanup() uses { fs } -> void {\n" +
" fs.delete(\"/tmp/cache\")\n" +
"}",
Comment on lines +1446 to +1469
"// SYN026 — before\n" +
"fn deferCleanup() uses { fs } -> void {\n" +
" requestIdleCallback(() => fs.delete(\"/tmp/cache\")) // SYN026\n" +
"}\n\n" +
"// after — extract the work; caller controls scheduling\n" +
"fn cleanup() uses { fs } -> void {\n" +
" fs.delete(\"/tmp/cache\")\n" +
"}\n" +
"```\n\n" +
"**Fix (escape hatch):** if idle-period scheduling is required at this layer:\n" +
"`unsafe \"schedules idle callback\" { requestIdleCallback(cb) }`\n\n" +
"SYN026 fires at `?bs 0.7+` as a non-blocking warning. " +
"Calls inside `unsafe { }` blocks or `unsafe \"reason\" fn` bodies are suppressed.",
example: {
fails:
"?bs 0.7\n" +
"fn deferCleanup() uses { fs } -> void {\n" +
" requestIdleCallback(() => fs.delete(\"/tmp/cache\"))\n" +
"}\n",
passes:
"?bs 0.7\n" +
"fn cleanup() uses { fs } -> void {\n" +
" fs.delete(\"/tmp/cache\")\n" +
"}\n",
Comment on lines +1755 to +1756
`any effects inside that callback are invisible to callers and cannot be declared in the fn header; ` +
`wrap in unsafe "${isRAF ? "schedules animation frame callback" : "schedules idle callback"}" { ${tok.text}(cb) }`,
Comment on lines +13 to +18
// Safe: cleanup work extracted and callable directly or via requestIdleCallback
fn clearStaleCache(cache: Map<string, any>) -> void {
cache.clear()
}

export { applyFrame, clearStaleCache };
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.

feat(compiler): SYN025/SYN026 — warn on requestAnimationFrame/requestIdleCallback (scheduling bypass, like SYN010)

2 participants