aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--AGENTS/CONVENTIONS.md219
-rw-r--r--AGENTS/CONVENTIONS/CALLS.md60
-rw-r--r--AGENTS/CONVENTIONS/EXCEPTIONS_AND_UNITS.md48
-rw-r--r--AGENTS/CONVENTIONS/LEXING.md34
-rw-r--r--AGENTS/CONVENTIONS/STRINGS.md20
-rw-r--r--AGENTS/CONVENTIONS/SYMBOLS_AND_ERRORS.md56
-rw-r--r--AGENTS/CONVENTIONS/TEMPLATES_AND_OPERATORS.md54
-rw-r--r--AGENTS/CONVENTIONS/TYPES.md48
8 files changed, 337 insertions, 202 deletions
diff --git a/AGENTS/CONVENTIONS.md b/AGENTS/CONVENTIONS.md
index ed9acb1..b764915 100644
--- a/AGENTS/CONVENTIONS.md
+++ b/AGENTS/CONVENTIONS.md
@@ -1,206 +1,21 @@
# Load-bearing conventions
These invariants span modules. Violating any of them breaks code you didn't touch.
-(Overview and build instructions: `ARCH.md`.)
+(Overview and build instructions: `ARCH.md`.) The details are split by topic under
+`AGENTS/CONVENTIONS/`.
-**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<Pair<INTEGER,REAL>>`). `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<std::deque<FuncSig>>` 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.
+- [LEXING.md](CONVENTIONS/LEXING.md) — English-only project text, raw `int` tokens,
+ UPPERCASE keywords, identifier rules, `__cps_` reserved prefix, caller-owned lexer
+ stream.
+- [TYPES.md](CONVENTIONS/TYPES.md) — type names as strings, fixed builtin IR
+ representations, structural C aliases, nominal user kinds, global enum namespace.
+- [STRINGS.md](CONVENTIONS/STRINGS.md) — STRING as a library class, literal forms,
+ and value-copy ownership rules.
+- [SYMBOLS_AND_ERRORS.md](CONVENTIONS/SYMBOLS_AND_ERRORS.md) — the single `Symbols`
+ table and scoping, designators, `dynamic_cast` dispatch chains, error reporting.
+- [CALLS.md](CONVENTIONS/CALLS.md) — argument marshalling, signature-based dispatch,
+ parameter modes, array parameters, builtins table.
+- [TEMPLATES_AND_OPERATORS.md](CONVENTIONS/TEMPLATES_AND_OPERATORS.md) — template
+ substitution and selection, operator and assignment overloads.
+- [EXCEPTIONS_AND_UNITS.md](CONVENTIONS/EXCEPTIONS_AND_UNITS.md) — C++ ABI exceptions,
+ compilation units, MAIN, library units, default standard library.
diff --git a/AGENTS/CONVENTIONS/CALLS.md b/AGENTS/CONVENTIONS/CALLS.md
new file mode 100644
index 0000000..261b06f
--- /dev/null
+++ b/AGENTS/CONVENTIONS/CALLS.md
@@ -0,0 +1,60 @@
+# Conventions: calls, parameters and builtins
+
+Part of [Load-bearing conventions](../CONVENTIONS.md). Templates and operator
+overloads, which build on call resolution, are in [TEMPLATES_AND_OPERATORS.md](TEMPLATES_AND_OPERATORS.md).
+
+**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. Method prototypes are pre-registered too.
+- 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.
+
+**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.
diff --git a/AGENTS/CONVENTIONS/EXCEPTIONS_AND_UNITS.md b/AGENTS/CONVENTIONS/EXCEPTIONS_AND_UNITS.md
new file mode 100644
index 0000000..023354d
--- /dev/null
+++ b/AGENTS/CONVENTIONS/EXCEPTIONS_AND_UNITS.md
@@ -0,0 +1,48 @@
+# Conventions: exceptions, compilation units and the standard library
+
+Part of [Load-bearing conventions](../CONVENTIONS.md).
+
+**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.
+
+**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.
+
+**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.
diff --git a/AGENTS/CONVENTIONS/LEXING.md b/AGENTS/CONVENTIONS/LEXING.md
new file mode 100644
index 0000000..81b01d1
--- /dev/null
+++ b/AGENTS/CONVENTIONS/LEXING.md
@@ -0,0 +1,34 @@
+# Conventions: text, tokens and lexing
+
+Part of [Load-bearing conventions](../CONVENTIONS.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.
+
+Identifiers may begin with an underscore. The case-insensitive `__cps_` prefix is
+reserved in every user identifier, including EXTERN declarations.
+
+**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.
diff --git a/AGENTS/CONVENTIONS/STRINGS.md b/AGENTS/CONVENTIONS/STRINGS.md
new file mode 100644
index 0000000..2908167
--- /dev/null
+++ b/AGENTS/CONVENTIONS/STRINGS.md
@@ -0,0 +1,20 @@
+# Conventions: STRING values and ownership
+
+Part of [Load-bearing conventions](../CONVENTIONS.md).
+
+**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.
diff --git a/AGENTS/CONVENTIONS/SYMBOLS_AND_ERRORS.md b/AGENTS/CONVENTIONS/SYMBOLS_AND_ERRORS.md
new file mode 100644
index 0000000..3e3c3ad
--- /dev/null
+++ b/AGENTS/CONVENTIONS/SYMBOLS_AND_ERRORS.md
@@ -0,0 +1,56 @@
+# Conventions: symbols, dispatch and diagnostics
+
+Part of [Load-bearing conventions](../CONVENTIONS.md).
+
+**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.
+
+**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.
+
+**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.
diff --git a/AGENTS/CONVENTIONS/TEMPLATES_AND_OPERATORS.md b/AGENTS/CONVENTIONS/TEMPLATES_AND_OPERATORS.md
new file mode 100644
index 0000000..7177b23
--- /dev/null
+++ b/AGENTS/CONVENTIONS/TEMPLATES_AND_OPERATORS.md
@@ -0,0 +1,54 @@
+# Conventions: templates and operator overloads
+
+Part of [Load-bearing conventions](../CONVENTIONS.md). Basic call resolution is in
+[CALLS.md](CALLS.md).
+
+**Templates substitute ASTs before ordinary emission.**
+
+- Keep explicit `TemplateArgs` separate from call callee names; canonical type ids
+ have no spaces (`Box<Pair<INTEGER,REAL>>`).
+- `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<std::deque<FuncSig>>` 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.
+
+**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.
diff --git a/AGENTS/CONVENTIONS/TYPES.md b/AGENTS/CONVENTIONS/TYPES.md
new file mode 100644
index 0000000..ff6ddf8
--- /dev/null
+++ b/AGENTS/CONVENTIONS/TYPES.md
@@ -0,0 +1,48 @@
+# Conventions: type system
+
+Part of [Load-bearing conventions](../CONVENTIONS.md).
+
+**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.
+
+**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.
+
+**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).