diff --git a/ideas/m68k-stack-memory-model.md b/ideas/m68k-stack-memory-model.md index bd8eddb3b..38422c071 100644 --- a/ideas/m68k-stack-memory-model.md +++ b/ideas/m68k-stack-memory-model.md @@ -94,7 +94,41 @@ does not prevent total stack exhaustion through deep call chains or recursion. The compiler should report the estimated frame size and reject a subroutine whose frame exceeds the target limit. -## 4. Local Initialization +## 4. Compiler Auto-Temporaries on the Stack (Optional) + +The compiler creates a small number of internal temporary variables during AST +desugaring. These `auto_heap_value_N` variables are generated by +`CodeDesugarer` (ongoto jump tables, ongoto index temporaries, pointer +dot-chain address temporaries) and `LiteralsToAutoVarsAndRecombineIdentifiers` +(array-literal heap hoisting). There are only four creation sites, each +producing one or two variables with short, well-defined lifetimes. + +These auto-temporaries are ideal candidates for frame-slot allocation rather +than static storage. They are written once, read a few times within a single +expression or statement, then dead. They are never `@shared`, never have their +address taken in user code, and never appear in inline assembly. + +The recommended approach is: + +1. Keep the existing `VarDecl` nodes in the AST. They are needed for semantic + analysis, type checking, and optimizer passes (even though the optimizer + rarely eliminates them). +2. Mark them as compiler temporaries in the IR, either via a flag on + `IRStStaticVariable` or by introducing a lightweight `IRFrameSlot` storage + class. +3. The M68K codegen allocates flagged temporaries to frame slots instead of + emitting them to `.bss` or static memory. + +This is a minor optimization on its own but becomes a natural part of the +full stack frame implementation. Once subroutine locals live on the stack, these +temporaries simply become frame-resident alongside user-declared locals. The +main benefit is avoiding static storage consumption for variables that never +need static addresses. + +For the 6502 targets, these auto-temporaries continue to be statically +allocated, consistent with the existing local-variable model. + +## 5. Local Initialization Prog8 variables are normally zero-initialized. The M68K implementation should preserve this semantic rule, but need not clear every byte of every frame. @@ -115,13 +149,13 @@ The simplest first implementation is: An alternative is to define stack locals as undefined on M68K, but that would make the target's semantics differ unnecessarily from the other targets. -## 5. Calling Convention Impact +## 6. Calling Convention Impact Yes, this changes the internal calling convention unless parameters remain static. Keeping parameters static would allow a partial implementation, but it would not make normal subroutines fully recursive or reentrant. -### 5.1 Normal Prog8 subroutines +### 6.1 Normal Prog8 subroutines The current convention writes arguments into named parameter variables before the call. That cannot work when those parameter variables are in the callee's @@ -146,7 +180,7 @@ stable address or when the value must survive a call. The exact choice must also account for byte, word, long, pointer, and float values, including alignment and multi-value returns. -### 5.2 Returns +### 6.2 Returns The existing return convention can remain initially: ordinary scalar returns use the established M68K result registers, while explicit `asmsub` register @@ -156,7 +190,7 @@ does not require a frame change, provided the epilogue runs before `rts`. All return paths must branch through a common epilogue or otherwise emit the same A5 restoration and callee-saved-register restoration. -### 5.3 `asmsub`, `extsub`, and AmigaOS calls +### 6.3 `asmsub`, `extsub`, and AmigaOS calls The external calling convention should remain separate from normal Prog8 subroutines: @@ -170,7 +204,7 @@ subroutines: This separation prevents the normal frame implementation from changing the existing AmigaOS ABI. -## 6. IR Changes +## 7. IR Changes The IR should describe storage symbolically rather than naming A5 directly. Possible additions include: @@ -194,7 +228,7 @@ generated from `IRSubroutine` metadata is an implementation choice. Backend- generated prologues are preferable initially because saved-register selection, instruction forms, and frame-pointer choice are backend details. -## 7. Backend Changes +## 8. Backend Changes `codeGenM68k` must: @@ -212,7 +246,7 @@ The register-allocation design must distinguish A5 from ordinary callee-saved registers. A5 is not merely another register that a subroutine may save and reuse; it is the base for all active frame slots. -## 8. VM Changes +## 9. VM Changes To execute the same IR model, the VM needs an activation record per call. A call must allocate the callee's frame slots, make the current frame available @@ -225,7 +259,7 @@ abstract frame-slot semantics. If VM support is deferred, the compiler should reject frame-based IR when the VM is selected rather than silently treating frame slots as static variables. -## 9. Address-Taking and Lifetime +## 10. Address-Taking and Lifetime `&local` would produce an address into the current frame. That address is valid only until the subroutine returns. The compiler must either: @@ -237,7 +271,7 @@ Variables referenced by inline assembly, external code, `@shared`, or a stored pointer should remain static unless the compiler can prove that the reference does not escape. -## 10. Defer, sys.exit(), and Stack Unwinding +## 11. Defer, sys.exit(), and Stack Unwinding Mechanical stack unwinding on M68K is straightforward: the epilogue restores the caller's frame pointer and stack pointer with `unlk a5`, and `rts` pops the @@ -284,7 +318,7 @@ Possible approaches: 2. **Keep defer-referenced locals static.** Variables whose addresses are taken, that are referenced by inline assembly, or that are referenced by `defer` stay in static storage; everything else can move to the stack. This matches - the address-escape rule in Section 9. + the address-escape rule in Section 10. 3. **Pass the parent frame pointer to the handler.** The handler subroutine receives the parent's A5 value and uses it to access parent locals. This is awkward because the handler itself may need a frame, requiring two frame @@ -294,7 +328,7 @@ For a first implementation, option 1 (inlining) or option 2 (hybrid static locals) is recommended. Option 3 adds calling-convention complexity that is not justified until the rest of the stack model is stable. -## 11. Effects on Other Targets +## 12. Effects on Other Targets The 6502 targets remain unchanged: @@ -306,7 +340,7 @@ The IR and frontend changes must therefore be conditional on the compilation target or represented in a way that the 6502 code generators can continue to lower without seeing frame storage. -## 12. Benefits +## 13. Benefits - Recursive M68K subroutines become possible. - Subroutines become reentrant and safer for callbacks and task-like use. @@ -314,7 +348,7 @@ lower without seeing frame storage. - Multiple simultaneous invocations receive independent local state. - The M68K backend gets a conventional, efficient local-access mechanism. -## 13. Risks and Open Questions +## 14. Risks and Open Questions - Stack exhaustion becomes a runtime possibility. - Prologues, epilogues, argument marshalling, and selective initialization add @@ -323,7 +357,7 @@ lower without seeing frame storage. - Address-taking and pointer lifetime rules need to be defined. - `defer` code must be able to access the parent subroutine's locals; the current handler-subroutine design needs to be replaced or restricted when - locals live on the stack (see Section 10). + locals live on the stack (see Section 11). - Inline assembly must respect A5 reservation. - Frame layout must cooperate with register allocation and spill slots. - Zero initialization may require additional analysis and generated code. @@ -331,7 +365,7 @@ lower without seeing frame storage. - Existing assembly or code that assumes parameter variables have static symbols may need to remain explicitly static. -## 14. Suggested Implementation Order +## 15. Suggested Implementation Order 1. Define the M68K frame-slot metadata and A5-based layout rules. 2. Keep all parameters static temporarily and implement ordinary local frames diff --git a/intermediate/src/prog8/intermediate/IRInstructions.kt b/intermediate/src/prog8/intermediate/IRInstructions.kt index 3e8b4946e..e3c9e25d7 100644 --- a/intermediate/src/prog8/intermediate/IRInstructions.kt +++ b/intermediate/src/prog8/intermediate/IRInstructions.kt @@ -812,9 +812,6 @@ val instructionFormats = mutableMapOf( Opcode.MUL to InstructionFormat.from("BWL,<>r1,fr1,a | F,a"), Opcode.SUBIM to InstructionFormat.from("BWL,a | F,a"), - Opcode.MULR to InstructionFormat.from("BWL,<>r1,fr1,r1,fr1,a | F,a"), Opcode.MULSR to InstructionFormat.from("BWL,<>r1,fr1,r1,fr1,a | F,a"), @@ -913,7 +910,7 @@ val instructionFormats = mutableMapOf( Opcode.POP to InstructionFormat.from("BWL,>r1 | F,>fr1"), Opcode.PUSHST to InstructionFormat.from("N"), Opcode.POPST to InstructionFormat.from("N"), - Opcode.CONCAT to InstructionFormat.from("BW,<>r1,r1,