diff options
Diffstat (limited to 'AGENTS')
| -rw-r--r-- | AGENTS/ARCH.md | 17 | ||||
| -rw-r--r-- | AGENTS/CONVENTIONS.md | 62 | ||||
| -rw-r--r-- | AGENTS/EXTENDING.md | 32 | ||||
| -rw-r--r-- | AGENTS/PITFALLS.md | 50 | ||||
| -rw-r--r-- | AGENTS/SUBSYSTEMS.md | 60 |
5 files changed, 171 insertions, 50 deletions
diff --git a/AGENTS/ARCH.md b/AGENTS/ARCH.md index cbf4b65..4ed2404 100644 --- a/AGENTS/ARCH.md +++ b/AGENTS/ARCH.md @@ -11,7 +11,12 @@ stdin ──> Lexer ──> Parser ──> vector<unique_ptr<StmtAST>> ──> C Fixed value representation (ABI — much code infers types backwards from these): INTEGER = `i64`, REAL = `double`, BOOLEAN = `i1`, CHAR = `i8`, STRING = opaque `ptr` -to a NUL-terminated buffer. +to a NUL-terminated buffer. User-defined types (§4): enum = `i64` (0-based ordinal), +pointer = opaque `ptr`, record = named struct `%record.NAME`. Enum/pointer are +LLVM-identical to INTEGER/STRING **by design** — their identity lives in the type +*name*, enforced upstream (see CONVENTIONS.md). Byte sizes are never computed +host-side: the module has no DataLayout string, so every size is a null-GEP +`sizeof` constant (`TypeSystem::getSizeOfConstant`) folded by the backend. ## Build & run @@ -45,15 +50,17 @@ untested.) |---|---| | `include/cps/` | All headers, included as `"cps/Foo.h"` | | `lib/Lexer/Lexer.cc` | Entire lexer: one function, `Lexer::gettok()` | -| `lib/Parser/Parser.cc` | Statements, expressions, precedence table | -| `lib/Parser/ParserFunction.cc` | FUNCTION/PROCEDURE/CALL/RETURN parsing | +| `lib/Parser/Parser.cc` | Statements, expressions, designators, precedence table | +| `lib/Parser/ParserFunction.cc` | FUNCTION/PROCEDURE/CALL/RETURN + parameter-list parsing | | `lib/Parser/ParserFile.cc` | OPENFILE/READFILE/WRITEFILE/CLOSEFILE parsing | +| `lib/Parser/ParserType.cc` | TYPE/ENDTYPE declarations, SET/DEFINE reserved stubs | | `lib/CodeGen/CodeGen.cc` | Hub: dispatch, coercion, control flow, INPUT/OUTPUT, main() | +| `lib/CodeGen/Designator.cc` | CodeGen methods for the TYPE system: type pre-pass, `emitLValue`/`loadFromLValue`, `emitCoercedExpr` gate, user-kind binary-op interception | | `lib/CodeGen/*Handler.cc`, `FunctionGen.cc`, `RuntimeCheck.cc`, `TypeSystem.cc` | Helpers owned by CodeGen | | `CMakeLists.txt` | 3 static libs (CPSLexer, CPSParser, CPSCodeGen) + `cpsc`. **New .cc files must be added to the source lists here** | -AST nodes live in `include/cps/AST.h` (+ `FunctionAST.h`, `FileAST.h`); they are pure -data with virtual destructors only — no codegen/visitor methods. +AST nodes live in `include/cps/AST.h` (+ `FunctionAST.h`, `FileAST.h`, `TypeAST.h`); +they are pure data with virtual destructors only — no codegen/visitor methods. ## Detail docs diff --git a/AGENTS/CONVENTIONS.md b/AGENTS/CONVENTIONS.md index 03a9452..7c6956d 100644 --- a/AGENTS/CONVENTIONS.md +++ b/AGENTS/CONVENTIONS.md @@ -19,12 +19,39 @@ in string/char literals. Comments are `//` to end of line. in a function-local `static int LastChar` and calls `getchar()` directly. No re-lexing, no file paths, and unit tests can't lex two programs in one process. -**Types travel as uppercase `std::string`** (`"INTEGER"`, `"REAL"`, `"BOOLEAN"`, -`"CHAR"`, `"STRING"`, `"VOID"`) — never an enum. `TypeSystem` resolves the string to -`TypeInfo{LLVMType, ElementSize, Kind, ...}`. The parser's `ParseTypeName` accepts *any* +**Types travel as uppercase `std::string`** (`"INTEGER"`, `"REAL"`, ..., plus +user-defined names) — 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. +**User-kind typing is nominal, and checked upstream only.** Enum/Record/Pointer +identity is 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 binary-op interception at the top of `emitExpr`'s +`BinaryExprAST` branch are the only doors. `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 +and the STRING null→`""` guard. + **Fixed IR representation per type.** INTEGER = `i64`, REAL = `double`, BOOLEAN = `i1`, CHAR = `i8`, STRING = opaque `ptr` to a NUL-terminated buffer. Some code still infers pseudocode types *backwards* from LLVM types (`getExprTypeInfo`'s fallback, @@ -63,16 +90,29 @@ next token that can start a statement or end a block (`syncToStatementStart`), t continues; the failure is always diagnosed and reflected in the exit code. **Calls dispatch on the declared signature.** `FunctionGen::emitPrototype` records every -function's pseudocode signature (`FuncSig`: return type name + per-param type name and -BYREF flag); `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 alloca is passed), coerces BYVAL arguments to the declared -parameter type, and rejects whole-array arguments. RETURN values are coerced to the -declared return type. STRING works on both sides in both modes. Top-level prototypes are -pre-registered before statement emission, so call-before-definition is fine; a call with +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 alloca is passed), admits BYVAL +arguments through `emitCoercedExpr`, and dispatches whole-array arguments to +`ArrayHandler::emitArrayArgument`. RETURN values go through `emitCoercedExpr` against +the declared return type. 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. Function names get no mangling, so `emitPrototype` rejects names that collide with runtime/libc symbols -(`isReservedRuntimeName`) or with builtins. +(`isReservedRuntimeName`), builtins, or enum values. + +**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 with `[Fatal]`). +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 are whole +malloc+memcpy copies at callee entry; BYREF binds the caller's buffer. **Builtins are plain identifiers backed by one table.** All 12 builtins (LENGTH/MID/RIGHT/LEFT/LCASE/UCASE/ASC/CHR/IS_NUM/NUM_TO_STR/STR_TO_NUM/EOF) parse as diff --git a/AGENTS/EXTENDING.md b/AGENTS/EXTENDING.md index 07549e3..0e12241 100644 --- a/AGENTS/EXTENDING.md +++ b/AGENTS/EXTENDING.md @@ -28,16 +28,36 @@ precedence must be > 0) → branch in `ArithmeticHandler::emitBinaryOp`, which d on raw LLVM operand types. Exception: `&` is intercepted upstream in `CodeGen::emitExpr` (operands coerced to STRING, then `StringHandler::emitConcat`). -**New type:** register in `TypeSystem::registerBuiltins` (correct `ElementSize` — -arrays malloc `count * ElementSize`) + `TypeKind` case + `getZeroValue` + -`coerceValueToType` + `emitOutputValue` + INPUT branch in CodeGen + lexer keyword + -`ParseTypeName` case. +**New builtin type:** register in `TypeSystem::registerBuiltins` (sizes are derived — +never hand-written) + `TypeKind` case in the **exhaustive, default-less switches**: +`getZeroValue`, `coerceValueToType`, `emitOutputValue`, `TypeInfo::isOutputtable`, +the INPUT branch in CodeGen + lexer keyword + `ParseTypeName` case. The compiler +warns on a missing enumerator — that is the anti-silent-fall-through mechanism; +never add a `default`. + +**New user-defined type KIND (like enum/record/pointer):** payload struct + checked +accessor in `TypeInfo`, `declareUserType`/`defineXxx` pair in TypeSystem (two-pass: +names then definitions), decl AST node in `TypeAST.h`, `ParseTypeDecl` branch, +pre-pass branch in `CodeGen::runTypePrePass`, admissibility rules in +`emitCoercedExpr` + `emitUserKindBinaryOp`, and a decision for every hole: +OUTPUT/INPUT/WRITEFILE/file-name operands, FOR variables, builtin args, +`getExprTypeInfo` classification. Everything not explicitly admitted must be a +compile error. + +**New designator access form:** `AccessKind` case in `TypeAST.h` + +`ParseDesignatorSuffix` + the walk in `CodeGen::emitLValue` **and** the static +walk in `getExprTypeInfo`'s designator branch (they must agree) — plus the +degrade rule in `ParseIdentifierExpr`/`ParseAssignStmt` if the new form can +collapse to a legacy node. **New runtime check:** message global + `emitXxxCheck` in `RuntimeCheck` (build an i1 fail-condition, call `emitErrorAndExit` — it splits the block and leaves the builder at the continuation), then call it from the emit site with the AST node's `getLine()`. -Precedent: `emitIndexCheck`, called per-dimension from ArrayHandler. Note that failure -messages print to **stdout** (`[Fatal] line %d: ...`) and `exit(1)`. +Precedents: `emitIndexCheck` (per-dimension, from ArrayHandler), `emitEnumRangeCheck` +(after enum ± INTEGER — the only site that can manufacture an invalid ordinal), +`emitNullDerefCheck` (before every `^` deref), `emitArrayArgBoundsCheck` (bounded +array-parameter contract). Note that failure messages print to **stdout** +(`[Fatal] line %d: ...`) and `exit(1)`. **New libc dependency:** declare via `getOrInsertFunction` in the owning component's `setupExternalFunctions()` (re-declaring the same symbol in two handlers is fine — it diff --git a/AGENTS/PITFALLS.md b/AGENTS/PITFALLS.md index e24b4bc..7fddfff 100644 --- a/AGENTS/PITFALLS.md +++ b/AGENTS/PITFALLS.md @@ -11,10 +11,24 @@ test programs. stray `, ", ", b` produces parse errors (recovery resyncs at the next statement start, so following statements survive; the compile still fails). Concatenate with `&`. Similarly INPUT only accepts a plain identifier — no `INPUT arr[i]`. -- **Arrays cannot cross function boundaries.** `ArrayTable` is never scoped; metadata - SSA values belong to the declaring function, so referencing another function's array - produces invalid IR. Passing a whole array as a call argument is rejected with a - compile error. +- **Arrays cross function boundaries only as parameters.** `ArrayTable` is scoped per + function now, so referencing another function's array fails cleanly as + "Undeclared array". Whole arrays pass as `ARRAY OF T` / `ARRAY[l:u,...] OF T` + parameters (BYVAL copies the entire buffer and never frees it); a whole-array + argument against a scalar parameter is still a compile error. FUNCTIONs cannot + RETURN arrays. A whole-array variable is also rejected as an operand or as a + value of its element type (it used to leak its raw data pointer). +- **Arrays are additionally block-scoped.** An array DECLAREd inside an + IF/WHILE/REPEAT/FOR body is not visible after the block ("Undeclared array") — + its buffer setup and bound SSA values live inside the branch, so letting it + escape meant wild-pointer writes or invalid IR. Scalars remain function-scoped + (their alloca is hoisted to the entry block); declare at the top of the + function like the exam board intends and neither matters. +- **Parameter modes are sticky and BYREF is FUNCTION-forbidden (§8.3).** + `(BYREF a : T, b : T)` makes `b` BYREF too — programs that relied on the old + per-parameter reset (where `b` was silently BYVAL) change meaning. Literal + arguments against an accidentally-BYREF parameter now fail with "BYREF argument + must be a variable"; annotate `BYVAL` explicitly. - **`/` always yields REAL** (double FDiv), even for two INTEGERs; integer division is DIV/MOD only. CHR returns a STRING (2-byte heap buffer), not CHAR. STR_TO_NUM always returns REAL. NUM_TO_STR formats i64 with sprintf `"%d"` — corrupts values over 32 @@ -43,11 +57,27 @@ test programs. the already-open check and get two independent buffered streams — reads through the alias can see stale/empty data instead of a Fatal. +- **TYPE-system sharp edges (§4).** `TYPE`/`ENDTYPE`/`SET`/`DEFINE` are reserved + keywords now. Typing is *nominal*: two pointer types with the same pointee, or two + records with the same layout, are incompatible. Strict pointee matching rejects the + official guide's own worked example (`^INTEGER` pointed at a `Season` variable) — + deliberate deviation. Enum OUTPUT prints the 0-based ordinal; a DECLAREd-but-unset + enum *is* the first enumerator (no uninitialized state). `E ± INTEGER` needs the + enum on the left; out-of-range results die `[Fatal]` at run time. There is no NULL + literal: pointers zero-init to null, dereferencing one dies `[Fatal]`, and unset-ness + is deliberately untestable. `RETURN ^localVar` dangles — documented, unchecked. + Record STRING fields stay null until read (normalized to `""` per-load, not at copy + time). Enum value names are globally reserved (they block DECLARE/parameter/function + names). TYPE must be top-level; array fields inside records and bounded array + parameters with non-literal bounds are dedicated compile errors. A digit run + followed by `.` is swallowed into a REAL literal, so `1.foo` mislexes (only + already-invalid programs can hit it). + ## Not implemented (9618 features) -CASE OF, CONSTANT, TYPE/ENDTYPE (records/enums/pointers), random files -(OPENFILE...FOR RANDOM/SEEK/GETRECORD/PUTRECORD — blocked on TYPE; the RANDOM keyword -is reserved and gives a dedicated parse error), DATE, multi-expression OUTPUT, INPUT -into array elements, string escape sequences. `README.md` lists `type` as the intended -next work. No tests and no CI exist; verify changes by compiling sample programs -end-to-end (command sequence in `ARCH.md`). +CASE OF, CONSTANT, SET types (`SET`/`DEFINE` are reserved with dedicated errors), +random files (OPENFILE...FOR RANDOM/SEEK/GETRECORD/PUTRECORD — TYPE is done, so these +are unblocked now; the RANDOM keyword is reserved and gives a dedicated parse error), +DATE, multi-expression OUTPUT, INPUT into array elements, string escape sequences, +array-typed record fields, whole-array RETURN. No tests and no CI exist; verify +changes by compiling sample programs end-to-end (command sequence in `ARCH.md`). diff --git a/AGENTS/SUBSYSTEMS.md b/AGENTS/SUBSYSTEMS.md index 6a89e2b..9f243c2 100644 --- a/AGENTS/SUBSYSTEMS.md +++ b/AGENTS/SUBSYSTEMS.md @@ -4,28 +4,52 @@ Deep-dives on the CodeGen helper subsystems. (Invariants: `CONVENTIONS.md`; known bugs: `PITFALLS.md`.) **Arrays** (`ArrayHandler`): a DECLAREd array is one malloc'd flat buffer (memset to -zero), row-major, any rank, any bounds. Bounds and multipliers are *compile-time SSA -values* cached in `ArrayMetadata` (global `ArrayTable` map, keyed by name). Every access -goes through `emitCheckedIndices` (validates the index count for the caller's semantics, -then per-dimension coerce + `emitIndexCheck`) → `computeFlatIndex` → `loadElement` -(load + the STRING null-guard `select` to the empty string); never hand-roll indexing -or element loads. +zero), row-major, any rank, any bounds. Bounds and multipliers are *SSA values* cached +in `ArrayMetadata`; `ArrayTable` is scoped per function (`exchangeTable`, driven from +CodeGen's `FunctionDefAST` branch, mirroring the Symbols discipline). Element sizes are +`sizeof` constants (`ElementSizeC`), never host-side numbers. Every access goes through +`emitCheckedIndices` (validates the index count for the caller's semantics, then +per-dimension coerce + `emitIndexCheck`) → `computeFlatIndex` → `loadElement` (load + +the STRING null-guard); the public `emitElementAddress` exposes the same checked +pipeline as an address for the lvalue layer (`Form[i].YearGroup`, `^arr[i]`) — never +hand-roll indexing or element loads. Array parameters: `bindArrayParameter` rebuilds +callee-side metadata from the flattened `ptr + 2R bounds` ABI (BYVAL = malloc+memcpy +whole copy), `emitArrayArgument` validates and pushes the caller side. `OUTPUT arr` (and partially-indexed accesses) is intercepted by `ArrayHandler::tryEmitArrayOutput` — the first branch of OUTPUT dispatch, emitting -nested print loops — so any rework of OUTPUT must preserve that interception. +nested print loops (element kinds that are not outputtable, i.e. records/pointers, +are a compile error there) — so any rework of OUTPUT must preserve that interception. **Functions** (`FunctionGen`): plain LLVM functions, external linkage, exact pseudocode -name. Params are `tuple<name, typeString, isByRef>`. BYVAL args are copied into -entry-block allocas; BYREF args bind the incoming pointer directly as the variable's -storage. Missing RETURNS defaults the return type to INTEGER; procedures use "VOID". -`emitPrototype` records each function's pseudocode signature in a `FuncSig` map -(`FunctionGen::getSignature`); `CodeGen::marshalCallArgs` dispatches call arguments on -those *declared* param types and BYREF flags (BYVAL args are coerced to the declared -type), and `getExprTypeInfo` reads return types from the same map. Top-level prototypes -are pre-registered at the start of `compile()`, so calls may precede definitions; -calling a name with no signature is a compile error (no extern is fabricated). Body -emission is inverted: CodeGen injects a `StmtEmitter` callback, and must restore its -own insert point afterwards. +name. Params are `ParamDecl{name, typeString, mode, array rank/bounds}`. BYVAL args are +copied into entry-block allocas (whole structs for records); BYREF args bind the +incoming pointer directly as the variable's storage; array params are handed to the +binder callback CodeGen injects (`setArrayParamBinder`) so FunctionGen never sees +ArrayHandler. Missing RETURNS defaults the return type to INTEGER; procedures use +"VOID". `emitPrototype` records each function's pseudocode signature in a `FuncSig` +map (`FunctionGen::getSignature`); `CodeGen::marshalCallArgs` dispatches call arguments +on those *declared* `ParamSig`s, and `getExprTypeInfo` reads return types from the same +map. Top-level prototypes are pre-registered at the start of `compile()` (after the +TYPE pre-pass), so calls may precede definitions; calling a name with no signature is a +compile error (no extern is fabricated). Body emission is inverted: CodeGen injects a +`StmtEmitter` callback, and must restore its own insert point (and the exchanged +ArrayTable) afterwards. + +**User types** (`TypeSystem` + `Designator.cc`): TYPE declarations are top-level only +and registered by `CodeGen::runTypePrePass` in two passes — `declareUserType` reserves +every name (records become opaque named structs, so pointer pointees may +forward-reference: `TYPE NodePtr = ^Node` before `TYPE Node`), then +`defineEnum/defineRecord/definePointer` fill payloads in source order (a by-value +record field needs its record complete; inclusion via pointer is always fine). Enum +constants live in a TypeSystem-owned table, resolved in `emitExpr`'s +`VariableExprAST` branch after `Symbols` misses. The lvalue layer +(`emitLValue`/`loadFromLValue`) resolves designator chains to `{address, type name}`; +`emitCoercedExpr` is the single name-level admission gate; `emitUserKindBinaryOp` +intercepts binary operators ahead of ArithmeticHandler (which, like StringHandler and +FileHandler, is deliberately ignorant of user kinds). The enum range check after +`E ± INTEGER` is the only site that can produce an invalid ordinal — every other +producer (constants, same-name copies, zero-init, memset-0 arrays) is valid by +construction, which is why 0-based ordinals are load-bearing. **Runtime checks** (`RuntimeCheck`): only array bounds checking and the file checks are actually wired up. `emitDivZeroCheck` exists but has **zero call sites** — division by |
