Replace the early lowering of constant repeat/for loops into p8_regfile counter sequences with a first-class IRLoopChunk that carries the trip count and body. This removes the fragile REGFILE-DEPENDENT m68k dbra peephole and gives each backend explicit control.
13 KiB
Requirements
Status 2026-09: Rejected / won't do. Listed as "Won't do" in
docs/source/todo.rst. Enums will remain syntactic sugar for integer constants. Strong typing hides explicit size/cost and breaks the simple retro vibe for little safety gain (see Vibe Assessment below). Consider only a non-breaking warning (-Wenum-conversion) or opt-inenum strictif ever revisited.
Overview & Goals
The goal is to transition Prog8 enums from being merely syntactic sugar for integer constants into a first-class, strongly typed data type in the Prog8 language. This will improve type safety by preventing erroneous assignments or comparisons between different enums or between enums and raw integers.
Scope
In Scope
- Definition of a new
EnumSubTypewithin the compiler's type system to extend existingDataTypes. - Modifications to
SymbolTableandTypeCheckerto enforce enum type strictness. - Updates to AST processing phases (
SimplifiedAstMaker) to preserve enum declarations instead of lowering them to constants. - Code generation adjustments to handle enum variables correctly.
- Verification through new unit and integration tests.
Out of Scope
- Architectural changes to non-enum types.
- Backend-specific optimizations for enums (unless directly required for correctness).
Technical Design
Current Implementation
Currently, Enumeration nodes in the compilerAst are lowered into standard constant variable declarations (const) early in the processAst() phase. This is confirmed by SimplifiedAstMaker.kt which expects enums to have been removed.
Key Decisions
- Represent Enum as a
DataTypewithEnumSubType: We will implement a newEnumSubTypeclass (implementingISubType) incodeCore. The enum reuses the underlying integerBaseDataType(e.g.,BaseDataType.UBYTEfor small enums) and distinguishes viadt.subType is EnumSubTyperather than introducing a newBaseDataType.ENUM. This avoids touching everywhen(dt.base)/dt.isByteswitch across the entire compiler — only the specific places that need to check enum-ness look at the subtype. - Preserve
EnumerationAST nodes in CompilerAST: TheSimplifiedAstMakerwill be updated to treatEnumerationas a type definition during the initial compiler phases. - Reduction in
SimplifiedAstMaker: TheSimplifiedAstMakerwill lowerEnumTypeto its underlyingInttype constants when generating theSimpleAST. This keeps the SimpleAST, IR, and downstream code generation simple, as they only need to deal with integers. - Strict Type Checking & Casting: The
TypeCheckerwill be updated to disallow implicit conversions for enums. Explicit casting between an Enum and its underlying integer type will be allowed. Casting between different Enum types will be explicitly prohibited. - SimplifiedAstMaker Erasure & Folding: The
SimplifiedAstMakerwill be updated to perform the enum erasure (loweringEnumTypeto its underlyingInttype constants) and the associated constant folding for enum-typed expressions that can be reduced to integers at that point. This approach centralizes the erasure logic and keeps theConstantFoldingOptimizerfocused on general-purpose expression simplifications. - AST Printer Updates:
AstToSourceTextConverter(printAst1) will be updated to recognize and correctly output the new strongly typed enum node structure.AstPrinter(printAst2) does not need changes as it operates on the erasedSimpleAST.
Proposed Changes
codeCore: IntroduceEnumSubTypeclass implementingISubTypeto extendDataType. No newBaseDataType.ENUM— reuse the underlying integer base type.compilerAst/compiler: ModifySimplifiedAstMakerandAstPreprocessorto stop loweringEnumerationto constants. UpdateAstToSourceTextConverterto support printing the typed enum nodes.simpleAst/SymbolTable: Update theSymbolTableandTypeCheckerto register, validate enum types, and handle explicit casting rules (disallow implicit enum conversion, disallow cross-enum conversion).- Code Generators: No changes needed. The enum type is erased to its underlying integer representation during the
SimplifiedAstMakerphase, and the code generators only deal with plain integer values.
Underlying Integer Type
The enum's underlying integer type is inferred from the member values, reusing the existing logic in Antlr2KotlinVisitor.kt that computes the largestType across all members. No new syntax needed — if a member exceeds 255 the type automatically widens from ubyte to uword, matching current behavior.
Vibe Assessment (2026-09): Nay
Consistent with the var x = <value> rejection in docs/source/todo.rst:6: Prog8 is an explicit, size-aware retro language (ubyte vs uword vs long has direct RAM/code-size cost). Enums as plain const ubyte groups (docs/source/variables.rst:249) fit that - zero cost, zero ceremony, hardware names stay usable as ubyte. Strong typing turns the simplest type into the most complex (new ISubType, preserved Enumeration nodes, erasure, flag plumbing) for a domain where exhaustive checking is rare and intentional ubyte reuse is common. Keep sugar by default; if needed, prefer a warning over a breakage.
Cons
- Breaking change for existing code: Every
Priority::LOWpassed to aubyteparam, stored in aubytevariable, or used in arithmetic/comparison now requiresas ubyte(oras Prioritythe other way). Stdlib and user programs that treated enums as plain constants will not compile without mass edits. The verbosity outweighs the safety gain for Prog8's target domain (small 8-bit programs where enums are often just named numbers). - Limited practical safety gain: Enums in Prog8 are currently small, non-exhaustive constant sets used for readability, not for exhaustive
whenchecking or domain separation. Cross-enum mix-ups are rare in practice; the stricter checker would mostly catch intentionalubytereuse. - Array and generic friction:
const arr = [Priority::LOW, Priority::HIGH]type-checks asPriority[]but erases toubyte[]; mixed-enum literals,sizeof(Priority::LOW)(should besizeof(Priority)), and stdlib helpers that takeubyte[]now all need casts or overloads. - Compiler complexity for modest benefit: Adds a new
ISubType(EnumSubType), preservesEnumerationnodes throughprocessAst(), and adds erasure +isAssignableTobranches inTypeChecker/AstChecker. ExtrastripPrefixes/type-prefix branching andlegacy_enumsplumbing increases test matrix for a feature that is erased beforeSimpleAST/IR. - Tooling and docs churn:
AstToSourceTextConverter(-printast1), language-server hover/type info, and reference docs must be updated to showPriorityvsubyte. Constant-folding messages becomeexpected Priority but got ubytewhich is noisier for beginners. - Migration and flag fragmentation: A module-level
%option legacy_enumsavoids breakage but fragments the ecosystem: some modules strict, some legacy; imported modules with different flags must be reasoned about, and theif("legacy_enums" !in allOptions)guard must be threaded through every check. - Alternative is cheaper: The same class of bugs can be caught with a
-Wenum-conversionwarning (orisAssignableTowarning) without breaking builds, keeping enums as syntactic sugar. Strong typing can remain opt-in per enum (enum strict Priority { ... }) rather than global.
Risk & Mitigations
- Existing Code Compatibility: Existing programs relying on implicit enum-to-integer conversion will break. This is intentional to improve type safety.
- Mitigation: Provide clear compiler error messages when implicit conversion fails, and document the new explicit casting requirement.
- Mitigation: A
%option legacy_enumsflag disables strict enum type checking, allowing implicit conversions. Scope: module-level (any%option legacy_enumsanywhere in the module turns off strict checking for all blocks in that module). TheAstCheckerwraps the strict checks in a single guard:if("legacy_enums" !in allOptions) { /* enforce */ }. Low implementation cost since the enum erasure/preservation machinery always runs regardless.
Enum Member Type in Expressions
With strong typing, Priority::LOW should have type Priority, not ubyte. This affects several expression contexts:
sizeof(Priority::LOW)— should besizeof(Priority)(storage size of the type, which is the underlying integer size). Requires the compiler to resolve enum member references to their declaring type.- Array literals like
const arr = [Priority::LOW, Priority::HIGH]— the array type should be inferred asPriority[], notubyte[]. But since enums are erased to integers in the SimplifiedAST, this means the array element type is the underlying integer type (ubyte/uword). The strong typing only applies during CompilerAST type checking; after erasure it's a plain integer array. Soarrwould be stored asubyte[]oruword[]at the codegen level, but type-checked asPriority[]during the CompilerAST phase. This is consistent — the array erasure follows the same rule as scalar enum erasure. - Passing enum members to subroutines expecting
ubyte— blocked unless explicit cast (Priority::LOW as ubyte). This is the core strictness benefit. - Assigning an enum member to a
ubytevariable — same: requires explicit cast.
Parser Resolution Order
Currently enums are lowered to constants during parsing/early AST processing, so forward references work transparently. With preservation, the Enumeration node must be processed (type registered in the symbol table) before any code references its members. This is normally satisfied by Prog8's declaration-before-use rule. Edge cases to document:
- Two enums in the same block cannot reference each other's members in their value expressions (circular dependency would be unresolvable).
constvalues initialized with enum members must appear after the enum declaration.- The existing import system handles cross-module enum ordering correctly — imported modules are processed before the importing module.
No special machinery needed; the normal declaration ordering rules apply.
Testing
Validation Approach
Primary testing uses the virtual target and prog8c's IR debugging features. This allows fast iteration without needing 6502 emulators.
Debugging workflow for each change:
- Syntax check:
prog8c -check input.p8-- quick semantic validation - AST inspection:
prog8c -target virtual -printast1 input.p8-- inspect compiler AST (verify enum nodes are preserved) - Simple AST inspection:
prog8c -target virtual -printast2 input.p8-- inspect Simple AST (verify enum erasure to integers) - IR inspection:
prog8c -target virtual -out /tmp/out input.p8then examine the generated.p8irfile - IR comparison:
prog8c -target virtual -compareir baseline.p8ir new.p8ir-- compare IR instruction changes when optimizing - VM execution:
prog8c -target virtual -emu input.p8-- run and see stdout output - VM trace:
prog8c -vm input.p8ir -vmtrace-- step through IR instructions with location tracking - Disable optimizations:
prog8c -target virtual -noopt -emu input.p8-- isolate optimizer issues - Full regression:
gradle build --console=plain-- run all unit tests
Important: Always test with -noopt first to isolate optimizer issues from code generation bugs. Use -compareir to see what instructions change when optimizations are applied.
- Type Checking Tests: Add test cases that explicitly verify:
- Implicit assignment/comparison of incompatible types (expect failure).
- Explicit cast (Enum ↔ Int) (expect success).
- Explicit cast between different Enum types (expect failure).
- Compare two different enum types (expect failure).
- Use an enum member without proper type qualification.
- AST Printing Verification: Use
-printast1and-printast2to verify that strongly typed enum nodes are correctly displayed in the AST output. - Runtime Verification: Ensure that the code generation produces correct code for enums (e.g., correct values, proper size) using the
virtualtarget andvmtrace.
Final verification against the existing test suite via gradle build --console=plain.
Delivery Plan
Stage 1: Add Enum Type Support and Erasure Logic
Enum types are introduced and correctly preserved in the compiler AST.
- Implement
EnumSubTypeclass incodeCore(no newBaseDataType— reuse underlying numeric type). - Update
CompilerASTandSimplifiedAstMakerto preserve and later erase enum declarations.
Stage 2: Implement Strict Type Checking and Casting
Strong typing for enums is enforced by the compiler.
- Update
TypeCheckerandSymbolTableto enforce strict type checking for enums. - Implement explicit casting rules for enums and disallow implicit/cross-enum conversions.
Stage 3: Verification and Testing
Enum type implementation is verified with unit and integration tests.
- Update AST printing for enums.
- Add unit tests for type safety, casting, and AST representation verification.