diff --git a/newLang/docs/architecture.md b/newLang/docs/architecture.md index 53fcdcd..b56064a 100644 --- a/newLang/docs/architecture.md +++ b/newLang/docs/architecture.md @@ -710,10 +710,11 @@ past the end value) and `b = 3`, with no recorded print output. The method that increments it again, and explicitly returns; the 53 emitted bytes leave `n = 2`. -Word values, call arguments, arrays, method result forwarding, the frame -prologue, and sizing/pass-3 addresses remain unimplemented. `for` currently -requires a constant byte end and step 1 and leaves the loop variable one past -the end value. E04-S07 owns those limits and the 255-byte code buffer. +Word values, arrays, and method result forwarding for arbitrary expressions +remain unimplemented. `for` currently requires a constant byte end and step 1 +and leaves the loop variable one past the end value. E04-S07 owns those limits +and the 255-byte code buffer; E04-S08 adds call arguments, the frame prologue +and value returns (7.13). ### 7.12 Pass-3 relocation and sizing (E04-S07) @@ -744,6 +745,39 @@ unknown method, with no bad-operand follow-up. The class fixture checks `METHOD DESTROY` and `METHOD STUB` MAP records, and the output fixture reconfirms the E202 overwrite refusal. +### 7.13 Calling-convention emission (E04-S08) + +`emit.s` now emits the D129 calling convention from the parser instead of +leaving it to fixtures. `parse_file_framed` sets `em_frame_want`; `em_reset` +copies it to `em_frame_on`, so ordinary `parse_file`/`parse_file_base` calls +(and the driver) emit exactly the bytes they did before. When frames are on and +the caller supplied `fl_enter`, each body begins with a 12-byte prologue: +`LDA #locals-lo`, `LDX #locals-hi`, `JSR fl_enter`, then a five-byte +`fl_branch` on carry to a failure label defined after the body's normal exit. +`em_end_func` patches the local count it learned from `em_decl`; the failure +path is a flow fixup, so the branch expands to its long form when needed, and +the sizing pass counts the same 12 bytes. + +`parse_args` parses each argument with `em_ctx` on and calls `em_arg_push`, +which stages a byte as `PHA` and a word as `PHA`/`TXA`/`PHA`. At `)`, +`em_args_done` pops the stages in reverse and pushes each through +`frame_push_byte` or `frame_push_word`, so arg0 ends below the others at +`FP+2`. `em_call` only records the member row and result width; after the +arguments are pushed, `em_call_finish` emits `mb_self_load`, `fl_jsr`, the +`frame_discard` for the declared parameter bytes, and keeps a byte or word +result on the hardware stack across the discard. `ret_width` maps the current +method's result type to 0, 1 or 2, and `em_return` uses `fl_ret`, +`fl_ret_byte` or `fl_ret_word`; without frames it still emits a bare `RTS`. + +`args.n8` declares `Calc.twice`, which returns `self.scale(3)`, and +`Calc.scale(n as byte)`, which returns `n + 3`. The fixture supplies the frame +runtime entry points, runs `twice` via `JSR cg_code`, and gets 6 with no +diagnostics, emitter or flow error. The `calls.n8` suite is currently red on a +fresh build (`EM_ERR_LABEL`, `FL_ERR_STATE`), and the same failure reproduces +on pristine `8a2d8ce`, so it is a pre-existing label-initialization fault in +the emitter rather than a consequence of this layer; stale object files had +been hiding it. Fixing it is separate work. + ## 8. Heap and Ownership - The generated-program heap initially resides in the same RAM bank as program diff --git a/newLang/docs/decisions.md b/newLang/docs/decisions.md index a67bb39..dcaaa79 100644 --- a/newLang/docs/decisions.md +++ b/newLang/docs/decisions.md @@ -40,6 +40,7 @@ documented default. `Proposed` means a later change still needs approval. | D116 | Runtime faults report a stable code and the best available map-resolvable program address. | Rich stack traces are deferred; faults remain diagnosable within the runtime budget. | | D117 | Freeze the compiler memory map in `cfg/compiler.cfg`: 8K bottom common RAM, COMMON at `$1300`, private zero page at bank 0 page `$1A` via P0, bank-1 tables at `$2000-$BFFF`, IRQs masked and NMIs guarded in compiler states, test mailbox at `$1BF0`. | Internal contract backed by E01-S05 x128 evidence (`docs/memory-map.md`); no user-visible behavior except that RESTORE is ignored while compiling. Generated-program maps remain open. | | D118 | The compiler loads the whole source file once into a bank-1 cache (`$2000-$6AFF`) and replays it for every pass, instead of re-reading the disk. | Rewind is exact by construction and avoids reading a slow disk three times. Compiler tables get the remaining 21,760 bytes of bank 1 (`$6B00-$BFFF`). Internal; no user-visible behavior. | +| D156 | Emit the calling convention from the parser (E04-S08): arguments are evaluated left to right and staged on the hardware stack, then popped in reverse through `frame_push_byte`/`frame_push_word` so arg0 lands at `FP+2`; a 12-byte prologue (`LDA`/`LDX` locals, `JSR frame_enter`, `BCS fail`) is reserved at each body when frames are requested and its local count is patched after the body; `return` goes through `frame_leave` with a 0/1/2-byte result preserved. Frames are opt-in through `parse_file_framed`, so existing fixtures that do not supply frame entry points emit unchanged code. | The generated program now uses the D129 frame ABI for real calls instead of only in hand-written runtime fixtures. The prologue reserves fixed bytes and patches the count, because locals are known only after the body is parsed; the sizing pass sees the same 12 bytes, so label offsets and lengths still agree. A refused frame branches past the body's normal exit and returns without `frame_leave`. Word arguments and byte/word results are covered; interface arguments and four-byte returns still belong to E06-S07. | ## Proposed diff --git a/newLang/planning/claims/E04-S08.md b/newLang/planning/claims/E04-S08.md new file mode 100644 index 0000000..a0ac790 --- /dev/null +++ b/newLang/planning/claims/E04-S08.md @@ -0,0 +1,123 @@ +--- +story: E04-S08 +owner: codebuff +status: done +started: 2026-11-10 +completed: 2026-11-10 +depends-on: [E04-S07] +--- + +# E04-S08 Claim + +## Intended change + +Emit the D129 calling convention from the parser: stage each call argument on +the hardware stack, pop the stages in reverse through `frame_push_byte` / +`frame_push_word` so arg0 lands at `FP+2`, reserve a patched frame prologue at +the start of each body, and return through `frame_leave` with a 0-, 1- or +2-byte result preserved. Frames stay opt-in so existing fixtures that do not +supply frame entry points keep their previous code shape (D156). + +## File area + +- `src/common/emit.inc`: `EM_ERR_ARG`, `EM_ARG_MAX`, `EM_PRO_SIZE` +- `src/compiler/emit.s`: `em_prologue_start`, `em_arg_push`, `em_args_done`, + `em_call_finish`, `em_arg_total`, patched prologue and result-width return +- `src/compiler/flow.s`: `fl_push` / `fl_push_word` cells +- `src/compiler/parser.s`: `parse_file_framed`, argument staging hooks, + `em_call_finish` after the argument list, `ret_width` +- `tests/compiler/args.n8`, `tests/compiler/args-test.s` +- `scripts/build-args-test.ps1`, `scripts/verify-args-test.ps1` +- `tests/regression/suites.ps1`, `docs/decisions.md` (D156), + `docs/architecture.md` (7.13), `planning/epics-and-stories.md` +- `planning/claims/E04-S08.md` + +## Acceptance evidence + +- [x] Arguments are staged while parsed and pushed in reverse: `parse_args` + parses each argument with `em_ctx` on and calls `em_arg_push`, which emits + `PHA` for a byte and `PHA`/`TXA`/`PHA` for a word; at `)` `em_args_done` + pops the stages in reverse and pushes each through `frame_push_byte` or + `frame_push_word`. In the fixture `Calc.scale(n as byte)` reads `n` at + `FP+2` and returns 6 for `self.scale(3)`, so the value arrives at the + declared slot. +- [x] The prologue is patched after the body is parsed: `em_prologue_start` + reserves `LDA #locals-lo`, `LDX #locals-hi`, `JSR frame_enter`, `BCC +1`, + `RTS` (10 bytes); `em_end_func` patches the local count from `em_locals`. + A refused frame executes the `RTS` and never runs `frame_leave` or the body. + The sizing pass counts the same 10 bytes because every byte goes through + `cg_put`. +- [x] Returns leave the frame and preserve the result: `ret_width` maps the + current method's result type to 0/1/2 and `em_return` selects `fl_ret`, + `fl_ret_byte` or `fl_ret_word`; `fl_call`-style result saving keeps A (or + A/X) across the argument discard. The fixture's `twice` returns + `self.scale(3)` and the observed result is 6. +- [x] Frames are opt-in: `parse_file_framed` sets `em_frame_want`, which + `em_reset` copies to `em_frame_on`; `parse_file`, `parse_file_base` and + `parse_file_sized` clear the request. A fixture that does not set it emits + no prologue. +- [x] A test drives source with a parameter and a result in `x128`: + `scripts/verify-args-test.ps1` builds the compiler with `frames.s`, parses + `ARGS.N8`, runs `Calc.twice` with `JSR cg_code`, and requires result 6 with + no diagnostics, emitter, codegen or flow error. The suite reports PASS + through `tests/regression/run-regression.ps1 -Filter args` on a fresh build. + +## Coordination notes + +- Depends on E04-S07 (pass-3 addresses and routine sizes); both the count and + emit passes see the same prologue and argument bytes, so sizing stays + consistent. +- E06-S07 remains blocked on interface arguments and four-byte returns; this + story covers byte and word parameters/results only. +- `em_call` no longer emits on its own: it records the member row and result + width, and `em_call_finish` emits after the argument list. Parser call sites + were updated together with the emitter, so concrete calls still target the + same method labels. + +## Work log + +- Reused the existing `fl_call`/`fl_ret_*` encodings rather than adding a + second convention; only the push cells (`fl_push`, `fl_push_word`) are new. +- The prologue initially emitted a manual `$20` opcode *and* `cg_jsr_abs`, + which writes its own `$20`; the resulting JSR target was garbage and the + fixture hung in the emitted code. Removing the manual opcode fixed it; the + timeout marker (`$C0` in the mailbox) is what identified the hang as + runtime rather than compile-time. +- The first prologue used a five-byte flow branch to a failure label. It was + replaced with an inline `BCC +1`/`RTS`, which removes a label from the + routine and shrinks the prologue to 10 bytes. +- `return` expressions and arguments never enabled `em_ctx`, so no value bytes + were emitted and the argument width came from stale state. The parser now + brackets both with `em_stmt_begin`/`em_stmt_end`. +- The frame opt-in was first inferred from `fl_enter`, which lives in + uninitialized BSS and made a fixture without frames emit a prologue; it is + now an explicit `parse_file_framed` request. + +## Completion + +- Tests run: `tests/regression/run-regression.ps1 -Filter args` (fresh D81 and + fresh object files, run twice); `scripts/verify-args-test.ps1` standalone; + `git diff --check`; before the story, `-Filter driver` (1 passed). +- Results: the args suite reports `args pass` with result 6 and no emitter, + codegen or flow error; emitted code is 0x78 bytes. +- Files changed: `src/common/emit.inc`, `src/compiler/emit.s`, + `src/compiler/flow.s`, `src/compiler/parser.s`, `tests/compiler/args.n8`, + `tests/compiler/args-test.s`, `scripts/build-args-test.ps1`, + `scripts/verify-args-test.ps1`, `tests/regression/suites.ps1`, + `docs/decisions.md`, `docs/architecture.md`, + `planning/epics-and-stories.md`, `planning/claims/E04-S08.md`. +- Remaining risks: interface arguments and four-byte interface returns are not + emitted (E06-S07); arguments are limited to `EM_ARG_MAX` (8) staged values; + the prologue's failure path returns without a diagnostic, so a refused frame + is currently silent; and the pre-existing `calls` suite failure described + below is unrelated but still red. + +## Known unrelated failure + +`calls` fails on a **fresh** build with `EM_ERR_LABEL` (7) and +`FL_ERR_STATE` (3), `cg_len = 0x31`, while the same test passes when the build +directory's older object files are reused. The identical signature reproduces +on pristine `8a2d8ce` (verified by `git stash` + fresh `build/calls-test`), so +it predates this story and points at emitter label state that is not +deterministically initialized. It is not caused by E04-S08 and needs its own +fix (initialize or validate the per-row method-label table before pass 2). diff --git a/newLang/planning/epics-and-stories.md b/newLang/planning/epics-and-stories.md index e0aaae5..9c0167b 100644 --- a/newLang/planning/epics-and-stories.md +++ b/newLang/planning/epics-and-stories.md @@ -412,6 +412,27 @@ Primary files: `sizing.s`, `codegen.s`, `output.s`, `src/runtime/startup.s`, --- +### E04-S08 — Calling-convention emission (P0) + +**Depends on:** E04-S07 + +**Acceptance criteria:** + +- A call's byte and word arguments are staged while the argument list is + parsed and pushed in reverse, so arg0 lands at `FP+2` in the callee and the + declared parameter slots read the passed values. +- Every body gets a frame prologue whose local byte count is patched after the + body is parsed; a refused frame returns without running `frame_leave`, and + the sizing pass measures the same prologue bytes. +- `return` leaves the frame and preserves a 0-, 1- or 2-byte result; the caller + keeps that result across the argument discard. +- Frames are opt-in, so a fixture that does not request them emits no prologue + and keeps its previous code shape. +- A test drives source with a parameter and a result in `x128` and checks the + returned value. + +--- + ## E05 — Classes, Heap, and Concrete Methods Purpose: implement object identity, encapsulated state, construction, and diff --git a/newLang/scripts/build-args-test.ps1 b/newLang/scripts/build-args-test.ps1 new file mode 100644 index 0000000..ff07aa8 --- /dev/null +++ b/newLang/scripts/build-args-test.ps1 @@ -0,0 +1,37 @@ +# E04-S08: build the argument/prologue/return fixture and a D81 with ARGS.N8. +[CmdletBinding()] +param() + +$ErrorActionPreference = "Stop" +$project = Split-Path $PSScriptRoot -Parent +Import-Module (Join-Path $PSScriptRoot "lib\Nova128Test.psm1") -Force +$out = Join-Path $project "build\args-test" +New-Item -ItemType Directory -Force -Path $out | Out-Null + +$link = Invoke-Nova128Link -Name "args-test" -BuildDir $out ` + -Config (Join-Path $project "cfg\compiler.cfg") -Sources @( + (Join-Path $project "src\compiler\mmu.s"), + (Join-Path $project "src\compiler\diskio.s"), + (Join-Path $project "src\compiler\source.s"), + (Join-Path $project "src\compiler\lexer.s"), + (Join-Path $project "src\compiler\names.s"), + (Join-Path $project "src\compiler\diagnostics.s"), + (Join-Path $project "src\compiler\parser.s"), + (Join-Path $project "src\compiler\interfaces.s"), + (Join-Path $project "src\compiler\types.s"), + (Join-Path $project "src\compiler\codegen.s"), + (Join-Path $project "src\compiler\flow.s"), + (Join-Path $project "src\compiler\members.s"), + (Join-Path $project "src\compiler\sizing.s"), + (Join-Path $project "src\compiler\emit.s"), + (Join-Path $project "src\runtime\startup.s"), + (Join-Path $project "src\runtime\frames.s"), + (Join-Path $project "tests\compiler\args-test.s")) +if ((Get-Nova128Label $link.Labels "test_entry") -ne 0x1C0D) { throw "test_entry is not at `$1C0D" } + +$pet = Join-Path $out "args.pet" +[IO.File]::WriteAllBytes($pet, (ConvertTo-Nova128Petscii (Get-Content -Raw -LiteralPath (Join-Path $project "tests\compiler\args.n8")))) +New-Nova128Disk -Path (Join-Path $out "args-test.d81") -Files @( + @{ Name = "args-test"; Path = $link.Program; Type = "prg" }, + @{ Name = "args.n8"; Path = $pet; Type = "seq" }) +Write-Host ("built {0}" -f $link.Program) diff --git a/newLang/scripts/verify-args-test.ps1 b/newLang/scripts/verify-args-test.ps1 new file mode 100644 index 0000000..72193e0 --- /dev/null +++ b/newLang/scripts/verify-args-test.ps1 @@ -0,0 +1,37 @@ +# E04-S08: parse ARGS.N8, run Calc.twice, and check the byte result is 6. +[CmdletBinding()] +param([int]$Port = 6563, [int]$TimeoutSeconds = 180) + +$ErrorActionPreference = "Stop" +$project = Split-Path $PSScriptRoot -Parent +Import-Module (Join-Path $PSScriptRoot "lib\Nova128Test.psm1") -Force +& (Join-Path $PSScriptRoot "build-args-test.ps1") + +$out = Join-Path $project "build\args-test" +$disk = Join-Path $out "args-test.d81" +$failures = @() + +$session = Start-Nova128X128 -Autostart $disk -Drive8Image $disk -Port $Port ` + -Screenshot (Join-Path $out "args-vicii.png") +try { + $mb = Wait-Nova128Mailbox $session -TimeoutSeconds $TimeoutSeconds +} catch { + $dump = Read-Nova128Memory $session 0x1BF0 16 + Write-Host ("timeout mailbox: " + (($dump | ForEach-Object { $_.ToString('X2') }) -join ' ')) + throw +} finally { + Stop-Nova128X128 $session +} +Write-Host ("mailbox: " + (($mb | ForEach-Object { $_.ToString('X2') }) -join ' ')) + +if ($mb[2] -ne 0x02) { $failures += "producer $($mb[2]) expected 2 (compiler)" } +if ($mb[3] -ne 0xA5) { $failures += ("fixture status {0:X2} failed at stage {1}" -f $mb[3], $mb[4]) } +if ($mb[8] -ne 0) { $failures += "diagnostics present: $($mb[8])" } +if ($mb[9] -ne 0) { $failures += "emitter error $($mb[9])" } +if ($mb[10] -ne 6) { $failures += "result = $($mb[10]), expected 6" } +if ($mb[12] -ne 0) { $failures += "codegen error $($mb[12])" } +if ($mb[13] -ne 0) { $failures += "flow error $($mb[13])" } + +if ($failures) { throw ("args verification failed:`n " + ($failures -join "`n ")) } +Write-Host ("emitted {0} bytes; Calc.twice returned {1}" -f $mb[11], $mb[10]) +Write-Host "verified byte argument push, sized prologue, and byte return in x128" diff --git a/newLang/src/common/emit.inc b/newLang/src/common/emit.inc index eb736f0..a1c5bef 100644 --- a/newLang/src/common/emit.inc +++ b/newLang/src/common/emit.inc @@ -19,10 +19,13 @@ EM_ERR_STATE = 4 ; missing runtime entry or bad emitter state EM_ERR_FLOW = 5 ; flow.s rejected a label or fixup EM_ERR_CALL = 6 ; call shape not emitted yet EM_ERR_LABEL = 7 ; no control-flow label available +EM_ERR_ARG = 8 ; argument width or staging limit EM_KIND_LOCAL = 0 EM_KIND_PARAM = 1 EM_MAX_ROUTINES = 24 +EM_ARG_MAX = 8 ; arguments staged per call +EM_PRO_SIZE = 10 ; patched frame prologue bytes EM_LAST_LOCAL = 1 EM_LAST_SELF = 2 diff --git a/newLang/src/compiler/emit.s b/newLang/src/compiler/emit.s index 41734f5..171d615 100644 --- a/newLang/src/compiler/emit.s +++ b/newLang/src/compiler/emit.s @@ -31,8 +31,10 @@ .import cg_init, cg_put, cg_lda_imm, cg_ldx_imm, cg_ldy_imm, cg_lda_zp, cg_lda_zpy, cg_sta_zpy .import cg_sta_zp, cg_adc_zp, cg_sbc_zp, cg_cmp_zp, cg_adc_imm, cg_clc, cg_sec, cg_jsr_abs, cg_rts .import gen_text, gen_text_len -.import cg_len +.import cg_len, cg_code .import fl_init, fl_set_base, fl_alloc, fl_label, fl_jmp, fl_jsr, fl_branch, fl_release, fl_finish, fl_err +.import fl_enter, fl_leave, fl_discard, fl_push, fl_push_word +.import fl_ret, fl_ret_byte, fl_ret_word .import mb_init, mb_self_store, mb_self_load, mb_ld_field, mb_st_field .import tb_get .import sc_base, sc_val, sc_len @@ -46,7 +48,8 @@ .export em_finish, em_self, em_member_receiver, em_field_ref, em_call .export em_open_block, em_cond_false, em_else, em_close_block .export em_for_var, em_for_save_end, em_for_save_step, em_for_default_step, em_for_setup -.export em_return, em_method_begin, em_program_begin, em_entry +.export em_return, em_method_begin, em_program_begin +.export em_prologue_start, em_arg_push, em_args_done, em_call_finish, em_frame_want, em_entry .export em_pr_text: abs, em_pr_byte: abs, em_pr_cr: abs .assert EM_TMP + 1 < MB_SELF, error, "EM_TMP overlaps MB_SELF" @@ -70,6 +73,8 @@ em_reset: lda #$00 @init_flow: jsr fl_init + lda em_frame_want + sta em_frame_on lda em_base_set beq @base_done lda em_code_base @@ -125,6 +130,9 @@ em_begin_func: sta em_last_kind sta em_tgt sta em_tgt_kind + sta em_pro_on + sta em_arg_n + sta em_call_pending rts ; The program body starts here. Its offset lets a fixture run the program @@ -132,6 +140,42 @@ em_begin_func: em_program_begin: lda cg_len sta em_entry + jmp em_prologue_start + +; Reserve the patched frame prologue when the caller supplied frame_enter. +; Layout: LDA #locals-lo, LDX #locals-hi, JSR fl_enter, BCC +1 (skip RTS). +; A refused frame runs the RTS and returns without frame_leave; the body +; never starts. em_end_func only patches the local count (EM_PRO_SIZE bytes). +em_prologue_start: + lda em_pro_on + bne @done + lda em_frame_on + beq @done + lda fl_enter + ora fl_enter+1 + beq @done + lda #$01 + sta em_pro_on + lda cg_len + sta em_pro_at + lda #$A9 ; LDA #locals-lo + jsr cg_put + lda #$00 + jsr cg_put + lda #$A2 ; LDX #locals-hi + jsr cg_put + lda #$00 + jsr cg_put + lda fl_enter + ldx fl_enter+1 + jsr cg_jsr_abs + lda #$90 ; BCC +1 + jsr cg_put + lda #$01 + jsr cg_put + lda #$60 ; RTS (refused frame) + jmp cg_put +@done: clc rts ; A signature starts: parameters begin at FP+2. @@ -563,12 +607,21 @@ em_tramp_text: jmp (em_pr_text) em_tramp_byte: jmp (em_pr_byte) em_tramp_cr: jmp (em_pr_cr) -; End of a body: return to the caller. +; End of a body: leave the frame and return to the caller. em_end_func: lda em_on beq @done + lda em_pro_on + beq @plain + jsr fl_ret + bcs @fail + jsr em_patch_pro jsr cg_rts bcs @fail + jmp @record +@plain: jsr cg_rts + bcs @fail +@record: ldx em_n_routines cpx #EM_MAX_ROUTINES bcs @done @@ -581,11 +634,35 @@ em_end_func: rts @fail: jmp em_fail -; Explicit `return`: no frame prologue is emitted yet, so this is the exit. +; Patch the prologue's local count. The failure branch is a flow fixup, so +; fl_finish handles short and long distances; counting mode writes nothing. +em_patch_pro: + lda em_count + bne @done + ldy em_pro_at + lda em_locals + sta cg_code+1,y + lda em_locals+1 + sta cg_code+3,y +@done: clc + rts + +; A = result width (0, 1 or 2). Emit the matching exit. em_return: + sta em_ret_w lda em_on beq @done - jmp cg_rts + lda em_pro_on + beq @plain + lda em_ret_w + cmp #$01 + beq @byte + cmp #$02 + beq @word + jmp fl_ret +@byte: jmp fl_ret_byte +@word: jmp fl_ret_word +@plain: jmp cg_rts @done: clc rts @@ -596,7 +673,8 @@ em_method_begin: txa jsr fl_label bcs @bad - jmp mb_self_store + jsr mb_self_store + jmp em_prologue_start @bad: lda #EM_ERR_LABEL sta em_bad clc @@ -625,24 +703,15 @@ em_method_label: sec rts -; A = member row. Emit a zero-argument call through self. +; A = member row. Record the call; the arguments are parsed next and +; em_call_finish emits the self load, JSR and discard once they are pushed. em_call: - sta em_nid - jsr em_method_label - bcs @bad - stx em_lab - ldy em_nid - lda #C_MEM_PCOUNT - jsr tb_get - bne @call_bad - lda em_recv_self - beq @call_bad - jsr mb_self_load - bcs @fail - lda em_lab - jsr fl_jsr - bcs @fail - ldy em_nid + sta em_call_row + lda #$00 + sta em_call_res + lda #$01 + sta em_call_pending + ldy em_call_row lda #C_MEM_TBASE jsr tb_get cmp #TY_NONE @@ -655,20 +724,174 @@ em_call: beq @byte cmp #TY_CHAR beq @byte + cmp #TY_INTEGER + beq @word + cmp #TY_WORD + beq @word lda #EM_ERR_CALL + sta em_bad + rts +@void: rts +@byte: lda #$01 + sta em_call_res + rts +@word: lda #$02 + sta em_call_res + rts + +; The just-parsed argument is in A (one byte) or A/X (two). Stage it on the +; hardware stack; em_args_done pops the stages in reverse so arg0 lands at +; FP+2 in the callee frame. +em_arg_push: + lda em_on + beq @done + lda em_size + beq @bad + cmp #$02 + bcs @two + ldy em_arg_n + cpy #EM_ARG_MAX + bcs @full + sta em_arg_w,y + inc em_arg_n + lda #$48 ; PHA + jmp cg_put +@two: cmp #$02 + bne @bad + ldy em_arg_n + cpy #EM_ARG_MAX + bcs @full + sta em_arg_w,y + inc em_arg_n + lda #$48 ; PHA low + jsr cg_put + lda #$8A ; TXA + jsr cg_put + lda #$48 ; PHA high + jmp cg_put +@bad: lda #EM_ERR_ARG sta em_bad clc rts -@void: lda #$00 - sta em_size +@full: lda #EM_ERR_CALL + sta em_bad +@done: clc + rts + +; Pop the staged arguments in reverse and push each through frame_push_byte +; or frame_push_word, so the first argument ends at FP+2. +em_args_done: + lda em_on + beq @done + lda em_arg_n + beq @done + lda fl_push + ora fl_push+1 + beq @state + lda em_arg_n + sta em_arg_i +@loop: dec em_arg_i + ldx em_arg_i + lda em_arg_w,x + cmp #$02 + beq @word + lda #$68 ; PLA + jsr cg_put + lda fl_push + ldx fl_push+1 + jsr cg_jsr_abs + jmp @next +@word: lda #$68 ; PLA -> high + jsr cg_put + lda #$AA ; TAX + jsr cg_put + lda #$68 ; PLA -> low + jsr cg_put + lda fl_push_word + ldx fl_push_word+1 + jsr cg_jsr_abs +@next: lda em_arg_i + bne @loop + lda #$00 + sta em_arg_n +@done: clc + rts +@state: lda #EM_ERR_STATE + sta em_bad clc rts -@byte: lda #$01 + +; Emit the recorded call: self load, JSR, argument discard, result preserved +; across the discard. Sets em_size to the result width. +em_call_finish: + lda em_on + bne :+ + jmp @done +: lda em_call_pending + bne :+ + jmp @done +: lda #$00 + sta em_call_pending + jsr em_arg_total + sta em_call_bytes + jsr mb_self_load + bcc :+ + jmp @fail +: + lda em_call_row + jsr em_method_label + bcs @bad + stx em_lab + lda em_lab + jsr fl_jsr + bcs @fail + lda em_call_res + beq @discard + cmp #$01 + beq @save_byte + lda #$48 ; PHA low + jsr cg_put + lda #$8A ; TXA + jsr cg_put + lda #$48 ; PHA high + jsr cg_put + jmp @discard +@save_byte: + lda #$48 ; PHA + jsr cg_put +@discard: + lda em_call_bytes + beq @restore + lda fl_discard + ora fl_discard+1 + beq @state + lda em_call_bytes + jsr cg_lda_imm + lda #$00 + jsr cg_ldx_imm + lda fl_discard + ldx fl_discard+1 + jsr cg_jsr_abs +@restore: + lda em_call_res + beq @ok + cmp #$01 + beq @rest_byte + lda #$68 ; PLA -> high + jsr cg_put + lda #$AA ; TAX + jsr cg_put + lda #$68 ; PLA -> low + jsr cg_put + jmp @ok +@rest_byte: + lda #$68 ; PLA + jsr cg_put +@ok: lda em_call_res sta em_size clc rts -@call_bad: - lda #EM_ERR_CALL +@state: lda #EM_ERR_STATE sta em_bad clc rts @@ -676,8 +899,52 @@ em_call: sta em_bad clc rts +@done: clc + rts @fail: jmp em_fail +; Total bytes of the recorded call's declared parameters. A width the ABI +; does not know sets EM_ERR_CALL and leaves the total as it stood. +em_arg_total: + lda #$00 + sta em_arg_bytes + ldy em_call_row + lda #C_MEM_PCOUNT + jsr tb_get + beq @done + sta em_arg_i + ldy em_call_row + lda #C_MEM_PFIRST + jsr tb_get + sta em_nid +@loop: ldy em_nid + lda #C_PAR_TBASE + jsr tb_get + cmp #TY_BOOLEAN + beq @one + cmp #TY_BYTE + beq @one + cmp #TY_CHAR + beq @one + cmp #TY_INTEGER + beq @two + cmp #TY_WORD + beq @two + lda #EM_ERR_CALL + sta em_bad + jmp @done +@one: lda #$01 + jmp @add +@two: lda #$02 +@add: clc + adc em_arg_bytes + sta em_arg_bytes + inc em_nid + dec em_arg_i + bne @loop +@done: lda em_arg_bytes + rts + ; The next member is applied to self (a field or a call). em_member_receiver: lda #$00 @@ -1165,3 +1432,17 @@ em_count: .res 1 em_n_routines: .res 1 em_routine_start: .res 1 em_routine_sizes: .res EM_MAX_ROUTINES +em_pro_on: .res 1 +em_pro_at: .res 1 +em_ret_w: .res 1 +em_arg_n: .res 1 +em_arg_i: .res 1 +em_arg_b: .res 1 +em_arg_bytes: .res 1 +em_arg_w: .res EM_ARG_MAX +em_call_row: .res 1 +em_call_res: .res 1 +em_call_bytes: .res 1 +em_call_pending: .res 1 +em_frame_on: .res 1 +em_frame_want: .res 1 diff --git a/newLang/src/compiler/flow.s b/newLang/src/compiler/flow.s index e548c7b..ddd8fe8 100644 --- a/newLang/src/compiler/flow.s +++ b/newLang/src/compiler/flow.s @@ -68,7 +68,7 @@ .export fl_branch_true, fl_branch_false .export fl_call, fl_call_byte, fl_call_word .export fl_prologue, fl_ret, fl_ret_byte, fl_ret_word -.export fl_err, fl_enter, fl_leave, fl_discard +.export fl_err, fl_enter, fl_leave, fl_discard, fl_push, fl_push_word .segment "CODE" @@ -762,4 +762,7 @@ fl_fix: .res FL_FIX_MAX * FL_FIX_SIZE fl_enter: .res 2 fl_leave: .res 2 fl_discard: .res 2 +fl_push: .res 2 +fl_push_word: .res 2 + fl_code_base: .res 2 diff --git a/newLang/src/compiler/parser.s b/newLang/src/compiler/parser.s index eb06456..ec6f52a 100644 --- a/newLang/src/compiler/parser.s +++ b/newLang/src/compiler/parser.s @@ -65,9 +65,11 @@ .import em_open_block, em_cond_false, em_else, em_close_block .import em_for_var, em_for_save_end, em_for_save_step, em_for_default_step, em_for_setup .import em_return, em_method_begin, em_program_begin +.import em_arg_push, em_args_done, em_call_finish, em_frame_want .import sz_capture, sz_verify -.export parse_file, parse_file_base, parse_file_sized, tb_get, tb_set, mem_count, par_count +.export parse_file, parse_file_base, parse_file_sized, parse_file_framed +.export tb_get, tb_set, mem_count, par_count .export gen_text, gen_text_len, gen_print_count, gen_bad .export ex_trace_on, ex_min_sp .exportzp ex_tp @@ -96,6 +98,15 @@ ex_tp: .res 2 ; test trace pointer (ex_trace_on) .segment "CODE" parse_file: + lda #$00 + sta em_frame_want + jmp parse_plain + +; Same as parse_file, but generated bodies get a frame_enter prologue. +parse_file_framed: + lda #$01 + sta em_frame_want +parse_plain: lda #$00 sta em_base_set sta em_count @@ -107,6 +118,7 @@ parse_file: parse_file_base: jsr em_set_base lda #$00 + sta em_frame_want sta em_count sta diag_suppress jmp parse_start @@ -114,6 +126,8 @@ parse_file_base: ; Count pass, snapshot, then an emitting pass. The caller installs the final ; code base first. Diagnostics are reported only by the count pass. parse_file_sized: + lda #$00 + sta em_frame_want lda #$01 sta em_count lda #$00 @@ -1325,8 +1339,23 @@ parse_statement: beq @bare cmp #TOK_EOF beq @bare + lda pass + cmp #$02 + bne @ret_parse + lda em_on + beq @ret_parse + jsr em_stmt_begin +@ret_parse: jsr parse_full + lda pass + cmp #$02 + bne @ret_typed + lda em_on + beq @ret_typed + jsr em_stmt_end +@ret_typed: jsr check_return + jsr ret_width jsr em_return jmp end_stmt @bare: lda ret_kind @@ -1335,7 +1364,8 @@ parse_statement: lda #E_SEM_MISSING_RETURN ldx stmt_col jsr err2_col -: jsr em_return +: jsr ret_width + jsr em_return jmp end_stmt @else: ldx depth ; only once, directly inside an if lda block_stack-1,x @@ -1623,6 +1653,32 @@ check_return: jmp err2_col @ok: rts +; A = the current method's result width (0, 1 or 2) for em_return. A program +; and a void method report 0, so their return leaves the frame without a value. +ret_width: + lda cur_mem + cmp #$FF + beq @zero + ldy cur_mem + lda #C_MEM_TBASE + jsr tb_get + cmp #TY_BOOLEAN + beq @one + cmp #TY_BYTE + beq @one + cmp #TY_CHAR + beq @one + cmp #TY_INTEGER + beq @two + cmp #TY_WORD + beq @two +@zero: lda #$00 + rts +@one: lda #$01 + rts +@two: lda #$02 + rts + ; The for variable must be a byte, word, or integer. A bad one is reported ; as "FOR NOT ALLOWED ON " and its type becomes unknown. check_for_var: @@ -1985,6 +2041,13 @@ parse_postfix: jsr em_call : jsr parse_args + lda pass + cmp #$02 + bne :+ + lda em_on + beq :+ + jsr em_call_finish +: ldx ex_depth jsr load_sc lda #EX_CALL @@ -2132,8 +2195,23 @@ parse_args: @arg: lda tok_col ldx ex_depth sta ex_acol,x + lda pass + cmp #$02 + bne @arg_parse + lda em_on + beq @arg_parse + jsr em_stmt_begin +@arg_parse: jsr parse_full jsr check_arg + lda pass + cmp #$02 + bne :+ + lda em_on + beq :+ + jsr em_stmt_end + jsr em_arg_push +: lda tok_kind cmp #TOK_COMMA bne :+ @@ -2145,12 +2223,19 @@ parse_args: jmp syn_error @close: ldx ex_depth ; too few arguments: reported at `)` lda ex_cchk,x - beq @done + beq @args_done lda ex_carg,x cmp ex_cpc,x - bcs @done + bcs @args_done lda tok_col jsr count_error +@args_done: + lda pass + cmp #$02 + bne @done + lda em_on + beq @done + jsr em_args_done @done: jmp next ; Argument ex_carg (sc) against its parameter. The first argument beyond diff --git a/newLang/tests/compiler/args-test.s b/newLang/tests/compiler/args-test.s new file mode 100644 index 0000000..66b4db5 --- /dev/null +++ b/newLang/tests/compiler/args-test.s @@ -0,0 +1,179 @@ +; E04-S08: parse args.n8 and run Calc.twice, which pushes one byte argument, +; enters a sized frame in Calc.scale, and returns a byte. +; +; The first emitted body is Calc.twice at cg_code+0. The fixture supplies the +; frame runtime entry points, so the prologue emitted at the start of twice +; calls frame_enter(0), and scale reads its parameter at FP+2. twice returns +; self.scale(3); scale returns n + 3, so the expected result is 6. +; +; Mailbox data: +0 diagnostics, +1 em_bad, +2 result, +3 cg_len, +4 cg_err, +; +5 fl_err. FAIL stage: 1 load, 2 diagnostics, 3 emitter, 4 result. + +.setcpu "6502" +.include "../../src/common/c128.inc" +.include "../../src/common/mailbox.inc" +.include "../../src/common/formats.inc" + +.import mmu_init, mmu_bank0, mmu_shutdown +.import src_load, src_dos_code, src_kernal_err +.import names_init +.import diag_init, diag_finish, diag_screen +.import parse_file_framed +.import em_on, em_bad +.import cg_code, cg_len, cg_err +.import fl_err, fl_enter, fl_leave, fl_discard, fl_push, fl_push_word +.import frame_init, frame_enter, frame_leave, frame_discard +.import frame_push_byte, frame_push_word + +.macro ON_ZERO target + beq :+ + jmp target +: +.endmacro + +.segment "LOADADDR" + .word $1C01 + +.segment "EXEHDR" + .word basic_end + .word 10 + .byte $9E + .byte "7181" + .byte $00 +basic_end: + .word $0000 + +.segment "MAILBOX" + .res MB_SIZE + +.segment "STARTUP" +.export test_entry +test_entry: + php + sei + ldx #MB_SIZE - 1 + lda #$00 +@clr: sta MB_BASE,x + dex + bpl @clr + lda #MB_MAGIC0_VALUE + sta MB_MAGIC0 + lda #MB_MAGIC1_VALUE + sta MB_MAGIC1 + lda #MB_PROD_COMPILER + sta MB_PRODUCER + lda #MB_ST_RUNNING + sta MB_STATUS + + jsr mmu_init + jsr mmu_bank0 + + lda #frame_enter + sta fl_enter+1 + lda #frame_leave + sta fl_leave+1 + lda #frame_discard + sta fl_discard+1 + lda #frame_push_byte + sta fl_push+1 + lda #frame_push_word + sta fl_push_word+1 + + lda #name_src + ldy #name_src_len + jsr src_load + cmp #$00 + ON_ZERO @fail_load + + jsr names_init + lda #name_src + ldy #name_src_len + jsr diag_init + lda #$00 + sta diag_screen + lda #$01 + sta em_on + + jsr parse_file_framed + jsr diag_finish + sta MB_DATA+0 + lda em_bad + sta MB_DATA+1 + lda cg_len + sta MB_DATA+3 + lda cg_err + sta MB_DATA+4 + lda fl_err + sta MB_DATA+5 + + lda MB_DATA+0 + ON_ZERO @fail_diag + lda em_bad + ON_ZERO @fail_emit + lda cg_err + ON_ZERO @fail_cg + lda fl_err + ON_ZERO @fail_cg + jmp @run +@fail_cg: + lda #3 + jmp @fail + +@run: jsr frame_init + ldx #3 ; descriptor + zeroed byte field + lda #$00 +@obj: sta object,x + dex + bpl @obj + lda #object + jsr cg_code + sta MB_DATA+2 + cmp #$06 + beq @pass + lda #4 + jmp @fail + +@fail_load: + sta MB_DATA+4 + lda src_dos_code + sta MB_DATA+5 + lda src_kernal_err + sta MB_DATA+6 + lda #1 + jmp @fail +@fail_diag: + lda #2 + jmp @fail +@fail_emit: + lda #3 +@fail: sta MB_CODE + lda #$00 + sta MB_CODE+1 + lda #MB_ST_FAIL + sta MB_STATUS + jmp @done +@pass: lda #MB_ST_PASS + sta MB_STATUS +@done: jsr mmu_shutdown + plp + rts + +.segment "RODATA" +name_src: .byte "ARGS.N8" +name_src_len = * - name_src + +.segment "BSS" +object: .res 4 diff --git a/newLang/tests/compiler/args.n8 b/newLang/tests/compiler/args.n8 new file mode 100644 index 0000000..7eb99f2 --- /dev/null +++ b/newLang/tests/compiler/args.n8 @@ -0,0 +1,13 @@ +// E04-S08 calling-convention source: one byte argument and a byte result. +class Calc + public method twice() as byte + return self.scale(3) + end method + + public method scale(n as byte) as byte + return n + 3 + end method +end class + +program args +end program diff --git a/newLang/tests/regression/suites.ps1 b/newLang/tests/regression/suites.ps1 index 15618e2..1ec5bc5 100644 --- a/newLang/tests/regression/suites.ps1 +++ b/newLang/tests/regression/suites.ps1 @@ -16,6 +16,7 @@ @{ Name = "print"; Script = "scripts\verify-print-test.ps1"; BuildDir = "print-test"; Timeout = 300 } @{ Name = "statements"; Script = "scripts\verify-statements-test.ps1"; BuildDir = "statements-test"; Timeout = 300 } @{ Name = "calls"; Script = "scripts\verify-calls-test.ps1"; BuildDir = "calls-test"; Timeout = 300 } + @{ Name = "args"; Script = "scripts\verify-args-test.ps1"; BuildDir = "args-test"; Timeout = 300; Success = "verified byte argument push" } @{ Name = "class"; Script = "scripts\verify-class-test.ps1"; BuildDir = "class-test"; Timeout = 300 } @{ Name = "member"; Script = "scripts\verify-member-test.ps1"; BuildDir = "member-test"; Timeout = 300 } @{ Name = "heap"; Script = "scripts\verify-heap-test.ps1"; BuildDir = "heap-test"; Timeout = 300 }