AILANG Known Limitations
This document tracks known limitations, workarounds, and design constraints in AILANG.
For features that have been implemented, see Design Documents.
All entries below were live-verified at AILANG v0.28.0-141-g379990ad5 on 2026-07-10 with
ailang check / ailang run transcripts. Each open entry carries a Verified at date; fixed
items are listed under Recently Resolved. This is the entry policy from
M-V1-STABILITY-PROMISE:
every remaining limitation is a reproducible artifact, not lore.
Type System Limitations
Y-Combinator and Recursive Lambdas (By Design)
Status: Design constraint, not a bug
Verified at: v0.28.0 (2026-07-10, ailang check transcript below)
The Y-combinator and similar recursive lambda expressions fail with "occurs check" errors:
-- This fails:
let Y = \f. (\x. f(x(x)))(\x. f(x(x))) in
-- ailang check ->
-- Error: occurs check failed: α2 occurs in α2 -> α3 ! {...ε4}
Root Cause: Hindley-Milner type inference prevents infinite types to ensure decidability. The Y-combinator requires a recursive type α = α → β, which would create an infinite type.
Why This Exists:
- Type Inference Decidability — Allowing infinite types makes type inference undecidable
- AI-Friendly Design — AILANG prioritizes deterministic, verifiable type checking
- Semantic Clarity — Named recursion is more explicit than anonymous recursion
Workaround: Use named recursive functions:
func factorial(n: int) -> int =
if n <= 1 then 1 else n * factorial(n - 1)
Named func recursion is also supported by ailang verify via bounded unrolling
(--verify-recursive-depth N, v0.8.0+).
WASM Type-Checker Depth Limit (By Design, WASM-only)
Status: WASM-specific constraint with structured error
Since: v0.22.x
Verified at: v0.28.0 (2026-07-10 — structured depth budget exceeded error path present; WASM-host-only, not reproducible from the CLI, which is unaffected)
Affects: AILANG modules compiled to WebAssembly (browser demos using wasm/ailang.wasm). CLI is unaffected.
The WASM-compiled type-checker is bound by the host JavaScript engine's call-stack limit (~10–15K frames in Node, Chromium, Firefox). Modules with deeply-recursive type structure exceed it. On native Go the CLI handles them fine because goroutine stacks grow dynamically up to 1 GiB; on WASM we hit the cliff.
Example failure:
loadModule cognitive_commons/services/citizen failed:
WASM type-checker depth budget exceeded (5000 frames) in module
"cognitive_commons/services/citizen".
This module's type structure recurses too deeply for the WASM host stack.
The same source likely works on the AILANG CLI — this limit is specific
to browser execution.
Common triggers:
- Triple-nested
matchpatterns (match inside match inside match) - Multiple back-to-back matches on the same tagged-union value with field access:
-- Each line is a separate match on the same Result — the type-checker-- repeats tagged-union analysis on `s` three times.let x = match r { Ok(s) => s.x, Err(_) => 0.0 };let y = match r { Ok(s) => s.y, Err(_) => 0.0 };let z = match r { Ok(s) => s.z, Err(_) => 0.0 };
- Long chains of intra-package imports with many destructured constructors
Workarounds (in order of simplicity):
-
Flatten nested matches into sequential
let-bindings:-- Before: triple-nestedmatch decode(raw) {Err(e) => Err(e),Ok(j) => match getString(j, "error") {Some(p) => Err(p),None => match getString(j, "text") { ... }}}-- After: flatmatch decode(raw) {Err(e) => Err(e),Ok(j) => {let err = optional_string(j, "error");if length(err) > 0 then Err(err)else match getString(j, "text") { ... }}} -
Extract a helper that does one match and returns a record:
pure func unpack(r: Result[Score, string]) -> { has: bool, x: float, y: float } =match r {Ok(s) => { has: true, x: s.x, y: s.y },Err(_) => { has: false, x: 0.0, y: 0.0 }}-- Now `unpack(r)` returns everything at once; one match instead of three. -
Split the function into smaller top-level functions — each gets its own type-check pass.
Why This Exists:
- Host-stack limit — Go compiled to WebAssembly shares the host JavaScript engine's call stack, which has a fixed limit (~10K frames in Node/Chromium). Native Go has dynamically-growable per-goroutine stacks; WASM does not.
- Silent failure mode otherwise — Before this limit, the browser would freeze for 80–120 seconds before the JS engine threw
Maximum call stack size exceeded. The structured error fires immediately with actionable workarounds instead. - Workarounds are idiomatic — Helper extraction and flat matches are the AILANG style anyway; the cliff catches patterns that are also harder to read.
History: First hit 2026-05-20 by cognitive_commons/services/citizen.ail in the demos repo. The half-day diagnostic trail (silent freeze, no console error, DevTools locked) led to the limit being added so future demo authors get a clear error in seconds instead of an opaque hang. Full postmortem: demos/debug-notes/wasm-citizen-stack-overflow.md.
Prevention guide: the five patterns that trigger this cliff are actually general AILANG style — pure core, single-effect leaves, multi-effect orchestrator, destructure once, flat matches. Run ailang prompt | grep -B 1 -A 200 "Idiomatic AILANG: Pure Core" for the full guide with bad/good examples. (The WASM budget is just the toolchain-side enforcement mechanism; the patterns themselves are the architecture, so they live in the syntax prompt. The devtools prompt has a short toolchain-side pointer at ailang devtools-prompt | grep -A 20 "WASM Type-Checker Budget".) The patterns produce more readable, more testable code regardless of target — the WASM cliff is the universe nudging you toward the style.
Headless smoke test: demos/scripts/wasm-loadmodule-harness.js runs the actual WASM in Node and exits with code 4 when this limit fires (vs 0 for clean modules). CI-friendly.
Future improvement: Converting the type-checker to iterative work-stack passes would remove the limit entirely. Tracked in M-WASM-TYPECHECK-ITERATIVE (deferred — high-risk refactor for a low-frequency cliff).
Duplicate Record Types with Identical Fields
Status: Known limitation with workarounds
Since: v0.5.10
Verified at: v0.28.0 (2026-07-10 — Go-codegen path only; the interpreter is unaffected: ailang run on a duplicate-shaped record returns the correct field value)
Affects: Go code generation when multiple record types share identical field structures
When multiple record types declare the same field names and types, the Go
codegen may pick the wrong struct because GetRecordTypeByFields returns the
first match (the interpreter path is fine — this is a --emit-go codegen issue only):
-- starmap.ail
export type Vec3 = { x: float, y: float, z: float }
-- celestial.ail
export type SystemPos = { x: float, y: float, z: float }
func initSystem() -> StarSystem {
{ position: { x: 0.0, y: 0.0, z: 0.0 }, ... }
-- May generate &Vec3{} instead of &SystemPos{}
}
Workarounds:
- Merge duplicates if semantically equivalent — keep one type and import it everywhere.
- Rename to be unique —
GalacticCoordvsSystemPosinstead of twoVec3-shaped types. - Add a discriminator field — e.g.
_tag: string— to break the structural tie.
See Also: implemented/v0_5_10/m-codegen-nested-record-type.md
Parser Limitations
If-Else Branches Require Explicit Braces
Status: Design constraint (improved error message in v0.5.9)
Verified at: v0.28.0 (2026-07-10 — ailang check emits "if-else branches require explicit braces when using let bindings" with a fix suggestion)
Affects: Multi-statement branches in if-else expressions
AILANG is not layout-sensitive, so multi-statement branches must be wrapped in
explicit braces. Without them, only the first let is parsed as the branch:
-- Fails:
if x > maxX then [] else
let v = x * 2;
let rest = buildList(x + 1, maxX);
v :: rest
-- Error: if-else branches require explicit braces when using let bindings
Fix:
if x > maxX then [] else {
let v = x * 2;
let rest = buildList(x + 1, maxX);
v :: rest
}
Single-expression branches don't need braces.
Language Feature Gaps
Error Propagation Operator (?)
Status: Planned — Design Doc
Verified at: v0.28.0 (2026-07-10 — r? produces PAR_NO_PREFIX_PARSE: unexpected token in expression: ?)
The ? operator for early return on Result errors is designed but not yet
implemented. For now, use explicit match on Result:
match readFile(path) {
Ok(contents) => process(contents),
Err(e) => Err(e)
}
Typed Quasiquotes
Status: Planned — Design Doc Verified at: v0.28.0 (2026-07-10 — quasiquote syntax not accepted by the parser)
Typed quasiquotes for deterministic AST templates and secure string templating
are designed but not yet implemented. Use string interpolation ("${expr}",
v0.12.1+) or concat([..]) for now.
CSP Concurrency
Status: Deferred — Design Doc Verified at: v0.28.0 (2026-07-10 — no channel/session-type surface in the parser)
CSP-style concurrency with channels and session types is deferred to v1.0.0+.
AILANG currently focuses on deterministic, single-threaded execution. The
runtime does support effect-level concurrency primitives in some contexts (see
the Concurrency effect), but full CSP with session types remains future work.
Interactive stdin / Keyboard Input
Status: Line input works; two narrower gaps remain (see the three rows below).
Verified at: v0.28.0 (2026-07-10 — std/io.readLine and std/stream.asyncReadStdinLines present in the stdlib)
std/io provides readLine(), which reads one line from stdin and blocks until the user
presses Enter — covering prompt-and-read and REPL-style input. std/stream.asyncReadStdinLines
additionally provides a concurrent, non-blocking line source for selectEvents dispatch.
So "no stdin" is a myth: line-oriented input is fully supported today.
Two distinct things are not yet supported — keep them separate, they route differently:
| Need | Status | Tracking |
|---|---|---|
Line input (readLine, asyncReadStdinLines) | ✅ Supported | — |
Abort a long-running std/ai.step() mid-call via stdin/signal | ❌ In progress | #231 → m-agent-step-cancellation (extension + small fix) |
| Raw-mode single keypress (no Enter/echo) for human-controlled games | ❌ By design — not in core | non-deterministic, unavailable in WASM, no agentic use case; would be a host/extension capability if ever needed |
For a keyboard-controlled game today, design around it with a self-driving loop or
line-at-a-time (readLine) input.
For real-time output (games, progress bars, dashboards), see flush() and the C-style
string escapes (\x1b, \u{…}) added in v0.27.0 — examples/progress_bar.ail.
Recently Resolved
These limitations existed in earlier versions and are now fully resolved. Listed for users following older docs. All re-verified at v0.28.0, 2026-07-10.
- Polymorphic arithmetic in lambdas — Fixed in v0.7.0
(design doc).
let add = \x. \y. x + y in add(3.14)(2.71)now returns5.85. Re-verified v0.28.0 (ailang run→5.85). matchinside block-body lambdas in HOF arguments — Fixed (design doc m-dx-match-in-hof-block-lambda). Using brace-formmatchinside a\x. { ... }lambda passed to a HOF now type-checks and runs:map(\item. { let s = match item { 0 => "zero", _ => "ok" }; s }, [0,1,2])→[zero, ok, ok]. Re-verified v0.28.0. (The oldmatch ... with | …ML/Haskell form is retired — it now emitsPAR019; use brace-formmatch x { pat => expr }.)- Multi-statement block expressions —
{ e1; e2; e3 }sequencing now works fully.{ println("step 1"); println("step 2"); println("step 3") }runs all three. The oldlet _ = … inworkaround is no longer needed. Re-verified v0.28.0. - String interpolation — Implemented in v0.12.1 (
"Hello, ${name}!"). Phase 2 of M-CONCAT-DISAMBIG in v0.13.0 made++list-only (string++is now a type error). Re-verified v0.28.0 ("Value: ${x}"→Value: 42). - Pattern guards — Implemented in v0.6.2
(design doc).
match x { n if n > 100 => ..., n if n > 0 => ... }now evaluates guards correctly. Re-verified v0.28.0 (→big).
Notes on Parser Error Messages
Parser errors include:
- Error codes (e.g.,
PAR_UNEXPECTED_TOKEN) - Precise positions (file, line, column)
- Suggestions for fixing the error
Example:
PAR_UNEXPECTED_TOKEN at file.ail:2:14: expected ), got INT
Suggestion: Add ')' to close grouped expression
Diagnostic Heuristic Bounds (By Design)
Reversed-split-argument warning is best-effort
The compile-time warning for reversed split arguments (M-DX-SPLIT-ARG) is a
best-effort heuristic: it fires only when the first argument is a short (1–3
rune) string literal and the second is not a literal, e.g. split("/", name).
It intentionally does not flag split(sep, s) when the delimiter is a
variable (let sep = "/"; split(sep, name)) — there is no literal delimiter to
key on, and both arguments are string so the type system cannot distinguish
them. The warning is non-blocking and never produces false positives on the
correct data-first order split(s, delimiter). Prefer data-first and treat the
warning as a backstop for the literal-delimiter case.
Reporting New Limitations
Found a limitation not listed here? Please file an issue at: https://github.com/sunholo-data/ailang/issues
Include:
- AILANG version (
ailang --version) - Minimal reproduction code
- Expected vs actual behavior
- Whether it's a bug or design limitation