aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author_ov4 <ov4@ov4.org>2026-07-26 18:10:10 +0000
committer_ov4 <ov4@ov4.org>2026-07-26 18:10:10 +0000
commitc2c2e1771bf9b747f38dc145b5201b45c579d465 (patch)
tree09c752f360e56f523eb87e4a6dc17751be56210b
parenta44f1246dbe24fc813a212a5ef3f5811b1d6402a (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.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
-rw-r--r--README.md12
-rw-r--r--include/cps/ArrayHandler.h5
-rw-r--r--lib/CodeGen/ArrayHandler.cc13
-rw-r--r--lib/CodeGen/CodeGen.cc50
-rw-r--r--lib/CodeGen/Designator.cc17
-rw-r--r--lib/CodeGen/FunctionGen.cc10
-rw-r--r--lib/Parser/Parser.cc8
-rw-r--r--lib/Parser/ParserFunction.cc11
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
diff --git a/README.md b/README.md
index 79250d2..dd08059 100644
--- a/README.md
+++ b/README.md
@@ -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;