More path collapse for metaprogram.

This commit is contained in:
ed
2026-08-18 16:37:53 -04:00
parent b345ccd60e
commit 1cbddc6708
6 changed files with 186 additions and 190 deletions
+17
View File
@@ -268,6 +268,13 @@ local function _project_emission_inner(root_body_entry, ctx_table)
if not reg_use_schema then
gpr_keys = nil
end
local isa = M.instr(encoder)
local isa_kind = isa and isa.kind or "unknown"
local nop_words = (encoder == "nop" and 1) or (encoder == "nop2" and 2) or 0
local is_yield = (encoder == "mac_yield" or encoder == "mac_yield_tail")
local gp0_shape = type(encoder) == "string"
and encoder:match("^mac_format_([%w_]+)_color$")
or nil
items[#items + 1] = {
kind = "word",
encoder = encoder,
@@ -280,6 +287,11 @@ local function _project_emission_inner(root_body_entry, ctx_table)
invocation_ids = inv_ids,
outermost_invocation_id = outermost,
gpr_keys = gpr_keys,
ident = encoder,
isa_kind = isa_kind,
nop_words = nop_words,
is_yield = is_yield,
gp0_shape = gp0_shape,
}
word_events[#word_events + 1] = {
i = word_idx,
@@ -293,6 +305,11 @@ local function _project_emission_inner(root_body_entry, ctx_table)
outermost_invocation_id = outermost,
word_count = 1,
gpr_keys = gpr_keys,
ident = encoder,
kind = isa_kind,
nop_words = nop_words,
is_yield = is_yield,
gp0_shape = gp0_shape,
}
word_idx = word_idx + 1
end
+10
View File
@@ -536,6 +536,7 @@ M.HARDWARE_RELATIONS = {
{
id = "mtc2_gpr_visibility",
semantic = "MTC2",
consumer = "cop2_input",
token = "gte_mv_to_data_r",
direction = "gpr_to_cop2_data",
reads = { domain = "gpr", arg = 1 },
@@ -554,6 +555,7 @@ M.HARDWARE_RELATIONS = {
{
id = "mtc2_irgb_visibility",
semantic = "MTC2",
consumer = "cop2_input",
token = "gte_mv_to_data_r",
direction = "gpr_to_cop2_data",
reads = { domain = "gpr", arg = 1 },
@@ -572,6 +574,7 @@ M.HARDWARE_RELATIONS = {
{
id = "ctc2_gpr_visibility",
semantic = "CTC2",
consumer = "cop2_input",
token = "gte_mv_to_ctrl_r",
direction = "gpr_to_cop2_control",
reads = { domain = "gpr", arg = 1 },
@@ -588,6 +591,7 @@ M.HARDWARE_RELATIONS = {
{
id = "mfc2_gpr_visibility",
semantic = "MFC2",
consumer = "gpr_read",
token = "gte_mv_from_data_r",
direction = "cop2_data_to_gpr",
reads = { domain = "cop2.data", arg = 2 },
@@ -603,6 +607,7 @@ M.HARDWARE_RELATIONS = {
{
id = "cfc2_gpr_visibility",
semantic = "CFC2",
consumer = "gpr_read",
token = "gte_mv_from_ctrl_r",
direction = "cop2_control_to_gpr",
reads = { domain = "cop2.ctrl", arg = 2 },
@@ -621,6 +626,7 @@ M.HARDWARE_RELATIONS = {
{
id = "mfc0_gpr_visibility",
semantic = "MFC0",
consumer = "gpr_read",
token = "sys_mov_from_cop0",
direction = "cop0_control_to_gpr",
reads = { domain = "cop0.ctrl", arg = 2 },
@@ -643,6 +649,7 @@ M.HARDWARE_RELATIONS = {
{
id = "lwc2_to_gte_command",
semantic = "LWC2_to_GTE",
consumer = "cop2_input",
token = "gte_lw",
direction = "memory_to_cop2_data",
reads = { domain = "memory", arg = 2 },
@@ -658,6 +665,7 @@ M.HARDWARE_RELATIONS = {
{
id = "lwc2_to_other_consumer",
semantic = "LWC2_to_other",
consumer = "cop2_input",
token = "gte_lw",
direction = "memory_to_cop2_data",
reads = { domain = "memory", arg = 2 },
@@ -675,6 +683,7 @@ M.HARDWARE_RELATIONS = {
{
id = "swc2_memory_write",
semantic = "SWC2",
consumer = "gpr_read",
token = "gte_sw",
direction = "cop2_data_to_memory",
reads = { domain = "cop2.data", arg = 1 },
@@ -692,6 +701,7 @@ M.HARDWARE_RELATIONS = {
{
id = "mtc0_cu2_visibility",
semantic = "MTC0",
consumer = "gpr_read",
token = "sys_mov_to_cop0",
direction = "gpr_to_cop0_status",
reads = { domain = "gpr", arg = 1 },
+1 -1
View File
@@ -942,7 +942,7 @@ function M.run(ctx)
aggregated_components[#aggregated_components + 1] = c
end
if #per_source > 0 then
metadata_per_source[src] = compute_components_metadata(per_source, duffle.INSTRUCTION_LATENCY)
metadata_per_source[src] = compute_components_metadata(per_source, {})
end
end
if #aggregated_components > 0 then
+99 -16
View File
@@ -104,6 +104,67 @@ local ABBREV_BIND_VAR_LOCLIST = 0x6D -- 109: DW_TAG_variable no children + DW_
-- (a pointer_type that carries DW_AT_byte_size + DW_AT_type), so gdb misparses our 4-byte ref4 as (byte_size, type[0..2]) and lands the cursor mid-attribute.
local ABBREV_TYPED_VIEW_POINTER = 0x6E -- 110: DW_TAG_pointer_type no children + DW_AT_type = ref4 (typed-view / U4 / void chain)
-- One row per DIE kind build_inserted_children emits.
-- attrs list form + the value key filled from the atom / registry / local table.
local DIE_SCHEMA = {
base_type = {
abbrev = ABBREV_BASE_TYPE,
attrs = {
{ form = "string", key = "name" },
{ form = "data1", key = "byte_size" },
{ form = "data1", key = "encoding" },
},
},
abstract_subprogram = {
abbrev = ABBREV_ABSTRACT_SUBPROGRAM,
attrs = {
{ form = "string", key = "name" },
{ form = "data1", key = "inline" },
{ form = "data1", key = "external" },
{ form = "udata", key = "decl_file" },
{ form = "udata", key = "decl_line" },
},
},
subprogram = {
abbrev = ABBREV_SUBPROGRAM,
attrs = {
{ form = "string", key = "name" },
{ form = "addr", key = "low_pc" },
{ form = "addr", key = "high_pc" },
{ form = "string", key = "linkage_name" },
},
},
variable = {
abbrev = ABBREV_VARIABLE,
attrs = {
{ form = "string", key = "name" },
{ form = "ref4", key = "type" },
{ form = "exprloc", key = "location" },
},
},
structure_type = {
abbrev = ABBREV_STRUCT_TYPE,
attrs = {
{ form = "string", key = "name" },
{ form = "data1", key = "byte_size" },
},
},
member = {
abbrev = ABBREV_MEMBER,
attrs = {
{ form = "string", key = "name" },
{ form = "data2", key = "data_member_location" },
{ form = "ref4", key = "type" },
},
},
pointer_type = {
abbrev = ABBREV_TYPED_VIEW_POINTER,
attrs = {
{ form = "ref4", key = "type" },
},
},
}
-- DWARF5 §7.7.3 loclist opcodes.
local DW_LLE_end_of_list = 0x00
local DW_LLE_start_length = 0x08
@@ -1581,6 +1642,26 @@ local function build_inserted_children(main_cu_offset, main_cu_end_excl, atom_ta
S.bytes[#S.bytes + 1] = s
S.next_offset = S.next_offset + #s
end
local function emit_die(schema_name, values)
local row = DIE_SCHEMA[schema_name]
emit(uleb128(row.abbrev))
for _, attr in ipairs(row.attrs) do
local v = values[attr.key]
if attr.form == "string" then
emit(v .. "\0")
elseif attr.form == "data1" then
emit(string.char(v))
elseif attr.form == "udata" then
emit(uleb128(v))
elseif attr.form == "addr" or attr.form == "ref4" then
emit(elf_dwarf.write_u32_le(v))
elseif attr.form == "data2" then
emit(elf_dwarf.write_u16_le(v))
elseif attr.form == "exprloc" then
emit(v)
end
end
end
local function ref4_of(section_offset)
if section_offset == nil then return 0 end
return section_offset - main_cu_offset
@@ -1589,10 +1670,11 @@ local function build_inserted_children(main_cu_offset, main_cu_end_excl, atom_ta
-- 1) Emit the base_type DIE first (member ref4s reference it).
local base_type_section_offset = S.next_offset
emit(uleb128(ABBREV_BASE_TYPE))
emit("unsigned int\0") -- DW_FORM_string (DW_AT_name)
emit(string.char(4)) -- DW_FORM_data1 (DW_AT_byte_size)
emit(string.char(DW_ATE_unsigned)) -- DW_FORM_data1 (DW_AT_encoding)
emit_die("base_type", {
name = "unsigned int",
byte_size = 4,
encoding = DW_ATE_unsigned,
})
-- The function body below reads S.next_offset directly via the `next_offset` function;
-- this keeps offsets synchronized with emitted data.
local function next_offset() return S.next_offset end
@@ -1847,13 +1929,13 @@ local function build_inserted_children(main_cu_offset, main_cu_end_excl, atom_ta
for _, comp_name in ipairs(sorted_comp_names) do
local def = component_defs[comp_name]
abstract_offsets[comp_name] = next_offset()
emit(uleb128(ABBREV_ABSTRACT_SUBPROGRAM))
emit("mac_" .. comp_name .. "\0") -- DW_FORM_string (DW_AT_name)
emit(string.char(DW_INL_inlined)) -- DW_FORM_data1 (DW_AT_inline)
emit(string.char(0x01)) -- DW_FORM_data1 (DW_AT_external=1)
-- decl_file + decl_line resolve the abstract origin back to its definition site even when no inlined_subroutine instance maps to it.
emit(uleb128(resolve_provenance_file_index(def.def_file))) -- DW_FORM_udata (DW_AT_decl_file)
emit(uleb128(def.def_line)) -- DW_FORM_udata (DW_AT_decl_line)
emit_die("abstract_subprogram", {
name = "mac_" .. comp_name,
inline = DW_INL_inlined,
external = 0x01,
decl_file = resolve_provenance_file_index(def.def_file),
decl_line = def.def_line,
})
end
-- 4) Emit per-atom DW_TAG_subprograms (children of main CU).
@@ -1861,11 +1943,12 @@ local function build_inserted_children(main_cu_offset, main_cu_end_excl, atom_ta
-- The gcc global `<name>[]` is a DW_TAG_variable without children; our subprogram has the wave-context var children.
-- gdb's symbol resolution picks our subprogram (it has low_pc/high_pc + children) over the gcc global for function-context lookups.
for _, atom in ipairs(atom_table) do
emit(uleb128(ABBREV_SUBPROGRAM))
emit(atom.name .. "\0") -- DW_FORM_string (DW_AT_name)
emit(elf_dwarf.write_u32_le(atom.addr))
emit(elf_dwarf.write_u32_le(atom.addr + atom.size_bytes))
emit(atom.name .. "\0") -- DW_FORM_string (DW_AT_linkage_name; same as DW_AT_name for non-mangled C)
emit_die("subprogram", {
name = atom.name,
low_pc = atom.addr,
high_pc = atom.addr + atom.size_bytes,
linkage_name = atom.name,
})
-- Per debug-visible R_ alias (filtered to GPR 0..31 in `by_alias`): DW_TAG_variable.
-- Precedence chain (per atom, per RR_<R_Name>):
-125
View File
@@ -2390,131 +2390,6 @@ local DECL_PARSERS = {
-- Only the bare `atom_dbg_skip` marker reaches `parse_dbg_skip_marker`.
-- Unknown identifiers follow the same unrelated-token path as every other unsupported source token.
local function tape_skip_ident(ident)
return ident and (DECL_FORMS[ident] or ident == "Struct_" or ident == "Enum_")
end
local function collect_addrs_assigns_REMOVED(text)
local addrs = {}
local pos = 1
local n = #text
while pos <= n do
pos = duffle.skip_ws_and_cmt(text, pos)
if pos > n then break end
local ident, ident_end = duffle.read_ident(text, pos)
if ident == "addrs" then
local after = duffle.skip_ws_and_cmt(text, ident_end)
if text:sub(after, after) == "[" then
local inner, after_br = duffle.read_brackets(text, after)
local idx = inner and tonumber(duffle.trim(inner))
after_br = duffle.skip_ws_and_cmt(text, after_br or after)
if idx and text:sub(after_br, after_br) == "=" then
local rhs = duffle.skip_ws_and_cmt(text, after_br + 1)
local rhs_ident = duffle.read_ident(text, rhs)
if rhs_ident then addrs[idx] = rhs_ident end
pos = rhs
else
pos = after_br or (after + 1)
end
else
pos = ident_end
end
elseif ident then
pos = ident_end
else
pos = pos + 1
end
end
return addrs
end
local function collect_tb_emits(body, addrs)
local names = {}
local pos = 1
local n = #body
while pos <= n do
pos = duffle.skip_ws_and_cmt(body, pos)
if pos > n then break end
local ident, ident_end = duffle.read_ident(body, pos)
if ident == "tb_emit_" or ident == "tb_emit" then
local after = duffle.skip_ws_and_cmt(body, ident_end)
if body:sub(after, after) == "(" then
local inner, after_p = duffle.read_parens(body, after)
local name
if ident == "tb_emit_" then
name = duffle.trim(inner or ""):match("^([%w_]+)")
else
local args = duffle.split_top_level_commas(inner or "")
local last = duffle.trim(args[#args] or "")
local idx = last:match("^addrs%s*%[%s*(%d+)%s*%]$")
if idx then
name = addrs[tonumber(idx)]
else
name = last:match("([%w_]+)$")
end
end
if name then names[#names + 1] = name end
pos = after_p or (after + 1)
else
pos = ident_end
end
elseif ident then
pos = ident_end
else
pos = pos + 1
end
end
return names
end
-- Linear appearance order of tb_emit / tb_emit_ in each C function body.
-- Commented-out emits are skipped by skip_ws_and_cmt. No C if/loop CFG.
local function scan_tape_chains(source)
local addrs = collect_addrs_assigns(source)
local chains = {}
local pos = 1
local n = #source
while pos <= n do
pos = duffle.skip_ws_and_cmt(source, pos)
if pos > n then break end
local ident, ident_end = duffle.read_ident(source, pos)
if tape_skip_ident(ident) then
local after = duffle.skip_ws_and_cmt(source, ident_end)
if source:sub(after, after) == "(" then
local _, after_p = duffle.read_parens(source, after)
after = duffle.skip_ws_and_cmt(source, after_p or after)
end
if source:sub(after, after) == "{" then
local _, after_b = duffle.read_braces(source, after)
pos = after_b or (after + 1)
else
pos = after
end
elseif ident then
local after = duffle.skip_ws_and_cmt(source, ident_end)
if source:sub(after, after) == "(" then
local _, after_p = duffle.read_parens(source, after)
after = duffle.skip_ws_and_cmt(source, after_p or after)
if source:sub(after, after) == "{" then
local body, after_b = duffle.read_braces(source, after)
local names = collect_tb_emits(body or "", addrs)
if #names > 0 then
chains[#chains + 1] = names
end
pos = after_b or (after + 1)
else
pos = after
end
else
pos = ident_end
end
else
pos = pos + 1
end
end
return chains
end
-- ════════════════════════════════════════════════════════════════════════════
-- The single source walker
-- ════════════════════════════════════════════════════════════════════════════
+58 -47
View File
@@ -245,7 +245,7 @@ local BRANCH_PATTERN = "^branch_[%w_]+%s*%("
-- This keeps `consuming_encoder` canonical for any downstream tooling that consults the metadata field.
local JUMP_REL_PATTERN = "^jump_rel%s*%("
local function classify_tokens(tokens)
local function tok_class_view(tokens)
local n = #tokens
local tc = {}
local nop_run = 0 -- running count of consecutive nop words (forward pass)
@@ -811,6 +811,7 @@ local function analyze_hardware_relations(atom)
pending = {},
cu2_state = "unobserved",
cu2_transition = nil,
c2_ctrl_writes = {},
_analysis_complete = false,
}
-- The architectural zero register is always a known U4 zero and cannot be invalidated by an emitted writer.
@@ -847,44 +848,62 @@ local function analyze_hardware_relations(atom)
-- Both operations are part of this one event walk.
stage_cu2_transition(ev_ident, ev_args, ev_word, ev_line, ev_source, forward)
consume_cu2_transition(atom, ev, ev_word, forward)
if ev_ident == "gte_mv_to_ctrl_r" then
forward.c2_ctrl_writes = forward.c2_ctrl_writes or {}
local alias = ev_args[2]
if type(alias) ~= "string" or not alias:match("^gte_cr_") then
alias = tostring(ev.call_text or ""):match("gte_cr_[%w_]+")
or tostring(ev.root_call_text or ""):match("gte_cr_[%w_]+")
end
local src = ev_args[1]
if type(src) == "string" then src = src:match("[%w_]+") end
if alias then
forward.c2_ctrl_writes[#forward.c2_ctrl_writes + 1] = {
alias = alias,
src = src,
line = ev_line or atom.line,
}
end
end
-- ── 1. Inspect pending relations against the event as CONSUMER. ──
-- Walk pending in REVERSE so `table.remove` doesn't shift indexes still to be inspected.
local CONSUMER = {
cop2_input = function(ev, prod)
local relation = prod.relation
if relation.id == "lwc2_to_gte_command" then
return is_gte_command(ev) and is_cop2_consumer_of(ev, prod.destination, relation)
end
if relation.id == "lwc2_to_other_consumer" then
return (not is_gte_command(ev)) and is_cop2_consumer_of(ev, prod.destination, relation)
end
return is_cop2_consumer_of(ev, prod.destination, relation)
end,
gpr_read = function(ev, prod)
return is_gpr_consumer_of(ev, prod.destination)
end,
overwrite_same_dest = function(ev, prod)
local ident = ev.encoder or ev.ident
local args = ev.args or {}
return (ident == "gte_mv_to_data_r" or ident == "gte_mv_to_ctrl_r")
and args[2] == prod.destination
end,
}
for pending_idx = #pending, 1, -1 do
local prod = pending[pending_idx]
local relation = prod.relation
local semantic = relation.semantic
local is_match = false
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).
-- 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)
elseif semantic == "command_latch" then
-- Consumer is a subsequent MTC2/CTC2 overwrite of the same C2 destination.
-- The semantic is the post-command latch direction (command -> register);
-- This is intentionally separate from the preceding MTC2 -> command relation.
is_match = (ev_ident == "gte_mv_to_data_r" or ev_ident == "gte_mv_to_ctrl_r")
and ev_args[1] ~= nil
and ev_args[2] == prod.destination
end
local match_fn = CONSUMER[relation.consumer]
local is_match = match_fn and match_fn(ev, prod)
if is_match then
local gap = ev_word - prod.word - 1
local required = prod.required
-- A following COP2 command waits at the transfer boundary.
-- Software nops are not required for that consumer class.
if is_gte_command(ev)
and (semantic == "MTC2" or semantic == "CTC2")
and relation.id ~= "mtc2_irgb_visibility" then
and relation.consumer == "cop2_input"
and relation.id ~= "mtc2_irgb_visibility"
and relation.id ~= "lwc2_to_gte_command"
and relation.id ~= "lwc2_to_other_consumer" then
required = 0
end
local unknown_visibility = relation.visibility and relation.visibility.kind == "unknown_consumer"
@@ -1049,6 +1068,7 @@ local function analyze_hardware_relations(atom)
relation = {
id = "command_latch_input",
semantic = "command_latch",
consumer = "overwrite_same_dest",
direction = "gte_command_to_cop2_register",
token = canonical,
evidence = {
@@ -1385,6 +1405,7 @@ local function check_hazard_nop_use(atom, _pipe_ctx, findings)
relation = {
id = "command_latch_input",
semantic = "command_latch",
consumer = "overwrite_same_dest",
},
destination = latch.register,
word = ev_word,
@@ -2098,7 +2119,7 @@ end
--- plus `mac_*` idents whose bare name is missing from `pipe_ctx.components_by_name` (i.e. no `MipsAtomComp_` for it).
local function analyze_atom_paths(atom, pipe_ctx)
local tokens = atom.paths.tokens or duffle.tokenize_body(atom.body)
local tc = atom.paths.tok_class or classify_tokens(tokens)
local tc = atom.paths.tok_class or tok_class_view(tokens)
local n = #tokens
-- Build label + branch maps from the pre-computed classification (no re-scan).
@@ -2554,6 +2575,8 @@ local function ctrl_alias_from_text(text)
end
local function ctrl_writes_in_atom(atom)
local fs = atom.paths and atom.paths.forward_state
if fs and fs.c2_ctrl_writes then return fs.c2_ctrl_writes end
local out = {}
for _, ev in ipairs((atom.paths and atom.paths.word_events) or {}) do
if (ev.encoder or "") == "gte_mv_to_ctrl_r" then
@@ -2572,18 +2595,6 @@ local function ctrl_writes_in_atom(atom)
end
end
end
if #out == 0 then
for _, t in ipairs((atom.paths and atom.paths.tokens) or {}) do
local tok = t.tok or ""
if tok:match("^gte_mv_to_ctrl_r") then
local alias = ctrl_alias_from_text(tok)
local src = tok:match("%(%s*([%w_]+)")
if alias then
out[#out + 1] = { alias = alias, src = src, line = atom.line }
end
end
end
end
return out
end
@@ -2994,16 +3005,16 @@ local function arg_as_gpr(arg)
return arg:match("^R_[%w_]+$")
end
local function collect_gpr_traffic(tokens)
local function collect_gpr_traffic(atom)
local reads, writes = {}, {}
for _, t in ipairs(tokens or {}) do
local tok = t.tok or t
local ident = (tok or ""):match("^([%w_]+)") or ""
local events = (atom and atom.paths and atom.paths.word_events) or {}
for _, ev in ipairs(events) do
local ident = ev.encoder or ev.ident or ""
if ident:sub(1, 4) ~= "mac_"
and not (duffle.DELAY_MARKERS and duffle.DELAY_MARKERS[ident])
and ident ~= "nop" and ident ~= "atom_label" and ident ~= "atom_offset"
then
local args = token_arg_list(tok)
local args = ev.args or {}
local fx = duffle.instr(ident)
if fx then
for _, pos in ipairs(fx.reads or {}) do
@@ -3057,7 +3068,7 @@ local function check_atom_calls_inferred_traffic(atom, pipe_ctx, findings)
if #(info.reads or {}) == 0 and #(info.writes or {}) == 0 then return end
local tokens = (atom.paths and atom.paths.tokens) or atom.body_tokens
local reads, writes = collect_gpr_traffic(tokens)
local reads, writes = collect_gpr_traffic(atom)
for _, t in ipairs(tokens or {}) do
local ident = ((t.tok or t) or ""):match("^([%w_]+)") or ""
if ident:sub(1, 4) == "mac_" then
@@ -3280,7 +3291,7 @@ local function validate(ctx, src, corpus_pipe_ctx)
-- `paths.tokens` / `paths.line_in_body` / `paths.items` / `paths.word_events` are populated by `passes/emission_model.lua`.
-- Supply tokens when no emission projection is present.
if a.paths.tokens == nil then a.paths.tokens = a.body_tokens end
a.paths.tok_class = classify_tokens(a.paths.tokens)
a.paths.tok_class = tok_class_view(a.paths.tokens)
-- analyze_atom_paths fills the *cycles / branches / has_loops / unknown_macros* fields of a.paths.
analyze_atom_paths(a, pipe_ctx)