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: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, 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) : 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 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).
  • 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 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
T009cannot assign type to immutable
T010return type mismatch
T011expect must be a boolean
T013incompatible comparison
T014invalid primary expression
T020operator cannot be used on the operand types
T021unsupported operator
T022cannot iterate over type
T023range loop bounds not int
T024cannot assign to let 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

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. The RUN_TYPE_VALID=1 gate was removed in the v0.11.0 soundness PR.
  • 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:391Check entry 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.go would keep it honest.
  • Loose builtins. any parameters are a temporary measure pending MEP 12.
  • break and continue outside loops. No error code yet. Should be added.

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.