# Load-bearing conventions These invariants span modules. Violating any of them breaks code you didn't touch. (Overview and build instructions: `ARCH.md`.) **Project text must be in English.** This applies to documentation, source comments, user-facing messages, examples and test fixtures. Unicode test data must not contain Chinese text. Keep user instructions in README files concise; put architecture, implementation notes and AI guidance in the relevant `AGENTS/` directory. **Tokens are raw `int`.** `Lexer::gettok()` returns negative values for the `Token` enum (`include/cps/Lexer.h`) and non-negative values for literal ASCII chars (`'('`, `'+'`, `'<'`). Parser and CodeGen switch on both forms interchangeably; `BinaryExprAST` stores this same int as its operator. Payloads (identifier text, literal values) arrive out-of-band via public `Lexer` fields (`IdentifierStr`, `StringVal`, `NumVal`, `RealVal`, `CharVal`) that must be read before the next `gettok()`. **Keywords are UPPERCASE and case-sensitive.** `declare` silently lexes as an identifier. Assignment is `<-` (ASCII only), equality is single `=`, inequality `<>`, concatenation `&` (reaches the parser as raw ASCII 38 — no token). Identifiers use ASCII case-insensitive lookup/comparison through `IdentifierMap`, `IdentifierSet` and `sameIdentifier`; retain source spelling in ASTs and diagnostics. This includes functions, types, enum values, fields and builtins, but not C_TYPE import strings. Integer tokens carry a checked unsigned magnitude through lookahead; the parser applies the sign and i64 range check, including the minimum value. String/char literals support C++17 byte escapes and Unicode escapes (UTF-8 in STRING, ASCII only in CHAR). Comments are `//` to end of line. **The lexer reads a caller-owned stream.** Construct it with `Lexer(std::istream &)`; the stream must outlive the lexer. Lookahead, line number and error state belong to each instance. The driver opens the source file; stdin is not used for compilation. **Type names travel as `std::string`** (uppercase builtins such as `"INTEGER"` and `"REAL"`; user-defined names retain their spelling) — never an enum. `TypeSystem` resolves the string to `TypeInfo{LLVMType, Kind, per-kind payload}`. The parser's `ParseTypeName` accepts *any* identifier as a type name except `VOID` (rejected outside a RETURNS clause via its `AllowVoid` flag); other unknown types only fail (softly) in CodeGen. **C aliases are structural within the C domain.** `CIdentity` retains the canonical C base type and pointer levels independently of the alias `Name`. Use `sameType` for storage compatibility (including BYREF and array arguments); never equate C types just because their LLVM types or widths match. CScalar/CPointer values are handled by CTypeGen, not the legacy representation coercions. Cross-domain assignments, arguments and operators require explicit casts, including literals. Plain C char uses native signedness; pseudocode CHAR stays unsigned. Numeric conversions use source signedness, and native floating formats use numeric_limits. Allocation sizes still use LLVM sizeof; native C type widths use build-compiler sizeof. C alias declarations precede casts using their names; the parser tracks declared type names to distinguish `(Type)operand` from `(expression)`. **User-kind typing is nominal, and checked upstream only.** Enum/Record/Pointer identity is case-insensitive name equality (`P1 = ^INTEGER` and `P2 = ^INTEGER` are incompatible; same-layout records too). Their LLVM representations are indistinguishable from builtins, so values of these kinds must NEVER reach `ArithmeticHandler` or `coerceValueToType` without a prior name-level check — `CodeGen::emitCoercedExpr` (assignments, BYVAL args, RETURN, IF/WHILE/REPEAT/FOR operands, file operands, builtin args) and the typed `CodeGen::emitBinaryOp` dispatcher in `ExpressionGen.cc` guard these paths. The dispatcher resolves overloads and handles C/user kinds before builtin arithmetic. `coerceValueToType` hard-errors ("Internal: ...") if a user-kind target reaches it. **Enum value names are a flat, globally reserved namespace** owned by `TypeSystem` (not `Symbols`, so they survive FunctionGen's save/clear/restore). Expression resolution order is `Symbols` first, then enum constants; collisions with DECLARE/array/parameter/function names are compile errors at declaration time, so the order can never shadow. TYPE declarations are top-level only, registered by `compile()`'s two-pass pre-scan (names first — pointer pointees may forward-reference; definitions second, in source order — a by-value record field requires its record type to be already complete). **Designators degrade.** The parser turns `x` / `arr[i]` into the legacy `VariableExprAST`/`ArrayAccessExprAST` (and their assign statements) and only builds `DesignatorExprAST`/`DesignatorAssignStmtAST` for chains involving `.`, `^` or multiple accessors — existing programs keep byte-identical ASTs. `CodeGen::emitLValue` resolves any of these to `{address, type name}`, emitting index and null-deref checks on the way; `loadFromLValue` owns the rvalue read of the resolved value. **Fixed IR representation per builtin type.** INTEGER = `i64`, REAL = `double`, BOOLEAN = `i1`, CHAR = `i8`. STRING is the ordinary standard-library class `%record.STRING`, not a builtin pointer type. C_STRING remains a raw NUL-terminated C pointer. Some arithmetic code still infers types backwards from LLVM widths; preserve the builtin scalar ABI. **STRING values use the class machinery.** `string.cps` owns a std::string through an internal handle. Ordinary literals construct STRING through NEW(C_STRING); suffixed `"text"C` literals produce C_STRING directly. Ordinary literals retain legacy NUL truncation. `StringValueGen.cc` bridges construction, C_STR access and file operands; INPUT calls READ_INPUT. String functions and all concatenation/comparison operators are ordinary library definitions, with exact overload selection. Comparisons use case-sensitive byte ordering. A missing STRING class or required bridge method is a compile error. **Ownership follows value copies.** STRING constructors/assignment prevent shared handles. Records and BYVAL arrays containing STRING recursively copy their string-bearing fields/elements; RETURN and temporary cleanup preserve ownership. C_STRING casts borrow a STRING buffer or construct an owning STRING copy in the other direction. FileHandler owns copied open-file names and frees them on close; READFILE buffers are copied into STRING and released. Explicit MemoryHandler allocations remain separate from these internal objects. **One symbol namespace, one table.** `Symbols` (name → `SymbolInfo{Storage, TypeName, IsArray, ConstantValue, ScopeID, IsReference, IsGlobal, ReadOnly}`) in CodeGen is the only symbol table. Use `symbolAddress` for an object address: class variables and BYVAL class parameters store their current object address in a reference slot (`IsReference`). Use SymbolInfo::isConstant() for constant checks: scalar literals use ConstantValue; owning STRING constants use ReadOnly plus Storage. Read values through emitExpr. Writable/address-taking paths reject constants through requireWritable; internal STRING copy/assignment source bindings may read their storage. Register variables through `registerSymbol()` (parameters are also bound directly by FunctionGen). `prepareSymbolDeclaration` diagnoses same-scope conflicts involving constants before declaration emission. Unique scope IDs distinguish sibling blocks; `emitScopedBlock` restores only bindings introduced by constants or hiding constants, preserving other variables' existing function-wide visibility. FunctionGen saves/restores Symbols and inherits visible globals plus previously visible top-level constants (ScopeID 0). Locals may shadow globals. CodeGen isolates constant scope bookkeeping and array metadata around each function body. Arrays also restore metadata at control-flow block exits. **Dispatch is by `dynamic_cast` chain, not virtual methods.** `CodeGen::emitStmtImpl` in `StatementGen.cc` and `emitExpr` in `ExpressionGen.cc` walk linear if-chains of casts. **A statement type missing from the chain is silently ignored** — no error. Adding an AST node always means touching these chains. Emit statements through `emitStmt`, whose wrapper cleans up statement temporaries after dispatch. **Compiler error style: print an `error:` line, set the component's error flag, and keep compiling.** No exceptions, no exit mid-compile. `Lexer`/`Parser`/`CodeGen` each expose `hadError()`; AST expressions/statements share source-line metadata. Parser entry points record token locations. CodeGen saves/restores its diagnostic line across recursive emission and coercion; `reportError` appends `at line N`. `CodeGen::reportError(fmt, ...)` is the helper for codegen-side diagnostics (`ArrayHandler` reaches it through its `CodeGen &` parameter, `FunctionGen`/`ArithmeticHandler` hold a `bool &HadError` reference). The driver exits non-zero when any flag is set, printing `compilation terminated.` once. Errors that prevent compilation (missing input, file I/O or invalid IR) use `fatal error:`. Execution-stage errors do not print the compilation termination line. The parser drops a failed statement and resyncs at the next token that can start a statement or end a block (`syncToStatementStart`), then continues; the failure is always diagnosed and reflected in the exit code. **Calls share argument marshalling.** `CodeGen::emitCall` handles global expression calls, CALL statements and bare `name(args)` statements; `emitMemberCall` resolves member access and adds a hidden receiver address. Both use `marshalCallArgs` and FunctionGen for user calls; statement calls discard results. Explicit casts may bridge C void* and nominal pseudocode pointer types. **User calls dispatch on the declared signature.** `FunctionGen::emitPrototype` records every function's pseudocode signature (`FuncSig`: return type name + per-param `ParamSig` {type name, mode, array rank/bounds}); `CodeGen::marshalCallArgs` — the single call-marshalling path for both call-expr and call-stmt — requires BYREF arguments to be bare variables of *exactly* the declared type (their `symbolAddress` is passed), admits BYVAL arguments through `emitOwnedValue`, and dispatches whole-array arguments to `ArrayHandler::emitArrayArgument`. RETURN also uses `emitOwnedValue` against the declared return type. Class lvalues invoke a same-class BYREF OVERLOAD NEW when defined; STRING-bearing aggregates clone their owned fields. Other classes without a copy constructor copy bits; class rvalues transfer ownership. Other values use `emitCoercedExpr`. Top-level prototypes are pre-registered (after the TYPE pre-pass) before statement emission, so call-before-definition is fine; a call with no registered signature is a compile error. OVERLOAD signatures append canonical parameter types to the link name; ordinary names except MAIN get no mangling. `getOverloads` filters by visibility and `resolveCall` selects exact static types/ranks. Return types, modes and bounds never distinguish overloads. In all cases, `emitPrototype` rejects names that collide with runtime/libc symbols (`isReservedRuntimeName`), builtins, or enum values. Signatures retain the first declaration spelling for LLVM lookup, independently of call-site casing. Methods use `Class.Method` names and a hidden leading receiver address; their FuncSig parameters contain only explicit source parameters. Method prototypes are pre-registered too. **Templates substitute ASTs before ordinary emission.** Keep explicit `TemplateArgs` separate from call callee names; canonical type ids have no spaces (`Box>`). `TemplateClone.cc` owns deep cloning and structural substitution; every new body AST node needs coverage there. A type parameter binds one canonical type and a pack binds a list; expanded parameter names use unlexable `#` suffixes. FunctionGen stores overload signatures in `IdentifierMap>` so instantiating another template cannot invalidate a held `FuncSig *` during marshalling. CodeGen owns cloned ASTs for the entire compile and caches each instance by declaration plus canonical arguments. **Template selection and context are shared by typing and emission.** When multiple definitions share a function/procedure name, every ordinary and template definition must specify OVERLOAD. `resolveCall` computes static argument types/ranks once, then tries exact ordinary, non-variadic template and variadic template tiers; same-tier ties are errors. Candidate checking must stay silent and must not instantiate speculative class types. Explicit arguments pre-bind types without allowing conversions. Instances keep the template owner for diagnostics/library behavior and the first requester in `VisibilityUnit` for lookup (`value_or(CurrentUnit)`). Deferred bodies use the symbol snapshot at the template declaration to preserve positional own constants; save and restore all caller context. Class resolution hooks must honor visibility and reserve/cache the record before resolving fields. **Operator overloads share static selection.** OPERATOR is a reserved keyword; `OVERLOAD FUNCTION OPERATOR+(...)` records the token on PrototypeAST. Canonical names contain a space (`OPERATOR +`, `Class.OPERATOR AND`) to avoid collisions with ordinary identifiers. `OperatorGen.cc` probes visible global and left-class member signatures without emitting diagnostics or evaluating operands. Matching is exact; member/global ties are ambiguous and access is checked only after selection. Both getExprTypeInfo and emission must consult this resolver before hardcoded builtin result types. Returns may be any supported function result, including non-BOOLEAN comparisons/logic. emitCoercedExpr rejects implicit STRING to BOOLEAN; CASE separately requires BOOLEAN comparisons. Operators use shared argument marshalling and exception-aware calls. Member lvalues keep their original address; rvalues are materialized with Alive tracking and destroyed at statement end. **Parameter modes are sticky and BYREF is procedure-only (guide §8.3).** In `(BYREF a : T, b : T)` the `b` is BYREF too; unannotated parameters default to BYVAL (§8.1). A BYREF parameter in a FUNCTION is a parse error. **Array parameters**: `ARRAY OF T` (rank 1, bounds travel with the argument) or `ARRAY[l:u,...] OF T` (literal bounds, any rank, a contract checked at compile time when the caller's bounds are constants, else at run time by throwing an exception). The LLVM ABI flattens each rank-R array parameter into a data `ptr` plus `kBoundArgsPerDim` (=2) i64 bounds per dimension; the callee rebuilds its `ArrayMetadata` from those (`ArrayHandler::bindArrayParameter` via the binder callback CodeGen injects into FunctionGen). BYVAL arrays copy their buffer at callee entry and clone STRING-bearing elements; BYREF binds the caller's buffer. **Builtins are plain identifiers backed by one table.** Builtins (EOF/MALLOC/FREE/REALLOC) parse as ordinary calls — no lexer/parser support, and the names are not reserved words. The inline constexpr `Builtins` table in the internal `lib/CodeGen/Builtins.h` ({Name, Arity, ReturnTypeName}) is the single source of truth: `emitCall` checks arity against it before dispatching to the per-builtin emit branches (still an if-chain, ahead of the user-function fallback), and `getExprTypeInfo` reads return types from it. EOF and the memory builtins consume the call source line for runtime diagnostics; they touch mutable runtime state and can throw. **Program exceptions use the C++ ABI; compiler diagnostics do not.** `ExceptionHandler` emits anonymous integer exceptions (an internal placeholder), `invoke`/`landingpad`/`resume`, and paired `__cxa_begin_catch`/`__cxa_end_catch`. Its handler stack and catch depth are keyed by LLVM function, so synthesizing file helpers never inherits the caller's catches. User calls, file runtime calls and THROW must use `ExceptionHandler::emitCall`; ordinary nonthrowing libc calls stay plain calls. RETURN must finish all active catches after evaluating its value. `RuntimeCheck::emitCheck` prints the source line and error reason to stderr before throwing. Generated main catches anything unhandled, prints the generic termination message to stderr, and returns 1. Explicit bare THROW prints no diagnostic itself. Arrays in TRY and CATCH use separate block scopes; scalars retain existing function scope. **Compilation units and entry points.** SourceUnit borrows its AST list; it and the visibility matrix must outlive compile(). Select the owning unit before each pre-pass, prototype or body so diagnostics and name lookup use that file. TypeSystem's predicate filters types and their enum values; FuncSig records OwnerUnit. Rebuild Symbols from visible exports for each unit: imported constants are available immediately, own constants only at their declaration, and globals from the beginning. GLOBAL bodies contain only primitive scalar declarations and literal assignments; IsGlobal storage is an LLVM GlobalVariable and must survive FunctionGen's symbol reset. User MAIN is case insensitive, takes no parameters and returns INTEGER; its LLVM name is __cps_main with internal linkage. The generated i32 main wrapper calls it through ExceptionHandler, truncates its result and retains the final uncaught handler. Identifiers may begin with an underscore. The case-insensitive `__cps_` prefix is reserved in every user identifier, including EXTERN declarations. **Library units.** Line-leading `#LIB` marks SourceUnit::IsLibrary. RuntimeCheck uses the externally visible i32 `__cps_user_line`; FunctionGen’s shared call hook updates it only on non-library to library calls. Runtime helper line arguments use RuntimeCheck::lineValue too. Compiler diagnostics retain the owning file location. Assignment operators are member-only `OVERLOAD PROCEDURE OPERATOR<-` with one explicit parameter. emitAssignment resolves the exact RHS static type before fallback storage copying. Internal assignment/constructor BYREF bindings may use designators or materialized temporaries; ordinary BYREF calls still require bare variables. **Default standard library.** The driver extracts embedded std.cps, its imported units and matching objects to a temporary directory that survives linking/execution. User units implicitly import std.cps; normal transitive visibility applies. -no-std suppresses all default library inputs and objects. Explicit libraries retain the ordinary INCLUDE/command-line rules. Source files and objects are embedded by a build-time Perl generator. Saved -S output remains IR only.