aboutsummaryrefslogtreecommitdiff
path: root/AGENTS
diff options
context:
space:
mode:
Diffstat (limited to 'AGENTS')
-rw-r--r--AGENTS/ARCH.md17
-rw-r--r--AGENTS/CONVENTIONS.md62
-rw-r--r--AGENTS/EXTENDING.md32
-rw-r--r--AGENTS/PITFALLS.md50
-rw-r--r--AGENTS/SUBSYSTEMS.md60
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