03 — Language Reference (the compat contract)

This is the precise specification of the Sky language the Rust compiler must implement. It is the contract every other doc targets: if a program parses, type-checks, and lowers under the Haskell compiler, it must do so identically under the Rust one, and vice versa. Accept/reject behaviour is normative — where a rule looks quirky, it is relied upon by the 42 examples and reproduced verbatim (goal 00 §"Compat first").

Citations are file:line into the current Haskell tree at /Users/anzel/works/playground/sky. They pin the exact behaviour; a parser/typer author should not need to re-read the Haskell.

Two AST layers are referenced throughout:


1. Lexical structure

1.1 Encoding, layout, whitespace

PrimitiveRuleSource
spacesskip ' ', '\t', and -- line comments, but stop at newline (so layout-sensitive callers see the line boundary). Block comments NOT skipped here.Space.hs:21-49
freshLineskip ALL whitespace incl. newlines, -- line comments, nested {- -} block comments. Used when the parser may cross line boundaries.Space.hs:53-91
checkIndentsucceed iff col > indent (strictly). The continuation test.Space.hs:119-123
checkAlignedsucceed iff col == indent. Sibling-item alignment (e.g. let bindings, case arms).Space.hs:135-139
withIndent n prun p with _indent := n, restore on exit. Callers set the block reference to the body's start column.Primitives.hs:193-200

Model in one line: each construct's body parses with _indent set to that body's start column; tokens continue the body while col > indent; sibling items align at col == indent; crossing newlines needs freshLine, inline parsing uses spaces (halts at newline to preserve the boundary). Rust must reproduce this column arithmetic (including tab=4) exactly — indentation changes parse results.

1.2 Comments

FormRule
-- lineto end of line. Recognised inside spaces/freshLine; a -- after a token is a comment, never subtraction (Space.hs:38-48).
{- block -}nestable (depth-counted, Space.hs:95-116). Only skipped by freshLine, not spaces.

Comments are whitespace to the grammar. A separate post-parse raw-text scan (collectComments, src/Sky/Parse/Module.hs:103-187) re-attaches every comment with kind (CommentLine/CommentBlock) + position (CommentOwnLine/ CommentTrailing) + column for the formatter. The Rust CST keeps comments as trivia in-tree (doc 04) — same information, no second scan.

1.3 Identifiers

Lexed by first character (src/Sky/Parse/Variable.hs):

ClassStart charContinueMeaningSource
lower_, Unicode lowercase, or caseless letter (CJK/Arabic/Hebrew — isLetter && not isUpper)isAlphaNum || '_'value / function / field / type-var namesVariable.hs:17-35,53-54
upperASCII/Unicode uppercasesametype / constructor / module-segment namesVariable.hs:39-49

1.4 Numeric literals

src/Sky/Parse/Number.hs. Produces IntNum Int or FloatNum Double.

FormExampleResultSource
Decimal integer123IntNumber.hs:52-56
Hex integer0xFF, 0x1aIntNumber.hs:22-30
Float w/ point1.5, 123.456Float (needs digit after .)Number.hs:36-45
Float w/ exponent1.5e-2, 2.0E+10FloatNumber.hs:42-45,64-75
Integer w/ exponent1e6Float (bare 1e6 is a Float, not Int)Number.hs:46-51

1.5 String & char literals

src/Sky/Parse/String.hs. Three lexer results: SingleLine, MultiLine, CharLit.

Single-line string "..." (String.hs:39-47,162-173):

EscapeValueEscapeValue
\n \t \r \\ \" \' \0usual\a \b \f \vbell/backspace/formfeed/vtab
\xHH2-hex byte\uHHHH4-hex BMP code point
\u{H..}1–8 hex, full code point ≤ U+10FFFF, no surrogatesunknown \Xkept verbatim as \X (so a wrong escape is visible at compile time)

Code-point validity: 0 ≤ n ≤ 0x10FFFF, not a surrogate D800..DFFF (String.hs:120-122). A string cannot span a raw newline.

