MEP 6. Type Checker
| Field | Value |
|---|---|
| MEP | 6 |
| Title | Type Checker |
| Author | Mochi core |
| Status | Informational |
| Type | Informational |
| Created | 2026-05-08 |
Abstract
The Mochi type checker is a non-short-circuiting walk over the AST that accumulates errors and returns them all. This MEP documents the entry point, the builtin environment, the statement walk, the error catalogue, and the workflow for adding new rules.
Motivation
Type checker source files quickly become hard to read because every statement form contributes a separate function. A written record of which rule lives where, plus a flat list of error codes with their semantics, lets contributors locate the rule they need to extend without reading the whole file.
Specification
Entry point
types/check.go:391. The signature:
func Check(prog *parser.Program, env *Env) []error
The function is non-short-circuiting. It accumulates errors and returns all of them. A program that is rejected on every statement still emits one error per statement, which matters for IDE integration and for the golden tests that snapshot the error stream.
Builtin environment
types/check.go:392-562 registers the builtins. The list at v0.10.82:
print(...) : void. Variadic, acceptsany.len(any) : int. Loosely typed. Pure.append([T], T) : [T]. Variadic erased: actually accepts[any]and any element today.concat([T], ...) : [T]. Variadic. Pure.first([T]) : any. Pure.reverse(any) : any. Pure.distinct([T]) : [T]. Pure.push([T], T) : [T]. Pure.keys({K:V}) : [K]. Pure.values({K:V}) : [V]. Pure.collect(any) : [any]. Pure.range(int, ...) : [int]. Variadic. Pure.now() : int64. Effectful.json(any) : void. Effectful.to_json(any) : string. Pure.str(any) : string. Pure.parseIntStr(string, int) : int. Pure.int(any) : int. Pure.upper(string) : string. Pure.lower(any) : string. Pure.trim(string) : string. Pure.contains(string, string) : bool. Pure. (Plus list and map overloads registered later in the file.)- A pile more builtins for math, strings, lists, maps.
The looseness of many builtins (any parameters) is intentional today because we do not have parametric polymorphism. Tightening them is a work item in MEP 12.
Statement walk
Each statement form has a corresponding check function. The dispatch is in Check and recurses through checkStmt:
LetStmt. Resolve declared type if present. Infer value type if present. Unify. Bind. Errors: T000, T008.VarStmt. Same asLetStmtbut mutable.AssignStmt. Look up target. Validate it isvarmutable (T024). Walk index and field ops to refine the LHS type, check the RHS matches.FunStmt. Build aFuncType. Push a child env binding params and type params. Check body statements. Validate return type from trailingreturnstatements (T010).IfStmt. Cond bool (T040). Recurse into then and else.WhileStmt. Cond bool. Recurse.ForStmt. Source must be iterable (T022). Bind loop variable. Recurse.ReturnStmt. Compare value type with current function's return type.BreakStmt,ContinueStmt. Must be inside a loop (no error code yet).ExpectStmt. Value must be bool (T011).ExprStmt. Just type the expression for side effect; ignore result.FetchStmt. URL string (T028). Options map (T029). Bind target.UpdateStmt. Resolves the target type, walks the set map and where predicate, types each field assignment.TypeDecl. Builds either aStructType, aUnionType, or an alias. Registers the constructor functions for unions.ImportStmt. Resolves the module and binds identifiers. The exact semantics depend on the language tag (python,go,ts).ExternFunDecl,ExternVarDecl. Trust the declared type.Test,Bench. Recurse into the body.AgentDecl,StreamDecl,OnHandler,EmitStmt,FactStmt,RuleStmt,IntentDecl. Type checked at varying depth. Some are shallow.
Error catalogue
All error codes are defined in types/errors.go. Each entry has a Code, Message, and Help. When a code fires, the formatter renders output like:
1. error[T022]: cannot iterate over type int
--> tests/types/errors/cannot_iterate.mochi:1:1
1 | for i in 3 {
| ^
help:
Only `list<T>`, `map<K,V>`, or integer ranges are allowed in `for ... in ...` loops.
Highlights:
| Code | Meaning |
|---|---|
| T000 | let requires a type or a value |
| T001 | assignment to undeclared variable |
| T002 | undefined variable |
| T003 | unknown function |
| T004 | not callable |
| T005 | parameter missing a type |
| T006 | too many arguments |
| T007 | argument N type mismatch |
| T008 | type mismatch in assignment context |
| T009 | cannot assign type to immutable |
| T010 | return type mismatch |
| T011 | expect must be a boolean |
| T013 | incompatible comparison |
| T014 | invalid primary expression |
| T020 | operator cannot be used on the operand types |
| T021 | unsupported operator |
| T022 | cannot iterate over type |
| T023 | range loop bounds not int |
| T024 | cannot assign to let binding |
| T025 | unknown type |
| T026 | unknown field on struct |
| T027 | not a struct |
| T028 | fetch URL must be string |
| T029 | fetch options must be map |
| T030 | invalid type for fetch option |
| T031 | unknown stream |
| T032 | query source not a list |
| T033 | where condition not bool |
| T034 | join source not a list |
| T035 | on condition not bool |
| T036 | cannot take length of type |
| T037 | count expects list or group |
| T038 | avg expects numeric list or group |
| T039 | function expects N arguments |
| T040 | if condition must be bool |
| T041 | sum expects numeric list or group |
| T042 | having must be bool |
| T043 | operator cannot be used with any |
The series has gaps. New codes should be appended to the end with the next free integer.
Tests
types/check_test.go.
TestTypeChecker_Validruns every.mochifile intests/types/valid/against the checker and confirms no errors. TheRUN_TYPE_VALID=1gate was removed in the v0.11.0 soundness PR.TestTypeChecker_Errorsruns every.mochifile intests/types/errors/and snapshots the error stream against the matching.errfile.
The .err snapshot is a strict equality check. Renaming an error message is a deliberate breaking change.
Adding a new rule
Workflow:
- Add a fixture under
tests/types/valid/exercising the accepted shape. Runmake update-golden STAGE=typesto seed the golden. - Add a fixture under
tests/types/errors/exercising the rejected shape. - Implement the rule in
types/check.goortypes/infer.go. - If a new error code is needed, append it to
types/errors.go. - Update the relevant MEP.
Performance
Check does a single pass for each statement. Function bodies are checked when the function is declared. Recursive calls are typed against the declared signature and never re-entered. The cost is roughly linear in the number of AST nodes.
Rationale
Non-short-circuiting matters for IDE use cases. We want the user to see every error at once, not the first one. The cost is more code paths to handle gracefully when an earlier statement is malformed.
A flat error code table is easy to scan and easy to grep. A hierarchical taxonomy would be prettier but harder to extend without renumbering.
Backwards Compatibility
Informational. No backward compatibility implications.
Reference Implementation
types/check.go:391—Checkentry point.types/check.go:392-562— builtin registration.types/errors.go— error catalogue.types/check_test.go— golden tests.
Open Questions
- Generated error table. The Markdown table in this MEP is hand-maintained. A generator from
types/errors.gowould keep it honest. - Loose builtins.
anyparameters are a temporary measure pending MEP 12. breakandcontinueoutside loops. No error code yet. Should be added.
References
- See MEP 5 for the inference rules
Checkconsults. - See MEP 7 for the soundness obligations
Checkaims to enforce.
Copyright
This document is placed in the public domain.