diff options
| author | _ov4 <ov4@ov4.org> | 2026-07-26 18:10:10 +0000 |
|---|---|---|
| committer | _ov4 <ov4@ov4.org> | 2026-07-26 18:10:10 +0000 |
| commit | c2c2e1771bf9b747f38dc145b5201b45c579d465 (patch) | |
| tree | 09c752f360e56f523eb87e4a6dc17751be56210b | |
| parent | a44f1246dbe24fc813a212a5ef3f5811b1d6402a (diff) | |
fix: close type-safety holes found by adversarial review; update docs
Review findings (all reproduced, then fixed and re-verified):
- '^x' leaked as a raw address into any builtin context (s <- ^x printed
the variable's bytes as a C string; NOT ^x, '&', and ^x = ^x compiled).
Address-of is now rejected everywhere except a pointer-typed target.
- A whole-array variable passed the nominal gate as a value of its
ELEMENT type (s <- arrayOfSeason stored the raw data pointer; = on it
emitted invalid IR). Whole arrays are now rejected as operands and as
coercion sources; array parameters are unaffected.
- Enum values smuggled into INTEGER through array subscripts and DECLARE
bounds (a[Autumn], ARRAY[Spring:Winter]); both now go through
emitCoercedExpr like every other INTEGER context.
- An array DECLAREd inside an IF/WHILE/REPEAT/FOR body and used after the
block was a wild-pointer write (constant bounds) or a verifier dump
(runtime bounds). Arrays are now block-scoped: control-flow bodies
snapshot and restore the ArrayTable, so escape is a clean compile error.
- Duplicate parameter names silently shadowed (BYREF write-back defeated);
now a parse error. Trailing comma in parameter lists accepted; now a
parse error consistent with every other list.
- A variable/array/parameter/function could reuse a TYPE name; reserved.
- RETURN with a value inside a PROCEDURE was silently discarded (control
did not return); now an error, as is a bare RETURN in a FUNCTION.
- INPUT r.f mis-parsed into a three-error cascade; now one clear error.
- Assigning to an enum constant now says so instead of 'Unknown variable'.
Docs: ARCH/CONVENTIONS/EXTENDING/SUBSYSTEMS/PITFALLS rewritten for the
TYPE system (ABI additions, nominal-typing invariants, admission-gate
doors, array-parameter ABI, new recipes, new sharp edges); README
features/TODO updated — random files are unblocked.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
| -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 | ||||
| -rw-r--r-- | README.md | 12 | ||||
| -rw-r--r-- | include/cps/ArrayHandler.h | 5 | ||||
| -rw-r--r-- | lib/CodeGen/ArrayHandler.cc | 13 | ||||
| -rw-r--r-- | lib/CodeGen/CodeGen.cc | 50 | ||||
| -rw-r--r-- | lib/CodeGen/Designator.cc | 17 | ||||
| -rw-r--r-- | lib/CodeGen/FunctionGen.cc | 10 | ||||
| -rw-r--r-- | lib/Parser/Parser.cc | 8 | ||||
| -rw-r--r-- | lib/Parser/ParserFunction.cc | 11 |
13 files changed, 286 insertions, 61 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 @@ -18,13 +18,21 @@ IDK WHAT HAPPENDED IT IS 100% AI-GENERATED DO NOT OPEN ANY ISSUES JUST CLONE AND - text file handling (OPENFILE/READFILE/WRITEFILE/CLOSEFILE/EOF) with runtime misuse checking (wrong mode, not open, read past EOF, ... all die with a clean `[Fatal]` message instead of corrupting anything) +- user-defined types (CIE §4): enumerated types (with runtime range checking on + `enum ± INTEGER`), pointers (`^x` / `p^`, strict nominal typing, null-deref dies + with `[Fatal]` instead of segfaulting — linked lists work, `TYPE NodePtr = ^Node` + may forward-reference), records (nested, arrays of records, `Form[i].Field <- x`) +- whole arrays and records as FUNCTION/PROCEDURE parameters (`ARRAY OF T` or + `ARRAY[l:u,...] OF T` as a checked contract; BYVAL really copies), with the + spec-correct sticky BYREF/BYVAL and BYREF-is-procedure-only rules from §8.3 - ultra-fast compiling & executing ## TODO - search for `TODO` in my code -- type -- random files (OPENFILE FOR RANDOM / SEEK / GETRECORD / PUTRECORD) — needs `type` first +- random files (OPENFILE FOR RANDOM / SEEK / GETRECORD / PUTRECORD) — `type` is done, unblocked +- SET types (`SET` / `DEFINE` are reserved and give a clear "not implemented" error) +- CASE OF, CONSTANT, DATE ## Dependencies diff --git a/include/cps/ArrayHandler.h b/include/cps/ArrayHandler.h index f555ee9..def174a 100644 --- a/include/cps/ArrayHandler.h +++ b/include/cps/ArrayHandler.h @@ -91,6 +91,11 @@ public: // the Symbols save/clear/restore discipline); returns the previous table. std::map<std::string, ArrayMetadata> exchangeTable(std::map<std::string, ArrayMetadata> NewTable); + // Copy for block-scoping: control-flow bodies snapshot before and restore + // after, so an array declared inside a branch cannot be referenced past it + // (its buffer setup and bound SSA values live inside the branch). + std::map<std::string, ArrayMetadata> snapshotTable() const { return ArrayTable; } + // Callee side of an array parameter: rebuild metadata from the incoming // data pointer + per-dim bounds. MakeCopy (BYVAL) mallocs and memcpys the // whole buffer so callee writes stay invisible to the caller. diff --git a/lib/CodeGen/ArrayHandler.cc b/lib/CodeGen/ArrayHandler.cc index c647d58..8b37b8f 100644 --- a/lib/CodeGen/ArrayHandler.cc +++ b/lib/CodeGen/ArrayHandler.cc @@ -88,15 +88,17 @@ void ArrayHandler::emitArrayDeclare(ArrayDeclareStmtAST *Stmt, CodeGen &CG) { CG.reportError("'%s' is an enum value and cannot be declared as an array", Name.c_str()); return; } + if (Types.resolve(Name)) { + CG.reportError("'%s' is a type name and cannot be declared as an array", Name.c_str()); + return; + } std::vector<Value*> Lows; std::vector<Value*> Highs; for (const auto &Pair : Stmt->getBounds()) { - Value *L = CG.emitExpr(Pair.first.get()); - Value *R = CG.emitExpr(Pair.second.get()); - L = CG.coerceValueToType(L, CG.resolveType("INTEGER")); - R = CG.coerceValueToType(R, CG.resolveType("INTEGER")); + Value *L = CG.emitCoercedExpr(Pair.first.get(), CG.resolveType("INTEGER")); + Value *R = CG.emitCoercedExpr(Pair.second.get(), CG.resolveType("INTEGER")); if (!L || !R) return; @@ -178,8 +180,7 @@ bool ArrayHandler::emitCheckedIndices(const std::string &Name, } for (size_t i = 0; i < IndexExprs.size(); ++i) { - Value *Idx = CG.emitExpr(IndexExprs[i].get()); - Idx = CG.coerceValueToType(Idx, CG.resolveType("INTEGER")); + Value *Idx = CG.emitCoercedExpr(IndexExprs[i].get(), CG.resolveType("INTEGER")); if (!Idx) return false; Out.push_back(Idx); diff --git a/lib/CodeGen/CodeGen.cc b/lib/CodeGen/CodeGen.cc index 64e1b9f..0e372d0 100644 --- a/lib/CodeGen/CodeGen.cc +++ b/lib/CodeGen/CodeGen.cc @@ -402,6 +402,11 @@ void CodeGen::emitDeclareStmt(DeclareStmtAST *Stmt) { Stmt->getName().c_str()); return; } + if (Types->resolve(Stmt->getName())) { + reportError("'%s' is a type name and cannot be declared as a variable", + Stmt->getName().c_str()); + return; + } Function *TheFunction = Builder->GetInsertBlock()->getParent(); AllocaInst *Alloca = CreateEntryBlockAlloca(TheFunction, Info->LLVMType, Stmt->getName()); @@ -603,6 +608,18 @@ Value *CodeGen::emitExpr(ExprAST *Expr) { } if (auto *Bin = dynamic_cast<BinaryExprAST*>(Expr)) { + // Whole arrays evaluate to raw data pointers and ^x to raw addresses; + // neither is a value any operator may consume. + if (isWholeArrayVar(Bin->getLHS()) || isWholeArrayVar(Bin->getRHS())) { + reportError("A whole array cannot be used as an operand; index it"); + return nullptr; + } + if (dynamic_cast<AddrOfExprAST*>(Bin->getLHS()) || + dynamic_cast<AddrOfExprAST*>(Bin->getRHS())) { + reportError("'^' (address-of) can only be used where a pointer type is expected"); + return nullptr; + } + // Enum/Record/Pointer operands never reach ArithmeticHandler or the // '&' path: their admissible operations are decided by name here. const TypeInfo *LT = getExprTypeInfo(Bin->getLHS()); @@ -825,13 +842,18 @@ void CodeGen::emitIfStmt(IfStmtAST *Stmt) { Builder->CreateCondBr(CondV, ThenBB, ElseBB); + // Arrays are block-scoped: metadata SSA values and the buffer setup live + // inside the branch, so a declaration must not survive it. + auto SavedArrays = Arrays->snapshotTable(); Builder->SetInsertPoint(ThenBB); for (const auto &S : Stmt->getThenStmts()) emitStmt(S.get()); if (!Builder->GetInsertBlock()->getTerminator()) Builder->CreateBr(MergeBB); + Arrays->exchangeTable(SavedArrays); Builder->SetInsertPoint(ElseBB); for (const auto &S : Stmt->getElseStmts()) emitStmt(S.get()); if (!Builder->GetInsertBlock()->getTerminator()) Builder->CreateBr(MergeBB); + Arrays->exchangeTable(std::move(SavedArrays)); Builder->SetInsertPoint(MergeBB); } @@ -851,8 +873,11 @@ void CodeGen::emitWhileStmt(WhileStmtAST *Stmt) { Builder->CreateCondBr(CondV, LoopBB, AfterBB); + // Arrays declared in the body are block-scoped (see emitIfStmt). + auto SavedArrays = Arrays->snapshotTable(); Builder->SetInsertPoint(LoopBB); for (const auto &S : Stmt->getBody()) emitStmt(S.get()); + Arrays->exchangeTable(std::move(SavedArrays)); if (!Builder->GetInsertBlock()->getTerminator()) Builder->CreateBr(CondBB); @@ -869,8 +894,11 @@ void CodeGen::emitRepeatStmt(RepeatStmtAST *Stmt) { Builder->CreateBr(LoopBB); + // Arrays declared in the body are block-scoped (see emitIfStmt). + auto SavedArrays = Arrays->snapshotTable(); Builder->SetInsertPoint(LoopBB); for (const auto &S : Stmt->getBody()) emitStmt(S.get()); + Arrays->exchangeTable(std::move(SavedArrays)); if (!Builder->GetInsertBlock()->getTerminator()) Builder->CreateBr(CondBB); @@ -934,8 +962,11 @@ void CodeGen::emitForStmt(ForStmtAST *Stmt) { Builder->CreateCondBr(CondV, LoopBB, AfterBB); + // Arrays declared in the body are block-scoped (see emitIfStmt). + auto SavedArrays = Arrays->snapshotTable(); Builder->SetInsertPoint(LoopBB); for (const auto &S : Stmt->getBody()) emitStmt(S.get()); + Arrays->exchangeTable(std::move(SavedArrays)); if (!Builder->GetInsertBlock()->getTerminator()) Builder->CreateBr(IncBB); @@ -984,10 +1015,19 @@ void CodeGen::emitStmt(StmtAST *Stmt) { } if (auto *Ret = dynamic_cast<ReturnStmtAST*>(Stmt)) { + Function *F = Builder->GetInsertBlock()->getParent(); + const FuncSig *Sig = FuncGen->getSignature(F->getName().str()); + if (Sig && Sig->ReturnTypeName == "VOID" && Ret->getRetVal()) { + reportError("RETURN with a value is only allowed in a FUNCTION"); + return; + } + if (Sig && Sig->ReturnTypeName != "VOID" && !Ret->getRetVal()) { + reportError("RETURN in a FUNCTION requires a value"); + return; + } Value *RetVal = nullptr; if (Ret->getRetVal()) { - Function *F = Builder->GetInsertBlock()->getParent(); - if (const FuncSig *Sig = FuncGen->getSignature(F->getName().str())) { + if (Sig) { RetVal = emitCoercedExpr(Ret->getRetVal(), resolveType(Sig->ReturnTypeName)); } else { RetVal = emitExpr(Ret->getRetVal()); @@ -1006,7 +1046,11 @@ void CodeGen::emitStmt(StmtAST *Stmt) { if (auto *Assign = dynamic_cast<AssignStmtAST*>(Stmt)) { const SymbolInfo *Info = getSymbolInfo(Assign->getName()); if (!Info) { - reportError("Unknown variable name %s", Assign->getName().c_str()); + if (Types->lookupEnumConstant(Assign->getName())) { + reportError("Enum value %s is not an assignable location", Assign->getName().c_str()); + } else { + reportError("Unknown variable name %s", Assign->getName().c_str()); + } return; } if (Info->IsArray) { diff --git a/lib/CodeGen/Designator.cc b/lib/CodeGen/Designator.cc index a0ffe28..ad0cc49 100644 --- a/lib/CodeGen/Designator.cc +++ b/lib/CodeGen/Designator.cc @@ -200,6 +200,15 @@ Value *CodeGen::loadFromLValue(const LValueInfo &LV) { Value *CodeGen::emitCoercedExpr(ExprAST *E, const TypeInfo *Target) { if (!E || !Target) return nullptr; + // A whole-array reference evaluates to its raw data pointer, and + // getExprTypeInfo reports the ELEMENT type for it — both would slip + // through the name check and smash the pointer into the target. + if (isWholeArrayVar(E)) { + reportError("A whole array cannot be used as a value of type %s; index it", + Target->Name.c_str()); + return nullptr; + } + const TypeInfo *Src = getExprTypeInfo(E); if (Target->isUserKind()) { @@ -239,6 +248,14 @@ Value *CodeGen::emitCoercedExpr(ExprAST *E, const TypeInfo *Target) { return emitExpr(E); // same nominal type: no conversion needed } + // ^x is a raw address: admissible ONLY where a pointer type is expected. + // Without this check the ptr would coerce into STRING/BOOLEAN silently. + if (dynamic_cast<AddrOfExprAST*>(E)) { + reportError("'^' (address-of) can only be used where a pointer type is expected (got %s)", + Target->Name.c_str()); + return nullptr; + } + if (Src && Src->isUserKind()) { reportError("Cannot use a value of type %s where %s is expected", Src->Name.c_str(), Target->Name.c_str()); diff --git a/lib/CodeGen/FunctionGen.cc b/lib/CodeGen/FunctionGen.cc index 77a37ec..99c6a1e 100644 --- a/lib/CodeGen/FunctionGen.cc +++ b/lib/CodeGen/FunctionGen.cc @@ -38,6 +38,10 @@ void FunctionGen::createArgumentAllocas(Function *F, const std::vector<ParamDecl fprintf(stderr, "Error: parameter '%s' collides with an enum value\n", P.Name.c_str()); HadError = true; } + if (Types.resolve(P.Name)) { + fprintf(stderr, "Error: parameter '%s' collides with a type name\n", P.Name.c_str()); + HadError = true; + } Value *ArgVal = &(*AI); ++AI; ArgVal->setName(P.Name); @@ -98,6 +102,12 @@ Function *FunctionGen::emitPrototype(PrototypeAST *Proto) { HadError = true; return nullptr; } + if (Types.resolve(Proto->getName())) { + fprintf(stderr, "Error: '%s' is a type name and cannot be a FUNCTION/PROCEDURE name\n", + Proto->getName().c_str()); + HadError = true; + return nullptr; + } FuncSig Sig; Sig.ReturnTypeName = Proto->getReturnType(); diff --git a/lib/Parser/Parser.cc b/lib/Parser/Parser.cc index 9563650..260472c 100644 --- a/lib/Parser/Parser.cc +++ b/lib/Parser/Parser.cc @@ -542,7 +542,15 @@ std::unique_ptr<StmtAST> Parser::ParseStatementImpl() { return nullptr; } std::string Name = Lex.IdentifierStr; + int Line = Lex.getLine(); getNextToken(); + if (CurTok == '[' || CurTok == '.' || CurTok == '^') { + // Consume the whole designator so it cannot replay as statements. + std::vector<DesignatorAccess> Dummy; + ParseDesignatorSuffix(Dummy); + fprintf(stderr, "Error: INPUT target must be a plain variable at line %d\n", Line); + return nullptr; + } return std::make_unique<InputStmtAST>(Name); } else if (CurTok == tok_output) { diff --git a/lib/Parser/ParserFunction.cc b/lib/Parser/ParserFunction.cc index 776a980..81db2c7 100644 --- a/lib/Parser/ParserFunction.cc +++ b/lib/Parser/ParserFunction.cc @@ -106,6 +106,13 @@ std::optional<std::vector<ParamDecl>> Parser::ParsePrototypeArgs(bool AllowByRef return std::nullopt; } + for (const ParamDecl &Prev : Params) { + if (Prev.Name == P.Name) { + fprintf(stderr, "Error: Duplicate parameter name '%s' at line %d\n", + P.Name.c_str(), Lex.getLine()); + return std::nullopt; + } + } Params.push_back(std::move(P)); if (CurTok == ')') break; @@ -114,6 +121,10 @@ std::optional<std::vector<ParamDecl>> Parser::ParsePrototypeArgs(bool AllowByRef return std::nullopt; } getNextToken(); + if (CurTok == ')') { + fprintf(stderr, "Error: Expected another parameter after ',' at line %d\n", Lex.getLine()); + return std::nullopt; + } } getNextToken(); return Params; |