Char literal 'c' (String.hs:126-156): single char or one escape (\n \t \r \\ \'); other escapes kept as \X. Stored as a String payload (Src.Chr String), not a Rust char.

Triple-quoted multiline string """...""" (String.hs:22-37,177-188):

1.6 Multiline interpolation {{expr}} — desugaring contract

Applied in the canonicaliser, not the parser (src/Sky/Canonicalise/Expression.hs:42-47,529-651). This is a normative desugaring the Rust hir layer must reproduce.

desugarMultiline (Expression.hs:541-555) splits the raw string into alternating literal / expression chunks (splitInterpolation, Expression.hs:573-593), converts each, and left-folds with ++:

"""hello {{name}}! you are {{age}} years old"""
  ⟹  "hello " ++ Debug.toString name ++ "! you are " ++ Debug.toString age ++ " years old"

Exact rules:

RuleBehaviourSource
Concat operatorCan.Binop "++" Basics append with hardcoded annotation Forall [a] (a→a→a)Expression.hs:550-555
Every expr chunkwrapped in Can.Call (VarKernel "Debug" "toString") [resolved]even already-String exprsExpression.hs:598-606
Empty / single chunkStr "" / the chunk verbatim (no ++)Expression.hs:545-548
Type constraint on {{}} bodynoneDebug.toString accepts any type; no String requirement(consequence of 598-606)

Allowed forms inside {{...}} (body trimmed of spaces first, resolveInterpolationRef, Expression.hs:616-650):

  1. Bare lower identifier — resolved via env: top-level → VarTopLevel, kernel → VarKernel, else VarLocal (Expression.hs:627-635).
  2. Field access record.field (lower before .) → Can.Access (VarLocal record) field (Expression.hs:645-649).
  3. Qualified Module.func (Upper before .) → resolved through import alias to VarKernel; unknown alias → literal fallback Str "{{...}}" (Expression.hs:636-644).
  4. Single-arg call — split on first space: func argCan.Call func [arg], recursively (so String.fromInt n, errorToString e work) (Expression.hs:619-624).
  5. Anything else → literal fallback Str "{{...}}" (Expression.hs:650) — the developer sees their source as a signal to simplify. This fallback is observable and must be reproduced.

Note: the body is resolved by a hand-rolled splitter, not by re-invoking the real expression parser (the doc-comment at Expression.hs:536-538 is stale). Multi-arg calls, operators, and parens inside {{}} do NOT parse — they hit the literal fallback.

Escaping (splitInterpolation, Expression.hs:573-593):

InputOutput
\{{literal {{ (no interpolation)
\\single literal \
\X (other)verbatim \X (backslash preserved)
{{ with no closing }}treated as literal {{, scan continues
single { / }literal (catch-all copy)

Interpolation expressions ARE ordinary Canonical exprs → they flow into the type checker as arguments to Debug.toString and ++.

1.7 Operators & symbols

Operator chars: + - * / < > = ! & | ^ ~ % ? @ # $ : . \ ' (src/Sky/Parse/Symbol.hs:27-28). The parser lexes a maximal run of these as one operator token (Symbol.hs:11-19) but only a fixed set has meaning (§5.2). There are no user-defined operators (§7).


2. Module structure

src/Sky/Parse/Module.hs. A module is: optional header, imports, then declarations (moduleParser, Module.hs:190-220).

2.1 Header & exposing

module Sky.Core.List exposing (map, filter, List, Msg(..), Color(Red, Green), (|>))
Exposed itemSource ASTMeaningSource
(..)ExposingAllexpose everythingModule.hs:276-280
name (lower)ExposedValuea value/functionModule.hs:357-359
TypeExposedType _ Privateopaque type onlyModule.hs:348-349
Type(..)ExposedType _ Publictype + all constructorsModule.hs:337-341
Type(A, B)ExposedType _ (PublicCtors [..])type + selected constructorsModule.hs:342-347
(+)ExposedOperatoran operatorModule.hs:351-355

2.2 Imports

import Std.Db as Db exposing (Db, SqlValue(..))
import Sky.Core.Prelude exposing (..)

Src.Import { name, alias : Maybe String, exposing } (Module.hs:386-436):


3. Declarations

src/Sky/Parse/Declaration.hs. Five declaration kinds (DeclType, Declaration.hs:102-108); the module builder splits them into values / unions / aliases / infix (Module.hs:448-490).

3.1 Type alias

type alias Model = { count : Int, name : String }
type alias Cfg msg = { onSubmit : msg, label : String }
type alias Handler = Request -> Task Error Response

Declaration.hs:135-144. type alias Name vars = TypeAnnotation. Body may start on the next line. Parametric aliases carry lowercase type-var params. A record alias name doubles as a constructor (Elm convention): Model { ... } and positional Profile name age both construct (Declaration.hs:71-97).

Head-position alias unfolding (closed limitation, canonical Elm shape): an annotation whose head is an alias-of-a-function is unfolded before splitting args — view : Renderer Msg where type alias Renderer msg = Model -> Element msg peels correctly (unfoldHeadAlias in Sky.Canonicalise.Module; regression Sky.Canonicalise.HeadAliasFunctionSig). Rust must unfold the head alias only (argument/return leaf types keep nominal form).

3.2 ADT (union) type

type Msg = Increment | Decrement | SetName String | Move Int Int
type Color = Red | Green | Blue

Declaration.hs:149-222. type Name vars = Ctor argType* (| Ctor argType*)*. Constructor args are atomic types only (no bare arrows/applications without parens — typeAtomForCtor, Declaration.hs:227-255). The = and each | may sit on continuation lines.

Canonical Union carries CtorOpts (Canonical.hs:201-205) computed from shape — this classification is observable in codegen and must match:

CtorOptsCondition
Enumall constructors zero-arg
Unboxexactly one constructor, one arg
Normalotherwise

Prelude-shadow rejection: a user ADT whose type name OR constructor name collides with a Prelude entry (Int Float Bool String Char List Maybe Result Task Error True False Just Nothing Ok Err) is a hard error naming the stdlib origin (audit §3.2; e.g. type Result a = Just a | Nothing rejected).

3.3 Value / function definitions & annotations

count : Int
count = 0

add : Int -> Int -> Int
add a b = a + b

Declaration.hs:39-98. A binding is name pattern* = expr. A preceding name : Type annotation line is parsed separately (DeclAnnotation) and re-associated to the following same-named value by the module builder (Module.hs:452-474, popAnnotation). Unmatched annotations are dropped.

Multi-line signatures (closed limitation #10). Both continuation shapes parse:

name              name
    : T               : T1
                      -> T2

The : may sit on a fresh-indented continuation line (Declaration.hs:54-60); the -> may sit on a fresh-indented continuation line inside the type (typeAnnotation, src/Sky/Parse/Type.hs:37-59). Upper-named annotations (a record-alias constructor's signature) get the same treatment (Declaration.hs:77-97).

3.4 foreign import / infix

foreign import "go/pkg" is parsed to DeclForeign and currently dropped by the module builder (Declaration.hs:32-37,278-296; Module.hs:487-488). Infix fixity declarations exist in the Source AST (Src.Infix, Source.hs:158-168) but the parser produces none today (_binops is always empty). The Rust compiler needs neither for v1 compat; keep the AST slots for forward-compat.


4. Records

Records are structural (row-typed). Source expression forms (src/Sky/Parse/Expression.hs:304-354):

FormExampleASTSource
Literal{ x = 1, y = 2 }Src.Record [(name, expr)]Expression.hs:332-348
Empty{}Record []Expression.hs:309-311
Update{ model | count = 0 }Src.Update name fieldsExpression.hs:324-331
Field accessrecord.field (postfix, chainable)Src.AccessExpression.hs:241-250
Accessor fn.fieldSrc.AccessorExpression.hs:430-433

5. Expressions

Source AST: Src.Expr_ (Source.hs:172-199). Canonical: Can.Expr_ (Canonical.hs:72-98).

5.1 Atoms, application, negative-literal args

5.2 Operators — precedence, associativity, desugaring

The parser emits a flat Src.Binops [(operand, op)] final without consulting precedence (Source.hs:187). The canonicaliser flattens nested chains and runs precedence-climbing (canonicaliseBinops, src/Sky/Canonicalise/Expression.hs:223-299). Src.Paren is an opaque leaf — parentheses are never flattened into the outer climb (Expression.hs:265-269), so (a - b) * c keeps its grouping.

Precedence + associativity table (src/Sky/Parse/Symbol.hs:40-62). Each operator desugars to a kernel function call (Binop, resolveOpName, Expression.hs:303-319) — operators are not first-class beyond this:

OpPrecAssocDesugars toOpPrecAssocDesugars to
>>9LBasics.composeL+6LBasics.add
<<9RBasics.composeR-6LBasics.sub
^8R(Basics ^)++5RBasics.append
*7LBasics.mul::5RList.cons
/7LBasics.fdiv==4NBasics.eq
//7LBasics.idiv/=4NBasics.neq
%7L(Basics %)< > <= >=4Nlt gt le ge
|>0LBasics.apR&&3RBasics.and
<|0RBasics.apL||2RBasics.or

5.3 if / then / else

Expression.hs:439-491. if c then a else b, with else if chains folded into Src.If [(cond, then)] else (a list of guarded branches + final else). The else if lookahead treats else if as a unit (elseIfChain, Expression.hs:457-491). There is no dangling-else ambiguity — else is mandatory.

5.4 let / in

Expression.hs:496-575. let binding+ in expr. All bindings must start at the same column (bindingCol, letBindings/moreLetBindings, Expression.hs:511-537). Two binding forms:

FormExampleAST
Definex = e, f a b = eSrc.Define name pats body ann
Destructure(a, b) = e, { x, y } = r, Just x = mSrc.Destruct pat e

5.5 case / of

Expression.hs:599-669. case subject of then branches pattern -> body, each branch aligned at branchCol (caseBranches/moreCaseBranches, Expression.hs:632-652). Subject and of may be on separate lines (Expression.hs:600-627). Each branch body binds withIndent (max patCol bodyCol) so a following sibling arm is not slurped into the previous body (Expression.hs:656-669). Exhaustiveness is checked and enforced (§11).

5.6 Lambda

Expression.hs:369-377. \p1 p2 -> body. Params are patterns; body may be on the next line.

5.7 Tuples & unit

() is Unit. (a, b) and (a, b, c, ...) are Tuple e1 e2 [rest] (Source.hs:198). The Canonical form and the type form (Type.hs, TTuple a b [rest]) both carry ≥2 elements. Runtime fast-path targets 2- and 3-tuples (Tuple1 in Sky.Type.Type), but larger tuples parse.


6. Patterns

src/Sky/Parse/Pattern.hs; Source Src.Pattern_ (Source.hs:240-256), Canonical Can.Pattern_ (Canonical.hs:116-137). Patterns appear in: case arms, function/lambda params, let destructure.

PatternSyntaxASTSource
Wildcard_PAnythingPattern.hs:60-68
Variablex, _fooPVarPattern.hs:192-194
As-aliaspat as namePAliasPattern.hs:43-52
Unit()PUnitPattern.hs:78-80
Tuple(a, b, ...)PTuplePattern.hs:86-93
List[a, b]PListPattern.hs:100-112
Consx :: rest (right-assoc)PConsPattern.hs:33-42
ConstructorJust x, Nothing, Db.SetField v (qualified)PCtor / PCtorQualPattern.hs:136-148
Record{ a, b, c }PRecord (field names)Pattern.hs:115-120
Int / neg-Int3, -3PIntPattern.hs:151-168
Float / neg-Float3.14, -3.14PFloatPattern.hs:151-168
String"foo" (also accepts """...""")PStrPattern.hs:170-178
Char'c'PChrPattern.hs:180-184
BoolTrue / FalsePBoolPattern.hs:186-190

7. Type system surface

HM (Hindley-Milner) inference with a small set of Elm-style built-in constrained type variables. Internal representation src/Sky/Type/Type.hs; canonical types Canonical.hs:155-181.

7.1 Types that exist

Type formSyntaxAST
Functiona -> b (right-assoc)TLambda
Type vara, msg, comparableTVar
Applied constructorList Int, Result Error a, Maybe (Dict String String)TType mod name args / TTypeQual
Record (closed){ x : Int, y : String }TRecord fields Nothing
Record (open / row-poly){ r | x : Int }TRecord fields (Just r)
Unit()TUnit
Tuple(a, b), (a, b, c)TTuple
AliasresolvedTAlias mod name args aliasType (Canonical.hs:162)

Type application binds tighter than ->; parenthesise applied args (Maybe (Dict String String)).

7.2 Built-in constrained type variables (NOT typeclasses)

Sky.Type.Type SuperType (Type.hs:61-67). Certain type-variable names carry a built-in constraint, resolved structurally by the unifier — there is no user-facing class mechanism:

Var name familySuperTypeAdmissible types
numberNumberInt or Float
comparableComparabletypes supporting == < >
appendableAppendableString or List a
compappendCompAppendString or List comparable

Content variants (Type.hs:51-58): FlexVar/FlexSuper (inferred), RigidVar/RigidSuper (from user annotation — a rigid var cannot unify with a concrete type), Structure, Alias, Error (recovery). Annotations quantify free lowercase vars (Forall vars ty, Canonical.hs:180); any is special (§7.4).

7.3 Intentional omissions (reject / absent — reproduce exactly)

OmittedBehaviourRef
Higher-kinded typesHM only; no f a where f is abstractedlimitation #1
Type classes / traitsnone — only the 4 built-in super-vars above
Custom operatorsnone; operator set is fixed (§5.2)limitation #3
where clausesnone; use let..inlimitation #2
GADTs, existentials, rank-Nnone

7.4 any — wildcard soundness gate (load-bearing)

any is a magic type name with per-occurrence wildcard semantics, NOT a normal polymorphic var. Rules the typer must keep (CLAUDE.md "Wildcard-any soundness gate"):

7.5 Strict-HM arity gate (closed limitation #7 — reject behaviour)

A zero-arg-typed binding called with an argument, or a () -> X-typed binding referenced bare in a value slot, is a hard error [E2007] (Sky.Type.Constrain.Expression; typeE_ArityMismatch = "E2007", Diagnostic.hs:205-206). Message names declared arity D vs supplied arity S:

Wildcard-any sigs are exempt (real polymorphism preserved). The Sky.Core.Pure module provides () -> Task Error a companions for a uniform call shape.


8. The effect boundary (Task-everywhere)

Single rule: every observable side effect returns Task Error a. Tiers (CLAUDE.md "Effect boundary"):

TierTypeExamples
Purebare aString.length, List.map, Crypto.sha256, System.getenvOr
Fallible-pureResult e a / Maybe aString.toInt, JSON decoders, Auth.hashPassword
EffectTask Error aFile.*, Http.*, Db.*, Time.now, Random.*, Log.*, most System.*
DivergingInt -> aSystem.exit (polymorphic return; never comes back)

Error type is Sky.Core.Error (sky-stdlib/Sky/Core/Error.sky), a closed ADT — never String (non-regression rule §8: no Result String a / Task String a in public surfaces). Bridges: Task.fromResult, Task.andThenResult, Result.andThenTask, Task.mapError, Task.onError (sky-stdlib/Sky/Core/Task.sky).

Auto-force of let _ = TaskExpr (src/Sky/Build/Compile.hs around 19626): a discarded let _ = <TaskExpr> binding is wrapped in rt.AnyTaskRun so the effect fires. This is a lowering behaviour, but it is observable semantics — the Rust lowerer must reproduce it.

Top-level Task.run: a module-level binding of a Task value still needs an explicit Task.run (runs at binding-init time), e.g. apiKey = System.getenv "K" |> Task.run |> Result.withDefault "".


9. main entry points + auto-force


10. Import qualifier resolution

src/Sky/Canonicalise/Module.hs. Every non-aliased import M exposing (…) also registers M's last segment as an auto-qualifier. Two imports may both try to bind the same qualifier; resolution is the explicit-alias-wins rule (effectiveQualifier, Module.hs:976-991; claims built by buildExplicitAliasClaims, Module.hs:950-956):

SituationResult
Import has as Aliasbinds Alias unconditionally (Module.hs:978-979)
Bare import, last-seg not claimed by a different-module explicit aliasbinds last segment (Module.hs:988)
Bare import whose last-seg IS claimed by an explicit alias for a different moduleauto-qualifier suppressedNothing; explicit alias wins. Exposed names still land unqualified (Module.hs:985-987)

Worked example: import Std.Db as Db + import Lib.Db exposing (conn)Db.x resolves to Std.Db; conn (unqualified) resolves to Lib.Db; the bare Db shortcut for Lib.Db is dropped silently.

Same-module double import is always fine: import Std.Ui as Ui + import Std.Ui exposing (Element) — both resolve to the same canonical path, so the suppress guard (claimedPath /= importPath) is false and the collision gate counts one distinct source (Module.hs:986,1041,1045-1046).

E1001 collision (detectImportAliasCollisions, Module.hs:994-1088) fires only when a qualifier has ≥2 distinct canonical sources:

Message shape (formatClash, Module.hs:1048-1088):

<r>:<c>: Import error: two imports both bind the qualifier `<qualifier>`:
  - import <path1> (at r:c)
  - import <path2> (at r:c)
  Add `as <Alias>` to one of them, e.g. `import <lastPath> as <CamelCasePath>`.

The fix-it camel-cases the offending path (App.StateAppState). Kernel paths are folded onto their pseudo-module so multiple kernel paths to one dispatch table don't count as a collision (Module.hs:1024).

Diagnostic code: canonicalise-phase legacy errors (including this one) surface under E1001 (canonE_UndefinedName, Diagnostic.hs:169) via legacyToDiag (Module.hs:146-167) as a placeholder code; the message body is the text above with the line:col: prefix stripped. Precedence among canonicalise errors: alias-collision > import-hiding > prelude-shadow > ambiguous-use > unbound (Module.hs:392-405).


11. Exhaustiveness semantics

src/Sky/Type/Exhaustiveness.hs; wired in Compile.hs:4193-4214, gated at 4977-4994.

Coverage rules (reproduce exactly — conservative, no false positives):

Head shapeRule
Wildcard _, var, as-alias at headimmediately exhaustive (Exhaustiveness.hs:116-119)
Constructor (PCtor)must cover every ctor of the union's _u_alts; missing ctors reported by name (105-107,136-142)
Bool (PBool)must have both True and False, else report missing (108-112,143)
Unit (PUnit)always exhaustive (113,154-155)
Literal PInt/PStr/PChralways Missing ["_"] unless a wildcard arm exists — infinite space needs _ (114,145-147,157-158)
PFloat, List/PCons/Tuple/Record headsnot checked — fall through catch-all; contribute nothing to coverage. Effectively exhaustive if no ADT/Bool/Lit head and no wildcard (103-104,132-134,148)

12. Go reserved-name rewriting (codegen compat)

Not a Sky-language rule, but observable in emitted Go and required for example-run parity. Every Sky identifier in reservedGoNames (src/Sky/Build/Compile.hs:9766-9785) is rewritten with a trailing _ at codegen (Compile.hs:9623). The list: init; predeclared funcs (new make len cap copy append delete panic recover print println clear min max complex imag real close); all 23 keywords (type func var const interface struct map chan go defer goto fallthrough range return for switch case default break continue import package select); predeclared types/constants (bool byte rune string error any comparable, every int*/uint*/float*/complex* size, true false iota nil). Every top-level Sky binding is also module-prefixed (Main_view, Std_Ui_layout), so the reserved list only matters for locals/params. main in module Main is special-cased to Go func main() (§9). Full spec: doc 08.


13. Limitation ledger — observable status

Behaviour the Rust compiler must match. "Closed" items are the current accept/reject; "active" items are current rejections/quirks. (CLAUDE.md "Active limitations".)

#ItemStatusObservable behaviour to reproduce
1Higher-kinded typesactivereject
2where clausesactiveabsent (use let)
3Custom operatorsactivefixed operator set (§5.2)
4Negative-literal argsclosedf -1f (-1); f - 1 subtraction; f -x needs parens (§5.1)
5Dict.toList typed-key inline-onlyclosedinline + let-bound both typed
6Go interface satisfaction in sky checkclosedstructural-implements axiom admits FFI interface pairs
7Zero-arg arityclosed[E2007] hard error (§7.5)
8Recursive list ops O(N) stackclosed13/13 list ops constant Go stack (semantics unchanged)
9Zero-arg Css.* need ()closedCss.zero/auto/none/… are bare values
10Multi-line signaturesclosed:/-> on continuation lines parse (§3.3)
Head-alias function sigclosedview : Renderer Msg unfolds (§3.1)
type Result a = … shadowclosedhard reject Prelude type/ctor shadow (§3.2)
Unknown qualified nameclosedcanonicaliser rejects with did-you-mean, not deferred to go build

14. Diagnostic code registry (partial — for the reporter)

src/Sky/Reporting/Diagnostic.hs. Codes the Rust diagnostics crate must keep stable (tests + LSP key off them):

CodeMeaningRef
E0001parse / syntax errorModule.hs:36-37
E1001undefined name / (placeholder for) import alias collision & canonicalise-phase errorsDiagnostic.hs:169, §10
E2005record-update mismatchDiagnostic.hs:195
E2006function arity (generic HM)Diagnostic.hs:197-198
E2007strict-HM arity mismatch (declared D vs supplied S)Diagnostic.hs:205-206, §7.5
E2008unsupported Dict key — a Dict keyed by anything other than String/Int/Float/Char/Bool. Rust-only, no oracle counterpart: the oracle accepts these and panics at runtime (rt.Dict: unsupported key type), which "if it compiles, it works" forbids. Silent on a key type that is not concrete, so a key-polymorphic Dict k v is unaffectedty/src/dictkey.rs, ty/src/check.rs
E3001non-exhaustive case (hard error)Diagnostic.hs:210-211, §11
E3002redundant arm — defined, unused (do not emit)Diagnostic.hs:213-214
E4001typed kernel call with any-typed primitive argDiagnostic.hs:218-219

The Rust compiler is compat-first (goal 00 §"Compat first"): where the above is quirky-but-relied-upon (interpolation Debug.toString-wrap, explicit-alias- wins, main auto-force, literal-fallback interpolation, conservative exhaustiveness), reproduce it, then improve behind a documented change — never silently diverge. The Haskell compiler is the differential oracle (doc 11).