TODO: need to review snapshot

This commit is contained in:
2026-07-14 12:16:00 -04:00
parent 2d901003f9
commit 7d5b13aadb
14 changed files with 2461 additions and 124 deletions
+655
View File
@@ -21,6 +21,78 @@ local lfs = require("lfs")
local M = {}
-- ════════════════════════════════════════════════════════════════════════════
-- DWARF tag + form constants
-- ════════════════════════════════════════════
-- (DWARF5 §7.5.5 "Tag Encodings" + Table 7.1; gcc emits these exact values for
-- the DWARF3-extension and DWARF5 line units.)
M.DW_TAG = {
compile_unit = 0x11,
subprogram = 0x2E,
variable = 0x34,
structure_type = 0x13,
member = 0x0D,
base_type = 0x24,
typedef = 0x2A,
pointer_type = 0x0F,
const_type = 0x26,
volatile_type = 0x27,
inlined_subroutine = 0x1D,
-- We index the canonical gcc-emitted tags. Anything else falls through.
}
M.DW_AT = {
name = 0x03,
low_pc = 0x11,
high_pc = 0x12,
language = 0x13,
location = 0x02,
comp_dir = 0x1B,
byte_size = 0x0B,
encoding = 0x3E,
data_member_location = 0x38,
type = 0x49,
linkage_name = 0x6E,
external = 0x3F,
abstract_origin = 0x31,
call_file = 0x58,
call_line = 0x59,
inline = 0x20,
decl_file = 0x3A,
decl_line = 0x3B,
}
M.DW_FORM = {
addr = 0x01,
data1 = 0x0B,
data2 = 0x05,
data4 = 0x06,
string = 0x08,
strp = 0x0E,
exprloc = 0x18,
ref4 = 0x13,
udata = 0x0F,
ref_sig8 = 0x20,
implicit_const = 0x21,
flag_present = 0x19,
sec_offset = 0x17,
}
M.DW_ATE = {
address = 0x01,
boolean = 0x02,
complex_float = 0x03,
float = 0x04,
signed = 0x05,
signed_char = 0x06,
unsigned = 0x07,
unsigned_char = 0x08,
}
-- DWARF5 §7.5.6 DW_FORM_implicit_const
local DW_FORM_implicit_const = 0x21
-- ════════════════════════════════════════════════════════════════════════════
-- Format-constant tables
-- ════════════════════════════════════════════════════════════════════════════
@@ -121,6 +193,7 @@ M.DWARF_LINE_OPS = {
DW_LNS_advance_pc = 2,
DW_LNS_advance_line = 3,
DW_LNS_set_file = 4,
DW_LNS_negate_stmt = 6, -- spec: §6.2.5.2 — toggle the line-state is_stmt register
-- Extended sub-opcodes (§6.2.5.3)
DW_LNE_end_sequence = 1, -- spec: §6.2.5.3
DW_LNE_set_address = 2, -- spec: §6.2.5.3
@@ -171,6 +244,500 @@ function M.read_u16_le(buf, off)
return buf:byte(off) + buf:byte(off + 0x01) * 0x00000100
end
-- Pure-Lua 5.3 LEB128 readers (no `bit` library). `2^shift` arithmetic matches
-- the existing F' parser. Offsets are 0-based; returns (value, next_pos).
local function read_uleb128_at(buf, pos)
local value, shift = 0, 0
local len = #buf
while pos < len do
local b = buf:byte(pos + 1)
value = value + (b % 0x80) * (2 ^ shift)
shift = shift + 7
pos = pos + 1
if b < 0x80 then return value, pos end
end
return nil, pos
end
local function read_sleb128_at(buf, pos)
local value, shift = 0, 0
local len = #buf
while pos < len do
local b = buf:byte(pos + 1)
value = value + (b % 0x80) * (2 ^ shift)
shift = shift + 7
pos = pos + 1
if b < 0x80 then
if b >= 0x40 then value = value - (2 ^ shift) end
return value, pos
end
end
return nil, pos
end
-- Find the 0-based offset of the table-terminator byte (a single 0) for the
-- abbrev table starting at `table_start`. Returns nil on truncated input.
-- Walks declaration headers (code, tag, has_children, attr/form pairs,
-- DW_FORM_implicit_const constant) until it finds a 0 byte that follows a
-- complete declaration. Mirrors dwarf_injection.lua :: find_abbrev_table_end.
local function find_abbrev_table_end(table_bytes, table_start)
local pos, len = table_start, #table_bytes
if pos >= len or table_bytes:byte(pos + 1) == 0 then return pos end
while pos < len do
local _code, code_end = read_uleb128_at(table_bytes, pos)
if not _code then return nil end
pos = code_end
local _tag, tag_end = read_uleb128_at(table_bytes, pos)
if not _tag then return nil end
pos = tag_end
if pos >= len then return nil end
pos = pos + 1 -- has_children byte
while pos < len do
local attr, attr_end = read_uleb128_at(table_bytes, pos)
if not attr then return nil end
pos = attr_end
local form, form_end = read_uleb128_at(table_bytes, pos)
if not form then return nil end
pos = form_end
if attr == 0 and form == 0 then break end
if form == DW_FORM_implicit_const then
local _c, ce = read_sleb128_at(table_bytes, pos)
if not _c then return nil end
pos = ce
end
end
if pos >= len then return nil end
if table_bytes:byte(pos + 1) == 0 then return pos end
end
return nil
end
-- Read the null-terminated C string at 0-based offset `off` in `buf`.
-- Stops at the first 0 byte or end of buffer.
local function read_c_string_at(buf, off)
local len = #buf
local start = off
while off < len and buf:byte(off + 1) ~= 0 do off = off + 1 end
return buf:sub(start + 1, off)
end
-- Walk the .debug_abbrev table starting at 0-based offset `table_start` and
-- return a list of declarations: {code, tag, has_children, attrs={ {name, form}, ... }}.
-- Stops at the table terminator.
local function parse_abbrev_table(table_bytes, table_start)
local table_end = find_abbrev_table_end(table_bytes, table_start)
if not table_end then return nil, "no terminator" end
local decls = {}
local pos = table_start
while pos < table_end do
local code, code_end = read_uleb128_at(table_bytes, pos)
if not code then return nil, "truncated code" end
pos = code_end
local tag, tag_end = read_uleb128_at(table_bytes, pos)
if not tag then return nil, "truncated tag" end
pos = tag_end
local has_children = table_bytes:byte(pos + 1)
pos = pos + 1
local attrs = {}
while true do
local attr, attr_end = read_uleb128_at(table_bytes, pos)
if not attr then return nil, "truncated attr" end
pos = attr_end
local form, form_end = read_uleb128_at(table_bytes, pos)
if not form then return nil, "truncated form" end
pos = form_end
if attr == 0 and form == 0 then break end
attrs[#attrs + 1] = { name = attr, form = form }
if form == DW_FORM_implicit_const then
local _c, ce = read_sleb128_at(table_bytes, pos)
if not _c then return nil, "truncated const" end
pos = ce
end
end
decls[#decls + 1] = { code = code, tag = tag, has_children = has_children, attrs = attrs }
end
return decls
end
-- Read a ULEB attribute value at 0-based offset `pos` for the given `form`.
-- Returns (value, next_pos). For DW_FORM_string we return the inline string.
-- For DW_FORM_strp we return the inline string resolved from `str_buf`.
-- For DW_FORM_ref4 we return the absolute CU-relative offset. The caller
-- decides whether to interpret that as a section offset.
local function read_form_value(buf, str_buf, pos, form)
if form == M.DW_FORM.addr then
return M.read_u32_le(buf, pos + 1), pos + 4
elseif form == M.DW_FORM.string then
local s = read_c_string_at(buf, pos)
return s, pos + #s + 1
elseif form == M.DW_FORM.strp then
-- DW_FORM_strp: 4-byte offset into .debug_str.
local strp_off = M.read_u32_le(buf, pos + 1)
return read_c_string_at(str_buf, strp_off), pos + 4
elseif form == M.DW_FORM.udata then
return read_uleb128_at(buf, pos)
elseif form == M.DW_FORM.data1 then
return buf:byte(pos + 1), pos + 1
elseif form == M.DW_FORM.data2 then
return M.read_u16_le(buf, pos + 1), pos + 2
elseif form == M.DW_FORM.data4 then
return M.read_u32_le(buf, pos + 1), pos + 4
elseif form == M.DW_FORM.ref4 then
return M.read_u32_le(buf, pos + 1), pos + 4
elseif form == M.DW_FORM.sec_offset then
-- DW_FORM_sec_offset: 4-byte offset (size depends on DWARF version;
-- on DWARF5 32-bit it's always 4 bytes).
return M.read_u32_le(buf, pos + 1), pos + 4
elseif form == M.DW_FORM.flag_present then
return 1, pos
elseif form == M.DW_FORM.exprloc then
-- DW_FORM_exprloc: ULEB byte count + that many bytes of DW_OP_*.
local len, ne = read_uleb128_at(buf, pos)
if not len then return nil, pos end
return nil, ne + len
elseif form == DW_FORM_implicit_const then
-- The constant is declared in the abbrev; no value bytes in the DIE.
return nil, pos
elseif form == M.DW_FORM.ref_sig8 then
return M.read_u32_le(buf, pos + 1), pos + 8
else
return nil, pos
end
end
-- Index the .debug_info + .debug_abbrev sections of an existing ELF and
-- collect one entry per "interesting" type DIE in the FIRST compilation
-- unit. The index supports typed-view resolution for Phase 5:
--
-- index = {
-- by_name = { ["V4_S2"] = {kind="structure_type", die_offset, byte_size, fields={...}},
-- ["U4"] = {kind="base_type", die_offset, byte_size, encoding="unsigned"},
-- ["MipsCode"]= {kind="typedef", die_offset, target_kind=..., target_die_offset=...} },
-- by_offset = { [die_offset] = {kind, name, ...} }, -- reverse lookup
-- }
--
-- Only the main CU is indexed. We do not walk nested CUs (this matches the
-- Phase 2-4 scope: the main CU is the only one in this build).
-- @param info string -- .debug_info section bytes
-- @param abbrev string -- .debug_abbrev section bytes
-- @param str_buf string -- .debug_str section bytes (for DW_FORM_strp name resolution)
-- @param abbrev_offset integer -- 0-based offset of the main CU's abbrev table
-- @param cu_start integer|nil -- 0-based offset of the main CU (caller-known)
-- @return table|nil, string|nil -- (index, error)
function M.index_main_cu_types(info, abbrev, str_buf, abbrev_offset, cu_start)
if not info or #info < 12 or not abbrev or not abbrev_offset then return nil, "missing input" end
str_buf = str_buf or ""
-- Index the main table at abbrev_offset. The F' pass writes a trailing
-- 0 byte after the main table; the G' pass may append additional codes
-- (100-108) + a 0 terminator. The walker may encounter a code that
-- wasn't in the main table but exists later in the same .debug_abbrev;
-- on the first miss, walk the rest of the section to add any new
-- abbrevs we encounter.
local abbrev_decls, err = parse_abbrev_table(abbrev, abbrev_offset)
if not abbrev_decls then return nil, err end
local abbrev_by_code = {}
for _, d in ipairs(abbrev_decls) do abbrev_by_code[d.code] = d end
-- Resolve cu_start + cu_end_excl.
local cu_end_excl
if cu_start then
local ul = M.read_u32_le(info, cu_start + 1)
if ul == 0xFFFFFFFF then return nil, "DWARF64 not supported" end
cu_end_excl = cu_start + 4 + ul
else
local pos = 0
cu_start = nil
while pos < #info do
local ul = M.read_u32_le(info, pos + 1)
if ul == 0xFFFFFFFF then break end
local unit_end = pos + 4 + ul
if unit_end > #info then break end
local unit_abbrev = M.read_u32_le(info, pos + 9)
if unit_abbrev == abbrev_offset then
cu_start = pos
cu_end_excl = unit_end
break
end
pos = unit_end
end
if not cu_start then return nil, "no CU matches abbrev_offset" end
end
-- Walk the CU's DIE tree at 0-based offset cu_start + 12.
-- Emit a flat list of type-bearing DEIs; recursion handles nested children
-- (members inside structure_type, types inside subprograms, etc.).
-- A non-type DIE's subtree is SKIPPED by walking until the matching null.
local by_name = {}
local by_offset = {}
local pos_cursor = cu_start + 12
-- Root DIE is the compile_unit; skip past it.
if pos_cursor >= cu_end_excl then return nil, "no DIE bytes" end
local root_decl_code
root_decl_code, pos_cursor = read_uleb128_at(info, pos_cursor)
if not root_decl_code then return nil, "truncated root DIE code" end
local root_decl = abbrev_by_code[root_decl_code]
if not root_decl then return nil, "unknown root DIE abbrev" end
-- Skip root DIE attributes.
for _, attr in ipairs(root_decl.attrs) do
local _, ne = read_form_value(info, str_buf, pos_cursor, attr.form)
if not ne then return nil, "truncated root attr" end
pos_cursor = ne
end
-- If root has no children, return an empty index.
if not root_decl.has_children or root_decl.has_children == 0 then
return { by_name = by_name, by_offset = by_offset }
end
-- Helper: skip a subtree rooted at the current DIE (pos_cursor is
-- positioned at the first child). Walks down and right until the
-- matching null terminator is consumed. Returns the new pos_cursor.
-- For DIE trees that contain only the closed type + member shapes,
-- the depth never exceeds 2 (type DIE -> member DIE -> null).
local function skip_subtree(pos)
local depth = 1
while pos < cu_end_excl and depth > 0 do
local code = info:byte(pos + 1)
pos = pos + 1
if code == 0 then
depth = depth - 1
else
local d = abbrev_by_code[code]
if d and d.has_children ~= 0 then
depth = depth + 1
end
end
end
return pos
end
-- Pre-declare n_visited so the closure helpers can read it.
local n_visited = 0
-- Helper: read one DIE's attributes. Returns (name, byte_size, encoding,
-- type_ref, new_pos_cursor) on success; (nil, error_string) on truncation.
local function read_die_attributes(decl, pos)
local die_name = nil
local die_byte_size = nil
local die_encoding = nil
local die_type_ref = nil
for ai, attr in ipairs(decl.attrs) do
local before = pos
local val, ne = read_form_value(info, str_buf, pos, attr.form)
if not ne then
return nil, "truncated DIE attr"
end
pos = ne
if attr.name == M.DW_AT.name then
die_name = val
elseif attr.name == M.DW_AT.byte_size and
(decl.tag == M.DW_TAG.base_type or decl.tag == M.DW_TAG.structure_type) then
die_byte_size = val
elseif attr.name == M.DW_AT.encoding and decl.tag == M.DW_TAG.base_type then
die_encoding = val
elseif attr.name == M.DW_AT.type then
die_type_ref = val
end
end
return die_name, die_byte_size, die_encoding, die_type_ref, pos
end
-- Helper: read structure_type members. Returns (member_fields, new_pos).
local function read_member_fields(decl, pos)
local fields = {}
while pos < cu_end_excl do
local mcode = info:byte(pos + 1)
if mcode == 0 then
pos = pos + 1
break
end
local mdecl = abbrev_by_code[mcode]
if not mdecl then return nil, "unknown member abbrev" end
pos = pos + 1
local mname, mtype_ref, moffset
for _, a in ipairs(mdecl.attrs) do
local v, ne = read_form_value(info, str_buf, pos, a.form)
if not ne then return nil, "truncated member attr" end
pos = ne
if a.name == M.DW_AT.name then mname = v
elseif a.name == M.DW_AT.type then mtype_ref = v
elseif a.name == M.DW_AT.data_member_location then moffset = v end
end
fields[#fields + 1] = { name = mname, type_offset = mtype_ref, offset = moffset }
end
return fields, pos
end
-- Top-level walker: iterate siblings. For each DIE, decide whether to
-- record (if type), descend (if structure_type with members), or skip
-- its subtree.
while pos_cursor < cu_end_excl do
local code = info:byte(pos_cursor + 1)
if code == 0 then pos_cursor = pos_cursor + 1; break end
local decl = abbrev_by_code[code]
if not decl then
-- Lazily scan the rest of the section for the missing code.
-- The G' pass appends new abbrevs (100-108) after the main table's
-- terminator. On the first miss, walk the rest of the section.
local scan_pos = 0
while scan_pos < #abbrev do
if abbrev:byte(scan_pos + 1) == 0 then
scan_pos = scan_pos + 1
goto continue
end
local new_table, e3 = parse_abbrev_table(abbrev, scan_pos)
if not new_table then
scan_pos = scan_pos + 1
goto continue
end
for _, d in ipairs(new_table) do
if not abbrev_by_code[d.code] then
abbrev_by_code[d.code] = d
end
end
-- Find the terminator (0 byte) of this table.
local term = find_abbrev_table_end(abbrev, scan_pos)
if not term then break end
scan_pos = term + 1
::continue::
end
decl = abbrev_by_code[code]
if not decl then
return nil, string.format("unknown abbrev %d at offset 0x%x", code, pos_cursor)
end
end
local die_offset = pos_cursor
pos_cursor = pos_cursor + 1
local read_result = { read_die_attributes(decl, pos_cursor) }
if #read_result == 2 then
return nil, read_result[2]
end
local die_name, die_byte_size, die_encoding, die_type_ref, pos_after_attrs
= read_result[1], read_result[2], read_result[3], read_result[4], read_result[5]
n_visited = n_visited + 1
local is_type = (decl.tag == M.DW_TAG.base_type
or decl.tag == M.DW_TAG.structure_type
or decl.tag == M.DW_TAG.typedef
or decl.tag == M.DW_TAG.pointer_type
or decl.tag == M.DW_TAG.const_type)
local member_fields = nil
pos_cursor = pos_after_attrs
if decl.tag == M.DW_TAG.structure_type and decl.has_children ~= 0 then
local f, np = read_member_fields(decl, pos_cursor)
if not f then return nil, np end
member_fields = f
pos_cursor = np
elseif decl.has_children ~= 0 then
-- Skip the subtree (e.g., DW_TAG_subprogram, DW_TAG_variable, etc.).
pos_cursor = skip_subtree(pos_cursor)
end
if is_type and die_name then
local kind
if decl.tag == M.DW_TAG.base_type then kind = "base_type"
elseif decl.tag == M.DW_TAG.structure_type then kind = "structure_type"
elseif decl.tag == M.DW_TAG.typedef then kind = "typedef"
elseif decl.tag == M.DW_TAG.pointer_type then kind = "pointer_type"
elseif decl.tag == M.DW_TAG.const_type then kind = "const_type" end
local entry = {
kind = kind,
name = die_name,
die_offset = die_offset,
byte_size = die_byte_size,
encoding = die_encoding,
type_ref = die_type_ref,
fields = member_fields,
}
by_name[die_name] = entry
by_offset[die_offset] = entry
end
end
return { by_name = by_name, by_offset = by_offset }
end
-- Resolve a chain of pointer + const + typedef + structure_type down to a
-- canonical struct or base type. The returned entry has kind, name, byte_size,
-- and (for structure_type) fields with {name, offset, type_name, pointer_depth}.
-- Returns nil if the chain cannot be resolved (e.g., missing DIE).
-- @param index table -- M.index_main_cu_types result
-- @param start_offset integer -- CU-relative DW_FORM_ref4 offset of the start
-- @param max_depth integer -- cycle protection
-- @return table|nil -- {kind, name, byte_size, fields?, pointer_depth}
function M.resolve_type_chain(index, start_offset, max_depth)
max_depth = max_depth or 16
if not index or not start_offset then return nil end
local chain = {}
local cur_offset = start_offset
local depth = 0
while cur_offset and depth < max_depth do
local entry = index.by_offset[cur_offset]
if not entry then return nil end
chain[#chain + 1] = entry
if entry.kind == "pointer_type" then
cur_offset = entry.type_ref
elseif entry.kind == "const_type" then
cur_offset = entry.type_ref
elseif entry.kind == "typedef" then
cur_offset = entry.type_ref
else
break
end
depth = depth + 1
end
-- Compute pointer_depth = number of pointer/const wrappers.
local pointer_depth = 0
for _, e in ipairs(chain) do
if e.kind == "pointer_type" then pointer_depth = pointer_depth + 1 end
end
-- The last entry is the "naked" type.
local naked = chain[#chain]
if not naked then return nil end
-- If the last entry is a structure_type, resolve each field's type name
-- + pointer depth too (for `bind_args` shape expansion).
if naked.kind == "structure_type" and naked.fields then
local fields = {}
for _, f in ipairs(naked.fields) do
local field_entry = f.type_offset and index.by_offset[f.type_offset] or nil
local field_chain = {}
local d = 0
local co = f.type_offset
while co and d < 16 do
local e2 = index.by_offset[co]
if not e2 then break end
field_chain[#field_chain + 1] = e2
if e2.kind == "pointer_type" or e2.kind == "const_type" or e2.kind == "typedef" then
co = e2.type_ref
else
break
end
d = d + 1
end
local fpd = 0
for _, x in ipairs(field_chain) do if x.kind == "pointer_type" then fpd = fpd + 1 end end
fields[#fields + 1] = {
name = f.name,
offset = f.offset,
type_name = (field_chain[#field_chain] and field_chain[#field_chain].name) or "?",
pointer_depth = fpd,
}
end
return {
kind = "structure_type",
name = naked.name,
byte_size = naked.byte_size,
fields = fields,
pointer_depth = pointer_depth,
}
end
return {
kind = naked.kind,
name = naked.name,
byte_size = naked.byte_size,
encoding = naked.encoding,
pointer_depth = pointer_depth,
}
end
--- Return a 4-byte little-endian byte string for `value`.
--- Caller concatenates with `..` if composing multi-word blobs.
---
@@ -409,6 +976,11 @@ local SLEB_SIGN_BIT = 0x40
--- @param n integer -- non-negative
--- @return string
function M.uleb128(n)
if n == nil or type(n) ~= "number" then
io.stderr:write("[elf_dwarf.uleb128] got " .. type(n) .. ": " .. tostring(n) .. "\n")
io.stderr:write(debug.traceback() .. "\n")
error("uleb128 requires non-negative number")
end
assert(n >= 0, "uleb128 requires non-negative input")
local bytes = {}
repeat
@@ -511,4 +1083,87 @@ function M.parse_source_map_file(sm_path, expected_version)
return out
end
--- Parse a FORMAT_VERSION <expected_version> `*.atoms.provenance.txt` file.
--- Returns `{name -> {total = N, words = {{pos, call_file, call_line, comp_name, comp_file, comp_line}, ...}}}`.
--- Returns `{}` on format-version mismatch (and logs to stderr).
---
--- **Wire format** (emitted by `passes/atoms_source_map.lua` since the Phase 3 debug_ux work):
--- ```
--- # FORMAT_VERSION <n>
--- ATOM <name> "<abs-source-path>" <total>
--- WORD <n> CALL <src-file>:<src-line> RAW
--- WORD <n> CALL <src-file>:<src-line> MACRO <comp_name> "<comp-file>:<comp-line>"
--- ...
--- ENDATOM
--- ```
---
--- **Used by** `passes/dwarf_injection.lua` (Phase 3 — debug_ux) to:
--- - group consecutive MACRO rows into component invocations (one `DW_TAG_inlined_subroutine` each)
--- - emit abstract `DW_TAG_subprogram` per unique component name
--- - extend `.debug_line` so stepping into a `mac_X(...)` lands on the component's source line.
---
--- @param prov_path string -- path to *.atoms.provenance.txt
--- @param expected_version integer -- expected FORMAT_VERSION line
--- @return table<string, table>
function M.parse_provenance_file(prov_path, expected_version)
local out = {}
local cur_name, cur_words = nil, {}
for raw in io.lines(prov_path) do
local line = raw
if line:match("^#") then
local ver = line:match("^# FORMAT_VERSION%s+(%d+)")
if ver and tonumber(ver) ~= expected_version then
io.stderr:write(string.format(
"[elf_dwarf.parse_provenance_file] provenance version mismatch (got %s, expected %d) in %s\n",
ver, expected_version, prov_path))
return {}
end
-- skip other comments
elseif line:sub(1, 4) == "ATOM" then
-- ATOM <name> "<abs-source-path>" <total>
local _, _, name = line:find("ATOM%s+(%S+)%s+\"[^\"]*\"%s+(%d+)")
if name then
cur_name = name
cur_words = {}
out[name] = { total = 0, words = cur_words }
end
elseif line == "ENDATOM" then
if cur_name and out[cur_name] then
out[cur_name].total = #cur_words
end
cur_name, cur_words = nil, {}
elseif line:sub(1, 4) == "WORD" and cur_name then
-- Two accepted shapes:
-- WORD <n> CALL <call-file>:<call-line> RAW
-- WORD <n> CALL <call-file>:<call-line> MACRO <comp_name> "<comp-file>:<comp-line>"
local pos, call_file, call_line, comp_name, comp_file, comp_line =
line:match('WORD%s+(%d+)%s+CALL%s+(.-):(%d+)%s+MACRO%s+(%S+)%s+"([^"]*):(%d+)"')
if pos then
cur_words[#cur_words + 1] = {
pos = tonumber(pos),
call_file = call_file,
call_line = tonumber(call_line),
comp_name = comp_name,
comp_file = comp_file,
comp_line = tonumber(comp_line),
}
else
-- RAW row.
local raw_pos, raw_file, raw_line = line:match('WORD%s+(%d+)%s+CALL%s+(.-):(%d+)%s+RAW')
if raw_pos then
cur_words[#cur_words + 1] = {
pos = tonumber(raw_pos),
call_file = raw_file,
call_line = tonumber(raw_line),
comp_name = nil,
comp_file = nil,
comp_line = nil,
}
end
end
end
end
return out
end
return M