Source: src/hql/transpiler/pipeline/effect-checker.ts, src/hql/transpiler/pipeline/effects/
fx-decl ::= '(' 'fx' name params body... ')'
HQL provides a compile-time effect system for enforcing function purity. Functions declared with fx (instead of fn) are checked at compile time to ensure they contain no impure operations. This enables the compiler to reason about side effects and provides safety guarantees for functional programming.
type Effect = "Pure" | "Impure";
;; Declare a pure function
(fx add [x y]
(+ x y))
;; Pure function with type annotations
(fx square:number [x:number]
(* x x))
;; Pure anonymous (via function expression with pure flag)
(let double (fx [x] (* x 2)))
fx compiles identically to fn in the output JavaScript. The purity check is purely a compile-time enforcement -- no runtime overhead.
(fx add [x y] (+ x y))
Compiles to:
function add(x, y) {
return x + y;
}
The effect system tracks the type of values to determine method effect:
type ValueKind =
| "Array" | "String" | "Number" | "Boolean"
| "Map" | "Set" | "RegExp" | "Promise"
| "Unknown" | "Untyped";
ValueKind is inferred from:
x:string -> String)(new Map) -> Map)"hello" -> String, 42 -> Number)The TYPE_NAME_TO_KIND mapping converts TypeScript type names to ValueKind:
| Type Annotation | ValueKind |
|---|---|
string | String |
number | Number |
boolean | Boolean |
Array, number[], string[] | Array |
Map | Map |
Set | Set |
RegExp | RegExp |
Promise | Promise |
| (untyped) | Untyped |
| (unknown) | Unknown |
Generator functions use yield, which is an observable side effect:
;; COMPILE ERROR: Generator function 'gen' cannot be declared pure (fx)
(fx* gen [n] (yield n))
Pure functions cannot call known-impure functions:
(fn log [msg] (console.log msg)) ;; impure
;; COMPILE ERROR: impure call in pure function
(fx process [x]
(log x) ;; violation
(* x 2))
Pure functions cannot call mutating methods on collections:
;; COMPILE ERROR: mutation in pure function
(fx bad [arr:Array]
(.push arr 42) ;; Array.push is impure
arr)
Parameters that are callbacks can be annotated with purity constraints:
;; The callback parameter 'f' must be pure
(fx map-pure [f:(Pure (fn [] number)) items:Array]
(map f items))
The effect system uses receiver type (ValueKind) to determine if a method call is pure or impure.
When the receiver type is known, the system looks up typed method effects:
| Receiver | Pure Methods | Impure Methods |
|---|---|---|
Array | at, concat, entries, every, filter, find, findIndex, flat, flatMap, includes, indexOf, join, keys, lastIndexOf, map, reduce, reduceRight, slice, some, toReversed, toSorted, toSpliced, values, with, length | push, pop, shift, unshift, splice, sort, reverse, fill, copyWithin |
String | at, charAt, charCodeAt, codePointAt, concat, endsWith, includes, indexOf, lastIndexOf, localeCompare, match, matchAll, normalize, padEnd, padStart, repeat, replace, replaceAll, search, slice, split, startsWith, substring, toLocaleLowerCase, toLocaleUpperCase, toLowerCase, toUpperCase, trim, trimEnd, trimStart, length | (none) |
Map | entries, forEach, get, has, keys, size, values | clear, delete, set |
Set | entries, forEach, has, keys, size, values | add, clear, delete |
Number | toExponential, toFixed, toLocaleString, toPrecision, toString, valueOf | (none) |
RegExp | exec, test, toString | (none) |
When the receiver type is unknown, the system falls back to a general method effect table that classifies methods conservatively.
Static method calls (e.g., Math.floor, JSON.parse) have their own effect classifications:
Math.*, Number.isFinite, Number.isNaN, Number.isInteger, Number.parseFloat, Number.parseInt, String.fromCharCode, String.fromCodePoint, Object.keys, Object.values, Object.entries, Object.assign, Object.freeze, Object.is, Object.hasOwn, Array.isArray, Array.from, Array.of, JSON.parse, JSON.stringifyconsole.*, Math.randomKnown pure functions: parseInt, parseFloat, isNaN, isFinite, encodeURI, encodeURIComponent, decodeURI, decodeURIComponent, String, Number, Boolean, BigInt, Symbol.for
Known impure functions: fetch, alert, confirm, prompt, setTimeout, setInterval, clearTimeout, clearInterval, requestAnimationFrame, queueMicrotask
Array, Map, Set, WeakMap, WeakSet, Date, RegExp, Error, TypeError, RangeError, ReferenceError, SyntaxError, URIError, URL, URLSearchParams, Intl.NumberFormat, Intl.DateTimeFormat, Intl.Collator, PromiseWorker, WebSocket, EventSource, BroadcastChannel, XMLHttpRequest, AbortController, IntersectionObserver, MutationObserver, ResizeObserver, PerformanceObserverThe effect checker builds a signature table mapping function names to their effect metadata:
interface FunctionSignature {
name: string;
effect: Effect;
paramCount: number;
callableParams: Map<number, ParamEffectAnnotation>;
}
The signature table is built from:
FnFunctionDeclaration nodes in the IR (pure flag from fx)FunctionExpression nodes with namesThe effect system tracks which parameters of higher-order functions are callbacks, and at which positions:
;; Array.map's first argument (index 0) is a callback
;; Array.filter's first argument (index 0) is a callback
;; Array.reduce's first argument (index 0) is a callback
This enables checking that callbacks passed to pure higher-order functions are themselves pure.
The checkEffects function performs validation in two passes:
fx declaration:
fx compiles identically to fnEffect violations produce EffectValidationError with descriptive messages:
"Generator function 'X' cannot be declared pure (fx). Generators use 'yield' which is an effect.""Pure function 'X' calls impure function 'Y'""Pure function 'X' calls impure method '.Y' on type Z"src/hql/transpiler/pipeline/effect-checker.tssrc/hql/transpiler/pipeline/effects/effect-types.tssrc/hql/transpiler/pipeline/effects/effect-lattice.tssrc/hql/transpiler/pipeline/effects/effect-receiver.tssrc/hql/transpiler/pipeline/effects/effect-signatures.tssrc/hql/transpiler/pipeline/effects/effect-infer.tssrc/hql/transpiler/pipeline/effects/effect-env.tssrc/hql/transpiler/pipeline/effects/effect-errors.tstests/unit/effect-system.test.ts