Skip to main content

MEP 6. Type Checker

FieldValue
MEP6
TitleType Checker
AuthorMochi core
StatusInformational
TypeInformational
Created2026-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:487. 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:488-720 registers the builtins. The list at v0.10.82:

  • print(...) : unit. Variadic, accepts any.
  • 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) : unit. 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 as LetStmt but mutable.
  • AssignStmt. Look up target. Validate it is var mutable (T024). Walk index and field ops to refine the LHS type, check the RHS matches.
  • FunStmt. Build a FuncType. Push a child env binding params and type params. Check body statements. Validate return type from trailing return statements (T010).
  • ModelStmt. Bind the model identifier with the declared parameter and return types. Shallow today.
  • IfStmt. Cond bool (T040). Recurse into then, else-if, and else blocks. Each block gets a fresh child env.
  • WhileStmt. Cond bool (T040). Recurse body with loop context active.
  • 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 (T045).
  • 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 a StructType, a UnionType, 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:

CodeMeaning
T000let requires a type or a value
T001assignment to undeclared variable
T002undefined variable
T003unknown function
T004not callable
T005parameter missing a type
T006too many arguments
T007argument N type mismatch
T008type mismatch in assignment context
T009type mismatch in assignment (use var for mutation)
T010return type mismatch
T011expect must be a boolean
T013incompatible comparison
T014invalid primary expression
T015index must be an integer
T016missing index expression
T017slicing not allowed on map
T018type does not support indexing
T019map key type mismatch
T020operator cannot be used on the operand types
T021unsupported operator
T022cannot iterate over type
T023range loop bounds not int
T024cannot assign to immutable binding
T025unknown type
T026unknown field on struct
T027not a struct
T028fetch URL must be string
T029fetch options must be map
T030invalid type for fetch option
T031unknown stream
T032query source not a list
T033where condition not bool
T034join source not a list
T035on condition not bool
T036cannot take length of type
T037count expects list or group
T038avg expects numeric list or group
T039function expects N arguments
T040if condition must be bool
T041sum expects numeric list or group
T042having must be bool
T043operator cannot be used with any
T044impure call not allowed in where/having predicate
T045break/continue outside of loop
T046invalid cast: from-type as to-type is not allowed
T050non-exhaustive match on union: missing variant(s)

The series has gaps. New codes should be appended to the end with the next free integer.

Tests

types/check_test.go.

  • TestTypeChecker_Valid runs every .mochi file in tests/types/valid/ against the checker and confirms no errors.
  • TestTypeChecker_Errors runs every .mochi file in tests/types/errors/ and snapshots the error stream against the matching .err file.

The .err snapshot is a strict equality check. Renaming an error message is a deliberate breaking change.

Adding a new rule

Workflow:

  1. Add a fixture under tests/types/valid/ exercising the accepted shape. Run make update-golden STAGE=types to seed the golden.
  2. Add a fixture under tests/types/errors/ exercising the rejected shape.
  3. Implement the rule in types/check.go or types/infer.go.
  4. If a new error code is needed, append it to types/errors.go.
  5. 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:487: Check entry point.
  • types/check.go:488-720: 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.go would keep it honest.
  • Loose builtins. any parameters are a temporary measure pending MEP 12.
  • break and continue outside loops. Implemented as T045.

Problems

  1. keys is registered three times. types/check.go calls env.SetVar("keys", …) at lines 529, 687, and 712. Only the last registration is visible at lookup time. Fix: collapse to a single registration and pick the intended signature.

References

  • See MEP 5 for the inference rules Check consults.
  • See MEP 7 for the soundness obligations Check aims to enforce.

This document is placed in the public domain.