From c372c08dc940db58f1da9cadeb64b4cb3c837704 Mon Sep 17 00:00:00 2001 From: _ov4 Date: Sat, 17 Oct 2026 06:58:13 +0000 Subject: fix(classes): preserve method-only heap objects and document NEW Allocate at least one byte for method-only classes so their heap addresses remain usable through pointers. Complete constructor diagnostics, field-order and exception coverage, document construction and rebinding, and highlight NEW. Co-Authored-By: Claude Opus 5.5 --- AGENTS/CONVENTIONS.md | 4 +- AGENTS/PITFALLS.md | 12 +++- AGENTS/SUBSYSTEMS.md | 20 ++++++- doc/language/keyword/cpsc-keyword-class.7.md | 25 +++++++-- doc/language/keyword/cpsc-keyword-declare.7.md | 2 + include/cps/MemoryHandler.h | 2 +- lib/CodeGen/ConstructorGen.cc | 4 ++ plugin/syntaxes/cie-pseudocode.tmLanguage.json | 2 +- tests/i/class-heap-new/expected.txt | 9 +++ tests/i/class-heap-new/notes.md | 4 ++ tests/i/class-heap-new/program.cps | 65 ++++++++++++++++++++++ .../class-constructor-array-args-error/case.json | 8 +++ .../class-constructor-array-args-error/program.cps | 5 ++ .../class-constructor-duplicate-error/case.json | 8 +++ .../class-constructor-duplicate-error/program.cps | 6 ++ .../class-constructor-expression-error/case.json | 8 +++ .../class-constructor-expression-error/program.cps | 4 ++ .../iii/class-constructor-function-error/case.json | 8 +++ .../class-constructor-function-error/program.cps | 5 ++ tests/iii/class-constructor-global-error/case.json | 8 +++ .../iii/class-constructor-global-error/program.cps | 2 + .../class-constructor-nested-new-error/case.json | 8 +++ .../class-constructor-nested-new-error/program.cps | 5 ++ tests/iii/class-new-errors/case.json | 12 ++++ tests/iii/class-new-errors/program.cps | 5 ++ 25 files changed, 228 insertions(+), 13 deletions(-) create mode 100644 tests/iii/class-constructor-array-args-error/case.json create mode 100644 tests/iii/class-constructor-array-args-error/program.cps create mode 100644 tests/iii/class-constructor-duplicate-error/case.json create mode 100644 tests/iii/class-constructor-duplicate-error/program.cps create mode 100644 tests/iii/class-constructor-expression-error/case.json create mode 100644 tests/iii/class-constructor-expression-error/program.cps create mode 100644 tests/iii/class-constructor-function-error/case.json create mode 100644 tests/iii/class-constructor-function-error/program.cps create mode 100644 tests/iii/class-constructor-global-error/case.json create mode 100644 tests/iii/class-constructor-global-error/program.cps create mode 100644 tests/iii/class-constructor-nested-new-error/case.json create mode 100644 tests/iii/class-constructor-nested-new-error/program.cps diff --git a/AGENTS/CONVENTIONS.md b/AGENTS/CONVENTIONS.md index 83e73e2..87a95b7 100644 --- a/AGENTS/CONVENTIONS.md +++ b/AGENTS/CONVENTIONS.md @@ -97,7 +97,9 @@ and INPUT STRING buffers — declared in `SetupExternalFunctions`, never fetched bare `getFunction("malloc")`. **One symbol namespace, one table.** `Symbols` (name → `SymbolInfo{Storage, -TypeName, IsArray, ConstantValue, ScopeID}`) in CodeGen is the only symbol table. +TypeName, IsArray, ConstantValue, ScopeID, IsReference}`) in CodeGen is the only symbol table. +Use `symbolAddress` for an object address: class variables and BYVAL class parameters +store their current object address in a reference slot (`IsReference`). A non-null ConstantValue denotes a read-only CONSTANT with no Storage. Read it through `emitExpr`; all writable/address-taking paths must reject it through `requireWritable`. Register variables through `registerSymbol()` (parameters are diff --git a/AGENTS/PITFALLS.md b/AGENTS/PITFALLS.md index ec5d034..6ef422b 100644 --- a/AGENTS/PITFALLS.md +++ b/AGENTS/PITFALLS.md @@ -87,6 +87,14 @@ test programs. `this.Method(...)`; unqualified calls remain global. Parameters and local declarations shadowing fields warn without failing compilation. Method-only classes are allowed; empty classes and array fields remain unsupported. + PROCEDURE NEW is an ordinary case-insensitive member, also called automatically + after zero initialization by DECLARE and assignment-only keyword NEW. Fields and + array elements recursively require parameterless construction before the outer NEW. + Class variables and BYVAL class parameters use reference slots: keyword NEW rebinds + them, while ordinary assignment copies into their current object. Fields, array + elements, BYREF, this and pointer dereferences receive a value copy from NEW; + the temporary allocation leaks. Old address-of pointers retain the previous object. + Private access and ordinary argument checks apply to automatic construction too. - **Exception extensions.** TRY requires exactly one parameterless CATCH and ENDTRY. Bare THROW raises a new anonymous exception even inside CATCH; there is @@ -114,8 +122,8 @@ 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, string escape sequences, -array-typed record fields, whole-array RETURN, class constructors/destructors, -NEW and class inheritance. Compiler fixtures now live in `tests/` (see TESTING.md). +array-typed record fields, whole-array RETURN, destructors/DELETE +and class inheritance (INHERITS/SUPER). Compiler fixtures now live in `tests/` (see TESTING.md). Nightly CI runs the compiler suites, packaged EMBEDDED smoke checks and existing plugin tests. The initial standard conflicts and missing diagnostic locations are fixed on Linux. diff --git a/AGENTS/SUBSYSTEMS.md b/AGENTS/SUBSYSTEMS.md index b21386d..1d13c57 100644 --- a/AGENTS/SUBSYSTEMS.md +++ b/AGENTS/SUBSYSTEMS.md @@ -78,8 +78,22 @@ FunctionGen binds field aliases and `this` to the incoming receiver storage befo binding explicit parameters. CodeGen saves/restores `CurrentClass` around each function body; unqualified calls still resolve globally. Local declarations and parameters may shadow fields with a source-line warning; `this.Field` remains -explicit. `this` cannot be redeclared within methods. Constructors and language-level -inheritance remain unsupported. +explicit. `this` cannot be redeclared within methods. `TypeInfo::IsClass` distinguishes +classes from records without changing their shared LLVM representation. + +`ConstructorGen.cc` emits recursive field construction in declaration order followed +by the ordinary NEW method, sharing `emitMethodCall` access checks, argument +marshalling and exception-aware calls. DECLARE zeroes its original storage on every +execution; arrays loop over all flat elements and default-construct each one. +Constructor argument lists are class-only, and field/array defaults require a +parameterless NEW. Explicit NEW method calls do not reconstruct fields. +`SymbolInfo::IsReference` marks a pointer slot for declared class variables and BYVAL +class parameters; `symbolAddress` loads their current object address for all value, +lvalue, address-of, receiver and BYREF paths. Field aliases, this and BYREF parameters +bind addresses directly. Assignment-only `NewExprAST` uses checked MemoryHandler +allocation (at least one byte for method-only classes), zero initialization and +construction, then rebinds reference slots or copies into other destinations. Ordinary assignment always copies. Heap lifetimes +remain unmanaged; destructors, DELETE and INHERITS/SUPER are unsupported. **Runtime checks** (`RuntimeCheck`): array bounds, file misuse, enum ranges, null dereferences and array-parameter bounds contracts are checked at their emit sites. @@ -123,4 +137,4 @@ malloc/realloc buffer per line and strips one trailing `\r`; fopen modes are tex nothing is declared or emitted eagerly. Helper bodies are built with the *shared* Builder (RuntimeCheck emits through the same one), saving/restoring the insert block. -**Explicit memory** (`MemoryHandler`): lazily emits internal allocation/find/release/resize helpers and a linked list of live allocation start addresses. Only explicit MALLOC/REALLOC allocations enter the list; STRING/ARRAY internals retain their existing ownership. FREE validates non-null addresses, unlinks and frees the tracking node, releases the block and returns the original address. Zero-byte allocation returns null. REALLOC validates non-null addresses before its zero-size release path; null behaves as MALLOC. Positive-size resizing updates the node only after success, leaving the old block and record intact on failure. Node allocation precedes the user allocation; if the latter fails, its node is freed before the diagnostic and anonymous throw. Helpers use RuntimeCheck and ExceptionHandler, including source-line arguments and per-function unwind preparation. Sizes accept INTEGER and C integers and convert to native size_t. Address reuse can make stale aliases indistinguishable from current allocations; dereferences are not checked against this list. +**Explicit memory** (`MemoryHandler`): lazily emits internal allocation/find/release/resize helpers and a linked list of live allocation start addresses. Explicit MALLOC/REALLOC and keyword NEW allocations enter the list; STRING/ARRAY internals retain their existing ownership. FREE validates non-null addresses, unlinks and frees the tracking node, releases the block and returns the original address. Zero-byte allocation returns null. REALLOC validates non-null addresses before its zero-size release path; null behaves as MALLOC. Positive-size resizing updates the node only after success, leaving the old block and record intact on failure. Node allocation precedes the user allocation; if the latter fails, its node is freed before the diagnostic and anonymous throw. Helpers use RuntimeCheck and ExceptionHandler, including source-line arguments and per-function unwind preparation. Sizes accept INTEGER and C integers and convert to native size_t. Address reuse can make stale aliases indistinguishable from current allocations; dereferences are not checked against this list. diff --git a/doc/language/keyword/cpsc-keyword-class.7.md b/doc/language/keyword/cpsc-keyword-class.7.md index faa84df..f0d2e12 100644 --- a/doc/language/keyword/cpsc-keyword-class.7.md +++ b/doc/language/keyword/cpsc-keyword-class.7.md @@ -2,7 +2,7 @@ # NAME -cpsc-keyword-class, cpsc-keyword-public, cpsc-keyword-private, cpsc-keyword-endclass - define a class with fields and methods +cpsc-keyword-class, cpsc-keyword-public, cpsc-keyword-private, cpsc-keyword-endclass, cpsc-keyword-new - define a class with fields and methods # SYNOPSIS @@ -10,6 +10,9 @@ cpsc-keyword-class, cpsc-keyword-public, cpsc-keyword-private, cpsc-keyword-endc CLASS name PUBLIC publicField : type PRIVATE privateField : type + PUBLIC PROCEDURE NEW(parameters) + statements + ENDPROCEDURE PUBLIC PROCEDURE method(parameters) statements ENDPROCEDURE @@ -17,6 +20,8 @@ CLASS name RETURN expression ENDFUNCTION ENDCLASS +DECLARE object : ClassName(arguments) +object <- NEW ClassName(arguments) ``` # DESCRIPTION @@ -29,22 +34,32 @@ Within a method, bare field names refer to the current object's fields. The comp Parameters and local variables, arrays, or constants may shadow fields; this produces a warning with the source line and does not prevent compilation. The local name takes precedence; `this.Field` continues to access the field. The name `this` cannot be a class member or a method parameter or local declaration. -Classes retain record value semantics: zero initialization, whole-value copying, arrays, pointers, and ordinary function/procedure parameters. Methods modify the receiver's storage. The object reference `this` may be read, returned, or passed as an object, including BYREF to a procedure; `this <- Other` copies Other into the current object without rebinding the reference. PRIVATE fields still initialize and copy with the whole value. Constructors, destructors, NEW, INHERITS, and array fields are unsupported. +Classes retain record value semantics: zero initialization, whole-value copying, arrays, pointers, and ordinary function/procedure parameters. Methods modify the receiver's storage. The object reference `this` may be read, returned, or passed as an object, including BYREF to a procedure; `this <- Other` copies Other into the current object without rebinding the reference. PRIVATE fields still initialize and copy with the whole value. + +A constructor is a PROCEDURE named NEW, following guide section 10.2. It may be PUBLIC or PRIVATE; FUNCTION NEW and global PROCEDURE NEW are errors. Member names are case insensitive, so New and new also name the constructor. Only one NEW is allowed per class. Explicit `Object.NEW(arguments)`, `CALL Object.NEW(arguments)` and `this.NEW(arguments)` behave like ordinary member calls and may be repeated; they do not zero the object or reconstruct its fields. + +`DECLARE Object : ClassName`, `DECLARE Object : ClassName()` and `DECLARE Object : ClassName(arguments)` zero the object's storage, construct its fields recursively in declaration order, then call its NEW procedure. These automatic DECLARE, field and array calls are compiler extensions. Array elements and class-valued fields in records or classes use parameterless construction, before the enclosing constructor runs. A required constructor argument cannot be omitted; supplying arguments to a class without NEW is an error. Empty parentheses are allowed for a class without NEW. Constructor arguments after ARRAY OF are unsupported. A declaration executed again, such as inside a loop, zeroes and constructs its original storage again. + +The keyword NEW is allowed only as the complete right side of assignment: `Target <- NEW ClassName`, `Target <- NEW ClassName()` or `Target <- NEW ClassName(arguments)`. Target must have exactly the named class type. NEW uses the checked MALLOC allocation path, zeroes storage and constructs fields before calling NEW. A method-only class receives at least one byte of heap storage so its object address is non-null. Allocation failure reports a runtime error and throws; constructor exceptions also propagate to TRY/CATCH. Private constructor access and argument checks follow ordinary member-call rules. + +When Target is a declared class variable, including a function local or BYVAL parameter, NEW rebinds that variable to the heap object. No `^` dereference is needed to access it. Address-of, BYREF calls and method calls use its current object. Pointers taken before rebinding continue to refer to the previous object. Ordinary `A <- B` remains a value copy into A's current object and does not rebind A. BYVAL parameters copy the caller's value and can rebind independently. + +For an array element, field (including a bare field inside a method), BYREF parameter, this, or pointer dereference, NEW constructs a heap object and copies its value into the existing target. That temporary allocation is retained. There is no automatic reclamation, destructor or DELETE support; INHERITS, SUPER and array fields remain unsupported. # EXAMPLE ```text CLASS Counter PRIVATE Value : INTEGER - PUBLIC PROCEDURE Set(Number : INTEGER) + PUBLIC PROCEDURE NEW(Number : INTEGER) this.Value <- Number ENDPROCEDURE FUNCTION Get() RETURNS INTEGER RETURN Value ENDFUNCTION ENDCLASS -DECLARE Item : Counter -Item.Set(3) +DECLARE Item : Counter(3) +Item <- NEW Counter(5) OUTPUT Item.Get() ``` diff --git a/doc/language/keyword/cpsc-keyword-declare.7.md b/doc/language/keyword/cpsc-keyword-declare.7.md index 0fd5cf3..428a934 100644 --- a/doc/language/keyword/cpsc-keyword-declare.7.md +++ b/doc/language/keyword/cpsc-keyword-declare.7.md @@ -19,6 +19,8 @@ C_INT, C_CHAR, C_STRING and C_DOUBLE are native C aliases for int, char, char* a ARRAY bounds are inclusive and may be expressions; multiple ranges create multiple dimensions. Access elements with `name[index, ...]`. Bounds are checked at run time; an invalid index throws an exception that TRY/CATCH can handle. Whole-array assignment is unsupported. Declare arrays before using them; arrays declared inside a control-flow block stay in that block. +For class variables, an optional argument list after the type supplies constructor arguments, such as `DECLARE Item : Counter(3)`. Declarations zero storage and automatically construct class fields and array elements before calling the outer constructor. See [cpsc-keyword-class(7)](cpsc-keyword-class.7.md) for construction and NEW rebinding rules. + # EXAMPLE ```text diff --git a/include/cps/MemoryHandler.h b/include/cps/MemoryHandler.h index ded838b..2768ebe 100644 --- a/include/cps/MemoryHandler.h +++ b/include/cps/MemoryHandler.h @@ -8,7 +8,7 @@ class TypeSystem; class RuntimeCheck; class ExceptionHandler; -// Only explicit memory builtins own entries in this runtime's allocation list. +// Explicit memory builtins and keyword NEW own entries in this allocation list. // Internal STRING/ARRAY storage retains its existing lifetime rules. class MemoryHandler { llvm::LLVMContext &Context; diff --git a/lib/CodeGen/ConstructorGen.cc b/lib/CodeGen/ConstructorGen.cc index f726590..99f21df 100644 --- a/lib/CodeGen/ConstructorGen.cc +++ b/lib/CodeGen/ConstructorGen.cc @@ -62,6 +62,10 @@ void CodeGen::emitAssignment(const LValueInfo &Target, ExprAST *Expr, const Symb } Value *Size = Builder->CreateIntCast(Types->getSizeOfConstant(Class->LLVMType), Type::getIntNTy(*TheContext, sizeof(std::size_t) * CHAR_BIT), false); + // Method-only classes have zero-sized LLVM structs but still need + // a non-null object address (MALLOC(0) deliberately returns null). + Size = Builder->CreateSelect(Builder->CreateICmpEQ(Size, ConstantInt::get(Size->getType(), 0)), + ConstantInt::get(Size->getType(), 1), Size); Value *Object = Memory->emitAllocate(Size, New->getLine()); Builder->CreateStore(Types->getZeroValue(Class->Name), Object); if (!emitConstruct(Class->Name, Object, New->getArgs(), New->getLine())) return; diff --git a/plugin/syntaxes/cie-pseudocode.tmLanguage.json b/plugin/syntaxes/cie-pseudocode.tmLanguage.json index 191eec2..6cdd586 100644 --- a/plugin/syntaxes/cie-pseudocode.tmLanguage.json +++ b/plugin/syntaxes/cie-pseudocode.tmLanguage.json @@ -21,7 +21,7 @@ "name": "keyword.control.throw.cpp" }, { - "match": "(?