mirror of
https://github.com/Ed94/pikuma_ps1.git
synced 2026-08-05 07:08:49 +00:00
Better static analysis for C0 <-> C2 data race hazards.
This commit is contained in:
@@ -0,0 +1,194 @@
|
||||
--- passes/emission_model.lua: Per-atom emission projection.
|
||||
---
|
||||
--- The `emission-model` pass owns `atom.paths` (the canonical per-atom mutable surface)
|
||||
--- for every atom-with-body and every raw atom-with-body declared in `ctx.shared.corpus.source_order`.
|
||||
--- For each such atom, the pass invokes `duffle.project_emission(body_text, component_index, word_counts)`
|
||||
--- and stores the ordered `items` stream plus the dense `word_events` / `markers` / `invocations` views on `atom.paths`.
|
||||
---
|
||||
--- 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` keeps build-stopping semantics (no policy change in this task).
|
||||
---
|
||||
--- Source-order discipline:
|
||||
--- * `corpus.source_order` is the canonical ordering of source records.
|
||||
--- * For each source, the pass iterates `src.scan.atoms` and `src.scan.raw_atoms` IN SOURCE ORDER, preserving declaration order.
|
||||
---
|
||||
--- Per-atom projection fields on `atom.paths`:
|
||||
--- `tokens`, `line_in_body`, `items`, `word_events`, `markers`, `invocations`, `errors`, `warnings`.
|
||||
--- The dense views are built from `items` only; the pass never re-walks source text or tokens.
|
||||
---
|
||||
--- Component expansion and construction validation:
|
||||
--- * known `mac_X(...)` calls recursively expand component bodies;
|
||||
--- * invocation records retain monotonic IDs, parent IDs, immediate call text, and the immutable outermost root call text;
|
||||
--- * component cycles retain balanced invocation boundaries and emit a `cycle` construction error without recursing indefinitely;
|
||||
--- * declared-vs-measured component word counts emit `count_mismatch` construction errors; opaque uncounted macros emit warnings.
|
||||
---
|
||||
--- The pass does NOT consult `_code_macros` / `_code_macro_bodies`. Those private tables are owned by `passes.scan_source` and stripped before this pass runs.
|
||||
|
||||
local M = {}
|
||||
|
||||
-- ─────────────────────────────────────────────────────────────────────────
|
||||
-- Bootstrap: load `duffle_paths.lua` via debug.getinfo so the module works standalone (run as `luajit passes/emission_model.lua`) and when require'd from the orchestrator.
|
||||
-- ─────────────────────────────────────────────────────────────────────────
|
||||
local _bootstrap_dir = debug.getinfo(1, "S").source:match("^@?(.*[/\\])") or "./"
|
||||
local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua")
|
||||
|
||||
-- ─────────────────────────────────────────────────────────────────────────
|
||||
-- Helpers
|
||||
-- ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
-- Convert the recursive walk's body-relative line numbers into physical source lines once.
|
||||
-- Consumers read these canonical fields rather than rebuilding line state or tokenizing source again.
|
||||
local function stamp_root_provenance(projection, atom_record, src, corpus)
|
||||
local root_line_of = src.scan and src.scan.line_of
|
||||
local root_body_line = root_line_of and root_line_of((atom_record.body_off or 1) - 1)
|
||||
or atom_record.line or 0
|
||||
local component_index = corpus.component_body_index or {}
|
||||
local word_items = {}
|
||||
|
||||
for _, item in ipairs(projection.items) do
|
||||
if item.kind == "word" then word_items[#word_items + 1] = item end
|
||||
end
|
||||
|
||||
local function body_line_for(event, item)
|
||||
local body_line_of = root_line_of
|
||||
local body_off = atom_record.body_off or 0
|
||||
local ids = event.invocation_ids or {}
|
||||
local inner_id = ids[#ids]
|
||||
local inner_inv = inner_id and projection.invocations[inner_id]
|
||||
if inner_inv then
|
||||
local component = component_index[inner_inv.component_name]
|
||||
if component and component.line_of then
|
||||
-- Component-body walkers already receive the declaration source's full line index, so their item.line is physical.
|
||||
return item.line or 0
|
||||
end
|
||||
end
|
||||
local first_line = body_line_of and body_line_of(math.max(1, body_off - 1)) or root_body_line
|
||||
return (first_line or 0) + (item.line or 1) - 1
|
||||
end
|
||||
|
||||
-- Stamp root-source path onto invocation records whose `call_path` was left empty by the walker.
|
||||
-- The walker passes `body_entry.source` to `emit_invoke_begin` as the call_path argument; for the root body_entry created by `M.project_emission` that source is ""
|
||||
-- (the caller passes only the body text).
|
||||
-- After this stamp every invocation record has a physical call_path that matches what `passes/atoms_source_map.lua` matches the in-memory provenance projection.
|
||||
local root_path = src.path or ""
|
||||
for _, inv in ipairs(projection.invocations) do
|
||||
if inv.call_path == nil or inv.call_path == "" then
|
||||
inv.call_path = root_path
|
||||
end
|
||||
end
|
||||
|
||||
for index, we in ipairs(projection.word_events) do
|
||||
local item = word_items[index] or {}
|
||||
local body_line = body_line_for(we, item)
|
||||
item.line = body_line
|
||||
we.body_line = body_line
|
||||
|
||||
local call_line = body_line
|
||||
local outer_id = we.outermost_invocation_id or 0
|
||||
local outer_inv = projection.invocations[outer_id]
|
||||
if outer_inv then call_line = (root_body_line or 0) + (outer_inv.call_line or 1) - 1 end
|
||||
we.call_line = call_line
|
||||
|
||||
if we.def_path == nil or we.def_path == "" then we.def_path = src.path or "" end
|
||||
if we.def_line == nil or we.def_line == 0 then we.def_line = atom_record.line or 0 end
|
||||
if we.call_path == nil or we.call_path == "" then we.call_path = src.path or "" end
|
||||
end
|
||||
end
|
||||
|
||||
-- Project one atom record into `atom.paths`.
|
||||
-- Mutates the atom record in-place and returns the projection (for pass-level error/warning accumulation).
|
||||
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 proj = duffle.project_emission(body, cbi, wc)
|
||||
local paths = {
|
||||
tokens = atom_record.body_tokens or {},
|
||||
line_in_body = duffle.build_body_line_index(body),
|
||||
items = proj.items,
|
||||
word_events = proj.word_events,
|
||||
markers = proj.markers,
|
||||
invocations = proj.invocations,
|
||||
errors = proj.errors,
|
||||
warnings = proj.warnings,
|
||||
}
|
||||
stamp_root_provenance(proj, atom_record, src, corpus)
|
||||
atom_record.paths = paths
|
||||
return proj
|
||||
end
|
||||
|
||||
-- ─────────────────────────────────────────────────────────────────────────
|
||||
-- Run the emission-model pass.
|
||||
-- ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
--- @param ctx PassCtx -- { shared = { corpus = ... }, out_root, dry_run, ... }
|
||||
--- @return PassResult
|
||||
function M.run(ctx)
|
||||
local outputs = {}
|
||||
local errors = {}
|
||||
local warnings = {}
|
||||
|
||||
local corpus = ctx and ctx.shared and ctx.shared.corpus
|
||||
if type(corpus) ~= "table" then error("emission_model: ctx.shared.corpus is required (canonical projection)", 0) end
|
||||
if type(corpus.source_order) ~= "table" then error("emission_model: ctx.shared.corpus.source_order is required", 0) end
|
||||
|
||||
-- Walk every source in canonical source order; for each source, iterate atoms.
|
||||
-- Atom declarations (`kind == "atom"` / `"raw_atom"`) receive the canonical `atom.paths` projection; component declarations
|
||||
-- (`comp_bare` / `comp_proc`) are recursively expanded by atom projections and do not get an independent projection themselves.
|
||||
-- Test-only fixtures that need per-component word events may still consume `duffle.expand_word_events`
|
||||
-- (which remains available; emission-model owns the canonical per-atom projection).
|
||||
for _, src in ipairs(corpus.source_order) do
|
||||
local scan = src.scan or {}
|
||||
for _, atom in ipairs(scan.atoms or {}) do
|
||||
if atom and atom.body and (atom.kind == "atom" or atom.kind == "raw_atom") then
|
||||
local proj = project_atom(atom, src, corpus)
|
||||
for _, e in ipairs(proj.errors) do
|
||||
-- Preserve `kind` (cycle / count_mismatch / unbalanced) so readers can dispatch on the diagnostic class without re-parsing the message string.
|
||||
errors[#errors + 1] = {
|
||||
kind = e.kind,
|
||||
line = e.line,
|
||||
msg = e.msg,
|
||||
source = e.source or src.path,
|
||||
}
|
||||
end
|
||||
for _, w in ipairs(proj.warnings) do
|
||||
warnings[#warnings + 1] = {
|
||||
kind = w.kind,
|
||||
line = w.line,
|
||||
msg = w.msg,
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
for _, atom in ipairs(scan.raw_atoms or {}) do
|
||||
if atom and atom.body then
|
||||
local proj = project_atom(atom, src, corpus)
|
||||
for _, e in ipairs(proj.errors) do
|
||||
errors[#errors + 1] = {
|
||||
kind = e.kind,
|
||||
line = e.line,
|
||||
msg = e.msg,
|
||||
source = e.source or src.path,
|
||||
}
|
||||
end
|
||||
for _, w in ipairs(proj.warnings) do
|
||||
warnings[#warnings + 1] = {
|
||||
kind = w.kind,
|
||||
line = w.line,
|
||||
msg = w.msg,
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return {
|
||||
outputs = outputs,
|
||||
errors = errors,
|
||||
warnings = warnings,
|
||||
}
|
||||
end
|
||||
|
||||
return M
|
||||
Reference in New Issue
Block a user