IR: fix CONCAT register direction

This commit is contained in:
Irmen de Jong committed 2026-09-04 19:42:31 +02:00
1 parent 58f544fe14
commit bc62b2de81
2 files changed
+51 -20

No files matched your search

+50 -16
View File
@@ -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
@@ -812,9 +812,6 @@ val instructionFormats = mutableMapOf(
Opcode.MUL to InstructionFormat.from("BWL,<>r1,<i | F,<>fr1,<i"),
Opcode.MULM to InstructionFormat.from("BWL,<r1,<>a | F,<fr1,<>a"),
Opcode.SUBIM to InstructionFormat.from("BWL,<i,<>a | F,<i,<>a"),
Opcode.MULR to InstructionFormat.from("BWL,<>r1,<r2 | F,<>fr1,<fr2"),
Opcode.MUL to InstructionFormat.from("BWL,<>r1,<i | F,<>fr1,<i"),
Opcode.MULM to InstructionFormat.from("BWL,<r1,<>a | F,<fr1,<>a"),
Opcode.MULSR to InstructionFormat.from("BWL,<>r1,<r2 | F,<>fr1,<fr2"),
Opcode.MULS to InstructionFormat.from("BWL,<>r1,<i | F,<>fr1,<i"),
Opcode.MULSM to InstructionFormat.from("BWL,<r1,<>a | F,<fr1,<>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,<r2,<r3"),
Opcode.CONCAT to InstructionFormat.from("BW,>r1,<r2,<r3"),
Opcode.CLC to InstructionFormat.from("N"),
Opcode.SEC to InstructionFormat.from("N"),
Opcode.CLI to InstructionFormat.from("N"),