build_noramlized_v3s4 mostly reviewed.

This commit is contained in:
ed
2026-08-18 16:10:29 -04:00
parent 290bb0e07a
commit bbda5efaea
12 changed files with 382 additions and 170 deletions
+8 -1
View File
@@ -550,7 +550,14 @@ local function _project_emission_inner(root_body_entry, ctx_table)
local tok_line = line_of(body_off + bt.rel) or 0
if M.DELAY_MARKERS[ident] then
emit_marker("delay", ident, nil, tok_line)
local rest = M.trim(tok:sub(after or (#tok + 1)))
local rest = tok:sub(after or (#tok + 1))
while true do
rest = M.trim(rest)
if rest:sub(1, 2) ~= "/*" then break end
local close = rest:find("*/", 3, true)
if not close then rest = ""; break end
rest = rest:sub(close + 2)
end
if rest ~= "" then
process_token({ tok = rest, rel = bt.rel })
end
+42 -12
View File
@@ -329,6 +329,24 @@ local function strip_mac_prefix(ident)
return ident
end
--- Strip a leading delay marker (`LdSlot_` / `BdSlot_` / `GteDelay_` / `DmaSlot_`)
--- plus following whitespace and block comments. Returns the remainder, or ""
--- when the token is only the marker.
--- `BdSlot_ nop` becomes `nop`. Bare `LdSlot_` becomes "".
--- @param tok string
--- @return string
local function strip_leading_delay_marker(tok)
local ident = duffle.read_ident(tok, 1)
if not ident or not duffle.DELAY_MARKERS[ident] then return tok end
local rest = tok:sub(#ident + 1):match("^%s*(.*)$") or ""
while rest:sub(1, 2) == "/*" do
local close = rest:find("*/", 3, true)
if not close then return "" end
rest = rest:sub(close + 2):match("^%s*(.*)$") or ""
end
return rest
end
--- (internal) Recursive word-count lookup. `cache` is the memoization table shared across all components
--- in a single source's `count_all_components` pass; the in-progress -1 sentinel detects cycles (A -> B -> A).
--- @param name string -- the component name (without `mac_`)
@@ -347,18 +365,30 @@ local function word_count_rec(name, comp_by_name, wc, cache)
for _, t in ipairs(tokens) do
local trimmed = t.tok
if trimmed ~= "" then
local lookup = strip_mac_prefix(duffle.read_ident(trimmed, 1))
if lookup == "atom_label" or lookup == "atom_offset" then
-- Pure metaprogram anchors; emit zero words.
elseif lookup and comp_by_name[lookup] then
-- It's a `mac_X(...)` call. Recurse.
n = n + word_count_rec(lookup, comp_by_name, wc, cache)
elseif lookup and wc and wc[lookup] then
-- Encoding macro or pseudo-instruction (e.g. mask_upper = 2, nop2 = 2).
n = n + wc[lookup]
else
-- Unrecognized token. Fall back to 1 word.
n = n + 1
local work = trimmed
while true do
local marker = duffle.read_ident(work, 1)
if marker and duffle.DELAY_MARKERS[marker] then
work = strip_leading_delay_marker(work)
if work == "" then break end
else
break
end
end
if work ~= "" then
local lookup = strip_mac_prefix(duffle.read_ident(work, 1))
if lookup == "atom_label" or lookup == "atom_offset" then
-- Pure metaprogram anchors; emit zero words.
elseif lookup and comp_by_name[lookup] then
-- It's a `mac_X(...)` call. Recurse.
n = n + word_count_rec(lookup, comp_by_name, wc, cache)
elseif lookup and wc and wc[lookup] then
-- Encoding macro or pseudo-instruction (e.g. mask_upper = 2, nop2 = 2).
n = n + wc[lookup]
else
-- Unrecognized token. Fall back to 1 word.
n = n + 1
end
end
end
end
+1 -1
View File
@@ -7,7 +7,7 @@
--- Public boundary:
--- * `M.run(ctx)` is the only entry point.
--- * The pass returns `{outputs = {}, errors = ..., warnings = ...}`.
--- Pass kind = `validation` → `PASS_KIND_STOP_ON_ERROR.validation` preserves the existing build-stopping policy.
--- Pass kind = `validation`. Findings record on the result; the orchestrator does not exit non-zero.
---
--- Source-order discipline:
--- * `corpus.source_order` sets the source-record order.
+30 -23
View File
@@ -129,32 +129,38 @@ end
--- This preserves backward compatibility for any top-level marker that may exist outside a control-transfer instruction.
--- @param labels table<string, integer>
--- @param branches table[]
--- @param errors table[]
--- @return BranchOffset[]
local function compute_offsets(labels, branches)
local function compute_offsets(labels, branches, errors)
local results = {}
for _, br in ipairs(branches) do
local target = labels[br.target]
if not target then
error("Branch target '" .. br.target .. "' has no atom_label (at word " .. br.branch_word .. ")")
errors[#errors + 1] = {
line = br.line or 0,
msg = "Branch target '" .. br.target .. "' has no atom_label (at word " .. br.branch_word .. ")",
}
else
local consuming = br.consuming_encoder
if consuming == "jump_reg" or consuming == "call_reg" or consuming == "jump_link" then
errors[#errors + 1] = {
line = br.line or 0,
msg = "atom_offset cannot be used with " .. consuming
.. " (register-form jumps have no offset field); at word " .. br.branch_word,
}
else
-- All other consuming instructions (including `branch_*`, `jump`, `call_addr`, and nil for top-level markers) use the same relative offset value.
-- The MIPS encoding differs per opcode but the duffle `enc_i` macro handles the truncation to the immediate-field width.
results[#results + 1] = {
target = br.target,
tag = br.tag,
branch_word = br.branch_word,
offset = target - br.branch_word - 1,
consuming_encoder = br.consuming_encoder,
consuming_arg_pos = br.consuming_arg_pos,
}
end
end
local consuming = br.consuming_encoder
local offset
if consuming == "jump_reg" or consuming == "call_reg" or consuming == "jump_link" then
-- Register-form jumps have no offset field. `atom_offset` cannot be used here.
error("atom_offset cannot be used with " .. consuming
.. " (register-form jumps have no offset field); at word " .. br.branch_word)
end
-- All other consuming instructions (including `branch_*`, `jump`, `call_addr`, and nil for top-level markers) use the same relative offset value.
-- The MIPS encoding differs per opcode but the duffle `enc_i` macro handles the truncation to the immediate-field width.
offset = target - br.branch_word - 1
results[#results + 1] = {
target = br.target,
tag = br.tag,
branch_word = br.branch_word,
offset = offset,
consuming_encoder = br.consuming_encoder,
consuming_arg_pos = br.consuming_arg_pos,
}
end
return results
end
@@ -237,8 +243,9 @@ local M = {}
--- @param ctx PassCtx
--- @param dir string -- the absolute source directory
--- @param sources SourceFile[] -- sources in this directory
--- @param errors table[]
--- @return string|nil -- the offsets_h path
local function process_directory(ctx, dir, sources)
local function process_directory(ctx, dir, sources, errors)
local atoms_data = {}
local function append_atom(atom)
@@ -248,7 +255,7 @@ local function process_directory(ctx, dir, sources)
atoms_data[#atoms_data + 1] = {
name = atom.raw_name or atom.name,
total_words = #(paths.word_events or {}),
offsets = compute_offsets(labels, branches),
offsets = compute_offsets(labels, branches, errors),
}
end
@@ -286,7 +293,7 @@ function M.run(ctx)
-- Per-directory aggregation: every source in the same directory contributes to one `gen/offsets.h`.
local sources_by_dir = corpus.sources_by_dir or duffle.group_sources_by_dir(corpus.source_order)
for dir, sources in pairs(sources_by_dir) do
local out_path = process_directory(ctx, dir, sources)
local out_path = process_directory(ctx, dir, sources, errors)
if out_path then
outputs[#outputs + 1] = { offsets_h = out_path }
end
+225 -41
View File
@@ -1674,9 +1674,20 @@ parse_reg_use_schema_body = function(body, type_registry, opts)
errors[#errors + 1] = { kind = "reguse_malformed" }
return nil, errors
end
local members = {}
local views = {}
local union_readonly = nil
local inner_pos = 1
local function note_readonly(flag)
if union_readonly == nil then
union_readonly = flag
elseif union_readonly ~= flag then
errors[#errors + 1] = { kind = "reguse_mixed_const" }
return false
end
return true
end
while inner_pos <= #inner do
inner_pos = duffle.skip_ws_and_cmt(inner, inner_pos)
if inner_pos > #inner then break end
@@ -1689,56 +1700,228 @@ parse_reg_use_schema_body = function(body, type_registry, opts)
errors[#errors + 1] = { kind = "reguse_const_reg_spelling" }
return nil, errors
end
if m_type ~= "Reg" then
if m_type == "Reg_" then
local after_ty = duffle.skip_ws_and_cmt(inner, m_type_end)
if inner:sub(after_ty, after_ty) ~= "(" then
errors[#errors + 1] = { kind = "reguse_malformed" }
return nil, errors
end
local type_inner, after_paren = duffle.read_parens(inner, after_ty)
if not type_inner then
errors[#errors + 1] = { kind = "reguse_malformed" }
return nil, errors
end
local typed_fields = fields_for_reg_type(duffle.trim(type_inner), type_registry)
after_paren = duffle.skip_ws_and_cmt(inner, after_paren)
local inst_names, new_inner = parse_reg_names(inner, after_paren)
if not inst_names or #inst_names == 0 then
errors[#errors + 1] = { kind = "reguse_malformed" }
return nil, errors
end
if not note_readonly(false) then return nil, errors end
if not typed_fields then
if require_types then
errors[#errors + 1] = { kind = "reguse_unknown_reg_type", type_name = duffle.trim(type_inner) }
return nil, errors
end
pending = true
else
for _, inst in ipairs(inst_names) do
local names = {}
for _, field in ipairs(typed_fields) do
names[#names + 1] = inst .. "." .. field
end
views[#views + 1] = { names = names, lanes = true }
end
end
inner_pos = new_inner
elseif m_type == "struct" then
local after_struct = duffle.skip_ws_and_cmt(inner, m_type_end)
if inner:sub(after_struct, after_struct) ~= "{" then
local _, tag_end = duffle.read_ident(inner, after_struct)
if not tag_end then
errors[#errors + 1] = { kind = "reguse_malformed" }
return nil, errors
end
after_struct = duffle.skip_ws_and_cmt(inner, tag_end)
end
if inner:sub(after_struct, after_struct) ~= "{" then
errors[#errors + 1] = { kind = "reguse_malformed" }
return nil, errors
end
local struct_inner, after_struct_braces = duffle.read_braces(inner, after_struct)
if not struct_inner then
errors[#errors + 1] = { kind = "reguse_malformed" }
return nil, errors
end
local lane_names = {}
local s_pos = 1
while s_pos <= #struct_inner do
s_pos = duffle.skip_ws_and_cmt(struct_inner, s_pos)
if s_pos > #struct_inner then break end
local s_ty, s_ty_end = duffle.read_ident(struct_inner, s_pos)
if not s_ty then
s_pos = s_pos + 1
goto continue_struct
end
if s_ty == "Reg_" then
local after_ty = duffle.skip_ws_and_cmt(struct_inner, s_ty_end)
if struct_inner:sub(after_ty, after_ty) ~= "(" then
errors[#errors + 1] = { kind = "reguse_malformed" }
return nil, errors
end
local type_inner, after_paren = duffle.read_parens(struct_inner, after_ty)
if not type_inner then
errors[#errors + 1] = { kind = "reguse_malformed" }
return nil, errors
end
local typed_fields = fields_for_reg_type(duffle.trim(type_inner), type_registry)
after_paren = duffle.skip_ws_and_cmt(struct_inner, after_paren)
local inst_names, new_s = parse_reg_names(struct_inner, after_paren)
if not inst_names or #inst_names == 0 then
errors[#errors + 1] = { kind = "reguse_malformed" }
return nil, errors
end
if not note_readonly(false) then return nil, errors end
if not typed_fields then
if require_types then
errors[#errors + 1] = { kind = "reguse_unknown_reg_type", type_name = duffle.trim(type_inner) }
return nil, errors
end
pending = true
else
for _, inst in ipairs(inst_names) do
for _, field in ipairs(typed_fields) do
lane_names[#lane_names + 1] = inst .. "." .. field
end
end
end
s_pos = new_s
elseif s_ty == "Reg" then
local s_after = duffle.skip_ws_and_cmt(struct_inner, s_ty_end)
local s_readonly = false
local maybe_const, maybe_end = duffle.read_ident(struct_inner, s_after)
if maybe_const == "const" then
s_readonly = true
s_after = duffle.skip_ws_and_cmt(struct_inner, maybe_end)
end
if not note_readonly(s_readonly) then return nil, errors end
local names, new_s = parse_reg_names(struct_inner, s_after)
if not names or #names == 0 then
errors[#errors + 1] = { kind = "reguse_malformed" }
return nil, errors
end
for _, n in ipairs(names) do lane_names[#lane_names + 1] = n end
s_pos = new_s
else
errors[#errors + 1] = { kind = "reguse_malformed" }
return nil, errors
end
::continue_struct::
end
if #lane_names == 0 and not pending then
errors[#errors + 1] = { kind = "reguse_malformed" }
return nil, errors
end
if #lane_names > 0 then
views[#views + 1] = { names = lane_names, lanes = true }
end
inner_pos = duffle.skip_ws_and_cmt(inner, after_struct_braces)
if inner:sub(inner_pos, inner_pos) == ";" then inner_pos = inner_pos + 1 end
elseif m_type == "Reg" then
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 not note_readonly(m_readonly) then 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
views[#views + 1] = { names = names, lanes = false }
inner_pos = new_inner
else
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
if #views == 0 then
if not pending then
errors[#errors + 1] = { kind = "reguse_malformed" }
return nil, errors
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
local has_lanes = false
for _, v in ipairs(views) do
if v.lanes then has_lanes = true end
end
if not add_slot(slot_name, aliases, union_readonly) then return nil, errors end
if has_lanes then
local width = nil
for _, v in ipairs(views) do
if not v.lanes then
errors[#errors + 1] = { kind = "reguse_malformed" }
return nil, errors
end
if width == nil then
width = #v.names
elseif #v.names ~= width then
errors[#errors + 1] = { kind = "reguse_union_width" }
return nil, errors
end
end
if width then
for i = 1, width do
local slot_name = views[1].names[i]
if inst_name then slot_name = inst_name .. "." .. slot_name end
local aliases = {}
for _, v in ipairs(views) do
local n = v.names[i]
if inst_name then n = inst_name .. "." .. n end
if not add_alias(n, slot_name) then return nil, errors end
aliases[#aliases + 1] = n
end
if not add_slot(slot_name, aliases, union_readonly) then return nil, errors end
end
end
else
local members = {}
for _, v in ipairs(views) do
for _, n in ipairs(v.names) do members[#members + 1] = n end
end
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
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
end
end
if inst_name then after_close = inst_end 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
@@ -2801,6 +2984,7 @@ local SCHEMA_BODY_ERROR = {
reguse_duplicate_slot = true,
reguse_const_reg_spelling = true,
reguse_mixed_const = true,
reguse_union_width = true,
}
-- Re-parse every RegUse_* body against the merged type_name_registry.
+12 -4
View File
@@ -879,6 +879,14 @@ local function analyze_hardware_relations(atom)
end
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
required = 0
end
local unknown_visibility = relation.visibility and relation.visibility.kind == "unknown_consumer"
if unknown_visibility then
hazards[#hazards + 1] = {
@@ -906,7 +914,7 @@ local function analyze_hardware_relations(atom)
, (relation.evidence and relation.evidence.confidence or "unknown")
),
}
elseif prod.required ~= nil and gap < prod.required then
elseif required ~= nil and gap < required then
local payload = {
check = "transfer_hazards",
kind = prod.violation_kind or "error",
@@ -923,7 +931,7 @@ local function analyze_hardware_relations(atom)
consumer_word = ev_word,
consumer_token = ev_ident,
gap = gap,
required = prod.required,
required = required,
evidence_confidence = relation.evidence and relation.evidence.confidence or "unknown",
evidence_source = relation.evidence and relation.evidence.source or "",
msg = string.format("%s at line %d: %s relation %s (producer %s at word %d, %s:%d) violated: consumer at word %d (gap=%d, required=%d) [%s]"
@@ -941,7 +949,7 @@ local function analyze_hardware_relations(atom)
hazards[#hazards + 1] = payload
end
local satisfied = nil
if not unknown_visibility then satisfied = gap >= prod.required end
if not unknown_visibility then satisfied = gap >= required end
-- Record the relation touch on `paths.relations` even when the gap is satisfied.
-- Unknown relations are informational, not numeric pass/fail measurements.
relations[#relations + 1] = {
@@ -951,7 +959,7 @@ local function analyze_hardware_relations(atom)
consumer_word = ev_word,
destination = prod.destination,
gap = gap,
required = prod.required,
required = required,
satisfied = satisfied,
}
table.remove(pending, pending_idx)
+10 -14
View File
@@ -142,8 +142,7 @@ local PASSES = {
},
["static-analysis"] = {
module = "passes.static_analysis",
-- "diagnostic" — every `error`/`warning` finding is written to the report file;
-- The orchestrator does NOT exit non-zero on these findings (see PASS_KIND_STOP_ON_ERROR).
-- "diagnostic" — every `error`/`warning` finding is written to the report file.
-- Report severity is independent from process exit policy.
kind = "diagnostic",
deps = {"scan-source", "word-counts", "components", "emission-model"},
@@ -206,16 +205,13 @@ local function request_roots_for_group(args, group_name)
end
end
-- Pass-kind taxonomy: Which kinds stop the build on errors?
--
-- Pass-kind taxonomy: findings always print. No pass kind stops the build.
-- Report severity is independent from process exit policy.
-- A "diagnostic" pass still writes every `error`/`warning` finding into its report file,
-- but `report_validation_errors` returns early for non-stopping kinds, so nothing is printed to stderr and the orchestrator does not exit non-zero.
-- Adding a new pass kind requires listing it here explicitly; An unknown kind must not silently fall back to "true".
-- Adding a new pass kind requires listing it here explicitly; an unknown kind must not silently fall back to "true".
local PASS_KIND_STOP_ON_ERROR = {
["shared"] = false,
["header-output"] = true,
["validation"] = true,
["header-output"] = false,
["validation"] = false,
["diagnostic"] = false,
["report"] = false,
}
@@ -693,19 +689,19 @@ end
-- Main Orchestrator
-- ════════════════════════════════════════════════════════════════════════════
--- (internal) If the pass's kind is in PASS_KIND_STOP_ON_ERROR and it reported errors, write each error to stderr.
--- Returns true if any validation errors were reported.
--- (internal) Write every pass error to stderr.
--- Returns true only when the pass kind still stops the build.
--- @param pass_name string
--- @param pass PassDescriptor
--- @param result PassResult
--- @return boolean
local function report_validation_errors(pass_name, pass, result)
local has_errors = result.errors and #result.errors > 0
if not (has_errors and PASS_KIND_STOP_ON_ERROR[pass.kind]) then return false end
local has_errors = result.errors and #result.errors > 0
if not has_errors then return false end
for _, e in ipairs(result.errors) do
io.stderr:write(string.format("[%s] line %d: %s\n", pass_name, e.line or 0, e.msg or ""))
end
return true
return PASS_KIND_STOP_ON_ERROR[pass.kind] == true
end
--- (internal) Run each pass in `order` in topological sequence.