diff --git a/scripts/duffle.lua b/scripts/duffle.lua index 78d4ada..80470ac 100644 --- a/scripts/duffle.lua +++ b/scripts/duffle.lua @@ -2236,6 +2236,36 @@ local function _project_emission_inner(root_body_entry, ctx_table) local invocation_stack = {} -- stack of currently-open invocation records local next_inv_id = 0 + local reg_use_schema = ctx_table.reg_use_schema + local reg_use_param = ctx_table.reg_use_param + local atom_name = ctx_table.atom_name + + local slot_readonly = {} + if reg_use_schema then + for _, slot in ipairs(reg_use_schema.slots or {}) do + slot_readonly[slot.name] = slot.readonly == true + end + end + + local function apply_sub(sub_map, operand) + if sub_map and type(operand) == "string" and sub_map[operand] then + return sub_map[operand] + end + return operand + end + + local function resolve_gpr_key(operand) + if type(operand) ~= "string" then return nil end + if operand:sub(1, 2) == "R_" then return operand end + if not (reg_use_schema and reg_use_param) then return nil end + local prefix = reg_use_param .. "." + if operand:sub(1, #prefix) ~= prefix then return nil end + local member_path = operand:sub(#prefix + 1) + local slot = reg_use_schema.alias_to_slot[member_path] + if not slot then return nil, member_path end + return "reguse:" .. atom_name .. ":" .. slot, nil, slot + end + local function open_invocation_ids_snapshot() local ids = {} for _, inv in ipairs(invocation_stack) do @@ -2246,7 +2276,7 @@ local function _project_emission_inner(root_body_entry, ctx_table) local function emit_word(encoder, args, line, word_call_text, def_source_now, def_line_now, - immediate_call_text, root_call_text_w) + immediate_call_text, root_call_text_w, sub_map) local inv_ids = open_invocation_ids_snapshot() local outermost = inv_ids[1] or 0 -- For words emitted at the root atom body, `immediate_call_text` is nil and the walker's `word_call_text` (the word's own token, e.g. "nop") becomes the effective call_text. @@ -2254,6 +2284,42 @@ local function _project_emission_inner(root_body_entry, ctx_table) -- The call that triggered the body expansion we're currently walking. local eff_call_text = immediate_call_text or word_call_text local eff_root_call_text = root_call_text_w + local gpr_keys = nil + if reg_use_schema or sub_map then + gpr_keys = {} + for pos, arg in ipairs(args or {}) do + local effective = apply_sub(sub_map, arg) + local key, unresolved, slot = resolve_gpr_key(effective) + gpr_keys[pos] = key + if unresolved then + errors[#errors + 1] = { + kind = "reguse_unresolved", + line = line, + msg = string.format("RegUse operand %q does not resolve in schema %q", + effective, (reg_use_schema and reg_use_schema.name) or "?"), + } + end + if key and slot and slot_readonly[slot] then + local effects = M.INSTRUCTION_GPR_EFFECTS or {} + local row = effects[encoder] + if row and row.writes then + for _, wpos in ipairs(row.writes) do + if wpos == pos then + errors[#errors + 1] = { + kind = "reguse_const_write", + line = line, + msg = string.format("RegUse slot %q is Reg const; %s writes it", + slot, encoder), + } + end + end + end + end + end + end + if not reg_use_schema then + gpr_keys = nil + end items[#items + 1] = { kind = "word", encoder = encoder, @@ -2265,6 +2331,7 @@ local function _project_emission_inner(root_body_entry, ctx_table) root_call_text = eff_root_call_text, invocation_ids = inv_ids, outermost_invocation_id = outermost, + gpr_keys = gpr_keys, } word_events[#word_events + 1] = { i = word_idx, @@ -2277,6 +2344,7 @@ local function _project_emission_inner(root_body_entry, ctx_table) invocation_ids = inv_ids, outermost_invocation_id = outermost, word_count = 1, + gpr_keys = gpr_keys, } word_idx = word_idx + 1 end @@ -2520,6 +2588,7 @@ local function _project_emission_inner(root_body_entry, ctx_table) local line_of = body_entry.line_of or M.LineIndex("") local def_source = body_entry.source or "" local def_line = body_entry.declaration or 0 + local sub_map = body_entry.sub_map -- Per-token dispatch: each matched branch returns; only the fall-through -- "opaque word" emit handles direct encoders + mac_X-without-component. local function process_token(bt) @@ -2575,12 +2644,22 @@ local function _project_emission_inner(root_body_entry, ctx_table) -- Propagate trackers into the recursive walk: -- immediate_call_text = this call's tok (the IMMEDIATE outer call for words emitted in this body) -- root_call_text = the OUTERMOST call (immutable across the recursion) + local formal_names = ctx_table.component_index[bare] + and ctx_table.component_index[bare].arg_names + local child_map = nil + if formal_names then + child_map = {} + for i, fname in ipairs(formal_names) do + child_map[fname] = apply_sub(sub_map, args[i]) + end + end walk_body_entry({ body_tokens = comp.body_tokens or {}, body_off = comp.body_off or 0, line_of = comp.line_of, source = comp.source, declaration = comp.declaration, + sub_map = child_map, }, inv.id, invocation_root_call_text, @@ -2618,7 +2697,7 @@ local function _project_emission_inner(root_body_entry, ctx_table) local n = resolve_count(ident, tok_line) local out_ident = (ident == "nop2") and "nop" or ident for _ = 1, n do - emit_word(out_ident, args, tok_line, tok, def_source, def_line, walk_immediate_call_text, walk_root_call_text) + emit_word(out_ident, args, tok_line, tok, def_source, def_line, walk_immediate_call_text, walk_root_call_text, sub_map) end end @@ -2683,7 +2762,7 @@ end --- @param components table -- bare-name → component definition (corpus.components); REQUIRED — consumed at the invocation-construction site to stamp --- `invocation.debug_skip`. A missing or non-table `components` raises a fail-loud error rather than silently falling back. --- @return EmissionProjection -function M.project_emission(body_text, component_index, word_counts, components) +function M.project_emission(body_text, component_index, word_counts, components, reg_use_ctx) -- The recursive walk delegates to `_project_emission_inner` so component bodies (which arrive as -- `{body_tokens, body_off, line_of, source, declaration}` records from `corpus.component_body_index`) -- re-enter the same walker with the same shared output state. @@ -2726,6 +2805,10 @@ function M.project_emission(body_text, component_index, word_counts, components) component_index = component_index or {}, word_counts = word_counts or {}, components = components, + reg_use_schema = reg_use_ctx and reg_use_ctx.reg_use_schema, + reg_use_param = reg_use_ctx and reg_use_ctx.reg_use_param, + atom_name = reg_use_ctx and reg_use_ctx.atom_name, + schema_name = reg_use_ctx and reg_use_ctx.schema_name, }) end @@ -2860,13 +2943,12 @@ function M.find_atom_proc_decl_for(source, before_pos, mips_atom_ptr_len) if source:sub(next_pos, next_pos) == "(" then local inner = M.read_parens(source, next_pos) if inner then - -- strip the _proc suffix to get the atom name local proc_suffix = "_proc" + local atom_name = ident if #ident > #proc_suffix and ident:sub(-#proc_suffix) == proc_suffix then - return ident:sub(1, #ident - #proc_suffix), inner + atom_name = ident:sub(1, #ident - #proc_suffix) end - -- no _proc suffix — return as-is - return ident, inner + return atom_name, inner, ident end end -- ident not followed by "(" — it's a qualifier; skip it diff --git a/scripts/passes/components.lua b/scripts/passes/components.lua index 369feb6..c11519a 100644 --- a/scripts/passes/components.lua +++ b/scripts/passes/components.lua @@ -159,6 +159,14 @@ local function extract_arg_names(args_str) return names end +local function formal_arg_names(args_str) + local names = extract_arg_names(args_str) + if not names then return nil end + if names[1] == "ab" then table.remove(names, 1) end + if #names == 0 then return nil end + return names +end + -- ════════════════════════════════════════════════════════════════════════════ -- Component projection (read from pre-scanned SourceScan) -- ════════════════════════════════════════════════════════════════════════════ @@ -197,6 +205,7 @@ local function project_components(source, scan) body_off = a.body_off, body_tokens = a.body_tokens, args = args, + arg_names = formal_arg_names(args), comment = comment, kind = a.kind, -- "comp_bare" | "comp_proc"; provenance emitter reads this. debug_skip = a.debug_skip == true, @@ -466,18 +475,9 @@ end --- @param args_str string|nil --- @return string local function signature_from_args(args_str) - local arg_names = extract_arg_names(args_str) - if arg_names and #arg_names > 0 then - -- Drop the leading `ab` (atom-builder) first arg if present. - -- Convention: `MipsAtomComp_Proc_` components always declare `ab` as the first function-arg - -- (type `MipsAtomBuilder_R`), mirroring the macro signature in `lottes_tape.h`. - if arg_names[1] == "ab" then - table.remove(arg_names, 1) - end - if #arg_names > 0 then - return table.concat(arg_names, ", ") - end - return "..." -- `ab` was the only arg; fall through to variadic + local names = formal_arg_names(args_str) + if names then + return table.concat(names, ", ") end return "..." end @@ -710,6 +710,7 @@ local function update_canonical_component_body_index(corpus, src, components, sc source = src.path, declaration = c.line, kind = c.kind, + arg_names = c.arg_names, } end end diff --git a/scripts/passes/emission_model.lua b/scripts/passes/emission_model.lua index 7d60291..c3a0248 100644 --- a/scripts/passes/emission_model.lua +++ b/scripts/passes/emission_model.lua @@ -155,8 +155,28 @@ local function project_atom(atom_record, src, corpus) local body = atom_record.body or "" local wc = corpus.word_counts or {} local cbi = corpus.component_body_index or {} + local schema = nil + if atom_record.reg_use_schema_name then + schema = corpus.reg_use_schemas and corpus.reg_use_schemas[atom_record.reg_use_schema_name] + end -- That construction site stamps `invocation.debug_skip` while appending each record to `proj.invocations`. - local proj = duffle.project_emission(body, cbi, wc, corpus.components) + local proj = duffle.project_emission(body, cbi, wc, corpus.components, { + reg_use_schema = schema, + reg_use_param = atom_record.reg_use_param_name, + atom_name = atom_record.name, + schema_name = atom_record.reg_use_schema_name, + }) + if atom_record.reg_use_schema_name and not schema then + proj.errors[#proj.errors + 1] = { + kind = "reguse_missing_schema", + msg = string.format("RegUse schema %q is missing", atom_record.reg_use_schema_name), + } + end + for _, err in ipairs(corpus.reg_use_errors or {}) do + if err.schema_name == atom_record.reg_use_schema_name then + proj.errors[#proj.errors + 1] = err + end + end local paths = { tokens = atom_record.body_tokens or {}, line_in_body = duffle.build_body_line_index(body), diff --git a/scripts/passes/scan_source.lua b/scripts/passes/scan_source.lua index b034ed5..29c986a 100644 --- a/scripts/passes/scan_source.lua +++ b/scripts/passes/scan_source.lua @@ -1416,13 +1416,49 @@ local function parse_mips_atom_proc(source, pos, ident_end, line_of, out) -- (`internal MipsAtom* X_proc(...)`), not from the first macro arg (which -- is now `aa`). The backward walk finds the function decl before open_paren -- and strips the `_proc` suffix. - local raw_name = duffle.find_atom_proc_decl_for(source, open_paren, MIPS_ATOM_PTR_LEN) + local raw_name, args_inner, func_ident = duffle.find_atom_proc_decl_for(source, open_paren, MIPS_ATOM_PTR_LEN) if not raw_name then raw_name = "?" end - local name = strip_ac_prefix(raw_name) + local name = strip_ac_prefix(raw_name) + local reg_use_schema_name = nil + local reg_use_param_name = nil + if args_inner then + local arg_tokens = duffle.split_top_level_commas(args_inner) + for _, tok in ipairs(arg_tokens) do + local trimmed = duffle.trim(tok) + local schema_suffix, param = trimmed:match("RegUse_([%w_]+)%s+([%w_]+)$") + if schema_suffix then + if reg_use_schema_name then + out.reg_use_errors[#out.reg_use_errors + 1] = { + kind = "reguse_multiple_params", + schema_name = "RegUse_" .. schema_suffix, + source_line = line_of(pos), + } + else + reg_use_schema_name = "RegUse_" .. schema_suffix + reg_use_param_name = param + end + end + end + end -- Position of body[1] in source = open_paren + 1 (start of inner) + last_brace_pos + 1 (past '{'). local body_off = open_paren + 2 + last_brace_pos register_atom(out, "atom_proc", line_of(pos), name, body, body_off, raw_name, pos, after_paren, source) + local entry = out.atoms[#out.atoms] + entry.reg_use_schema_name = reg_use_schema_name + entry.reg_use_param_name = reg_use_param_name + if reg_use_schema_name and func_ident then + local expected = "RegUse_" .. func_ident + if reg_use_schema_name ~= expected then + out.reg_use_errors[#out.reg_use_errors + 1] = { + kind = "reguse_name_mismatch", + schema_name = reg_use_schema_name, + func_ident = func_ident, + source_line = line_of(pos), + } + end + end + return after_paren end @@ -1530,6 +1566,171 @@ local function register_typedef_alias(underlying, name, pos, line_of, out) } end +local function parse_reg_use_schema_body(body) + local slots = {} + local alias_to_slot = {} + local slot_names = {} + local errors = {} + + local function add_alias(path, slot) + if alias_to_slot[path] then + errors[#errors + 1] = { kind = "reguse_duplicate_alias", path = path } + return false + end + alias_to_slot[path] = slot + return true + end + + local function add_slot(name, aliases, readonly) + if slot_names[name] then + errors[#errors + 1] = { kind = "reguse_duplicate_slot", name = name } + return nil + end + slot_names[name] = true + local slot = { name = name, aliases = aliases, readonly = readonly == true } + slots[#slots + 1] = slot + return slot + end + + local function parse_reg_names(text, pos) + local names = {} + while pos <= #text do + pos = duffle.skip_ws_and_cmt(text, pos) + local name, name_end = duffle.read_ident(text, pos) + if not name then return nil, pos end + names[#names + 1] = name + pos = duffle.skip_ws_and_cmt(text, name_end) + if text:sub(pos, pos) == "," then + pos = pos + 1 + else + break + end + end + if text:sub(pos, pos) == ";" then pos = pos + 1 end + return names, pos + end + + local pos = 1 + while pos <= #body do + pos = duffle.skip_ws_and_cmt(body, pos) + if pos > #body then break end + local first, first_end = duffle.read_ident(body, pos) + if not first then + pos = pos + 1 + goto continue + end + local after = duffle.skip_ws_and_cmt(body, first_end) + if first == "const" then + errors[#errors + 1] = { kind = "reguse_const_reg_spelling" } + return nil, errors + elseif first == "union" then + if body:sub(after, after) ~= "{" then + errors[#errors + 1] = { kind = "reguse_malformed" } + return nil, errors + end + local inner, after_braces = duffle.read_braces(body, after) + if not inner then + errors[#errors + 1] = { kind = "reguse_malformed" } + return nil, errors + end + local members = {} + local union_readonly = nil + local inner_pos = 1 + while inner_pos <= #inner do + inner_pos = duffle.skip_ws_and_cmt(inner, inner_pos) + if inner_pos > #inner then break end + local m_type, m_type_end = duffle.read_ident(inner, inner_pos) + if not m_type then + inner_pos = inner_pos + 1 + goto continue_inner + end + if m_type == "const" then + errors[#errors + 1] = { kind = "reguse_const_reg_spelling" } + return nil, errors + end + if m_type ~= "Reg" then + errors[#errors + 1] = { kind = "reguse_malformed" } + return nil, errors + end + local m_after = duffle.skip_ws_and_cmt(inner, m_type_end) + local m_readonly = false + local maybe_const, maybe_end = duffle.read_ident(inner, m_after) + if maybe_const == "const" then + m_readonly = true + m_after = duffle.skip_ws_and_cmt(inner, maybe_end) + end + if union_readonly == nil then + union_readonly = m_readonly + elseif union_readonly ~= m_readonly then + errors[#errors + 1] = { kind = "reguse_mixed_const" } + return nil, errors + end + local names, new_inner = parse_reg_names(inner, m_after) + if not names or #names == 0 then + errors[#errors + 1] = { kind = "reguse_malformed" } + return nil, errors + end + for _, n in ipairs(names) do members[#members + 1] = n end + inner_pos = new_inner + ::continue_inner:: + end + if #members == 0 then + errors[#errors + 1] = { kind = "reguse_malformed" } + return nil, errors + end + local after_close = duffle.skip_ws_and_cmt(body, after_braces) + local inst_name, inst_end = duffle.read_ident(body, after_close) + local aliases = {} + local slot_name + if inst_name then + slot_name = inst_name + for _, m in ipairs(members) do + local path = inst_name .. "." .. m + if not add_alias(path, slot_name) then return nil, errors end + aliases[#aliases + 1] = path + end + after_close = inst_end + else + slot_name = members[1] + for _, m in ipairs(members) do + if not add_alias(m, slot_name) then return nil, errors end + aliases[#aliases + 1] = m + end + end + if not add_slot(slot_name, aliases, union_readonly) then return nil, errors end + after_close = duffle.skip_ws_and_cmt(body, after_close) + if body:sub(after_close, after_close) == ";" then after_close = after_close + 1 end + pos = after_close + elseif first == "Reg" then + local readonly = false + local maybe_const, maybe_end = duffle.read_ident(body, after) + if maybe_const == "const" then + readonly = true + after = duffle.skip_ws_and_cmt(body, maybe_end) + end + local names, new_pos = parse_reg_names(body, after) + if not names or #names == 0 then + errors[#errors + 1] = { kind = "reguse_malformed" } + return nil, errors + end + for _, n in ipairs(names) do + if not add_alias(n, n) then return nil, errors end + if not add_slot(n, { n }, readonly) then return nil, errors end + end + pos = new_pos + else + errors[#errors + 1] = { kind = "reguse_malformed" } + return nil, errors + end + ::continue:: + end + if #slots == 0 then + errors[#errors + 1] = { kind = "reguse_malformed" } + return nil, errors + end + return { slots = slots, alias_to_slot = alias_to_slot }, errors +end + --- Parse: `typedef` declarations. --- --- Recognizes four shapes: @@ -1563,6 +1764,21 @@ local function parse_typedef_binds(source, pos, ident_end, line_of, out) local body, after_brace = find_body_braces(source, after_paren, open_paren + 1) if not body then return after_brace end register_struct_type(body, name, pos, line_of, out) + if name:sub(1, 7) == "RegUse_" then + local schema, schema_errors = parse_reg_use_schema_body(body) + if schema then + schema.name = name + schema.source_file = out._source_file + schema.source_line = line_of(pos) + out.reg_use_schemas[name] = schema + end + for _, err in ipairs(schema_errors or {}) do + err.schema_name = name + err.source_file = out._source_file + err.source_line = line_of(pos) + out.reg_use_errors[#out.reg_use_errors + 1] = err + end + end attach_debug_skip_marker(out, "unrelated") return after_brace @@ -1954,6 +2170,8 @@ local function scan_source(source, source_file, code_macros, code_macro_bodies) -- typedef chain walking (cycle-guarded, depth <= 8), and struct field sums. -- See `propagate_type_sizes()` below. type_name_registry = {}, + reg_use_schemas = {}, + reg_use_errors = {}, -- Shared `R_*_Code -> integer code` registry -- (passed in from M.run pass 1; same reference so preprocessor intercept writes are visible to the enum-value resolver). -- Stripped from `src.scan` before return. @@ -2186,13 +2404,15 @@ local function merge_corpus_registries(corpus) corpus.atom_auto_regs = corpus.atom_auto_regs or {} corpus.phase_auto_regs = corpus.phase_auto_regs or {} corpus.collisions = corpus.collisions or {} + corpus.reg_use_schemas = corpus.reg_use_schemas or {} + corpus.reg_use_errors = corpus.reg_use_errors or {} -- Replace the existing corpus collections with empty tables so a re-run on the same corpus produces identical state (deterministic merge). -- This is safe because M.run is the only writer to these tables within a single orchestrator invocation. for _, key in ipairs({ "register_alias_registry", "type_name_registry", "binds_by_name", "atoms_by_name", "atom_views", "atom_ctxs", "atom_phases", - "atom_infos", "collisions", + "atom_infos", "collisions", "reg_use_schemas", "reg_use_errors", }) do corpus[key] = {} end @@ -2285,6 +2505,15 @@ local function merge_corpus_registries(corpus) for _, info in ipairs(scan.atom_infos or {}) do corpus.atom_infos[#corpus.atom_infos + 1] = info end + + for name, schema in pairs(scan.reg_use_schemas or {}) do + if corpus.reg_use_schemas[name] == nil then + corpus.reg_use_schemas[name] = schema + end + end + for _, err in ipairs(scan.reg_use_errors or {}) do + corpus.reg_use_errors[#corpus.reg_use_errors + 1] = err + end end end end diff --git a/scripts/passes/static_analysis.lua b/scripts/passes/static_analysis.lua index 30c6bbd..a570c4e 100644 --- a/scripts/passes/static_analysis.lua +++ b/scripts/passes/static_analysis.lua @@ -451,6 +451,14 @@ local function is_cop2_consumer_of(consumer_event, destination, producer_rel) return false end +local function gpr_identity(event, pos) + local keys = event and event.gpr_keys + if keys and keys[pos] then return keys[pos] end + local arg = event and event.args and event.args[pos] + if type(arg) == "string" and arg:sub(1, 2) == "R_" then return arg end + return nil +end + -- True iff `consumer_event` reads the GPR operand at any position the destination register occupies. -- read_pos lookup consults `duffle.OPERAND_READ_POSITIONS` for the consumer's encoder and walks each `args[pos]` to find an operand-equal match. local function is_gpr_consumer_of(consumer_event, destination) @@ -458,9 +466,8 @@ local function is_gpr_consumer_of(consumer_event, destination) local read_pos = duffle.OPERAND_READ_POSITIONS or {} local positions = read_pos[consumer_token] if not positions then return false end - local args = consumer_event.args or {} for _, pos in ipairs(positions) do - if args[pos] == destination then return true end + if gpr_identity(consumer_event, pos) == destination then return true end end return false end @@ -552,6 +559,11 @@ local function is_gpr_operand(operand) return type(operand) == "string" and operand:sub(1, 2) == "R_" end +local function is_tracked_gpr(operand) + return is_gpr_operand(operand) + or (type(operand) == "string" and operand:sub(1, 7) == "reguse:") +end + local function constant_for_operand(gpr_values, operand) if operand == "R_0" then return 0 end local slot = is_gpr_operand(operand) and gpr_values[operand] or nil @@ -560,13 +572,13 @@ local function constant_for_operand(gpr_values, operand) end local function invalidate_gpr(gpr_values, operand) - if is_gpr_operand(operand) and operand ~= "R_0" then + if is_tracked_gpr(operand) and operand ~= "R_0" then gpr_values[operand] = { kind = "unknown" } end end local function store_gpr_constant(gpr_values, operand, value) - if not is_gpr_operand(operand) or operand == "R_0" then return end + if not is_tracked_gpr(operand) or operand == "R_0" then return end if value == nil then gpr_values[operand] = { kind = "unknown" } else gpr_values[operand] = { kind = "constant", value = wrap_u4(value) } end @@ -629,26 +641,31 @@ end -- Encoders without an explicit effect row conservatively invalidate every R_-prefixed operand. -- Recognized value rules are evaluated before their destination is invalidated. -- A failed/unknown evaluation writes `{kind = "unknown"}` instead. -local function apply_gpr_effects(ev_ident, ev_args, forward_state) +local function apply_gpr_effects(ev, forward_state) + local ev_ident = ev.encoder or ev.ident + local ev_args = ev.args or {} local gpr_values = forward_state.gpr_values local effects = duffle.INSTRUCTION_GPR_EFFECTS or {} local row = effects[ev_ident] if row == nil then - for _, operand in ipairs(ev_args or {}) do - invalidate_gpr(gpr_values, operand) + for pos, operand in ipairs(ev_args) do + local key = gpr_identity(ev, pos) or operand + if type(key) == "string" and (key:sub(1, 2) == "R_" or key:sub(1, 7) == "reguse:") then + if key ~= "R_0" then gpr_values[key] = { kind = "unknown" } end + end end return end local value_rule = (duffle.GPR_VALUE_RULES or {})[ev_ident] - local value = value_rule and evaluate_gpr_value_rule(value_rule, ev_args or {}, gpr_values) or nil + local value = value_rule and evaluate_gpr_value_rule(value_rule, ev_args, gpr_values) or nil for _, position in ipairs(row.writes or {}) do - local destination = ev_args and ev_args[position] - if is_gpr_operand(destination) then + local destination = gpr_identity(ev, position) + if destination then if value_rule and position == value_rule.dest and value ~= nil then store_gpr_constant(gpr_values, destination, value) else - invalidate_gpr(gpr_values, destination) + if destination ~= "R_0" then gpr_values[destination] = { kind = "unknown" } end end end end @@ -953,7 +970,7 @@ local function analyze_hardware_relations(atom) end -- ── 2. Apply GPR value effects. ── - apply_gpr_effects(ev_ident, ev_args, forward) + apply_gpr_effects(ev, forward) -- ── 3. Stage producers created by this event. ── local rows = rows_by_token[ev_ident] @@ -962,7 +979,7 @@ local function analyze_hardware_relations(atom) -- `stage = false` rows document a direction but do not create a later command-input producer (SWC2 and ordinary MTC0). if row.stage ~= false then local dest_arg = row.writes and row.writes.arg - local destination = dest_arg and ev_args[dest_arg] or nil + local destination = dest_arg and (gpr_identity(ev, dest_arg) or ev_args[dest_arg]) or nil if destination then -- Apply the destination_match filter when present. if row.destination_match and row.destination_match ~= destination then @@ -1352,7 +1369,7 @@ local function check_hazard_nop_use(atom, _pipe_ctx, findings) for _, row in ipairs(relations_table) do if row.token == ev_ident and row.stage ~= false then local dest_arg = row.writes and row.writes.arg - local destination = dest_arg and ev_args[dest_arg] or nil + local destination = dest_arg and (gpr_identity(ev, dest_arg) or ev_args[dest_arg]) or nil if destination and (not row.destination_match or row.destination_match == destination) then local required = row.visibility and row.visibility.required if required == nil and not (row.visibility and row.visibility.kind == "unknown_consumer") then @@ -1517,11 +1534,12 @@ local function check_load_delay_slots(atom, pipe_ctx, findings) -- Use `net_reads` to ignore RMW positions (write shadows read within the same instruction). if not is_load then for _, pos in ipairs(net_reads(event_ident, args)) do - local reg = args[pos] - if type(reg) == "string" and reg:sub(1, 2) == "R_" then + local reg = gpr_identity(event, pos) + if reg then local until_idx = volatile_until[reg] if until_idx and event_idx <= until_idx then local ev_line = line_for_word_event(event) + local authored = args[pos] or reg findings[#findings + 1] = { atom = atom.name, line = ev_line, @@ -1530,7 +1548,7 @@ local function check_load_delay_slots(atom, pipe_ctx, findings) msg = string.format("%s at line %d reads %s at word %d, but a prior load's " .. "delay slot is not over until word %d; insert a `nop` between the " .. "load and this instruction.", - atom.name, ev_line, reg, event_idx, until_idx), + atom.name, ev_line, authored, event_idx, until_idx), } end end @@ -1541,8 +1559,8 @@ local function check_load_delay_slots(atom, pipe_ctx, findings) local effect = gpr_effects[event_ident] if effect and effect.writes then for _, pos in ipairs(effect.writes) do - local reg = args[pos] - if type(reg) == "string" and reg:sub(1, 2) == "R_" then + local reg = gpr_identity(event, pos) + if reg then if is_load then -- Load: destination volatile for exactly 1 slot (the delay slot). volatile_until[reg] = event_idx + 1