From 57fdb9e037bf78b0b36b0bc9c1cf3440ab566f0a Mon Sep 17 00:00:00 2001 From: Ed_ Date: Tue, 4 Aug 2026 18:27:00 -0400 Subject: [PATCH] improvmenets to delay slot modeling (lua metaprogram) --- code/duffle/gen/duffle.macs.h | 2 +- code/duffle/gen/duffle.offsets.h | 2 +- code/hello_joypad/gen/hello_joypad.macs.h | 2 +- code/hello_joypad/gen/hello_joypad.offsets.h | 2 +- scripts/duffle.lua | 33 +++++-- scripts/passes/static_analysis.lua | 91 ++++++++++++++++---- 6 files changed, 105 insertions(+), 27 deletions(-) diff --git a/code/duffle/gen/duffle.macs.h b/code/duffle/gen/duffle.macs.h index d0af41b..45a6fcf 100644 --- a/code/duffle/gen/duffle.macs.h +++ b/code/duffle/gen/duffle.macs.h @@ -2,7 +2,7 @@ #pragma once #endif // Auto-generated by ps1_meta.lua — DO NOT EDIT -// Source: C:\projects\pikuma\ps1\code\duffle\lottes_tape.h +// Source: C:\projects\Pikuma\ps1\code\duffle\lottes_tape.h // Component atoms (MipsAtomComp_(ac_*)) -> macro variants (mac_*) #ifndef WORD_COUNT diff --git a/code/duffle/gen/duffle.offsets.h b/code/duffle/gen/duffle.offsets.h index 59d1a47..eabc7da 100644 --- a/code/duffle/gen/duffle.offsets.h +++ b/code/duffle/gen/duffle.offsets.h @@ -1,5 +1,5 @@ // Auto-generated by ps1_meta.lua (passes/offsets.lua) — DO NOT EDIT -// Source: C:\projects\pikuma\ps1\code\duffle\lottes_tape.h +// Source: C:\projects\Pikuma\ps1\code\duffle\lottes_tape.h #pragma once #pragma region lottes_tape diff --git a/code/hello_joypad/gen/hello_joypad.macs.h b/code/hello_joypad/gen/hello_joypad.macs.h index 4902919..137868b 100644 --- a/code/hello_joypad/gen/hello_joypad.macs.h +++ b/code/hello_joypad/gen/hello_joypad.macs.h @@ -2,7 +2,7 @@ #pragma once #endif // Auto-generated by ps1_meta.lua — DO NOT EDIT -// Source: C:\projects\pikuma\ps1\code\hello_joypad\hello_joypad.tape.c +// Source: C:\projects\Pikuma\ps1\code\hello_joypad\hello_joypad.tape.c // Component atoms (MipsAtomComp_(ac_*)) -> macro variants (mac_*) #ifndef WORD_COUNT diff --git a/code/hello_joypad/gen/hello_joypad.offsets.h b/code/hello_joypad/gen/hello_joypad.offsets.h index ba093f5..58aec02 100644 --- a/code/hello_joypad/gen/hello_joypad.offsets.h +++ b/code/hello_joypad/gen/hello_joypad.offsets.h @@ -1,5 +1,5 @@ // Auto-generated by ps1_meta.lua (passes/offsets.lua) — DO NOT EDIT -// Source: C:\projects\pikuma\ps1\code\hello_joypad\hello_joypad.tape.c +// Source: C:\projects\Pikuma\ps1\code\hello_joypad\hello_joypad.tape.c #pragma once #pragma region hello_joypad.tape diff --git a/scripts/duffle.lua b/scripts/duffle.lua index 35d925a..2ced6d8 100644 --- a/scripts/duffle.lua +++ b/scripts/duffle.lua @@ -1700,22 +1700,45 @@ M.HARDWARE_RELATIONS = { }, -- Memory -> COP2 data register (LWC2). -- The memory-side timing is not measured by the vendored GTE latch experiment, so this relation has no numeric retirement threshold. - -- The forward walker emits one info edge at the first command-input consumer and then clears the pending relation. + -- The LWC2 destination has TWO retirement regimes (per PSX-SPX): + -- * GTE-command consumer (`gte_cmdw_*`): the GTE pipeline LATCHES the LWC2 result, so a `gte_cmdw_*` + -- in the very next slot uses the latched value. Gap = 0 is allowed. + -- (Per `docs/psx-spx/docs/gtepipelinetimings.md:271-274`.) + -- * Any other consumer: standard MIPS load delay applies. Gap = 1 required. + -- (Per `docs/psx-spx/docs/cpuspecifications.md:407-419`.) + -- Two separate relations so the walker can dispatch by consumer type and emit + -- different severities (the GTE-command path is `info` because the latch is intentional; + -- the non-GTE-consumer path is `error` because the missing nop is a real bug). { - id = "lwc2_unknown_visibility", - semantic = "LWC2", + id = "lwc2_to_gte_command", + semantic = "LWC2_to_GTE", token = "gte_lw", direction = "memory_to_cop2_data", reads = { domain = "memory", arg = 2 }, writes = { domain = "cop2.data", arg = 1 }, - visibility = { kind = "unknown_consumer", required = nil }, + required = 0, -- GTE-command consumer: gap = 0 OK (latched). evidence = { - confidence = "unknown", + confidence = "measured", source = "gtepipelinetimings.md:271-274", }, violation_kind = "info", clear_on_consumer = true, }, + { + id = "lwc2_to_other_consumer", + semantic = "LWC2_to_other", + token = "gte_lw", + direction = "memory_to_cop2_data", + reads = { domain = "memory", arg = 2 }, + writes = { domain = "cop2.data", arg = 1 }, + required = 1, -- Non-GTE-consumer: standard MIPS load delay. + evidence = { + confidence = "inferred", + source = "cpuspecifications.md:407-419", + }, + violation_kind = "error", + clear_on_consumer = true, + }, -- COP2 data register -> memory (SWC2). A read of C2 state, not a CPU-to-COP2 write. -- The policy row stays in for direction/provenance; staging it as a later command-input producer is suppressed. { diff --git a/scripts/passes/static_analysis.lua b/scripts/passes/static_analysis.lua index f25f4b2..458b524 100644 --- a/scripts/passes/static_analysis.lua +++ b/scripts/passes/static_analysis.lua @@ -401,6 +401,18 @@ end -- therefore counts ONLY words strictly between the producer and the consumer. -- ───────────────────────────────────────────────────────────────────────── +-- True iff `consumer_event` is a GTE command (gte_cmdw_* or one of the human-readable aliases +-- mapped in `duffle.GTE_COMMAND_ALIASES`). Used by the LWC2 retirement-regime dispatch in the +-- forward walker: a GTE-command consumer can read the LWC2 result in the very next slot (the GTE +-- pipeline latches the LWC2 data); any other consumer must observe the standard MIPS load delay +-- (gap >= 1). +local function is_gte_command(consumer_event) + local tok = consumer_event.encoder or consumer_event.ident or "" + if tok:sub(1, 9) == "gte_cmdw_" then return true end + local aliases = duffle.GTE_COMMAND_ALIASES or {} + return aliases[tok] ~= nil +end + -- True iff `consumer_word` falls inside the COP2 command's input set OR inside the producer's `fanout_to` set (for IRGB writes). -- Used by the consumer-match step of the forward walker. local function is_cop2_consumer_of(consumer_event, destination, producer_rel) @@ -817,9 +829,17 @@ local function analyze_hardware_relations(atom) local relation = prod.relation local semantic = relation.semantic local is_match = false - if semantic == "MTC2" or semantic == "CTC2" or semantic == "LWC2" then + if semantic == "MTC2" or semantic == "CTC2" or semantic == "LWC2_to_GTE" or semantic == "LWC2_to_other" then -- Consumer is a GTE command whose input set contains the producer's COP2 destination (or a fan-out target). - is_match = is_cop2_consumer_of(ev, prod.destination, relation) + -- LWC2_to_GTE — GTE-command consumer: gap = 0 OK (the pipeline latches the LWC2 result). + -- LWC2_to_other — non-GTE consumer: standard load delay applies. + if relation.id == "lwc2_to_gte_command" then + is_match = is_gte_command(ev) and is_cop2_consumer_of(ev, prod.destination, relation) + elseif relation.id == "lwc2_to_other_consumer" then + is_match = (not is_gte_command(ev)) and is_cop2_consumer_of(ev, prod.destination, relation) + else + is_match = is_cop2_consumer_of(ev, prod.destination, relation) + end elseif semantic == "MFC2" or semantic == "CFC2" or semantic == "MFC0" then -- Consumer is any encoder that reads the producer's GPR destination as an operand. is_match = is_gpr_consumer_of(ev, prod.destination) @@ -1163,6 +1183,8 @@ end local function check_hazard_nop_use(atom, _pipe_ctx, findings) local forward = atom.paths and atom.paths.forward_state local events = atom.paths.word_events or {} + -- GPR effects table used to resolve load destinations when classifying load-delay-slot nops. + local gpr_effects = duffle.INSTRUCTION_GPR_EFFECTS or {} if not events or #events == 0 then return end -- Runtime-helper atoms / components (e.g. tape_exit, ac_yield) carry `debug_skip = true` from the bare -- `atom_dbg_skip` marker; their structural nops are part of the fixed handshake and not author choices. @@ -1180,9 +1202,10 @@ local function check_hazard_nop_use(atom, _pipe_ctx, findings) -- Classify the nop BEFORE its event is applied to the pending state. if ev_ident == "nop" and prev_ev ~= nil then - -- Skip BD-slot nops: they are exclusively owned by control_transfer_delay_slot_use. + -- Skip BD-slot nops that are exclusively owned by control_transfer_delay_slot_use + -- (the nop after a branch/jump — covered by that check separately). -- Every BD-slot nop is structural; this check never reports on it. - -- (The earlier `if not suppressed then is_bd_slot = true end` form inverted the suppression — the `mac_yield()` handshake's `jump_reg(R_AtomJmp)` was incorrectly flagged.) + -- (The earlier `if not suppressed then is_bd_slot = true end` form inverted the suppression - the `mac_yield()` handshake's `jump_reg(R_AtomJmp)` was incorrectly flagged.) local prev_ident = prev_ev.encoder or "" local bd_policies = duffle.CONTROL_TRANSFER_DELAY_SLOT_POLICIES or {} local is_bd_slot = bd_policies[prev_ident] ~= nil @@ -1243,20 +1266,52 @@ local function check_hazard_nop_use(atom, _pipe_ctx, findings) else -- Track the slot_kind so the BD-separation case can assert the mac_yield handshake is still suppressed. local slot_kind = "plain" - findings[#findings + 1] = { - check = "hazard_nop_use", - kind = "info", - atom = atom.name, - line = ev_line, - source = ev.def_path or ev.source or "", - nop_classification = "modeled-redundant", - nop_word_index = ev_word, - retired_relation = nil, - slot_kind = slot_kind, - msg = string.format("%s at line %d: nop at word %d is modeled-redundant (no pending modeled relation)" - , atom.name, ev_line, ev_word - ), - } + -- MIPS load-delay slot: a `load_*` wrote a register in the previous slot, and the result + -- is unavailable for 1 cycle. This `nop` is structurally required; classifying it as + -- `modeled-required` is the correct signal (removing it would make the following + -- instruction read the OLD value of the loaded register, a load-use hazard). The + -- `load_delay_violations` check (Concern 3) catches the actual read-side error; here + -- we suppress the `modeled-redundant` misclassification. + -- The set of load instructions mirrors the LOAD_INSTRUCTION_IDENTS in `check_load_delay_slots`. + local load_idents = { load_word = true, load_half = true, load_half_u = true, + load_byte = true, load_byte_u = true, gte_lw = true, gte_lwc2 = true } + local is_load_delay = load_idents[prev_ident] == true + if is_load_delay then + -- Determine the destination register from the load's `writes` field. + local prev_writes = gpr_effects[prev_ident] and gpr_effects[prev_ident].writes or {} + local prev_args = prev_ev.args or {} + local load_dest = prev_writes[1] and prev_args[prev_writes[1]] or "" + findings[#findings + 1] = { + check = "hazard_nop_use", + kind = "info", + atom = atom.name, + line = ev_line, + source = ev.def_path or ev.source or "", + nop_classification = "modeled-required", + nop_word_index = ev_word, + retired_relation = "load_delay_slot", + producer_destination = load_dest, + consumer_token = "", + msg = string.format("%s at line %d: nop at word %d is modeled-required (load-delay slot for %s)" + , atom.name, ev_line, ev_word, load_dest + ), + } + else + findings[#findings + 1] = { + check = "hazard_nop_use", + kind = "info", + atom = atom.name, + line = ev_line, + source = ev.def_path or ev.source or "", + nop_classification = "modeled-redundant", + nop_word_index = ev_word, + retired_relation = nil, + slot_kind = slot_kind, + msg = string.format("%s at line %d: nop at word %d is modeled-redundant (no pending modeled relation)" + , atom.name, ev_line, ev_word + ), + } + end end end end