Skip to content

Make effects a real, enforced, inferred type - #8

Open
dalurness wants to merge 5 commits into
mainfrom
feat/builtin-effect-tracking
Open

Make effects a real, enforced, inferred type#8
dalurness wants to merge 5 commits into
mainfrom
feat/builtin-effect-tracking

Conversation

@dalurness

Copy link
Copy Markdown
Owner

What & why

Effects are Clank's headline differentiator — but the checker didn't enforce them for builtins. This type-checked clean:

main : () -> <> () =   # declared PURE
  print("hello")       # ...but does I/O

Effect tracking was a syntactic perform-only pass (a W401 warning that run/check ignored), with no builtin effects, no cross-call propagation, and no higher-order effect polymorphism. The row-unification machinery existed but was dormant.

This moves effect tracking into type inference and makes an under-declared effect row a hard error (E401).

What changed

  • Ambient effect inference. inferExpr threads an effect-scope stack: applications union consumed arrows' effect rows, perform records its op, lambdas accumulate into their own row (attached to the last arrow), and handle discharges handled ops in a child scope. The def-body row is checked against the declared row — subsumption preserved (declaring more than you perform is fine; open row-var tails absorb extras).
  • Builtins carry real rows. io (print, fs writes, env, log, dt.now, cli, srv), io+exn (fs.read, http.*, proc.run/sh, csv file ops, streams; json.dec/as → exn), async (spawn, channels, sleep, ...), exn (raise).
  • Effect polymorphism. map/filter/fold/flat-map share an effect var between callback and result — map : ([a], (a -> <e> b)) -> <e> [b] — so a callback's effect (inline lambda or named function) propagates to the caller.
  • User functions carry their declared row (aliases resolved up front), so calls propagate effects transitively, including 0-arg calls.
  • Effect rows render with <>; effect-row variables get readable a/b/c names. clank eval --type 'fs.read'(Str -> <io, exn> Str).

Migration

10 test/*.clk files under-declared effects the checker now tracks (mostly adding exn to io mains that read files / decode JSON). Fixed. The cross-module effect-alias test moved to the propagation pattern, since discharging an imported effect's operation needs the effect declaration linked (ops aren't pub-exported).

Tests

12 checker tests (builtin io/exn/async enforcement, transitive + higher-order propagation incl. named callbacks, handle discharge, subsumption, pure pipelines stay pure) + 2 eval --type CLI tests. Full suite green.

Known follow-ups

  • clank doc builtin signatures don't yet show effect rows (the doc registry is a separate approximate type table); clank eval --type is authoritative.
  • Effects don't propagate through TAny-typed combinators (iter.*, srv.* handlers, spawn's thunk) — they accept effectful callbacks but don't propagate. Inline lambdas and the typed list HOFs do propagate.

🤖 Generated with Claude Code

dalurness and others added 5 commits July 20, 2026 04:21
Move effect checking from the syntactic collectEffects/perform-only pass
into type inference. inferExpr now threads an effect-scope stack: ExprApply
unions the effect rows of consumed arrows, ExprPerform records the op's
effect, ExprLambda accumulates into the lambda's own row (attached to its
last arrow so higher-order callers can propagate it), and ExprHandle
discharges handled ops in a child scope. The def-body row is compared
against the declared row and undeclared effects are a hard error E308
(was W401, a warning run/check ignored). Subsumption preserved: declaring
more than performed is fine; open row-var tails absorb extras.

Builtins still carry empty rows, so this is behavior-neutral for the suite
(user perform/handle semantics reproduced); Stage 3 turns on builtin
effect rows. Retires collectEffects/walkEffects.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Builtins now carry their effect rows: io (print, fs writes, env, log,
dt clock, cli, srv), io+exn (fs.read, http.*, proc.run/sh, csv file ops,
streams), exn (raise, json.dec/as), async (spawn, channels, sleep, ...).
map/filter/fold/flat-map are effect-polymorphic — a shared effect var
links the callback's row to the result so the callback's effect
propagates. User function types carry their declared effect row (aliases
resolved via a new up-front pass) so calls propagate effects transitively,
including 0-arg calls. Effect rows now render with <> not {}, and effect
row variables get readable a/b/c names.

Migrates 10 test .clk files whose signatures under-declared effects the
checker now tracks (mostly adding exn to io mains that read files / decode
JSON). Updates the cross-module effect-alias test to the propagation
pattern, since discharging an imported effect's op requires the effect
declaration to be linked (ops are not pub-exported).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add 12 checker tests (builtin io/exn/async enforcement, transitive and
higher-order propagation incl. named callbacks, handle discharge,
subsumption, pure pipelines stay pure) and 2 eval --type CLI tests
(builtin effect rows surfaced). Use code E401 in the effect-error range
(E400-499) for an undeclared effect — the error-promotion of the old
W401 warning. Update SPEC.md, effect-system.md, README, and the agent
skill to state effects are inferred and enforced; correct the stale
InferredInfo doc comment.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Deferred: clank doc builtin signatures don't yet show effect rows (the
doc registry is a separate approximate type table); clank eval --type is
the authoritative way to see a builtin's effects.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The macOS runners' dyld refuses to load Mach-O binaries lacking an
LC_UUID load command. Go 1.22 (EOL) emits test binaries without it, so
every 'go test' binary aborted with 'signal: abort trap' on macOS while
ubuntu and windows were unaffected; fail-fast then cancelled windows.

Bump both workflows to Go 1.26, matching the local dev toolchain, which
emits LC_UUID and restores green CI on all three OSes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.

1 participant