29 Commits
Author SHA1 Message Date
ed 5f49c071d1 posts-article minor changes 2026-09-08 00:31:36 -04:00
ed e6cd2539d8 last changes before publishing article 2026-09-07 15:45:21 -04:00
ed 0e9034cbd4 More minor adjustments while reviewing for the article. 2026-09-05 18:52:14 -04:00
ed 1faf3539d8 use tb_bind_ macro fro draw cube and tri, better type convention on binds. 2026-09-03 17:36:36 -04:00
ed 6b3fbab387 Adjusting type convention for raw memory. (Reviewing for articles)
Feel like opting for u1-4.. (b1-4 not meaning byte anymore, not sure why I bothered, just bools/bits)
2026-09-03 17:14:34 -04:00
ed 2c38a73709 Article writing review changes. 2026-09-01 00:55:07 -04:00
ed 159ead625e fixing weird identifiers... 2026-08-22 22:48:58 -04:00
ed 52888015de reviewing 2026-08-22 21:03:57 -04:00
ed a37ffe6f58 Last adjustments for metaprogram, hitting a intermission checkpoint to do docs, writeups, and other research distillations. 2026-08-21 14:02:41 -04:00
ed b61610d819 corrections 2026-08-21 10:13:45 -04:00
ed 1b950ab5b5 lua metaprogram: atom bundle support. 2026-08-21 10:13:32 -04:00
ed b2858b3c73 Final ideation before finish updates lua metaprogram before hello-cdrom 2026-08-21 00:23:28 -04:00
ed f1801343e2 tb_bind_ to help with named association of tb_data emissions 2026-08-20 22:35:34 -04:00
ed 85b2205603 remove redudant register shuffle in build_normalize_v3s4 2026-08-20 20:52:18 -04:00
ed 2d754650c9 oops 2026-08-20 20:21:51 -04:00
ed e2ffe538b6 reviewing atom bundle convention 2026-08-20 19:36:52 -04:00
ed 223d1832eb Improving program model, (better structs, better path awareness ties to those structs). 2026-08-20 14:05:49 -04:00
ed de13bc3ce9 auto-column alignment formatting pass on trailing type annotations. 2026-08-19 23:48:46 -04:00
ed 2a087f735e utilizing trailing type annotations more 2026-08-19 23:35:57 -04:00
ed 449216967b Review Pass: Type annotations. 2026-08-19 23:18:32 -04:00
ed c226e8a7d3 dropped usage of SCRATCH_GPRS nopw using proper setup for current tape runtime. 2026-08-19 22:00:18 -04:00
ed 81f37e0098 reading... 2026-08-19 21:49:36 -04:00
ed bde829bf59 WIP: reviewing lua, some upgrades and fixes along the way. 2026-08-19 21:11:09 -04:00
ed cf78cfa120 Rename _shift enums to _pos (may change to offset, but in either case not as accurate to call them _shift, and _shift_smount is longer). 2026-08-19 13:17:08 -04:00
ed 3440c9b59e more metaprogram review 2026-08-19 11:05:48 -04:00
ed 1cbddc6708 More path collapse for metaprogram. 2026-08-18 16:37:53 -04:00
ed b345ccd60e don't bother wiht icache flush atom. 2026-08-18 16:10:44 -04:00
ed bbda5efaea build_noramlized_v3s4 mostly reviewed. 2026-08-18 16:10:29 -04:00
ed 290bb0e07a WIP: Adjusting normalize atom proc to to the new tape runtime convention. Trying to reduce redundant scuff to scratch. 2026-08-18 13:22:40 -04:00
47 changed files with 9067 additions and 5864 deletions
@@ -30,7 +30,7 @@
"editorHoverWidget.background": "#2c334b" "editorHoverWidget.background": "#2c334b"
}, },
"semanticTokenColors": { "semanticTokenColors": {
"comment": { "foreground": "#868686", "fontStyle": "italic" }, "comment": { "foreground": "#868686", }, //"fontStyle": "italic" },
"keyword": { "foreground": "#d8bd5b" }, "keyword": { "foreground": "#d8bd5b" },
"string": { "foreground": "#d46a54" }, "string": { "foreground": "#d46a54" },
"number": { "foreground": "#b5cea8" }, "number": { "foreground": "#b5cea8" },
@@ -81,7 +81,7 @@
"tapeDelaySlot": { "foreground": "#ff5647" } "tapeDelaySlot": { "foreground": "#ff5647" }
}, },
"tokenColors": [ "tokenColors": [
{ "scope": ["comment", "comment.block", "comment.line", "comment.block.documentation"], "settings": { "foreground": "#868686", "fontStyle": "italic" } }, { "scope": ["comment", "comment.block", "comment.line", "comment.block.documentation"], "settings": { "foreground": "#868686", } }, //"fontStyle": "italic" } },
{ "scope": ["keyword", "keyword.control", "keyword.other"], "settings": { "foreground": "#d8bd5b" } }, { "scope": ["keyword", "keyword.control", "keyword.other"], "settings": { "foreground": "#d8bd5b" } },
{ "scope": ["string", "string.quoted"], "settings": { "foreground": "#d46a54" } }, { "scope": ["string", "string.quoted"], "settings": { "foreground": "#d46a54" } },
{ "scope": ["string.quoted.other"], "settings": { "foreground": "#d69d85" } }, { "scope": ["string.quoted.other"], "settings": { "foreground": "#d69d85" } },
+74 -29
View File
@@ -21,7 +21,7 @@ const TOKEN_TYPES = [
"tapeGprRegister", "tapeGprRegister",
"tapeCop2Register", "tapeCop2Register",
"tapeDuffleType", "tapeDuffleType",
"tapeAttribute", "tapeAt__ibute",
"keyword", "keyword",
"macro", "macro",
]; ];
@@ -33,23 +33,59 @@ const TOKEN_MODIFIER_INDEX = new Map(TOKEN_MODIFIERS.map((name, index) => [name,
const ATOM_KEYWORDS = new Set(["MipsAtom_", "MipsAtom_Proc_"]); const ATOM_KEYWORDS = new Set(["MipsAtom_", "MipsAtom_Proc_"]);
const COMPONENT_KEYWORDS = new Set(["MipsAtomComp_", "MipsAtomComp_Proc_"]); const COMPONENT_KEYWORDS = new Set(["MipsAtomComp_", "MipsAtomComp_Proc_"]);
const ANNOTATIONS = new Set([ const ANNOTATIONS = new Set([
"atom_info", "atom_bind", "atom_reads", "atom_writes", "atom_label", "atom_info",
"atom_offset", "atom_reg", "atom_type", "atom_ctx", "atom_phase", "atom_bind",
"atom_auto_reg", "phase_auto_reg", "atom_dbg_skip", "atom_reads",
"atom_writes",
"atom_label",
"atom_offset",
"atom_reg",
"atom_type",
"atom_ctx",
"atom_phase",
"atom_auto_reg",
"phase_auto_reg",
"atom_dbg_skip",
]); ]);
const DSL_KEYWORDS = new Set([ const DSL_KEYWORDS = new Set([
"FI_", "I_", "NI_", "Relative_", "Struct_", "Enum_", "Union_", "Array_", "enum", "struct", "union",
"Slice_", "TypeR_", "TypeV_", "align_", "internal", "local_persist", "global", "offset_of", "static_assert", "typeof", "typeof_ptr", "typeof_same",
"RO_", "LP_", "gknown", "expect_", "cexpr_", "glue", "tmpl",
"A_", "FI_", "I_", "NI_",
"Array_", "Enum_", "Proc_", "Relative_", "Struct_", "Union_", "Slice_",
// "TypeR_", "TypeV_",
"align_",
"internal", "local_persist", "global",
"RO_", "LP_",
"gknown", "expect_", "cexpr_",
"O_", "OA_", "S_", "C_", "T_", "T_same", "R_", "V_",
"r_", "v_", "rt_", "vt_",
"asm", "asm_words", "asm_rpins", "asm_clobber", "asm", "asm_words", "asm_rpins", "asm_clobber",
"O_", "S_", "C_", "T_", "tmpl", "glue", "r_", "v_", "tr_", "tv_",
"rgcc", "r_use", "r_set", "r_mod", "r_imm", "r_mem", "rgcc", "r_use", "r_set", "r_mod", "r_imm", "r_mem",
"u1_", "u2_", "u4_", "u8_", "s1_", "s2_", "s4_", "s8_",
"u1_r", "u2_r", "u4_r", "u8_r", "u1_v", "u2_v", "u4_v", "u8_v", "b1_", "b2_", "b4_", "b8_",
"u1_", "u2_", "u4_", "u8_",
"s1_", "s2_", "s4_", "s8_",
"b1_r", "b2_r", "b4_r", "b8_r",
"b1_v", "b2_v", "b4_v", "b8_v",
"u1_r", "u2_r", "u4_r", "u8_r",
"u1_v", "u2_v", "u4_v", "u8_v",
"u4_lo", "u4_hi",
]); ]);
const DELAY_SLOT_KEYWORDS = new Set(["LdSlot_", "BdSlot_", "DmaSlot_", "GteDelay_"]); const DELAY_SLOT_KEYWORDS = new Set([
"LdSlot_",
"BdSlot_",
"DmaSlot_",
"GteDelay_"
]);
const CONTROL_FLOW_PREFIXES = /^(?:branch_|jump_|call_)/; const CONTROL_FLOW_PREFIXES = /^(?:branch_|jump_|call_)/;
@@ -100,10 +136,8 @@ function modifierMask(modifiers) {
} }
function isRegUseAccess(tokens, tokenIndex) { function isRegUseAccess(tokens, tokenIndex) {
const prev = tokens[tokenIndex - 1]; const prev = tokens[tokenIndex - 1]; if (!prev || prev.text !== ".") return false;
if (!prev || prev.text !== ".") return false; const prevPrev = tokens[tokenIndex - 2]; if (!prevPrev || prevPrev.kind !== "identifier") return false;
const prevPrev = tokens[tokenIndex - 2];
if (!prevPrev || prevPrev.kind !== "identifier") return false;
const next = tokens[tokenIndex + 1]; const next = tokens[tokenIndex + 1];
if (next && next.text === ".") return false; if (next && next.text === ".") return false;
if (prevPrev.text === "r") return true; if (prevPrev.text === "r") return true;
@@ -120,8 +154,7 @@ function classifyDocument(source, filePath, workspaceIndex, shouldCancel = () =>
for (let tokenIndex = 0; tokenIndex < scanned.tokens.length; tokenIndex += 1) { for (let tokenIndex = 0; tokenIndex < scanned.tokens.length; tokenIndex += 1) {
if (shouldCancel()) break; if (shouldCancel()) break;
const token = scanned.tokens[tokenIndex]; const token = scanned.tokens[tokenIndex]; if (token.kind !== "identifier") continue;
if (token.kind !== "identifier") continue;
let type = null; let type = null;
let modifiers = []; let modifiers = [];
@@ -131,37 +164,49 @@ function classifyDocument(source, filePath, workspaceIndex, shouldCancel = () =>
if (declaration) { if (declaration) {
type = ROLE_TO_TYPE[declaration.role] || null; type = ROLE_TO_TYPE[declaration.role] || null;
modifiers = declaration.modifiers.slice(); modifiers = declaration.modifiers.slice();
} else if (ATOM_KEYWORDS.has(token.text)) { }
else if (ATOM_KEYWORDS.has(token.text)) {
type = "tapeAtomKeyword"; type = "tapeAtomKeyword";
} else if (COMPONENT_KEYWORDS.has(token.text)) { }
else if (COMPONENT_KEYWORDS.has(token.text)) {
type = "keyword"; type = "keyword";
} else if (ANNOTATIONS.has(token.text)) { }
else if (ANNOTATIONS.has(token.text)) {
type = "tapeAnnotation"; type = "tapeAnnotation";
} else if (context && context.callee === "atom_bind" && context.argIndex === 0) { }
else if (context && context.callee === "atom_bind" && context.argIndex === 0) {
type = "tapeBindType"; type = "tapeBindType";
} else if (context && context.callee === "atom_phase" && context.argIndex === 0) { }
else if (context && context.callee === "atom_phase" && context.argIndex === 0) {
type = "tapePhase"; type = "tapePhase";
modifiers = ["declaration"]; modifiers = ["declaration"];
} else if (context && context.callee === "atom_ctx" && context.argIndex === 0) { }
else if (context && context.callee === "atom_ctx" && context.argIndex === 0) {
type = "tapeAtomName"; type = "tapeAtomName";
} else if (context && context.callee === "atom_label" && context.argIndex === 0) { }
else if (context && context.callee === "atom_label" && context.argIndex === 0) {
type = "tapeLabel"; type = "tapeLabel";
modifiers = ["declaration"]; modifiers = ["declaration"];
} else if (context && context.callee === "atom_offset" && context.argIndex <= 1) { }
else if (context && context.callee === "atom_offset" && context.argIndex <= 1) {
type = "tapeLabel"; type = "tapeLabel";
} else if (context && context.callee === "atom_reads") { }
else if (context && context.callee === "atom_reads") {
type = registerType(token.text, index); type = registerType(token.text, index);
if (type) modifiers = ["tapeRead"]; if (type) modifiers = ["tapeRead"];
} else if (context && context.callee === "atom_writes") { }
else if (context && context.callee === "atom_writes") {
type = registerType(token.text, index); type = registerType(token.text, index);
if (type) modifiers = ["tapeWrite"]; if (type) modifiers = ["tapeWrite"];
} else if (context && context.callee === "atom_auto_reg") { }
else if (context && context.callee === "atom_auto_reg") {
if (context.argIndex === 0) type = "tapeAtomName"; if (context.argIndex === 0) type = "tapeAtomName";
if (context.argIndex === 1) { if (context.argIndex === 1) {
type = "tapeGprRegister"; type = "tapeGprRegister";
modifiers = ["declaration", "tapeAuto"]; modifiers = ["declaration", "tapeAuto"];
} }
} else if (context && context.callee === "phase_auto_reg") { }
else if (context && context.callee === "phase_auto_reg") {
if (context.argIndex === 0) type = "tapePhase"; if (context.argIndex === 0) type = "tapePhase";
if (context.argIndex === 1) { if (context.argIndex === 1) {
type = "tapeGprRegister"; type = "tapeGprRegister";
+7 -3
View File
@@ -39,7 +39,8 @@ async function activate(context) {
const result = scanSource(source, uri.fsPath); const result = scanSource(source, uri.fsPath);
nextIndex = mergeIndexes(nextIndex, result.index); nextIndex = mergeIndexes(nextIndex, result.index);
for (const error of result.errors) output.appendLine(formatError(uri.fsPath, error)); for (const error of result.errors) output.appendLine(formatError(uri.fsPath, error));
} catch (error) { }
catch (error) {
output.appendLine(`${uri.fsPath}: ${error.stack || error.message || error}`); output.appendLine(`${uri.fsPath}: ${error.stack || error.message || error}`);
} }
} }
@@ -55,7 +56,9 @@ async function activate(context) {
debounceHandle = setTimeout(() => { debounceHandle = setTimeout(() => {
debounceHandle = null; debounceHandle = null;
rebuildIndex().catch((error) => output.appendLine(error.stack || String(error))); rebuildIndex().catch((error) => output.appendLine(error.stack || String(error)));
}, 100); },
100
);
} }
const provider = { const provider = {
@@ -77,7 +80,8 @@ async function activate(context) {
output.appendLine(formatError(document.uri.fsPath || document.uri.toString(), error)); output.appendLine(formatError(document.uri.fsPath || document.uri.toString(), error));
} }
return builder.build(); return builder.build();
} catch (error) { }
catch (error) {
output.appendLine(`${document.uri}: ${error.stack || error.message || error}`); output.appendLine(`${document.uri}: ${error.stack || error.message || error}`);
return new vscode.SemanticTokensBuilder(legend).build(); return new vscode.SemanticTokensBuilder(legend).build();
} }
+14 -7
View File
@@ -10,7 +10,8 @@ function isIdentifierContinue(code) {
return isIdentifierStart(code) || (code >= 48 && code <= 57); return isIdentifierStart(code) || (code >= 48 && code <= 57);
} }
function lex(source) { function lex(source)
{
if (typeof source !== "string") throw new TypeError("source must be a string"); if (typeof source !== "string") throw new TypeError("source must be a string");
const tokens = []; const tokens = [];
@@ -47,7 +48,8 @@ function lex(source) {
}); });
} }
while (offset < source.length) { while (offset < source.length)
{
const ch = source[offset]; const ch = source[offset];
if (/\s/.test(ch)) { if (/\s/.test(ch)) {
@@ -60,12 +62,14 @@ function lex(source) {
continue; continue;
} }
if (ch === "/" && source[offset + 1] === "*") { if (ch === "/" && source[offset + 1] === "*")
{
const start = offset; const start = offset;
advance(); advance();
advance(); advance();
let closed = false; let closed = false;
while (offset < source.length) { while (offset < source.length)
{
if (source[offset] === "*" && source[offset + 1] === "/") { if (source[offset] === "*" && source[offset + 1] === "/") {
advance(); advance();
advance(); advance();
@@ -78,12 +82,14 @@ function lex(source) {
continue; continue;
} }
if (ch === "\"" || ch === "'") { if (ch === "\"" || ch === "'")
{
const quote = ch; const quote = ch;
const start = offset; const start = offset;
advance(); advance();
let closed = false; let closed = false;
while (offset < source.length) { while (offset < source.length)
{
if (source[offset] === "\\") { if (source[offset] === "\\") {
advance(); advance();
if (offset < source.length) advance(); if (offset < source.length) advance();
@@ -134,7 +140,8 @@ function buildCallContexts(tokens) {
if (token.text === ")") { if (token.text === ")") {
if (stack.length === 0) { if (stack.length === 0) {
errors.push({ kind: "unmatched-close-paren", offset: token.start }); errors.push({ kind: "unmatched-close-paren", offset: token.start });
} else { }
else {
const frame = stack.pop(); const frame = stack.pop();
if (frame.callee !== null) calls.push({ ...frame, closeTokenIndex: tokenIndex }); if (frame.callee !== null) calls.push({ ...frame, closeTokenIndex: tokenIndex });
} }
+20 -11
View File
@@ -4,8 +4,10 @@ const path = require("node:path");
const { buildCallContexts, lex, nearestCall } = require("./lexer"); const { buildCallContexts, lex, nearestCall } = require("./lexer");
const BASE_TYPES = [ const BASE_TYPES = [
"B1", "B2", "B4", "B8", "F4", "F8", "S1", "S2", "S4", "S8", "B1", "B2", "B4", "B8",
"U1", "U2", "U4", "U8", "MipsAtom", "MipsCode", "Reg", "F4", "F8", "S1", "S2", "S4", "S8",
"U1", "U2", "U4", "U8",
"MipsAtom", "MipsCode", "Reg",
]; ];
const C_BUILTINS = new Set([ const C_BUILTINS = new Set([
@@ -15,11 +17,13 @@ const C_BUILTINS = new Set([
]); ]);
const BASE_ATTRIBUTES = [ const BASE_ATTRIBUTES = [
"FI_", "I_", "NI_", "Relative_", "Struct_", "Enum_", "Union_", "Array_", "FI_", "I_", "NI_",
"Slice_", "TypeR_", "TypeV_", "align_", "internal", "local_persist", "global", "Relative_", "Struct_", "Enum_", "Union_", "Array_", "Slice_",
"RO_", "LP_", "gknown", "expect_", "cexpr_", "align_", "internal", "local_persist", "global",
"RO_", "LP_",
"gknown", "expect_", "cexpr_",
"asm", "asm_words", "asm_rpins", "asm_clobber", "asm", "asm_words", "asm_rpins", "asm_clobber",
"O_", "S_", "C_", "T_", "tmpl", "glue", "r_", "v_", "tr_", "tv_", "O_", "OA_", "S_", "C_", "T_", "tmpl", "glue", "r_", "v_", "rt_", "vt_",
"rgcc", "r_use", "r_set", "r_mod", "r_imm", "r_mem", "rgcc", "r_use", "r_set", "r_mod", "r_imm", "r_mem",
"u1_", "u2_", "u4_", "u8_", "s1_", "s2_", "s4_", "s8_", "u1_", "u2_", "u4_", "u8_", "s1_", "s2_", "s4_", "s8_",
"u1_r", "u2_r", "u4_r", "u8_r", "u1_v", "u2_v", "u4_v", "u8_v", "u1_r", "u2_r", "u4_r", "u8_r", "u1_v", "u2_v", "u4_v", "u8_v",
@@ -161,7 +165,8 @@ function findFunctionNameBefore(tokens, calleeTokenIndex) {
return null; return null;
} }
function scanSource(source, filePath) { function scanSource(source, filePath)
{
const lexical = lex(source); const lexical = lex(source);
const balanced = buildCallContexts(lexical.tokens); const balanced = buildCallContexts(lexical.tokens);
const tokens = lexical.tokens; const tokens = lexical.tokens;
@@ -191,7 +196,8 @@ function scanSource(source, filePath) {
if (!index.macros.has(alias)) index.macros.set(alias, "component"); if (!index.macros.has(alias)) index.macros.set(alias, "component");
} }
for (let tokenIndex = 0; tokenIndex < tokens.length; tokenIndex += 1) { for (let tokenIndex = 0; tokenIndex < tokens.length; tokenIndex += 1)
{
const token = tokens[tokenIndex]; const token = tokens[tokenIndex];
if (token.kind !== "identifier") continue; if (token.kind !== "identifier") continue;
@@ -240,12 +246,14 @@ function scanSource(source, filePath) {
mark(token, "gprRegister", ["declaration", "tapeAuto"]); mark(token, "gprRegister", ["declaration", "tapeAuto"]);
} }
if (token.text === "define" && tokens[tokenIndex - 1] && tokens[tokenIndex - 1].text === "#") { if (token.text === "define" && tokens[tokenIndex - 1] && tokens[tokenIndex - 1].text === "#")
{
const name = tokens[tokenIndex + 1]; const name = tokens[tokenIndex + 1];
if (name && name.kind === "identifier" && name.line === token.line) { if (name && name.kind === "identifier" && name.line === token.line) {
if (/^(?:RegUse_|Struct_|Enum_|Union_|TypeR_|TypeV_|Relative_|Binds_)/.test(name.text)) { if (/^(?:RegUse_|Struct_|Enum_|Union_|TypeR_|TypeV_|Relative_|Binds_)/.test(name.text)) {
index.types.add(name.text); index.types.add(name.text);
} else if (/^(?:ac_|mac_)/.test(name.text)) { }
else if (/^(?:ac_|mac_)/.test(name.text)) {
const alias = name.text.startsWith("ac_") ? componentAlias(name.text) : name.text; const alias = name.text.startsWith("ac_") ? componentAlias(name.text) : name.text;
const rest = []; const rest = [];
for (let restIndex = tokenIndex + 2; restIndex < tokens.length && tokens[restIndex].line === name.line; restIndex += 1) { for (let restIndex = tokenIndex + 2; restIndex < tokens.length && tokens[restIndex].line === name.line; restIndex += 1) {
@@ -256,7 +264,8 @@ function scanSource(source, filePath) {
index.macros.set(alias, prefixDomain(alias) || "component"); index.macros.set(alias, prefixDomain(alias) || "component");
if (rest.length) index.componentCallees.set(alias, rest); if (rest.length) index.componentCallees.set(alias, rest);
} }
} else { }
else {
index.macros.set(name.text, domain || "utility"); index.macros.set(name.text, domain || "utility");
} }
} }
+3 -1
View File
@@ -44,7 +44,7 @@
* *
* Annotation rules * Annotation rules
* ---------------- * ----------------
* 1. atom_info(...) is OPTIONAL. Atoms without atom_info are silently skipped by the metaprogram. * 1. atom_info(...) is optional. Atoms without atom_info are silently skipped by the metaprogram.
* 2. If present, atom_info takes up to three sub-calls, all order-independent within the arg list: * 2. If present, atom_info takes up to three sub-calls, all order-independent within the arg list:
* - atom_bind(Binds_X) * - atom_bind(Binds_X)
* - atom_reads(...) * - atom_reads(...)
@@ -160,6 +160,8 @@
* ----------------------------------------------------------------------------*/ * ----------------------------------------------------------------------------*/
#define atom_bind(binds_struct) /* atom_bind(binds_struct) */ #define atom_bind(binds_struct) /* atom_bind(binds_struct) */
#define Binds_(type) (tmpl(Binds,type)) // TODO(Ed): Do we want to use this?
/* ============================================================================ /* ============================================================================
* atom_label / atom_offset — branch target machinery * atom_label / atom_offset — branch target machinery
* *
+16 -5
View File
@@ -29,8 +29,7 @@
#define asm __asm__ #define asm __asm__
#define A_(data) (& data) #define A_(data) (& (data))
#define align_(value) __attribute__((aligned (value))) // for easy alignment
#define align_(value) __attribute__((aligned (value))) // for easy alignment #define align_(value) __attribute__((aligned (value))) // for easy alignment
#define C_(type,data) ((type)(data)) // for enforced precedence #define C_(type,data) ((type)(data)) // for enforced precedence
#define expect_(x, y) __builtin_expect(x, y) // so compiler knows the common path #define expect_(x, y) __builtin_expect(x, y) // so compiler knows the common path
@@ -45,7 +44,8 @@
#define R_ restrict #define R_ restrict
#define V_ volatile #define V_ volatile
#pragma region Fictional //, used for intiution #pragma region Fictional
//, used for intiution
#define EUB_ restrict // Execute Unit Bound: Data is siloed in the ALU Register File. The Load/Store Unit is bypassed. (Route to Execution Unit. Keep in registers) #define EUB_ restrict // Execute Unit Bound: Data is siloed in the ALU Register File. The Load/Store Unit is bypassed. (Route to Execution Unit. Keep in registers)
#define ISO_ restrict // Isolated Provenance: Alternative to Exu_. Guarantees electrical memory isolation, #define ISO_ restrict // Isolated Provenance: Alternative to Exu_. Guarantees electrical memory isolation,
@@ -83,8 +83,8 @@
#define r_(ptr) C_(T_(ptr[0])*R_, ptr) // Constrain pointer to restrict #define r_(ptr) C_(T_(ptr[0])*R_, ptr) // Constrain pointer to restrict
#define v_(ptr) C_(T_(ptr[0])V_*, ptr) // #define v_(ptr) C_(T_(ptr[0])V_*, ptr) //
#define tr_(type, ptr) C_(type *R_, ptr) #define rt_(type, ptr) C_(type *R_, ptr)
#define tv_(type, ptr) C_(type V_*, ptr) #define vt_(type, ptr) C_(type V_*, ptr)
#define TypeR_(type) type *R_ type ## _R // type *restrict type_R #define TypeR_(type) type *R_ type ## _R // type *restrict type_R
#define TypeV_(type) type V_* type ## _V // type volatile* type_V #define TypeV_(type) type V_* type ## _V // type volatile* type_V
@@ -120,6 +120,10 @@ typedef unsigned char TSet_(B1);
typedef __UINT16_TYPE__ TSet_(B2); typedef __UINT16_TYPE__ TSet_(B2);
typedef __UINT32_TYPE__ TSet_(B4); typedef __UINT32_TYPE__ TSet_(B4);
#define b1_(value) C_(B1, value)
#define b2_(value) C_(B2, value)
#define b4_(value) C_(B4, value)
#define u1_(value) C_(U1, value) #define u1_(value) C_(U1, value)
#define u2_(value) C_(U2, value) #define u2_(value) C_(U2, value)
#define u4_(value) C_(U4, value) #define u4_(value) C_(U4, value)
@@ -127,6 +131,13 @@ typedef __UINT32_TYPE__ TSet_(B4);
#define s2_(value) C_(S2, value) #define s2_(value) C_(S2, value)
#define s4_(value) C_(S4, value) #define s4_(value) C_(S4, value)
#define b1_r(value) C_(B1*R_, value)
#define b2_r(value) C_(B2*R_, value)
#define b4_r(value) C_(B4*R_, value)
#define b1_v(value) C_(B1 V_*, value)
#define b2_v(value) C_(B2 V_*, value)
#define b4_v(value) C_(B4 V_*, value)
#define u1_r(value) C_(U1 *R_, value) #define u1_r(value) C_(U1 *R_, value)
#define u2_r(value) C_(U2 *R_, value) #define u2_r(value) C_(U2 *R_, value)
#define u4_r(value) C_(U4 *R_, value) #define u4_r(value) C_(U4 *R_, value)
-8
View File
@@ -79,14 +79,6 @@
* Why bundle the `__asm__()` wrapper? * Why bundle the `__asm__()` wrapper?
* - The integer R_T4 (= 12, via R_T4_Code) already indicates the register. * - The integer R_T4 (= 12, via R_T4_Code) already indicates the register.
* - The string "$12" is derived from it via reg_str, so they cannot drift apart. * - The string "$12" is derived from it via reg_str, so they cannot drift apart.
* - Spelling `__asm__(reg_str(R_T4_Code))` at every call site is noise.
*
* tmpl defined in dsl.h (token-paste glue).
* rgcc define here (gcc_asm.h) because the `__asm__` keyword is GCC-specific.
* Anyone porting to a different compiler's asm dialect overrides rgcc,
* and the integer→string derivation in rlit can be retargeted in one place.
*
* For clobber lists and asm-template strings, use the bare `rlit(R_T4_Code)`.
* ------------------------------------------------------------------------ */ * ------------------------------------------------------------------------ */
#define rgcc(n) __asm__(rlit(n)) #define rgcc(n) __asm__(rlit(n))
+28 -20
View File
@@ -13,7 +13,7 @@
// source: C:\projects\Pikuma\ps1\code\duffle\gte.h // source: C:\projects\Pikuma\ps1\code\duffle\gte.h
// source: C:\projects\Pikuma\ps1\code\duffle\pad.h // source: C:\projects\Pikuma\ps1\code\duffle\pad.h
// source: C:\projects\Pikuma\ps1\code\duffle\dsl.atom.h // source: C:\projects\Pikuma\ps1\code\duffle\dsl.atom.h
// source: C:\projects\Pikuma\ps1\code\duffle\lottes_tape.h // source: C:\projects\Pikuma\ps1\code\duffle\tape.h
// source: C:\projects\Pikuma\ps1\code\duffle\bios.h // source: C:\projects\Pikuma\ps1\code\duffle\bios.h
// source: C:\projects\Pikuma\ps1\code\duffle\psyq.h // source: C:\projects\Pikuma\ps1\code\duffle\psyq.h
// source: C:\projects\Pikuma\ps1\code\duffle\pad.c // source: C:\projects\Pikuma\ps1\code\duffle\pad.c
@@ -35,8 +35,10 @@
* These do NOT yield. They are expanded inline inside Tape Atoms. * These do NOT yield. They are expanded inline inside Tape Atoms.
* ---------------------------------------------------------------------------*/ * ---------------------------------------------------------------------------*/
// The 'Yield' sequence for Tape Atoms (mac_yield). // The 'Yield' sequence for Tape Atoms (mac_yield).
// In Forth this is considered the "NEXT" mechanism.
#define mac_yield(...) \ #define mac_yield(...) \
load_word(R_AtomJmp, R_TapePtr, 0) \ load_word(R_AtomJmp, R_TapePtr, 0) \
LdSlot_ \
, add_ui_self( R_TapePtr, S_(MipsCode)) \ , add_ui_self( R_TapePtr, S_(MipsCode)) \
, jump_reg( R_AtomJmp) \ , jump_reg( R_AtomJmp) \
, BdSlot_ nop , BdSlot_ nop
@@ -54,6 +56,12 @@ WORD_COUNT(mac_yield_load, 1)
, BdSlot_ nop , BdSlot_ nop
WORD_COUNT(mac_yield_tail, 3) WORD_COUNT(mac_yield_tail, 3)
/* atom_dbg_skip */
#define mac_yield_to(code_ptr) \
jump_reg(code_ptr) \
, BdSlot_ nop
WORD_COUNT(mac_yield_to, 2)
/* atom_dbg_skip */ /* atom_dbg_skip */
#define mac_load_half_v3(tx, ty, tz, base, offset) \ #define mac_load_half_v3(tx, ty, tz, base, offset) \
load_half(tx, base, offset + OA_(U2,[0])) \ load_half(tx, base, offset + OA_(U2,[0])) \
@@ -217,7 +225,7 @@ WORD_COUNT(mac_gte_ld_ir123_v3s4, 3)
, mac_gte_mv_from_mac123_v3s4(a) \ , mac_gte_mv_from_mac123_v3s4(a) \
GteDelay_ /* Read MAC1/2/3 → a.xyz (overwrites source-A's load targets) */ \ GteDelay_ /* Read MAC1/2/3 → a.xyz (overwrites source-A's load targets) */ \
, mac_shift_aright_v3s4_self(a, 12) /* Right-shift MAC by 12 (S12.20 → S12.0 OuterProduct12) */ , mac_shift_aright_v3s4_self(a, 12) /* Right-shift MAC by 12 (S12.20 → S12.0 OuterProduct12) */
WORD_COUNT(mac_gte_op_cross_v3s4, 16) WORD_COUNT(mac_gte_op_cross_v3s4, 13)
/* atom_dbg_skip */ /* atom_dbg_skip */
#define mac_gte_store_f3(r_primitive_cursor) \ #define mac_gte_store_f3(r_primitive_cursor) \
@@ -227,21 +235,21 @@ WORD_COUNT(mac_gte_op_cross_v3s4, 16)
WORD_COUNT(mac_gte_store_f3, 3) WORD_COUNT(mac_gte_store_f3, 3)
/* atom_dbg_skip */ /* atom_dbg_skip */
#define mac_gte_load_tri_verts(r_vert_base, r_v0, r_v1, r_v2) \ #define mac_gte_load_tri_verts(vbase, v0, v1, v2) \
shift_lleft(R_AT, r_v0, v3s2_byteoff) \ shift_lleft(R_AT, v0, v3s2_byteoff) \
, add_u_self(R_AT, r_vert_base) \ , add_u_self(R_AT, vbase) \
, load_word(R_V0, R_AT, O_(V3_S2,x)) \ , load_word(R_V0, R_AT, O_(V3_S2,x)) \
, load_word(R_V1, R_AT, O_(V3_S2,z)) \ , load_word(R_V1, R_AT, O_(V3_S2,z)) \
, LdSlot_ gte_mv_to_data_r(R_V0, C2_VXY0) \ , LdSlot_ gte_mv_to_data_r(R_V0, C2_VXY0) \
, gte_mv_to_data_r(R_V1, C2_VZ0) \ , gte_mv_to_data_r(R_V1, C2_VZ0) \
, shift_lleft(R_AT, r_v1, v3s2_byteoff) \ , shift_lleft(R_AT, v1, v3s2_byteoff) \
, add_u_self(R_AT, r_vert_base) \ , add_u_self(R_AT, vbase) \
, load_word(R_V0, R_AT, O_(V3_S2,x)) \ , load_word(R_V0, R_AT, O_(V3_S2,x)) \
, load_word(R_V1, R_AT, O_(V3_S2,z)) \ , load_word(R_V1, R_AT, O_(V3_S2,z)) \
, LdSlot_ gte_mv_to_data_r(R_V0, C2_VXY1) \ , LdSlot_ gte_mv_to_data_r(R_V0, C2_VXY1) \
, gte_mv_to_data_r(R_V1, C2_VZ1) \ , gte_mv_to_data_r(R_V1, C2_VZ1) \
, shift_lleft(R_AT, r_v2, v3s2_byteoff) \ , shift_lleft(R_AT, v2, v3s2_byteoff) \
, add_u_self(R_AT, r_vert_base) \ , add_u_self(R_AT, vbase) \
, load_word(R_V0, R_AT, O_(V3_S2,x)) \ , load_word(R_V0, R_AT, O_(V3_S2,x)) \
, load_word(R_V1, R_AT, O_(V3_S2,z)) \ , load_word(R_V1, R_AT, O_(V3_S2,z)) \
, LdSlot_ gte_mv_to_data_r(R_V0, C2_VXY2) \ , LdSlot_ gte_mv_to_data_r(R_V0, C2_VXY2) \
@@ -269,10 +277,10 @@ WORD_COUNT(mac_gte_store_g4_p3, 1)
WORD_COUNT(mac_gte_sqr_v3, 8) WORD_COUNT(mac_gte_sqr_v3, 8)
/* atom_dbg_skip */ /* atom_dbg_skip */
#define mac_gte_sqr_v3s4(r_sx, r_sy, r_sz, delay_slot) \ #define mac_gte_sqr_v3s4(sx, sy, sz, delay_slot) \
gte_mv_to_data_r(r_sx, C2_IR1) \ gte_mv_to_data_r(sx, C2_IR1) \
, gte_mv_to_data_r(r_sy, C2_IR2) \ , gte_mv_to_data_r(sy, C2_IR2) \
, gte_mv_to_data_r(r_sz, C2_IR3) \ , gte_mv_to_data_r(sz, C2_IR3) \
, delay_slot \ , delay_slot \
, gte_cmdw_sqr , gte_cmdw_sqr
WORD_COUNT(mac_gte_sqr_v3s4, 5) WORD_COUNT(mac_gte_sqr_v3s4, 5)
@@ -291,7 +299,7 @@ WORD_COUNT(mac_gte_sqr_v3s4, 5)
, shift_aright_var(r_dx, r_dx, r_shift) \ , shift_aright_var(r_dx, r_dx, r_shift) \
, shift_aright_var(r_dy, r_dy, r_shift) \ , shift_aright_var(r_dy, r_dy, r_shift) \
, shift_aright_var(r_dz, r_dz, r_shift) , shift_aright_var(r_dz, r_dz, r_shift)
WORD_COUNT(mac_gte_gpf_scale, 12) WORD_COUNT(mac_gte_gpf_scale, 13)
#define mac_trans_mt3s3s4(r_mtx, r_off, r_t0, r_t1, r_t2) \ #define mac_trans_mt3s3s4(r_mtx, r_off, r_t0, r_t1, r_t2) \
load_word( r_t0, r_off, O_(V3_S4,x)) \ load_word( r_t0, r_off, O_(V3_S4,x)) \
@@ -303,12 +311,12 @@ WORD_COUNT(mac_gte_gpf_scale, 12)
WORD_COUNT(mac_trans_mt3s3s4, 6) WORD_COUNT(mac_trans_mt3s3s4, 6)
/* atom_dbg_skip */ /* atom_dbg_skip */
#define mac_lzcr_round_even_half_shift(r_shift, r_mag_sq, r_mag_sq_copy) \ #define mac_lzcr_round_even_half_shift(shift, mag_sq, mag_sq_copy) \
and_i(r_shift, r_shift, gte_lzcr_even_mask) \ and_i(shift, shift, gte_lzcr_even_mask) \
, or_u(r_mag_sq_copy, r_mag_sq, 0) \ , or_u(mag_sq_copy, mag_sq, 0) \
, li_s( r_mag_sq, 31) \ , li_s( mag_sq, 31) \
, sub_s( r_mag_sq, r_mag_sq, r_shift) \ , sub_s( mag_sq, mag_sq, shift) \
, shift_aright(r_mag_sq, r_mag_sq, 1) , shift_aright(mag_sq, mag_sq, 1)
WORD_COUNT(mac_lzcr_round_even_half_shift, 5) WORD_COUNT(mac_lzcr_round_even_half_shift, 5)
#define mac_gte_general_purpose_interopolation(to_ir0, to_ir1, to_ir2, to_ir3, fr_mac1, fr_mac2, fr_mac3, nop_slot1, nop_slot2) \ #define mac_gte_general_purpose_interopolation(to_ir0, to_ir1, to_ir2, to_ir3, fr_mac1, fr_mac2, fr_mac3, nop_slot1, nop_slot2) \
+2 -2
View File
@@ -10,7 +10,7 @@
// source: C:\projects\Pikuma\ps1\code\duffle\gte.h // source: C:\projects\Pikuma\ps1\code\duffle\gte.h
// source: C:\projects\Pikuma\ps1\code\duffle\pad.h // source: C:\projects\Pikuma\ps1\code\duffle\pad.h
// source: C:\projects\Pikuma\ps1\code\duffle\dsl.atom.h // source: C:\projects\Pikuma\ps1\code\duffle\dsl.atom.h
// source: C:\projects\Pikuma\ps1\code\duffle\lottes_tape.h // source: C:\projects\Pikuma\ps1\code\duffle\tape.h
// source: C:\projects\Pikuma\ps1\code\duffle\bios.h // source: C:\projects\Pikuma\ps1\code\duffle\bios.h
// source: C:\projects\Pikuma\ps1\code\duffle\psyq.h // source: C:\projects\Pikuma\ps1\code\duffle\psyq.h
// source: C:\projects\Pikuma\ps1\code\duffle\pad.c // source: C:\projects\Pikuma\ps1\code\duffle\pad.c
@@ -33,7 +33,7 @@ enum {
atom_offset_example_atom_proc_skip = _atom_offset_example_atom_proc_skip, atom_offset_example_atom_proc_skip = _atom_offset_example_atom_proc_skip,
}; };
// --- atom: build_normalize_v3s4 (67 words) --- // --- atom: normalize_v3s4 (62 words) ---
#define _atom_offset_aligned_done_srav_path 3 #define _atom_offset_aligned_done_srav_path 3
#define _atom_offset_srav_path_aligned_done 4 #define _atom_offset_srav_path_aligned_done 4
+2 -2
View File
@@ -1,14 +1,14 @@
#ifdef INTELLISENSE_DIRECTIVES #ifdef INTELLISENSE_DIRECTIVES
# include "dsl.h" # include "dsl.h"
# include "gp.h" # include "gp.h"
# include "lottes_tape.h" # include "tape.h"
#endif #endif
ATOM_FILE_DEBUGGER_LINE_MARKER(gp_atom_c); ATOM_FILE_DEBUGGER_LINE_MARKER(gp_atom_c);
#pragma region MACs (Mips Atom Components) #pragma region MACs (Mips Atom Components)
FI_ Slice_MipsCode ac_gcmd_push(AtomBuilder_R ab, U4 cmd, U4 reg_transfer, U4 reg_base, U2 port) FI_ Slice_MipsCode ac_gcmd_push(AtomBuilder_R ab, U2 cmd, Reg reg_transfer, Reg reg_base, U2 port)
atom_dbg_skip MipsAtomComp_Proc_(ab, { atom_dbg_skip MipsAtomComp_Proc_(ab, {
mac_load_word_imm(reg_transfer, cmd), mac_load_word_imm(reg_transfer, cmd),
store_word( reg_transfer, reg_base, port), store_word( reg_transfer, reg_base, port),
+72 -87
View File
@@ -6,26 +6,13 @@
* Primitive commands: gp0_cmd_poly_f3 = 0x20 (byte opcode) * Primitive commands: gp0_cmd_poly_f3 = 0x20 (byte opcode)
* Packed 32-bit cmd: gp0_word_poly_f3(r, g, b) (32-bit, shifted) * Packed 32-bit cmd: gp0_word_poly_f3(r, g, b) (32-bit, shifted)
* *
* Type ordering: domain?_(direction)?_action_target_modifier_type?
* Examples: add_ui (add + unsigned + immediate)
* add_s (add + signed, R-type implicit)
* shift_lleft (shift + logical + left)
* shift_aright (shift + arithmetic + right)
* call_reg(rs) (call + register, $ra implicit)
* gte_mv_to_data_r (gte + mv + to + data + register)
* gte_lw_v0_xy(base) (gte + lw + v0 + xy)
* load_upper_i (load-upper + immediate, unique verb)
*
* --- GPU-domain layer cake --- * --- GPU-domain layer cake ---
* Every gp.h macro follows the same 4-layer composition as mips.h and gte.h: * Every gp.h macro follows the same 4-layer composition as mips.h and gte.h:
* 4. Semantic encoders gp0_word_poly_f3(r,g,b) * 4. Semantic encoders gp0_word_poly_f3(r,g,b)
* 3. Composite encoders enc_color_word(cmd, r, g, b) * 3. Composite encoders enc_color_word(cmd, r, g, b)
* 2. Per-field encoders enc_gp0_color_r(r), enc_gp0_color_g(g), ... * 2. Per-field encoders enc_gp0_color_r(r), enc_gp0_color_g(g), ...
* 1. Bitfield layout consts gp0_color_red_shift = 0, gp0_color_red_width = 8 * 1. Bitfield layout consts gp0_color_red_pos = 0, gp0_color_red_width = 8
* 0. Opcode IDs gp0_cmd_poly_f3 = 0x20 * 0. Opcode IDs gp0_cmd_poly_f3 = 0x20
*
* Vendor mnemonics (gte_mtc2, gte_mfc2, etc.) are NOT in this header.
* They live in the opt-in `gp_vendor_sym.h` for users who prefer the PSYQ-style names.
* ============================================================================ */ * ============================================================================ */
#ifdef INTELLISENSE_DIRECTIVES #ifdef INTELLISENSE_DIRECTIVES
@@ -75,7 +62,7 @@ enum {
* ============================================================================ * ============================================================================
* 8-bit GP0 opcodes (the upper byte of a primitive's first word). These are the BYTE only. * 8-bit GP0 opcodes (the upper byte of a primitive's first word). These are the BYTE only.
* NO macro body past this point uses a raw shift or raw mask. * NO macro body past this point uses a raw shift or raw mask.
* Mirrors the OPCODE_SHIFT / RS_SHIFT convention from mips.h. * Mirrors the OPCODE_POS / RS_POS convention from mips.h.
* ============================================================================ */ * ============================================================================ */
enum { enum {
gp0_cmd_Nop = 0x00, gp0_cmd_Nop = 0x00,
@@ -117,9 +104,9 @@ enum {
gp0_cmd_SetDrawOffset = 0xE5, gp0_cmd_SetDrawOffset = 0xE5,
gp0_cmd_SetMaskBit = 0xE6, gp0_cmd_SetMaskBit = 0xE6,
/* bitfield shifts / widths ---- /* bitfield offset pos / widths ----
* Generic GP0/GP1 command byte (upper 8 bits of every word sent to either port). */ * Generic GP0/GP1 command byte (upper 8 bits of every word sent to either port). */
gp0_cmd_shift = 24, gp0_cmd_pos = 24,
gp0_cmd_width = 8, gp0_cmd_width = 8,
/* Color word layout (lives in Poly_F3.color, Poly_G4.c0..c3, etc.): /* Color word layout (lives in Poly_F3.color, Poly_G4.c0..c3, etc.):
@@ -127,10 +114,10 @@ enum {
* bits 23..16 = BLUE * bits 23..16 = BLUE
* bits 15..08 = GREEN * bits 15..08 = GREEN
* bits 07..00 = RED (PSX GPU is BGR, NOT RGB) */ * bits 07..00 = RED (PSX GPU is BGR, NOT RGB) */
gp0_color_cmd_shift = 24, gp0_color_cmd_width = 8, gp0_color_cmd_pos = 24, gp0_color_cmd_width = 8,
gp0_color_blue_shift = 16, gp0_color_blue_width = 8, gp0_color_blue_pos = 16, gp0_color_blue_width = 8,
gp0_color_green_shift = 8, gp0_color_green_width = 8, gp0_color_green_pos = 8, gp0_color_green_width = 8,
gp0_color_red_shift = 0, gp0_color_red_width = 8, gp0_color_red_pos = 0, gp0_color_red_width = 8,
}; };
/* ============================================================================ /* ============================================================================
@@ -143,12 +130,12 @@ enum {
* ============================================================================ */ * ============================================================================ */
/* ---- Layer 1.5: per-field encoders ---- */ /* ---- Layer 1.5: per-field encoders ---- */
#define enc_gp0_cmd(cmd) ((cmd) << gp0_cmd_shift) #define enc_gp0_cmd(cmd) ((cmd) << gp0_cmd_pos)
#define enc_gp0_color_cmd(cmd) ((cmd) << gp0_color_cmd_shift) #define enc_gp0_color_cmd(cmd) ((cmd) << gp0_color_cmd_pos)
#define enc_gp0_color_r(r) ((r) << gp0_color_red_shift) #define enc_gp0_color_r(r) ((r) << gp0_color_red_pos)
#define enc_gp0_color_g(g) ((g) << gp0_color_green_shift) #define enc_gp0_color_g(g) ((g) << gp0_color_green_pos)
#define enc_gp0_color_b(b) ((b) << gp0_color_blue_shift) #define enc_gp0_color_b(b) ((b) << gp0_color_blue_pos)
/* ---- Layer 2: composite encoders ---- */ /* ---- Layer 2: composite encoders ---- */
#define enc_color_word(cmd, r, g, b) (enc_gp0_color_cmd(cmd) | enc_gp0_color_r(r) | enc_gp0_color_g(g) | enc_gp0_color_b(b)) #define enc_color_word(cmd, r, g, b) (enc_gp0_color_cmd(cmd) | enc_gp0_color_r(r) | enc_gp0_color_g(g) | enc_gp0_color_b(b))
@@ -212,37 +199,37 @@ enum {
gp1_disp_VInterlace = 0x1, gp1_disp_VInterlace = 0x1,
/* ---- Layer 1: GP1 display-mode + range + draw-area shifts/widths ---- */ /* ---- Layer 1: GP1 display-mode + range + draw-area shifts/widths ---- */
gp1_disp_hres_shift = 0, gp1_disp_hres_width = 2, gp1_disp_hres_pos = 0, gp1_disp_hres_width = 2,
gp1_disp_vres_shift = 2, gp1_disp_vres_width = 1, gp1_disp_vres_pos = 2, gp1_disp_vres_width = 1,
gp1_disp_color_shift = 4, gp1_disp_color_width = 1, gp1_disp_color_pos = 4, gp1_disp_color_width = 1,
gp1_disp_interlace_shift = 5, gp1_disp_interlace_width = 1, gp1_disp_interlace_pos = 5, gp1_disp_interlace_width = 1,
/* GP1 horizontal display range: bits 0..11 = X2, bits 12..23 = X1 */ /* GP1 horizontal display range: bits 0..11 = X2, bits 12..23 = X1 */
gp1_hrange_x1_shift = 12, gp1_hrange_x1_width = 12, gp1_hrange_x1_pos = 12, gp1_hrange_x1_width = 12,
gp1_hrange_x2_shift = 0, gp1_hrange_x2_width = 12, gp1_hrange_x2_pos = 0, gp1_hrange_x2_width = 12,
/* GP1 vertical display range: bits 0..9 = Y2, bits 10..19 = Y1 */ /* GP1 vertical display range: bits 0..9 = Y2, bits 10..19 = Y1 */
gp1_vrange_y1_shift = 10, gp1_vrange_y1_width = 10, gp1_vrange_y1_pos = 10, gp1_vrange_y1_width = 10,
gp1_vrange_y2_shift = 0, gp1_vrange_y2_width = 10, gp1_vrange_y2_pos = 0, gp1_vrange_y2_width = 10,
/* GP1 draw area (top-left or bottom-right): bits 0..9 = X, bits 10..19 = Y /* GP1 draw area (top-left or bottom-right): bits 0..9 = X, bits 10..19 = Y
* (10-bit signed — caller pre-signs) */ * (10-bit signed — caller pre-signs) */
gp1_draw_x_shift = 0, gp1_draw_x_width = 10, gp1_draw_x_pos = 0, gp1_draw_x_width = 10,
gp1_draw_y_shift = 10, gp1_draw_y_width = 10, gp1_draw_y_pos = 10, gp1_draw_y_width = 10,
}; };
/* ---- Layer 1.5: GP1 per-field encoders ---- */ /* ---- Layer 1.5: GP1 per-field encoders ---- */
#define enc_gp1_disp_hres(h) ((h) << gp1_disp_hres_shift) #define enc_gp1_disp_hres(h) ((h) << gp1_disp_hres_pos)
#define enc_gp1_disp_vres(v) ((v) << gp1_disp_vres_shift) #define enc_gp1_disp_vres(v) ((v) << gp1_disp_vres_pos)
#define enc_gp1_disp_color(c) ((c) << gp1_disp_color_shift) #define enc_gp1_disp_color(c) ((c) << gp1_disp_color_pos)
#define enc_gp1_disp_interlace(i) ((i) << gp1_disp_interlace_shift) #define enc_gp1_disp_interlace(i) ((i) << gp1_disp_interlace_pos)
#define enc_gp1_hrange_x1(x1) ((x1) << gp1_hrange_x1_shift) #define enc_gp1_hrange_x1(x1) ((x1) << gp1_hrange_x1_pos)
#define enc_gp1_hrange_x2(x2) ((x2) << gp1_hrange_x2_shift) #define enc_gp1_hrange_x2(x2) ((x2) << gp1_hrange_x2_pos)
#define enc_gp1_vrange_y1(y1) ((y1) << gp1_vrange_y1_shift) #define enc_gp1_vrange_y1(y1) ((y1) << gp1_vrange_y1_pos)
#define enc_gp1_vrange_y2(y2) ((y2) << gp1_vrange_y2_shift) #define enc_gp1_vrange_y2(y2) ((y2) << gp1_vrange_y2_pos)
#define enc_gp1_draw_x(x) ((x) << gp1_draw_x_shift) #define enc_gp1_draw_x(x) ((x) << gp1_draw_x_pos)
#define enc_gp1_draw_y(y) ((y) << gp1_draw_y_shift) #define enc_gp1_draw_y(y) ((y) << gp1_draw_y_pos)
/* ---- Layer 2: GP1 composite encoders ---- */ /* ---- Layer 2: GP1 composite encoders ---- */
#define enc_gp1_disp_mode_word(h, v, c, i) (enc_gp0_cmd(gp1_cmd_DisplayMode) | enc_gp1_disp_hres(h) | enc_gp1_disp_vres(v) | enc_gp1_disp_color(c) | enc_gp1_disp_interlace(i)) #define enc_gp1_disp_mode_word(h, v, c, i) (enc_gp0_cmd(gp1_cmd_DisplayMode) | enc_gp1_disp_hres(h) | enc_gp1_disp_vres(v) | enc_gp1_disp_color(c) | enc_gp1_disp_interlace(i))
@@ -391,17 +378,15 @@ enum {
* Primitive structs (8 polygon variants + tag) * Primitive structs (8 polygon variants + tag)
* ============================================================================ * ============================================================================
* Each struct follows the GPU-documented memory layout for the corresponding primitive command. * Each struct follows the GPU-documented memory layout for the corresponding primitive command.
* The PolyTag is the OT-link header; the rest of the struct is the primitive's body. * PolyTag is an OT-link header. Rest of the struct is the primitive's body.
* *
* The current working layouts match the existing demo
* (floor_tri uses Poly_F3; cube_tri uses Poly_G4).
* They are NOT necessarily byte-identical to the PSX-SPX reference layout. * They are NOT necessarily byte-identical to the PSX-SPX reference layout.
* The demo layout uses color+vertex interleaving that doesn't match the standard PSX SDK file format. * The demo layout uses color+vertex interleaving that doesn't match the standard PSX SDK file format.
* For PSX-SDK file compatibility, the textured variants (FT*, GT*) would need layout adjustments. * For PSX-SDK file compatibility, the textured variants (FT*, GT*) would need layout adjustments.
* ============================================================================ */ * ============================================================================ */
/* ---------- RGB8 (3-byte packed color) ---------- */ /* ---------- RGB8 (3-byte packed color) ---------- */
typedef Struct_(RGB8) { B1 r; B1 g; B1 b; }; typedef Struct_(RGB8) { U1 r; U1 g; U1 b; };
#define rgb8(r,g,b) ((RGB8){r,g,b}) #define rgb8(r,g,b) ((RGB8){r,g,b})
/* ---------- PolyTag (the OT-link header; 1 word) ---------- */ /* ---------- PolyTag (the OT-link header; 1 word) ---------- */
@@ -431,7 +416,7 @@ typedef Struct_(PolyTag) {
typedef Struct_(Poly_F3) { typedef Struct_(Poly_F3) {
U4 tag; U4 tag;
RGB8 color; RGB8 color;
B1 code; U1 code;
union { union {
struct { V2_S2 p0; V2_S2 p1; V2_S2 p2; }; struct { V2_S2 p0; V2_S2 p1; V2_S2 p2; };
A3_V2_S2 points; A3_V2_S2 points;
@@ -442,7 +427,7 @@ typedef Struct_(Poly_F3) {
typedef Struct_(Poly_F4) { typedef Struct_(Poly_F4) {
U4 tag; U4 tag;
RGB8 color; RGB8 color;
B1 code; U1 code;
union { union {
struct { V2_S2 p0; V2_S2 p1; V2_S2 p2; V2_S2 p3; }; struct { V2_S2 p0; V2_S2 p1; V2_S2 p2; V2_S2 p3; };
A4_V2_S2 points; A4_V2_S2 points;
@@ -451,18 +436,18 @@ typedef Struct_(Poly_F4) {
/* ---------- Poly_G3 (Gouraud Triangle; 7 words) ---------- */ /* ---------- Poly_G3 (Gouraud Triangle; 7 words) ---------- */
typedef Struct_(Poly_G3) { typedef Struct_(Poly_G3) {
U4 tag; RGB8 c0; B1 code; U4 tag; RGB8 c0; U1 code;
V2_S2 p0; RGB8 c1; B1 pad1; V2_S2 p0; RGB8 c1; U1 pad1;
V2_S2 p1; RGB8 c2; B1 pad2; V2_S2 p1; RGB8 c2; U1 pad2;
V2_S2 p2; V2_S2 p2;
}; };
/* ---------- Poly_G4 (Gouraud Quad; 9 words) ---------- */ /* ---------- Poly_G4 (Gouraud Quad; 9 words) ---------- */
typedef Struct_(Poly_G4) { typedef Struct_(Poly_G4) {
U4 tag; RGB8 c0; B1 code; U4 tag; RGB8 c0; U1 code;
V2_S2 p0; RGB8 c1; B1 pad1; V2_S2 p0; RGB8 c1; U1 pad1;
V2_S2 p1; RGB8 c2; B1 pad2; V2_S2 p1; RGB8 c2; U1 pad2;
V2_S2 p2; RGB8 c3; B1 pad3; V2_S2 p2; RGB8 c3; U1 pad3;
V2_S2 p3; V2_S2 p3;
}; };
@@ -471,7 +456,7 @@ typedef Struct_(Poly_G4) {
typedef Struct_(Poly_FT3) { typedef Struct_(Poly_FT3) {
U4 tag; U4 tag;
RGB8 color; RGB8 color;
B1 code; U1 code;
U4 tpage; U4 tpage;
U4 clut; U4 clut;
V2_S2 p0; U1 u0; U1 v0; V2_S2 p0; U1 u0; U1 v0;
@@ -483,7 +468,7 @@ typedef Struct_(Poly_FT3) {
typedef Struct_(Poly_FT4) { typedef Struct_(Poly_FT4) {
U4 tag; U4 tag;
RGB8 color; RGB8 color;
B1 code; U1 code;
U4 tpage; U4 tpage;
U4 clut; U4 clut;
V2_S2 p0; U1 u0; U1 v0; V2_S2 p0; U1 u0; U1 v0;
@@ -494,9 +479,9 @@ typedef Struct_(Poly_FT4) {
/* ---------- Poly_GT3 (Gouraud Textured Triangle) ---------- */ /* ---------- Poly_GT3 (Gouraud Textured Triangle) ---------- */
typedef Struct_(Poly_GT3) { typedef Struct_(Poly_GT3) {
U4 tag; RGB8 c0; B1 code; U4 tag; RGB8 c0; U1 code;
V2_S2 p0; RGB8 c1; B1 pad1; V2_S2 p0; RGB8 c1; U1 pad1;
V2_S2 p1; RGB8 c2; B1 pad2; V2_S2 p1; RGB8 c2; U1 pad2;
V2_S2 p2; V2_S2 p2;
U4 tpage; U4 tpage;
U4 clut; U4 clut;
@@ -507,10 +492,10 @@ typedef Struct_(Poly_GT3) {
/* ---------- Poly_GT4 (Gouraud Textured Quad) ---------- */ /* ---------- Poly_GT4 (Gouraud Textured Quad) ---------- */
typedef Struct_(Poly_GT4) { typedef Struct_(Poly_GT4) {
U4 tag; RGB8 c0; B1 code; U4 tag; RGB8 c0; U1 code;
V2_S2 p0; RGB8 c1; B1 pad1; V2_S2 p0; RGB8 c1; U1 pad1;
V2_S2 p1; RGB8 c2; B1 pad2; V2_S2 p1; RGB8 c2; U1 pad2;
V2_S2 p2; RGB8 c3; B1 pad3; V2_S2 p2; RGB8 c3; U1 pad3;
V2_S2 p3; V2_S2 p3;
U4 tpage; U4 tpage;
U4 clut; U4 clut;
@@ -553,16 +538,16 @@ typedef Struct_(Poly_GT4) {
* ============================================================================ */ * ============================================================================ */
enum { enum {
/* ---- Layer 1: TPage bitfield shifts / widths ---- */ /* ---- Layer 1: TPage bitfield shifts / widths ---- */
gp0_tpage_x_shift = 0, gp0_tpage_x_width = 4, gp0_tpage_x_pos = 0, gp0_tpage_x_width = 4,
gp0_tpage_y_shift = 4, gp0_tpage_y_width = 1, gp0_tpage_y_pos = 4, gp0_tpage_y_width = 1,
gp0_tpage_semi_trans_shift = 5, gp0_tpage_semi_trans_width = 2, gp0_tpage_semi_trans_pos = 5, gp0_tpage_semi_trans_width = 2,
gp0_tpage_color_depth_shift = 7, gp0_tpage_color_depth_width = 2, gp0_tpage_color_depth_pos = 7, gp0_tpage_color_depth_width = 2,
gp0_tpage_dither_shift = 9, gp0_tpage_dither_width = 1, gp0_tpage_dither_pos = 9, gp0_tpage_dither_width = 1,
gp0_tpage_draw_to_disp_shift = 10, gp0_tpage_draw_to_disp_width = 1, gp0_tpage_draw_to_disp_pos = 10, gp0_tpage_draw_to_disp_width = 1,
gp0_tpage_tex_disable_shift = 11, gp0_tpage_tex_disable_width = 1, gp0_tpage_tex_disable_pos = 11, gp0_tpage_tex_disable_width = 1,
/* TPage color-depth payload values (NOT bit positions — these go in /* TPage color-depth payload values (NOT bit positions — these go in
* the 2-bit field at gp0_tpage_color_depth_shift). */ * the 2-bit field at gp0_tpage_color_depth_pos). */
gp0_tpage_color_4bpp = 0x0, gp0_tpage_color_4bpp = 0x0,
gp0_tpage_color_8bpp = 0x1, gp0_tpage_color_8bpp = 0x1,
gp0_tpage_color_16bpp = 0x2, gp0_tpage_color_16bpp = 0x2,
@@ -578,13 +563,13 @@ enum {
}; };
/* ---- Layer 1.5: TPage per-field encoders. Mirrors enc_gte_sf/mx/v in gte.h. ---- */ /* ---- Layer 1.5: TPage per-field encoders. Mirrors enc_gte_sf/mx/v in gte.h. ---- */
#define enc_gp0_tpage_x(x) ((x) << gp0_tpage_x_shift) #define enc_gp0_tpage_x(x) ((x) << gp0_tpage_x_pos)
#define enc_gp0_tpage_y(y) ((y) << gp0_tpage_y_shift) #define enc_gp0_tpage_y(y) ((y) << gp0_tpage_y_pos)
#define enc_gp0_tpage_semi_trans(s) ((s) << gp0_tpage_semi_trans_shift) #define enc_gp0_tpage_semi_trans(s) ((s) << gp0_tpage_semi_trans_pos)
#define enc_gp0_tpage_color_depth(c) ((c) << gp0_tpage_color_depth_shift) #define enc_gp0_tpage_color_depth(c) ((c) << gp0_tpage_color_depth_pos)
#define enc_gp0_tpage_dither(d) ((d) << gp0_tpage_dither_shift) #define enc_gp0_tpage_dither(d) ((d) << gp0_tpage_dither_pos)
#define enc_gp0_tpage_draw_to_disp(d) ((d) << gp0_tpage_draw_to_disp_shift) #define enc_gp0_tpage_draw_to_disp(d) ((d) << gp0_tpage_draw_to_disp_pos)
#define enc_gp0_tpage_tex_disable(t) ((t) << gp0_tpage_tex_disable_shift) #define enc_gp0_tpage_tex_disable(t) ((t) << gp0_tpage_tex_disable_pos)
/* ---- Layer 2: TPage composite encoder. Mirrors enc_gte_cmdw in gte.h ---- */ /* ---- Layer 2: TPage composite encoder. Mirrors enc_gte_cmdw in gte.h ---- */
#define enc_gp0_tpage_word(x, y, semi_trans, color_depth, dither, draw_to_disp, tex_disable) \ #define enc_gp0_tpage_word(x, y, semi_trans, color_depth, dither, draw_to_disp, tex_disable) \
@@ -615,16 +600,16 @@ typedef Struct_(TexturePage) { U4 raw; };
* ============================================================================ */ * ============================================================================ */
enum { enum {
/* ---- Layer 1: CLUT bitfield shifts / widths ---- */ /* ---- Layer 1: CLUT bitfield shifts / widths ---- */
gp0_clut_y_shift = 0, gp0_clut_y_width = 6, gp0_clut_y_pos = 0, gp0_clut_y_width = 6,
gp0_clut_x_shift = 6, gp0_clut_x_width = 9, gp0_clut_x_pos = 6, gp0_clut_x_width = 9,
/* CLUT-load cmd-byte variants — the upper byte of the GP0 word. */ /* CLUT-load cmd-byte variants — the upper byte of the GP0 word. */
gp0_clut_cmd_Load4bpp = 0x20, gp0_clut_cmd_Load4bpp = 0x20,
gp0_clut_cmd_Load8bpp = 0x25, gp0_clut_cmd_Load8bpp = 0x25,
}; };
/* ---- Layer 1.5: CLUT per-field encoders ---- */ /* ---- Layer 1.5: CLUT per-field encoders ---- */
#define enc_gp0_clut_x(x) ((x) << gp0_clut_x_shift) #define enc_gp0_clut_x(x) ((x) << gp0_clut_x_pos)
#define enc_gp0_clut_y(y) ((y) << gp0_clut_y_shift) #define enc_gp0_clut_y(y) ((y) << gp0_clut_y_pos)
/* ---- Layer 2: CLUT composite encoder ---- */ /* ---- Layer 2: CLUT composite encoder ---- */
#define enc_gp0_clut_word(cmd, x, y) (enc_gp0_cmd(cmd) | enc_gp0_clut_x(x) | enc_gp0_clut_y(y)) #define enc_gp0_clut_word(cmd, x, y) (enc_gp0_cmd(cmd) | enc_gp0_clut_x(x) | enc_gp0_clut_y(y))
+141 -135
View File
@@ -3,7 +3,8 @@
# include "gen/offsets.h" # include "gen/offsets.h"
# include "gte.h" # include "gte.h"
# include "gp.h" # include "gp.h"
# include "lottes_tape.h" # include "tape.h"
# include "math.atom.h"
#endif #endif
ATOM_FILE_DEBUGGER_LINE_MARKER(gte_atom_c); ATOM_FILE_DEBUGGER_LINE_MARKER(gte_atom_c);
@@ -63,10 +64,10 @@ FI_ Slice_MipsCode ac_gte_store_f3(AtomBuilder_R ab, U4 r_primitive_cursor) atom
}) })
/* Words: 18; Translates indices to vertex addresses and pushes them to GTE */ /* Words: 18; Translates indices to vertex addresses and pushes them to GTE */
I_ Slice_MipsCode ac_gte_load_tri_verts(AtomBuilder_R ab, U4 r_vert_base, U4 r_v0, U4 r_v1, U4 r_v2) atom_dbg_skip MipsAtomComp_Proc_(ab, { I_ Slice_MipsCode ac_gte_load_tri_verts(AtomBuilder_R ab, Reg vbase, Reg v0, Reg v1, Reg v2) atom_dbg_skip MipsAtomComp_Proc_(ab, {
shift_lleft(R_AT, r_v0, v3s2_byteoff), add_u_self(R_AT, r_vert_base), load_word(R_V0, R_AT, O_(V3_S2,x)), load_word(R_V1, R_AT, O_(V3_S2,z)), LdSlot_ gte_mv_to_data_r(R_V0, C2_VXY0), gte_mv_to_data_r(R_V1, C2_VZ0), shift_lleft(R_AT, v0, v3s2_byteoff), add_u_self(R_AT, vbase), load_word(R_V0, R_AT, O_(V3_S2,x)), load_word(R_V1, R_AT, O_(V3_S2,z)), LdSlot_ gte_mv_to_data_r(R_V0, C2_VXY0), gte_mv_to_data_r(R_V1, C2_VZ0),
shift_lleft(R_AT, r_v1, v3s2_byteoff), add_u_self(R_AT, r_vert_base), load_word(R_V0, R_AT, O_(V3_S2,x)), load_word(R_V1, R_AT, O_(V3_S2,z)), LdSlot_ gte_mv_to_data_r(R_V0, C2_VXY1), gte_mv_to_data_r(R_V1, C2_VZ1), shift_lleft(R_AT, v1, v3s2_byteoff), add_u_self(R_AT, vbase), load_word(R_V0, R_AT, O_(V3_S2,x)), load_word(R_V1, R_AT, O_(V3_S2,z)), LdSlot_ gte_mv_to_data_r(R_V0, C2_VXY1), gte_mv_to_data_r(R_V1, C2_VZ1),
shift_lleft(R_AT, r_v2, v3s2_byteoff), add_u_self(R_AT, r_vert_base), load_word(R_V0, R_AT, O_(V3_S2,x)), load_word(R_V1, R_AT, O_(V3_S2,z)), LdSlot_ gte_mv_to_data_r(R_V0, C2_VXY2), gte_mv_to_data_r(R_V1, C2_VZ2), shift_lleft(R_AT, v2, v3s2_byteoff), add_u_self(R_AT, vbase), load_word(R_V0, R_AT, O_(V3_S2,x)), load_word(R_V1, R_AT, O_(V3_S2,z)), LdSlot_ gte_mv_to_data_r(R_V0, C2_VXY2), gte_mv_to_data_r(R_V1, C2_VZ2),
}) })
/* Words: 3; Stores the 3 transformed (V2_S2 screen) vertices of the /* Words: 3; Stores the 3 transformed (V2_S2 screen) vertices of the
@@ -96,11 +97,11 @@ FI_ Slice_MipsCode ac_gte_sqr_v3(AtomBuilder_R ab, U4 r_sx, U4 r_sy, U4 r_sz, U4
}) })
/* ─── SQR FIRE — mtc2 3 GPRs into IR1/IR2/IR3, then fire SQR. ─── */ /* ─── SQR FIRE — mtc2 3 GPRs into IR1/IR2/IR3, then fire SQR. ─── */
FI_ Slice_MipsCode ac_gte_sqr_v3s4(AtomBuilder_R ab, Reg r_sx, Reg r_sy, Reg r_sz, MipsCode delay_slot) FI_ Slice_MipsCode ac_gte_sqr_v3s4(AtomBuilder_R ab, Reg sx, Reg sy, Reg sz, MipsCode delay_slot)
atom_dbg_skip MipsAtomComp_Proc_(ab, { atom_dbg_skip MipsAtomComp_Proc_(ab, {
gte_mv_to_data_r(r_sx, C2_IR1), gte_mv_to_data_r(sx, C2_IR1),
gte_mv_to_data_r(r_sy, C2_IR2), gte_mv_to_data_r(sy, C2_IR2),
gte_mv_to_data_r(r_sz, C2_IR3), gte_mv_to_data_r(sz, C2_IR3),
delay_slot, gte_cmdw_sqr, delay_slot, gte_cmdw_sqr,
}) })
@@ -157,16 +158,13 @@ FI_ Slice_MipsCode ac_trans_mt3s3s4(AtomBuilder_R ab
* *
* Note: C2_LZCR (cop2r31) is a fixed read-only C2 data register — the caller must read it via mfc2 from C2_LZCR; * Note: C2_LZCR (cop2r31) is a fixed read-only C2 data register — the caller must read it via mfc2 from C2_LZCR;
* there is no register choice at the hardware level. Only the GPR that holds the result is caller-determined. */ * there is no register choice at the hardware level. Only the GPR that holds the result is caller-determined. */
FI_ Slice_MipsCode ac_lzcr_round_even_half_shift(AtomBuilder_R ab, FI_ Slice_MipsCode ac_lzcr_round_even_half_shift(AtomBuilder_R ab, Reg shift, Reg mag_sq, Reg mag_sq_copy)
U4 r_shift,
U4 r_mag_sq,
U4 r_mag_sq_copy)
atom_dbg_skip MipsAtomComp_Proc_(ab, { atom_dbg_skip MipsAtomComp_Proc_(ab, {
and_i(r_shift, r_shift, gte_lzcr_even_mask), and_i(shift, shift, gte_lzcr_even_mask),
or_u(r_mag_sq_copy, r_mag_sq, 0), or_u(mag_sq_copy, mag_sq, 0),
li_s( r_mag_sq, 31), li_s( mag_sq, 31),
sub_s( r_mag_sq, r_mag_sq, r_shift), sub_s( mag_sq, mag_sq, shift),
shift_aright(r_mag_sq, r_mag_sq, 1), shift_aright(mag_sq, mag_sq, 1),
}) })
FI_ Slice_MipsCode ac_gte_general_purpose_interopolation(AtomBuilder_R ab FI_ Slice_MipsCode ac_gte_general_purpose_interopolation(AtomBuilder_R ab
@@ -186,59 +184,94 @@ MipsAtomComp_Proc_(ab, {
gte_mv_from_data_r(fr_mac3, C2_MAC3), gte_mv_from_data_r(fr_mac3, C2_MAC3),
}) })
FI_ Slice_MipsCode ac_gte_mv_from_data_r_mac123(AtomBuilder_R ab FI_ Slice_MipsCode ac_gte_mv_from_data_r_mac123(AtomBuilder_R ab, Reg fr_mac1, Reg fr_mac2, Reg fr_mac3)
, Reg fr_mac1, Reg fr_mac2, Reg fr_mac3)
MipsAtomComp_Proc_(ab, { MipsAtomComp_Proc_(ab, {
gte_mv_from_data_r(fr_mac1, C2_MAC1), gte_mv_from_data_r(fr_mac1, C2_MAC1),
gte_mv_from_data_r(fr_mac2, C2_MAC2), gte_mv_from_data_r(fr_mac2, C2_MAC2),
gte_mv_from_data_r(fr_mac3, C2_MAC3), gte_mv_from_data_r(fr_mac3, C2_MAC3),
}) })
FI_ Slice_MipsCode ac_gte_mv_from_mac123_v3s4(AtomBuilder_R ab, Reg_(V3_S4) v) MipsAtomComp_ProcMap_(ab, mac_gte_mv_from_data_r_mac123(v.x, v.y, v.z)) FI_ Slice_MipsCode ac_gte_mv_from_mac123_v3s4(AtomBuilder_R ab, Reg_(V3_S4) v) MipsAtomComp_ProcMap_(ab, mac_gte_mv_from_data_r_mac123(v.x, v.y, v.z))
#pragma endregion MACs (Mips Atom Components) #pragma endregion MACs (Mips Atom Components)
#pragma region Atom Procs #pragma region Atom Procs
/* ─── Local copy of PSYQ's sqrtbl (1/sqrt lookup table for VectorNormal). ─── /* Normalize V3_S4 using the PSYQ/libgte reciprocal-sqrt method:
* Source: PSYQ 4.7 libgte sqrtbl at 0x800185B4 in hello_camera.elf. * |v|² = x² + y² + z²
* objdump -s --start-address=0x800185B4 --stop-address=0x800185F4 hello_camera.elf → 192 entries × 16-bit signed, in 1.12 fixed-point (max value 0x1000 = 1.0). * LZCR determines the exponent of |v|².
* Round that exponent even and shift |v|² into [1, 4).
* sqrtbl approximates 1/sqrt(mantissa).
* GPF multiplies v by that reciprocal-sqrt mantissa.
* srav_shift restores the exponent scale.
* Effectively: v_normalized = v * (1 / sqrt(|v|²)).
* *
* ─── Local port of PSYQ's sqrtbl (1/sqrt lookup table for VectorNormal). ───
* Source: PSYQ 4.7 libgte sqrtbl at 0x800185B4 in hello_camera.elf.
* objdump -s --start-address=0x800185B4 --stop-address=0x800185F4 hello_camera.elf -> 192 entries x 16-bit signed, stored in 1.12 fixed point.
* Data is identical to the libgte original (byte-for-byte verified). * Data is identical to the libgte original (byte-for-byte verified).
* *
* ─── Per-entry semantics (decoded from libgte msc02 VectorNormal) ─── * ─── Table semantics ───
* Each entry is `1/sqrt(x)` in 1.12 fixed point (value / 4096). * For table index i in [0, 192):
* The 192 entries span 4 octaves of the input magnitude, with 48 entries per octave: * x = 1 + i / 64
* Octave 0 (entries 0- 47): mantissa in [0x8000, 0x10000) output ~[1.000, 0.707] * tbl[i] = floor(4096 / sqrt(x))
* Octave 1 (entries 48- 95): mantissa in [0x10000, 0x20000) output ~[0.707, 0.500] * Thus the table uniformly samples 1/sqrt(x) over:
* Octave 2 (entries 96-143): mantissa in [0x20000, 0x40000) output ~[0.500, 0.354] * x in [1.0, 4.0)
* Octave 3 (entries144-191): mantissa in [0x40000, 0x80000) output ~[0.354, 0.251] * at steps of 1/64, with the result represented in 1.12 fixed point (0x1000 = 1.0).
* Within each octave, 8 sub-entries interpolate over the 8 fractional bits of the mantissa
* (the byte `(0x80 | (i mod 8))` for the lower-byte of the aligned value).
* Sampling the first value of each octave:
* [0] 0x1000 = 1.0000 ; 1 / sqrt(1.0000)
* [48] 0x0e4f = 0.8940 ; 1 / sqrt(1.2500)
* [96] 0x0d10 = 0.8164 ; 1 / sqrt(1.5000)
* [144] 0x0c0a = 0.7520 ; 1 / sqrt(1.7500)
* And representative sub-entries within octave 0 (mantissa in [0x8000, 0x8100)):
* [0] 0x1000 = 1.0000 ; 1 / sqrt(0x8000)
* [1] 0x0fe0 = 0.9922 ; 1 / sqrt(0x8100)
* [2] 0x0fc1 = 0.9846 ; 1 / sqrt(0x8200)
* [3] 0x0fa3 = 0.9773 ; 1 / sqrt(0x8300)
* [4] 0x0f85 = 0.9700 ; 1 / sqrt(0x8400)
* [5] 0x0f68 = 0.9629 ; 1 / sqrt(0x8500)
* [6] 0x0f4c = 0.9561 ; 1 / sqrt(0x8600)
* [7] 0x0f30 = 0.9492 ; 1 / sqrt(0x8700)
* *
* The algorithm's `addi -64 / sll 1 / lh` selects the entry at `(aligned - 64) * 2` for the case where `aligned` has its top bit at bit 24. * Representative entries:
* After the sllv/srav pair, `aligned` always lands in `[0x80, 0x100)` * [ 0] 0x1000 = 1.000000 ; 1 / sqrt(1.000000)
* (with top bit at bit 24 → after `sub $aligned - 64`, the index sits in `[0x40, 0x80) * 2 = [0x80, 0x100)` bytes = entries [64, 128) within the sqrtbl). * [ 16] 0x0e4f = 0.894287 ; 1 / sqrt(1.250000)
* The earlier 64 entries (octave 0) are reached when the magnitude after shifting puts the top bit below bit 24 (the `sllv` branch), * [ 32] 0x0d10 = 0.816406 ; 1 / sqrt(1.500000)
* and the load upper_halves of the table bracket the input range. * [ 48] 0x0c18 = 0.755859 ; 1 / sqrt(1.750000)
* The later 64 entries (octaves 2-3) are the `srav` branch when the magnitude's top bit is well above bit 24. * [ 64] 0x0b50 = 0.707031 ; 1 / sqrt(2.000000)
* [128] 0x093c = 0.577148 ; 1 / sqrt(3.000000)
* [191] 0x0804 = 0.500977 ; 1 / sqrt(3.984375)
* *
* Reproduced verbatim from libgte (verified against libpsn00b/psxgte/vector.s:100-123 — 24 rows × 8 halfwords, last entry 0x0804). * ─── How VectorNormal indexes it ───
* Let:
* mag_sq = x*x + y*y + z*z
* lzcr = leading-zero count of mag_sq
* For a non-zero magnitude, libgte first rounds LZCR down to an even number:
* lzcr_even = lzcr & ~1
*
* It then shifts mag_sq so that its significant bits land in one of two
* adjacent normalized ranges:
* if lzcr_even >= 24:
* aligned = mag_sq << (lzcr_even - 24)
* else:
* aligned = mag_sq >> (24 - lzcr_even)
*
* Because lzcr_even differs from the true LZCR by at most one bit:
* raw LZCR even -> aligned in [0x80, 0x100)
* raw LZCR odd -> aligned in [0x40, 0x080)
* therefore:
* aligned in [0x40, 0x100)
*
* Dividing this normalized integer by 64 gives exactly the table domain:
* x = aligned / 64
* x in [1.0, 4.0)
*
* The lookup is therefore:
* index = aligned - 0x40
* byte_offset = index * sizeof(S2)
* inv_len = sqrtbl[index]
* or equivalently, matching the libgte instructions:
* addi aligned, -64
* sll aligned, 1
* lh inv_len, sqrtbl + aligned
*
* ─── Why the domain spans [1, 4) instead of [1, 2) ───
* Square-root scaling depends on the parity of the exponent.
* Rounding LZCR to even absorbs exponent changes in pairs of bits, leaving the lookup mantissa normalized over a factor-of-four interval [1, 4).
*
* The corresponding exponent correction is retained separately as:
* srav_shift = (31 - lzcr_even) >> 1
*
* After GPF multiplies the original vector components by the table's reciprocal-square-root coefficient,
* this shift restores the exponent scale and yields the normalized vector.
*
* Reproduced verbatim from libgte; also matches PSn00bSDK VectorNormalS _norm_table (24 rows x 8 halfwords, final entry 0x0804).
**/ **/
internal S2 const gte_normalize_sqr_tbl[192] align_(2) = { internal S2 const gte_normalize_sqr_tbl[192] align_(2) = {
0x1000, 0x0fe0, 0x0fc1, 0x0fa3, 0x0f85, 0x0f68, 0x0f4c, 0x0f30, 0x1000, 0x0fe0, 0x0fc1, 0x0fa3, 0x0f85, 0x0f68, 0x0f4c, 0x0f30,
@@ -267,95 +300,68 @@ internal S2 const gte_normalize_sqr_tbl[192] align_(2) = {
0x0820, 0x081c, 0x0818, 0x0814, 0x0810, 0x080c, 0x0808, 0x0804, 0x0820, 0x081c, 0x0818, 0x0814, 0x0810, 0x080c, 0x0808, 0x0804,
}; };
typedef Struct_(Binds_NormalizeV3S4) { typedef Struct_(Binds_normalize_v3s4) { U2 src_offset; U2 dst_offset; };
U2 src_offset; /* offset of src V3_S4 within the BIOS scratchpad */ typedef Struct_(RegUse_normalize_v3s4) {
U2 dst_offset; /* offset of dst V3_S4 within the BIOS scratchpad */ union { Reg_(V3_S4) res, src; };
}; union { Reg r0, src_ptr, mac2; };
typedef Struct_(RegUse_build_normalize_v3s4) { union { Reg r1, dst_ptr; };
Reg scratch; /* scratchpad base; loaded via load_word_imm below. */ union { Reg r2, dst_offset, mac1, v_sqr_aligned, sqrtbl_byte_offset; };
Reg src_ptr; union { Reg r3, src_offset, align_delta, shift_count, sqrtbl_lookup; };
Reg dst_ptr; union { Reg r4, mac3, v_sqr_sum, srav_shift; };
Reg recip_est; /* |v|² sum + shift-input + sqrtbl[index] */ union { Reg r5, lzcr_raw, lzcr_even, inv_len; };
Reg norm; Reg shift;
Reg src_x;
union { Reg mac1_scratch, dst_offset; } t3;
union { Reg mac2_scratch; } t4;
union { Reg btarget, shift_count, lookup_addr, src_z, src_offset; } t5;
}; };
/* ─── Full normalize (all 4 stages inline) ─── /* ─── Full normalize (all 4 stages inline) ───
* Generic 4-stage GTE normalize (SQR → sum+LZCR → align+sqrtbl → GPF+srav). */ * Generic 4-stage GTE normalize (SQR → sum+LZCR → align+sqrtbl → GPF+srav). */
internal MipsAtom* build_normalize_v3s4(AtomArena_R aa, RegUse_build_normalize_v3s4 r) internal MipsAtom* normalize_v3s4(AtomArena_R aa, RegUse_normalize_v3s4 r)
MipsAtom_Proc_(aa, { MipsAtom_Proc_(aa, {
/* Load scratch base via immediate (always Scratchpad_Loc = 0x1F800000 — the BIOS load_half(r.src_offset, R_TapePtr, O_(Binds_normalize_v3s4, src_offset)),
* scratchpad, aliased by every consumer's ResolveLookAtScratch struct). */ load_half(r.dst_offset, R_TapePtr, O_(Binds_normalize_v3s4, dst_offset)),
mac_load_word_imm(r.scratch, Scratchpad_Loc), LdSlot_ add_u(r.src_ptr, R_ScratchBase, r.src_offset),
/* Tape pop: src_offset, dst_offset = 4 bytes (packed into 1 U4: low16=src, high16=dst). LdSlot_ add_u(r.dst_ptr, R_ScratchBase, r.dst_offset),
* Loads back-to-back fill each other's load-delay slots; the subsequent add_u LdSlot_ add_ui_self(R_TapePtr, S_(Binds_normalize_v3s4)),
* (2 cycles after the matching load) sees a valid value. */
load_half(r.t5.src_offset, R_TapePtr, O_(Binds_NormalizeV3S4, src_offset)),
load_half(r.t3.dst_offset, R_TapePtr, O_(Binds_NormalizeV3S4, dst_offset)),
LdSlot_ add_u(r.src_ptr, r.scratch, r.t5.src_offset),
LdSlot_ add_u(r.dst_ptr, r.scratch, r.t3.dst_offset),
LdSlot_ add_ui_self(R_TapePtr, S_(Binds_NormalizeV3S4)),
/* Load src.x/y/z from r_src_ptr (caller-determined address) into r_tmp/r_recip_est/r_branch_tmp. mac_load_v3s4(r.src, r.src_ptr, 0),
* r.rt1_src_x holds src.x throughout stages 1-2 — r_mac2_scratch is clobbered to MAC2 in stage 1.5 (line below).
* t5.src_offset/dst_offset are dead by here; t5 is reused for src.z in the mac_load_word_v3 below. */
mac_load_word_v3(r.src_x, r.recip_est, r.t5.src_z, r.src_ptr, 0),
/* Stage 1: mtc2 src → IR1/2/3, SQR fires. */ /* Stage 1: mtc2 src → IR1/2/3, SQR fires. */
LdSlot_ mac_gte_sqr_v3s4(r.src_x, r.recip_est, r.t5.src_z, LdSlot_ nop), LdSlot_ mac_gte_sqr_v3s4(r.src.x, r.src.y, r.src.z, LdSlot_ nop),
/* Stage 2: mfc2 MAC1/2/3, sum, mtc2 LZCS. */ /* Stage 2: mfc2 MAC1/2/3, sum, mtc2 LZCS. src_ptr is dead; reuse as mac2. */
mac_gte_mv_from_data_r_mac123(r.t3.mac1_scratch, r.t4.mac2_scratch, r.norm), LdSlot_ nop, mac_gte_mv_from_data_r_mac123(r.mac1, r.mac2, r.mac3), LdSlot_ nop,
add_u_self( r.norm, r.t3.mac1_scratch), add_u_self( r.v_sqr_sum, r.mac1),
add_u_self( r.norm, r.t4.mac2_scratch), add_u_self( r.v_sqr_sum, r.mac2),
gte_mv_to_data_r( r.norm, C2_LZCS), GteDelay_ nop2, gte_mv_to_data_r( r.v_sqr_sum, C2_LZCS), GteDelay_ nop2,
gte_mv_from_data_r(r.shift, C2_LZCR), GteDelay_ nop, gte_mv_from_data_r(r.lzcr_raw, C2_LZCR), GteDelay_ nop,
/* Stage 3: round LZCR to even, compute half-shift, align |v|² to bit 24. /* Stage 3: even(LZCR), half-shift, align |v|² to bit 24. */
* r_norm holds |v|² sum; r_shift holds the LZCR count from mfc2. mac_lzcr_round_even_half_shift(r.lzcr_raw, r.v_sqr_sum, r.v_sqr_aligned),
* After the component: r_shift = even(LZCR), r_norm = half-shift, r_mac1_scratch = |v|². */ add_si( r.align_delta, r.lzcr_even, -24),
mac_lzcr_round_even_half_shift(r.shift, r.norm, r.t3.mac1_scratch), branch_lt_zero(r.align_delta, atom_offset(aligned_done, srav_path)), BdSlot_ nop, /* bltz → srav_path (LZCR < 24 path) */
/* r_branch_tmp = LZCR - 24 (overwrites r_branch_tmp; src.z no longer needed after SQR) */
add_si( r.t5.btarget, r.shift, -24),
branch_lt_zero(r.t5.btarget, atom_offset(aligned_done, srav_path)), BdSlot_ nop, /* bltz → srav_path (LZCR < 24 path) */
jump_rel(atom_offset(srav_path, aligned_done)), /* b → aligned_done (LZCR >= 24 path) */ jump_rel(atom_offset(srav_path, aligned_done)), /* b → aligned_done (LZCR >= 24 path) */
BdSlot_ shift_lleft_var(r.t3.mac1_scratch, r.t3.mac1_scratch, r.t5.btarget), /* src=sum (r_mac1_scratch), dst=same */ BdSlot_ shift_lleft_var(r.v_sqr_aligned, r.v_sqr_aligned, r.align_delta),
atom_label(srav_path) atom_label(srav_path)
li_s( r.t5.shift_count, 24), li_s( r.shift_count, 24),
sub_s(r.t5.shift_count, r.t5.shift_count, r.shift), sub_s(r.shift_count, r.shift_count, r.lzcr_even),
shift_aright_var(r.t3.mac1_scratch, r.t3.mac1_scratch, r.t5.shift_count), /* src=sum (r_mac1_scratch), dst=same */ shift_aright_var(r.v_sqr_aligned, r.v_sqr_aligned, r.shift_count),
atom_label(aligned_done) atom_label(aligned_done)
// Save the shift count to r_shift before the next 5 instructions overwrite r_norm (the sqrtbl lookup loads 1/|v| into r_norm, which becomes IR0 in stage 4). add_si( r.sqrtbl_byte_offset, r.v_sqr_aligned, -64),
or_u(r.shift, r.norm, 0), /* r_shift ← shift count (preserved through stage 4) */ shift_lleft(r.sqrtbl_byte_offset, r.sqrtbl_byte_offset, 1),
/* r_mac1_scratch holds |v|² aligned (top bit at bit 7). */ mac_load_word_imm(r.sqrtbl_lookup, & gte_normalize_sqr_tbl), add_u_self(r.sqrtbl_lookup, r.sqrtbl_byte_offset),
add_si( r.t3.mac1_scratch, r.t3.mac1_scratch, -64), load_half(r.inv_len, r.sqrtbl_lookup, 0),
shift_lleft(r.t3.mac1_scratch, r.t3.mac1_scratch, 1), LdSlot_ nop,
mac_load_word_imm(r.t5.lookup_addr, & gte_normalize_sqr_tbl), add_u_self(r.t5.lookup_addr, r.t3.mac1_scratch),
load_half(r.norm, r.t5.lookup_addr, 0), /* r_norm = sqrtbl[aligned-64] = 1/|v| (IR0 in stage 4) */
/* r_branch_tmp held the sqrtbl base+index, NOT src.z. Reload src.z from scratch now that r_branch_tmp is free. */ mac_gte_general_purpose_interopolation(r.inv_len,
LdSlot_ load_word(r.t5.src_z, r.src_ptr, O_(V3_S4,z)), /* r_branch_tmp = src.z (for IR3 in stage 4) */ r.src.x, r.src.y, r.src.z,
r.res.x, r.res.y, r.res.z,
/* Stage 4: GPF + srav finalize (r_shift = shift count, r_norm = 1/|v|). */ GteDelay_ load_word(R_AtomJmp, R_TapePtr, 0), LdSlot_ // ac_yield: word 1
LdSlot_ mac_gte_general_purpose_interopolation( GteDelay_ add_ui_self( R_TapePtr, S_(MipsCode)) // ac_yield: word
r.norm,
r.src_x, /* IR1 = src.x (preserved in r_tmp — r_mac2_scratch was clobbered to MAC2 in stage 1.5) */
r.recip_est,
r.t5.src_z, /* IR3 = src.z (reloaded) */
r.t4.mac2_scratch, r.recip_est, r.t5.src_z,
GteDelay_ nop,
GteDelay_ nop
), ),
/* sra by r_shift = (31-LZCR)/2 (saved before sqrtbl lookup) */ mac_shift_aright_var_v3s4_self(r.res, r.srav_shift),
mac_shift_aright_var_v3_self(r.t4.mac2_scratch, r.recip_est, r.t5.src_z, r.shift), mac_store_v3s4(r.res, r.dst_ptr, 0),
/* Store result.x/y/z to r_dst_ptr (caller-determined dst address). */
mac_store_word_v3(r.t4.mac2_scratch, r.recip_est, r.t5.src_z, r.dst_ptr, 0),
mac_yield() jump_reg(R_AtomJmp), BdSlot_ nop // ac_yield: word 3-4
}) })
/* ─── GTE OP cross product (a × b → out) ─── /* ─── GTE OP cross product (a × b → out) ───
* Generalized V3_S4 cross product via GTE OP (OuterProduct12 libpsyx convention). * Generalized V3_S4 cross product via GTE OP (OuterProduct12 libpsyx convention).
* The >> 12 shift converts S12.20 → S12.0 OuterProduct12. */ * The >> 12 shift converts S12.20 → S12.0 OuterProduct12. */
@@ -363,21 +369,21 @@ typedef Struct_(Binds_gte_cross_v3s4) { V3_S4* src_a; V3_S4* src_b; V3_S4* out;
typedef Struct_(RegUse_gte_cross_v3s4) { typedef Struct_(RegUse_gte_cross_v3s4) {
Reg_(V3_S4) a; Reg_(V3_S4) a;
Reg_(V3_S4) b; Reg_(V3_S4) b;
union { Reg out, t0; } x; Reg out;
union { Reg src_a, t1, rt11; } y; Reg src_a;
union { Reg src_b, t2, rt22; } z; Reg src_b;
}; };
internal MipsAtom* gte_cross_v3s4(AtomArena_R aa, RegUse_gte_cross_v3s4 r) internal MipsAtom* gte_cross_v3s4(AtomArena_R aa, RegUse_gte_cross_v3s4 r)
atom_info(atom_bind(Binds_gte_cross_v3s4)) MipsAtom_Proc_(aa, { atom_info(atom_bind(Binds_gte_cross_v3s4)) MipsAtom_Proc_(aa, {
load_word(r.y.src_a, R_TapePtr, O_(Binds_gte_cross_v3s4,src_a)), load_word(r.src_a, R_TapePtr, O_(Binds_gte_cross_v3s4,src_a)),
load_word(r.z.src_b, R_TapePtr, O_(Binds_gte_cross_v3s4,src_b)), load_word(r.src_b, R_TapePtr, O_(Binds_gte_cross_v3s4,src_b)),
load_word(r.x.out, R_TapePtr, O_(Binds_gte_cross_v3s4,out)), load_word(r.out, R_TapePtr, O_(Binds_gte_cross_v3s4,out)),
LdSlot_ add_ui_self(R_TapePtr, S_(Binds_gte_cross_v3s4)), LdSlot_ add_ui_self(R_TapePtr, S_(Binds_gte_cross_v3s4)),
mac_load_v3s4(r.a, r.y.src_a, 0), LdSlot_ mac_load_v3s4(r.a, r.src_a, 0), LdSlot_
mac_load_v3s4(r.b, r.z.src_b, 0), LdSlot_ mac_load_v3s4(r.b, r.src_b, 0), LdSlot_
mac_gte_op_cross_v3s4(r.a, r.b), /* RT diagonal + IR + OP + MAC read + shift */ mac_gte_op_cross_v3s4(r.a, r.b), /* RT diagonal + IR + OP + MAC read + shift */
mac_store_v3s4(r.a, r.x.out, 0), mac_store_v3s4(r.a, r.out, 0),
mac_yield() mac_yield()
}) })
+17 -42
View File
@@ -1,21 +1,10 @@
/* ============================================================================ /* ============================================================================
* duffle DSL Suffix Conventions * duffle DSL Suffix Conventions
* ============================================================================ * ============================================================================
*
* Every mnemonic in this header follows the same suffix grammar: * Every mnemonic in this header follows the same suffix grammar:
* *
* Primitive commands: gp0_cmd_poly_f3 = 0x20 (byte opcode) * Primitive commands: gp0_cmd_poly_f3 = 0x20 (byte opcode)
* Packed 32-bit cmd: gp0_word_poly_f3(r, g, b) (32-bit, shifted) * Packed 32-bit cmd: gp0_word_poly_f3(r, g, b) (32-bit, shifted)
*
* Type ordering: domain?_(direction)?_action_target_modifier_type?
* Examples: add_ui (add + unsigned + immediate)
* add_s (add + signed, R-type implicit)
* shift_lleft (shift + logical + left)
* shift_aright (shift + arithmetic + right)
* call_reg(rs) (call + register, $ra implicit)
* gte_mv_to_data_r (gte + mv + to + data + register)
* gte_lw_v0_xy(base) (gte + lw + v0 + xy)
* load_upper_i (load-upper + immediate, unique verb)
* ============================================================================ */ * ============================================================================ */
#ifdef INTELLISENSE_DIRECTIVES #ifdef INTELLISENSE_DIRECTIVES
@@ -29,21 +18,7 @@
/* ============================================================================ /* ============================================================================
* gte.h — Geometry Transformation Engine (COP2) for the PS1 * gte.h — Geometry Transformation Engine (COP2) for the PS1
* ============================================================================ * ============================================================================
* * DSL for emitting GTE/MIPS instruction words from C.
* Hand-rolled DSL for emitting GTE/MIPS instruction words from C.
* No GCC inline-assembly string syntax in the code body.
*
* STYLE NOTES
* -----------
* - Per-field encoders are named `enc_gte_<field>(value)` and each one self-masks its argument before shifting.
* Mirrors the `enc_op / enc_rs / enc_rt / ...` family in mips.h.
* - The composite `enc_gte_cmdw(sf, mx, v, cv, lm, cmd)` is a flat OR of the per-field encoders, plus the COP2/CO base.
* - Pre-baked shortcuts (`gte_cmd_rtpt`, `gte_cmd_rtps`, …) are defined for the common cases so call sites read like assembly source.
* - All register/field values are enums (not `#define`s) so they show up in debugger symbol tables and IDE autocomplete.
*
* SEE ALSO
* --------
* - mips.h: The MIPS encoder layer this builds on.
*/ */
/* C2 data registers */ /* C2 data registers */
@@ -170,20 +145,20 @@ enum {
* +------------+--+-----+------+------+------+------+---+--------+----------+ * +------------+--+-----+------+------+------+------+---+--------+----------+
* \_____ GTE_PAYLOAD _____/ \__ GTE_CMD __/ * \_____ GTE_PAYLOAD _____/ \__ GTE_CMD __/
* *
* Shifts/masks below are the *bit positions* and *bit widths* of each configurable field, used by the ENC_GTE_CMD encoder. * Offset position & masks below are the *bit positions* and *bit widths* of each configurable field, used by the ENC_GTE_CMD encoder.
* Mirrors the OPCODE_SHIFT / RS_SHIFT convention used in mips.h. * Mirrors the OPCODE_POS / RS_POS convention used in mips.h.
*/ */
gte_shift_sf = 19, gte_width_sf = 1, gte_pos_sf = 19, gte_width_sf = 1,
gte_shift_mx = 17, gte_width_mx = 2, gte_pos_mx = 17, gte_width_mx = 2,
gte_shift_v = 15, gte_width_v = 2, gte_pos_v = 15, gte_width_v = 2,
gte_shift_cv = 13, gte_width_cv = 2, gte_pos_cv = 13, gte_width_cv = 2,
gte_shift_lm = 10, gte_width_lm = 1, gte_pos_lm = 10, gte_width_lm = 1,
gte_shift_cmd = 0, gte_width_cmd = 6, gte_pos_cmd = 0, gte_width_cmd = 6,
/* Fake command number (bits 24-20) — IGNORED by the GTE hardware per PSX-SPX `geometrytransformationenginegte.md` line 48. /* Fake command number (bits 24-20) — IGNORED by the GTE hardware per PSX-SPX `geometrytransformationenginegte.md` line 48.
* libgte's compiler emits non-zero values in this field as a disassembly signature. */ * libgte's compiler emits non-zero values in this field as a disassembly signature. */
gte_shift_fake_cmd = 20, gte_pos_fake_cmd = 20,
gte_width_fake_cmd = 5, gte_width_fake_cmd = 5,
}; };
@@ -340,13 +315,13 @@ enum { _C2_TX_SUBS_ = 0
#define gte_cmd_base (enc_op(op_cop2) | (1 << 25)) #define gte_cmd_base (enc_op(op_cop2) | (1 << 25))
/* Per-field encoders. Each one does (value & mask) << shift on its own. */ /* Per-field encoders. Each one does (value & mask) << shift on its own. */
#define enc_gte_sf(sf) ((sf) << gte_shift_sf ) #define enc_gte_sf(sf) ((sf) << gte_pos_sf )
#define enc_gte_mx(mx) ((mx) << gte_shift_mx ) #define enc_gte_mx(mx) ((mx) << gte_pos_mx )
#define enc_gte_v(v) ((v) << gte_shift_v ) #define enc_gte_v(v) ((v) << gte_pos_v )
#define enc_gte_cv(cv) ((cv) << gte_shift_cv ) #define enc_gte_cv(cv) ((cv) << gte_pos_cv )
#define enc_gte_lm(lm) ((lm) << gte_shift_lm ) #define enc_gte_lm(lm) ((lm) << gte_pos_lm )
#define enc_gte_cmd(cmd) ((cmd) << gte_shift_cmd ) #define enc_gte_cmd(cmd) ((cmd) << gte_pos_cmd )
#define enc_gte_fake_cmd(x) ((x) << gte_shift_fake_cmd) #define enc_gte_fake_cmd(x) ((x) << gte_pos_fake_cmd)
/* Composite: all six GTE fields + the COP2/CO base. */ /* Composite: all six GTE fields + the COP2/CO base. */
#define enc_gte_cmdw(sf, mx, v, cv, lm, cmd) ( \ #define enc_gte_cmdw(sf, mx, v, cv, lm, cmd) ( \
+1 -1
View File
@@ -2,7 +2,7 @@
# include "gen/macs.h" # include "gen/macs.h"
# include "gen/offsets.h" # include "gen/offsets.h"
# include "math.h" # include "math.h"
# include "lottes_tape.h" # include "tape.h"
#endif #endif
ATOM_FILE_DEBUGGER_LINE_MARKER(math_atom_c); ATOM_FILE_DEBUGGER_LINE_MARKER(math_atom_c);
+16 -17
View File
@@ -5,7 +5,7 @@
#define MEM_ALIGNMENT_DEFAULT 4 #define MEM_ALIGNMENT_DEFAULT 4
#define assert_bounds(point, start, end) for(;0;){ \ #define assert_bounds(point, start, end) do{ \
assert((start) <= (point)); \ assert((start) <= (point)); \
assert((point) <= (end)); \ assert((point) <= (end)); \
} while(0) } while(0)
@@ -23,10 +23,10 @@ FI_ void mem_bump(U4 cap, U4*R_ used, U4 amount) {
used[0] += amount; used[0] += amount;
} }
FI_ U4 mem_copy (U4 dest, U4 src, U4 len) { return (U4)(__builtin_memcpy ((void*)dest, (void const*)src, len)); } FI_ U4 mem_copy (U1_R dest, U1_R src, U4 len) { return (U4)(__builtin_memcpy ((void*)dest, (void const*)src, len)); }
FI_ U4 mem_copy_overlapping(U4 dest, U4 src, U4 len) { return (U4)(__builtin_memmove((void*)dest, (void const*)src, len)); } FI_ U4 mem_copy_overlapping(U1* dest, U1* src, U4 len) { return (U4)(__builtin_memmove((void*)dest, (void const*)src, len)); }
FI_ U4 mem_fill (U4 dest, U4 value, U4 len) { return (U4)(__builtin_memset ((void*)dest, (int) value, len)); } FI_ U4 mem_fill (U1_R dest, U4 value, U4 len) { return (U4)(__builtin_memset ((void*)dest, (int) value, len)); }
FI_ B4 mem_zero (U4 dest, U4 len) { if(dest == 0){return false;} mem_fill(dest, 0, len); return true; } FI_ B4 mem_zero (U1_R dest, U4 len) { if(dest == 0){return false;} mem_fill(dest, 0, len); return true; }
#pragma region DAG #pragma region DAG
@@ -58,31 +58,30 @@ typedef Struct_(Str8) { UTF8* ptr; U4 len; };
typedef Struct_(Slice_Str8) { Str8* ptr; U4 len; }; typedef Struct_(Slice_Str8) { Str8* ptr; U4 len; };
#define slit(string_literal) (Str8){ (UTF8*) string_literal, S_(string_literal) - 1 } #define slit(string_literal) (Str8){ (UTF8*) string_literal, S_(string_literal) - 1 }
typedef Struct_(Slice) { B1* ptr; U4 len; }; // Untyped Slice (byte-addressable; .len in elements) typedef Struct_(Slice) { U1* ptr; U4 len; };
FI_ Slice slice_ut_(U4 ptr, U4 len) { return (Slice){(B1*)ptr, len}; } FI_ Slice slice_ut_(U1* ptr, U4 len) { return (Slice){ptr, len}; }
#define Slice_(type) Struct_(tmpl(Slice,type)) { type* ptr; U4 len; } #define Slice_(type) Struct_(tmpl(Slice,type)) { type* ptr; U4 len; }
typedef Slice_(B1);
#define slice_assert(s) do { assert((s).ptr != 0); assert((s).len > 0); } while(0) #define slice_assert(s) do { assert((s).ptr != 0); assert((s).len > 0); } while(0)
#define slice_end(slice) ((slice).ptr + S_slice(slice) / S_(B1)) /* byte-ptr arithmetic; .len is in elements per slice convention */ #define slice_end(slice) ((slice).ptr + S_slice(slice))
#define S_slice(s) ((s).len * S_((s).ptr[0])) #define S_slice(s) ((s).len * S_((s).ptr[0]))
#define slice_ut(ptr,len) slice_ut_(u4_(ptr), u4_(len)) #define slice_ut(ptr,len) slice_ut_(C_(U1*,ptr), u4_(len))
#define slice_ut_arr(a) slice_ut_(u4_(a), S_(a)) #define slice_ut_arr(a) slice_ut_(C_(U1*,a), S_(a))
#define slice_to_ut(s) slice_ut_(u4_((s).ptr), S_slice(s)) #define slice_to_ut(s) slice_ut_(C_(U1*,(s).ptr), S_slice(s))
#define slice_iter(container, iter) (T_((container).ptr) iter = (container).ptr; iter != slice_end(container); ++ iter) #define slice_iter(container, iter) (T_((container).ptr) iter = (container).ptr; iter != slice_end(container); ++ iter)
#define slice_arg_from_array(type, ...) & (tmpl(Slice,type)) { .ptr = Array_decl(type,__VA_ARGS__), .len = Array_len( Array_decl(type,__VA_ARGS__)) } #define slice_arg_from_array(type, ...) & (tmpl(Slice,type)) { .ptr = Array_decl(type,__VA_ARGS__), .len = Array_len( Array_decl(type,__VA_ARGS__)) }
#define slice_from_array(type, array) (tmpl(Slice,type)) { .ptr = array, .len = Array_len(array) } #define slice_from_array(type, array) (tmpl(Slice,type)) { .ptr = array, .len = Array_len(array) }
FI_ void slice_zero_(Slice s) { slice_assert(s); mem_zero(u4_(s.ptr), s.len); } FI_ void slice_zero_(Slice s) { slice_assert(s); mem_zero(s.ptr, s.len); }
#define slice_zero(s) slice_zero_(slice_to_ut(s)) #define slice_zero(s) slice_zero_(slice_to_ut(s))
FI_ void slice_copy_(Slice dest, Slice src) { FI_ void slice_copy_(Slice dest, Slice src) {
assert(S_slice(dest) >= S_slice(src)); assert(S_slice(dest) >= S_slice(src));
slice_assert(dest); slice_assert(dest);
slice_assert(src); slice_assert(src);
mem_copy(u4_(dest.ptr), u4_(src.ptr), S_slice(src)); mem_copy(dest.ptr, src.ptr, S_slice(src));
} }
#define slice_copy(dest, src) do { \ #define slice_copy(dest, src) do { \
static_assert(T_same(dest, src)); \ static_assert(T_same(dest, src)); \
@@ -95,6 +94,7 @@ FI_ Slice slice_bump(U4_R used, U4 start, U4 len, U4 amount) {
return slice_ut(ptr, amount); return slice_ut(ptr, amount);
} }
typedef Slice_(B1);
typedef Slice_(U1); typedef Slice_(U1);
typedef Slice_(U4); typedef Slice_(U4);
@@ -117,7 +117,7 @@ I_ Slice farena_push(FArena_R arena, U4 amount, Opt_farena o) {
U4 to_commit = align_pow2(desired, o.alignment ? o.alignment : MEM_ALIGNMENT_DEFAULT); U4 to_commit = align_pow2(desired, o.alignment ? o.alignment : MEM_ALIGNMENT_DEFAULT);
U4 ptr = arena->start + arena->used; U4 ptr = arena->start + arena->used;
mem_bump(arena->capacity, & arena->used, to_commit); mem_bump(arena->capacity, & arena->used, to_commit);
return (Slice){ (B1*)ptr, to_commit }; return (Slice){ (U1*)ptr, to_commit };
} }
FI_ void farena_reset (FArena_R arena) { arena->used = 0; } FI_ void farena_reset (FArena_R arena) { arena->used = 0; }
FI_ void farena_rewind(FArena_R arena, U4 save_point) { FI_ void farena_rewind(FArena_R arena, U4 save_point) {
@@ -134,8 +134,7 @@ FI_ U4 farena_unused_start(FArena arena) { return arena.start + arena.used; }
#pragma region BIOS Scratchpad #pragma region BIOS Scratchpad
/* BIOS scratchpad location. 1 KB at 0x1F800000. /* BIOS scratchpad location. 1 KB at 0x1F800000.
* TapeHostFrame occupies the final 44 bytes while tape code executes. * TapeHostFrame occupies the final 44 bytes while tape code executes. */
* Atom scratch is bounded by the TapeHostFrame_Loc declaration in lottes_tape.h. */
enum { enum {
Scratchpad_Loc = 0x1F800000, Scratchpad_Loc = 0x1F800000,
Scratchpad_Len = 0x400, /* 1 KB */ Scratchpad_Len = 0x400, /* 1 KB */
+13 -6
View File
@@ -3,7 +3,7 @@
# include "gen/offsets.h" # include "gen/offsets.h"
# include "bios.h" # include "bios.h"
# include "mips.h" # include "mips.h"
# include "lottes_tape.h" # include "tape.h"
#endif #endif
ATOM_FILE_DEBUGGER_LINE_MARKER(mips_atom_c); ATOM_FILE_DEBUGGER_LINE_MARKER(mips_atom_c);
@@ -55,9 +55,15 @@ FI_ Slice_MipsCode ac_shift_aright_var_v3s4_self(AtomBuilder_R ab, Reg_(V3_S4) d
* 3. $t0 = bios_table_addr ; t0 = &BIOS A-function table * 3. $t0 = bios_table_addr ; t0 = &BIOS A-function table
* 4. jalr $t0, $ra ; call BIOS(flushcache) * 4. jalr $t0, $ra ; call BIOS(flushcache)
* nop ; branch delay slot * nop ; branch delay slot
* 5. lw $ra, 4($sp); jr $ra ; restore & return * 5. lw $ra, 4($sp)
* 6. sp += 8 * 6. sp += 8 ; load-delay
* 7. jr $ra
* nop ; BD
*/ */
#if 0
// Note: Can't do this without having a way to do C-Runtime frame call from Tape ABI.
// Don't support this without adjusting scratchpad to save tape frame in some way.
internal MipsAtom_(mips_flush_icache) { internal MipsAtom_(mips_flush_icache) {
add_ui(R_SP, R_SP, -MipsStackAlignment), // sp -= 8 add_ui(R_SP, R_SP, -MipsStackAlignment), // sp -= 8
store_word(R_RA, R_SP, S_(U4)), // sw $ra, 4($sp) store_word(R_RA, R_SP, S_(U4)), // sw $ra, 4($sp)
@@ -65,9 +71,10 @@ internal MipsAtom_(mips_flush_icache) {
add_ui(R_T0, R_0, bios_table_addr), // addiu $t0, $0, 0xA0 add_ui(R_T0, R_0, bios_table_addr), // addiu $t0, $0, 0xA0
jump_link(R_T0, R_RA), nop, // jalr $t0, $ra, BD slot jump_link(R_T0, R_RA), nop, // jalr $t0, $ra, BD slot
load_word(R_RA, R_SP, S_(U4)), // lw $ra, 4($sp) load_word(R_RA, R_SP, S_(U4)), // lw $ra, 4($sp)
jump_reg(R_RA), // jr $ra add_ui(R_SP, R_SP, MipsStackAlignment), // sp += 8 (load-delay)
add_ui(R_SP, R_SP, MipsStackAlignment), // sp += 8 (BD) jump_reg(R_RA), nop, // jr $ra, BD slot
mac_yield(), // mac_yield(),
}; };
#endif
#pragma endregion Baked Atoms #pragma endregion Baked Atoms
+15 -19
View File
@@ -81,13 +81,8 @@ enum {
* (e.g. for asm clobber lists and register-variable declarations via `rgcc(R_X)`). * (e.g. for asm clobber lists and register-variable declarations via `rgcc(R_X)`).
* The enum value is bound to the `#define` so the two forms cannot drift apart. * The enum value is bound to the `#define` so the two forms cannot drift apart.
* *
* Only registers that get stringified need a `_Code` form; the rest are plain enum values.
* If you need to add a new one, follow the pattern:
* #define R_T7_Code 15
* R_T7 = R_T7_Code, // in the enum
*
* User code should always reference the enum form (`R_T4`) at arithmetic sites and let * User code should always reference the enum form (`R_T4`) at arithmetic sites and let
* `rlit(R_T4_Code)` / `rgcc(R_T4)` handle the stringify cases — never write the bare number `12`. * `rlit(R_T4_Code)` / `rgcc(R_T4)` handle the stringify cases
* ============================================================================ */ * ============================================================================ */
#define R_0_Code 0 #define R_0_Code 0
#define R_AT_Code 1 #define R_AT_Code 1
@@ -252,12 +247,12 @@ enum {
enum { _BitOffsets = 0 enum { _BitOffsets = 0
/* Bit Offsets for MIPS Instruction Fields */ /* Bit Offsets for MIPS Instruction Fields */
, OPCODE_SHIFT = 26 , OPCODE_POS = 26
, RS_SHIFT = 21 , RS_POS = 21
, RT_SHIFT = 16 , RT_POS = 16
, RD_SHIFT = 11 , RD_POS = 11
, SHAMT_SHIFT = 6 /* Shift Amount */ , SHAMT_POS = 6 /* Shift Amount: Offset Position */
, FC_SHIFT = 0 , FC_POS = 0
/* IMM_MASK is the 16-bit two's-complement truncation for the immediate field. /* IMM_MASK is the 16-bit two's-complement truncation for the immediate field.
* It is NOT a range guard — it is load-bearing for negative branch offsets * It is NOT a range guard — it is load-bearing for negative branch offsets
@@ -268,12 +263,12 @@ enum { _BitOffsets = 0
, IMM_MASK = 0xFFFF , IMM_MASK = 0xFFFF
}; };
#define enc_op(op) ((op) << OPCODE_SHIFT) #define enc_op(op) ((op) << OPCODE_POS)
#define enc_rs(rs) ((rs) << RS_SHIFT) #define enc_rs(rs) ((rs) << RS_POS)
#define enc_rt(rt) ((rt) << RT_SHIFT) #define enc_rt(rt) ((rt) << RT_POS)
#define enc_rd(rd) ((rd) << RD_SHIFT) #define enc_rd(rd) ((rd) << RD_POS)
#define enc_shamt(shamt) ((shamt) << SHAMT_SHIFT) #define enc_shamt(shamt) ((shamt) << SHAMT_POS)
#define enc_fc(fc) ((fc) << FC_SHIFT) #define enc_fc(fc) ((fc) << FC_POS)
#define enc_imm(imm) ((imm) & IMM_MASK) #define enc_imm(imm) ((imm) & IMM_MASK)
/* MIPS R-Type Instruction Format (Register-to-Register) */ /* MIPS R-Type Instruction Format (Register-to-Register) */
@@ -586,6 +581,7 @@ enum { _BitOffsets = 0
, jump_link(rtmp_0, rret_addr) \ , jump_link(rtmp_0, rret_addr) \
, nop \ , nop \
, load_word(rret_addr, rstack_ptr, 4) \ , load_word(rret_addr, rstack_ptr, 4) \
, jump_reg(rret_addr) \
, add_ui(rstack_ptr, rstack_ptr, MipsStackAlignment) \ , add_ui(rstack_ptr, rstack_ptr, MipsStackAlignment) \
, jump_reg(rret_addr) \
, nop \
) asm_clobber: clbr_volatile_gprs ) ) asm_clobber: clbr_volatile_gprs )
+1 -1
View File
@@ -3,7 +3,7 @@
# include "gen/offsets.h" # include "gen/offsets.h"
# include "mips.h" # include "mips.h"
# include "dsl.atom.h" # include "dsl.atom.h"
# include "lottes_tape.h" # include "tape.h"
# include "pad.h" # include "pad.h"
#endif #endif
+2 -11
View File
@@ -6,16 +6,10 @@
# include "pad.h" # include "pad.h"
#endif #endif
/* Uses ONE 8-byte frame allocated via the compiler's standard prologue. /* Uses an 8-byte frame allocated via the compiler's standard prologue.
* 4 wasted-arg words for B(12h) InitPAD2 are at [SP+0..15] but are not explicitly allocated. * 4 wasted-arg words for B(12h) InitPAD2 are at [SP+0..15] but are not explicitly allocated.
* Compiler handles the MIPS O32 "wasted stack" convention for us by treating the B-call as a 4-arg call. * Compiler handles the MIPS O32 "wasted stack" convention for us by treating the B-call as a 4-arg call.
* */
* The buffer pointers are passed as arguments so the compiler keeps them in callee-saved registers;
* The B(12h) asm volatile block does NOT clobber those registers (it clobbers only the volatile GPRs + B-table arg registers explicitly).
* The C-level writes after the call re-load the pointers from their callee-saved homes.
*
* The clobber list for both B-calls names the full BIOS destroy set documented in kernelbios.md:167-174 (R1..R15, R24..R25, R31, HI/LO).
* The kernel-ABI "volatile GPRs" subset is clb_mem_drain; the rest of the destroy set is enumerated explicitly here. */
NI_ void pad_bios_init_start(PadBiosRaw* raw0, PadBiosRaw* raw1) NI_ void pad_bios_init_start(PadBiosRaw* raw0, PadBiosRaw* raw1)
{ {
/* Pin raw0 + raw1 to $a0 + $a1 via rgcc; the B(12h) call uses these directly. /* Pin raw0 + raw1 to $a0 + $a1 via rgcc; the B(12h) call uses these directly.
@@ -24,9 +18,6 @@ NI_ void pad_bios_init_start(PadBiosRaw* raw0, PadBiosRaw* raw1)
register PadBiosRaw* p1 rgcc(R_A1) = raw1; register PadBiosRaw* p1 rgcc(R_A1) = raw1;
(void)p0; (void)p1; (void)p0; (void)p1;
// TODO(Ed): Properly annotate the raw values in the inline asm instructions.
// Use enums.
/* B(12h) InitPAD2(raw0, 0x22, raw1, 0x22) /* B(12h) InitPAD2(raw0, 0x22, raw1, 0x22)
* $a0 = raw0 (rgcc-bound; survives the sequence below) * $a0 = raw0 (rgcc-bound; survives the sequence below)
* $a1 = raw1 (preserved into $a2 before $a1 is overwritten) * $a1 = raw1 (preserved into $a2 before $a1 is overwritten)
+2 -9
View File
@@ -4,10 +4,7 @@
# include "math.h" # include "math.h"
#endif #endif
/* PSX button bit positions — 1:1 with PSX-SPX docs at docs/psx-spx/docs/controllersandmemorycards.md:405-421. // PSX button bit positions: PSX-SPX docs/psx-spx/docs/controllersandmemorycards.md:405-421.
* Wire is active-low (0 = pressed).
* The decoder atom computes buttons = (~raw_buttons) & 0xFFFF;
* active-low-to-active-high inversion is applied bit-by-bit. */
typedef Enum_(U2, PadBtns) { typedef Enum_(U2, PadBtns) {
Bit_(Pad_Select, 0), Bit_(Pad_Select, 0),
Bit_(Pad_L3, 1), Bit_(Pad_L3, 1),
@@ -62,11 +59,7 @@ typedef Enum_(U4, PadStatus) {
PadStatus_Invalid, PadStatus_Invalid,
}; };
/* Distinct from the game-facing PadStatus enum: PadRawStatus_Ok and PadRawStatus_Timeout are raw BIOS values; // Distinct from the game-facing PadStatus enum: PadRawStatus_Ok and PadRawStatus_Timeout are raw BIOS values
* PadStatus_* are game-facing post-decode states. PadUnknownId_Sentinel is written by the decoder
* when the controller id does not match any known controller type.
* PadAxisCentered_Word: Four-byte 0x80 pattern used to clear / center
* four byte axes at PadState.left_x through PadState.right_y. */
typedef Enum_(U1, PadRawStatus) { typedef Enum_(U1, PadRawStatus) {
PadRawStatus_Ok = 0x00, PadRawStatus_Ok = 0x00,
PadRawStatus_Timeout = 0xFF, PadRawStatus_Timeout = 0xFF,
+1 -1
View File
@@ -104,7 +104,7 @@ void gte_matrix_set_translation(MT3_S2S4* mat) asm("SetTransMatrix");
// Einheit, Metrication to unit vector. "Normalization", not Orthogonal "Normal, Normalis". Directionalization. // Einheit, Metrication to unit vector. "Normalization", not Orthogonal "Normal, Normalis". Directionalization.
// RGA(Lengyel): Normalize the bulk of a zero-weight direction. This is not finite-point unitization (which forces w=1). // RGA(Lengyel): Normalize the bulk of a zero-weight direction. This is not finite-point unitization (which forces w=1).
S4 normalize_v3s4(V3_S4* v0, V3_S4* v1) asm("VectorNormal"); S4 psy_normalize_v3s4(V3_S4* v0, V3_S4* v1) asm("VectorNormal");
// RGA(Lengyel): Apply the matrix expansion of a rigid transformation. // RGA(Lengyel): Apply the matrix expansion of a rigid transformation.
// Motor antiproduct is equivalent for unitized points; LA form is what GTE consumes. // Motor antiproduct is equivalent for unitized points; LA form is what GTE consumes.
@@ -13,56 +13,32 @@
#pragma region Tape Drive #pragma region Tape Drive
/* ----------------------------------------------------------------------------------------------------------- /* -----------------------------------------------------------------------------------------------------------
* TAPE DRIVE ABI * THREADED ATOMS - TAPE EXECUTION & ABI
* _________
* | ___ |
* | o___o | ,-----<-----.
* |__/___\__| V ^
* \_[Enter]_[A]->[A]->[A]->[A(B)]->[A]->[Exit]
* ----------------------------------------------------------------------------------------------------------- * -----------------------------------------------------------------------------------------------------------
* Note(Ed): One of the main purposes of this codebase is to help me learn this, * This ABI and its associated legos were directly inspired by researching the work of Timothy Lottes and
* as such the information below may not* be entirely realized or finalized conceptually. * Onat Türkçüoğlu; Forth, threaded code system, and various other people or programming techniques.
* ----------------------------------------------------------------------------------------------------------- *
* This ABI and its associated legos were directly inspired by researching the work of * The setup is simple:
* Timothy Lottes and Onat Türkçüoğlu; along with many others. It's the simplest bootstrap of a * A tape is a linear stream containing addresses of directly executable native-code fragments ("Atoms").
* directly executed chain of assemby arrays (Atoms) that terminate with a yield sequence to the next atom. * Most atoms terminate in a small yield sequence which loads the next atom address from the tape.
* These eventually lead to a terminal atom for the tape which is defined below as "tape_exit". * It's a runtime composed of directly executed native machine-code sequences (Atoms) that usually terminate
* in a yield sequence to the next atom. These eventually lead to a terminal atom for the tape
* which is defined below as "tape_exit". Traditionally referred to as Direct Threaded Execution.
* *
* It behaves as one of the simplest runtime harnesses ontop of a host-enviornment's execution engine * It behaves as one of the simplest runtime harnesses ontop of a host-enviornment's execution engine
* to author and compose programs with. From here various conventions can be further applied. * to author and compose programs with. From here various conventions can be further applied.
* To make things easier to understand it may be better to focus on what this ABI does not have. * To make things easier to understand it may be better to focus on what this ABI does not have.
* It does not have have any branching within the tape but relative branches within atoms or between atoms. * The tape itself does not have have any branching behavior.
* Branching nearly is always downstream. Automatic stack usage is non-existent. * Branches, loops, skips, or other control-flow policies must be implemented explicitly by atoms.
* Push/Pop, FIFO, or Arena/Bump data structures are used by atoms explicitly. * Push/Pop, FIFO, or Arena/Bump data structures are utilized by atoms explicitly.
* In it's current form with the C11 macro DSL, the user also has fullfill manual register allocation per atom. * There is no implicit call-stack, return stack, or per-atom stack-frame.
* * The user must also explictly handle register allocation per atom (by default).
* One of the remarkable things about utilizing this ABI is its essentially interopable with CPUs, GPUs, FPGA, * However they could procedurally automate it using metaprogramming functionality.
* or, basically anything from the 5th generation consoles and onward.
* The ABI directly reflects how all computational hardware must be architected in order to execute
* digital logic effectively on current era tech.
* On the PS1 we don't have access to a few features like multi-threading, speculative execution, or L3 cache;
* but, we can set the foundation for legoing whats required for eventually expanding this ABI's paradigm
* and core atoms to take those newer hardware features into account. For example, you can easily expand
* this to support wave-based execution model on a PS2 or PS3. Not having a stack or
* automatic register allocation means the user cannot ignore excessive argument shuffle across workload or
* waves and thier phases. Crossing ABI boundaries to other runtimes that do has obviouss penalties.
*
* Learning data-oriented code becomes a natural progression. Your not fighting a stack-based procedural
* paradigm that wants to argument shuffle. There is no ambiguity due to the lack of constraints, for example,
* on how the user may "call" a procedure in traditional random dispatch runtimes. The user does have to
* hammer down "rules" or patterns for massaging the compiler to dissolve those call frames; just to get
* the asesmbly into its desired form. The form is obvious, and once the user gets to author these compoonents
* it becomes a game of tetris.
*
* Another feature is this ABI is very compatible with bootstrapping and developing simple toolchains built off
* of bit-packed annotated command streams the user can directly author, maintatain, and immediately execute.
* That being like a color forth, or maybe something more familar like an immediate mode library
* for various systems such as GUIs. This can make the tetris less of a chore with some helpful policy
* generation for allocation of registers, helping to choose resuable components, designing DSL on the fly, etc.
* -----------------------------------------------------------------------------------------------------------
* TODO(Ed): We need pretty ascii diagrams and proper guides, articles, etc.
* -----------------------------------------------------------------------------------------------------------
* For now this ideation has just started functioning. I'm abusing C11 & a lua metaprogram to help establish
* a hybrid toolchain to ideate on a traditional text-based authoring UX for this paradigm.
* If pcsx-redux provides viable hot-reload and persistent data storage beyond save-states
* (just copying ram to filesystem), I can author a color forth to mess around with.
* With either an editor in-emulator or on the actual machine itself. Assembly is tedius,
* but I think this codebase most likely has a pretty ergonomic flavor worst case...
* */ * */
/* Register Allocation Info */ /* Register Allocation Info */
enum { enum {
@@ -99,8 +75,8 @@ enum {
R_Atom7 = R_T7, R_Atom7 = R_T7,
R_Atom8 = R_T8, R_Atom8 = R_T8,
R_Atom9 = R_T9, R_Atom9 = R_T9,
R_Atom10 = R_V0, // Tend to be used with gte DMAs R_Atom10 = R_V0, // Tend to be used with gte moves
R_Atom11 = R_V1, // Tend to be used with gte DMAs R_Atom11 = R_V1, // Tend to be used with gte moves
R_Atom12 = R_A0, R_Atom12 = R_A0,
R_Atom13 = R_A1, R_Atom13 = R_A1,
R_Atom14 = R_A2, R_Atom14 = R_A2,
@@ -121,7 +97,7 @@ typedef U2 Reg; // Register parameter used with atom or atom component procedure
typedef U4 const MipsCode; // Underlying type to mips asm words. typedef U4 const MipsCode; // Underlying type to mips asm words.
typedef Slice_(MipsCode); typedef Slice_(MipsCode);
typedef U4 const MipsAtom; // Underlying type to a mips atom defnition typedef U4 const MipsAtom; // Underlying type to a mips atom definition
typedef Slice_(MipsAtom); typedef Slice_(MipsAtom);
// Sometimes a user will define a bundle of atoms that represent a procedure of work as: // Sometimes a user will define a bundle of atoms that represent a procedure of work as:
@@ -163,6 +139,12 @@ typedef Slice_(MipsAtom);
// Used for trivial mappings from one atom component proc to the command of a more baser (meant for type-mapping) // Used for trivial mappings from one atom component proc to the command of a more baser (meant for type-mapping)
#define MipsAtomComp_ProcMap_(ab, base_command) atom_dbg_skip MipsAtomComp_Proc_(ab, {base_command }) #define MipsAtomComp_ProcMap_(ab, base_command) atom_dbg_skip MipsAtomComp_Proc_(ab, {base_command })
// WIP: Atoms Assocated closely with each other to form a tape procedure. (Maybe also a phase in a procedure/pipeline?)
#define AtomBundle_(name) Struct_(tmpl(AtomBundle,name))
#define AtomBundle_Len(name) S_(tmpl(AtomBundle,name))/S_(MipsAtom*)
#define AtomBundleEntry_(bundle,entry) tmpl(bundle,entry)
/* Line-table anchor: gcc only adds a file to the .debug_line file table when the contains line-numbered content. /* Line-table anchor: gcc only adds a file to the .debug_line file table when the contains line-numbered content.
Files containing only atoms and atom components. Files containing only atoms and atom components.
Place `ATOM_FILE_LINE_MARKER();` once at file scope in any `.atom.c` that defines atoms. Place `ATOM_FILE_LINE_MARKER();` once at file scope in any `.atom.c` that defines atoms.
@@ -170,7 +152,7 @@ typedef Slice_(MipsAtom);
The constant is in `.rodata` so the linker may eliminate it. */ The constant is in `.rodata` so the linker may eliminate it. */
#define ATOM_FILE_DEBUGGER_LINE_MARKER(file_name) internal U4 const tmpl(atom_file_debugger_line_marker,file_name) = 0 #define ATOM_FILE_DEBUGGER_LINE_MARKER(file_name) internal U4 const tmpl(atom_file_debugger_line_marker,file_name) = 0
typedef Slice_MipsAtom Tape; typedef Struct_(Tape) { union { MipsAtom* ptr; U4* inlaid_data; }; U4 len; };
typedef Struct_(TapeHostFrame) { typedef Struct_(TapeHostFrame) {
U4 s0; U4 s0;
@@ -236,15 +218,14 @@ FI_ void tape_run(Tape tape) { C_(TapeEntryFn*, tape_enter)(tape.ptr); }
// Procedural authoring of tapes: // Procedural authoring of tapes:
typedef Relative_(FArena) Struct_(TapeBuilder) { U4 ptr; U4 capacity; U4 used; }; typedef Relative_(FArena) Struct_(TapeBuilder) { U4 ptr; U4 capacity; U4 used; };
FI_ void tb_init(TapeBuilder* tb, FArena* arena) { tb->ptr = arena->start; tb->used = 0; } FI_ void tb_init(TapeBuilder* tb, FArena* arena) { tb->ptr = arena->start; tb->used = 0; }
FI_ TapeBuilder tb_make_old( FArena* arena) { return (TapeBuilder){ arena->start, 0 }; }
FI_ TapeBuilder tb_make(Slice mem) { return (TapeBuilder){ u4_(mem.ptr), mem.len, 0 }; } /* capacity in elements (matches used units) */ FI_ TapeBuilder tb_make(Slice mem) { return (TapeBuilder){ u4_(mem.ptr), mem.len, 0 }; } /* capacity in elements (matches used units) */
FI_ void tb_emit(TapeBuilder* tb, MipsAtom* atom) { u4_r(tb->ptr)[tb->used] = u4_(atom); ++ tb->used; } FI_ void tb_emit(TapeBuilder* tb, MipsAtom* atom) { u4_r(tb->ptr)[tb->used] = u4_(atom); ++ tb->used; }
FI_ void tb_data(TapeBuilder* tb, U4 data) { u4_r(tb->ptr)[tb->used] = u4_(data); ++ tb->used; } FI_ void tb_data(TapeBuilder* tb, U4 data) { u4_r(tb->ptr)[tb->used] = u4_(data); ++ tb->used; }
#define tb_emit_(atom) tb_emit(& tb, atom) #define tb_emit_(atom) tb_emit(& tb, atom)
#define tb_data_(field, data) tb_data(& tb, u4_(data))
FI_ void tb_emit_bundle(TapeBuilder_R tb, Slice_MipsAtom atoms) { mem_copy(u4_(tb->ptr), u4_(atoms.ptr), S_slice(atoms)); tb->used += atoms.len; } FI_ void tb_bind(TapeBuilder* tb, Slice data) { mem_copy(b1_r(tb->ptr + tb->used * S_(MipsCode)), data.ptr, data.len); tb->used += data.len / S_(MipsCode); }
#define tb_bind_(tb,type,...) tb_bind(tb, (Slice){ (U1*)(& (type){__VA_ARGS__}), S_(type) }); static_assert(S_(type) % S_(MipsCode) == 0)
FI_ Tape tb_end (TapeBuilder* tb) { tb_emit(tb,tape_exit); return (Tape){ C_(U4*,tb->ptr), tb->used }; } FI_ Tape tb_end (TapeBuilder* tb) { tb_emit(tb,tape_exit); return (Tape){ C_(U4*,tb->ptr), tb->used }; }
FI_ Tape tb_slice(TapeBuilder tb) { return (Tape){ C_(U4*,tb.ptr), tb.used }; } FI_ Tape tb_slice(TapeBuilder tb) { return (Tape){ C_(U4*,tb.ptr), tb.used }; }
@@ -252,6 +233,12 @@ FI_ Tape tb_slice(TapeBuilder tb) { return (Tape){ C_(U4
FI_ void tb_scope_run_end(TapeBuilder* tb) { tb_emit(tb,tape_exit); tape_run(tb_slice(tb[0])); } FI_ void tb_scope_run_end(TapeBuilder* tb) { tb_emit(tb,tape_exit); tape_run(tb_slice(tb[0])); }
#define tb_scope_run(tb) for(U4 tbs_once=0;tbs_once==0;++tbs_once,tb_scope_run_end(tb)) #define tb_scope_run(tb) for(U4 tbs_once=0;tbs_once==0;++tbs_once,tb_scope_run_end(tb))
// NOTE(Ed): Wip still ideating convention. Possibly will never use a composite.
#define tb_emit_wbind_(tb,atom,...) tb_emit(tb,atom); tb_bind_(tb,tmpl(Binds,atom),__VA_ARGS__)
#define tb_emit_wbind2_(tb,atom,type,...) tb_emit(tb,atom); tb_bind_(tb,type,__VA_ARGS__)
#pragma endregion Tape Drive #pragma endregion Tape Drive
#pragma region Macro Mips Atom Components #pragma region Macro Mips Atom Components
@@ -261,9 +248,10 @@ FI_ void tb_scope_run_end(TapeBuilder* tb) { tb_emit(tb,tape_exit); tape_run(tb_
* ---------------------------------------------------------------------------*/ * ---------------------------------------------------------------------------*/
// The 'Yield' sequence for Tape Atoms (mac_yield). // The 'Yield' sequence for Tape Atoms (mac_yield).
// In Forth this is considered the "NEXT" mechanism.
atom_dbg_skip MipsAtomComp_(ac_yield) { atom_dbg_skip MipsAtomComp_(ac_yield) {
load_word(R_AtomJmp, R_TapePtr, 0), load_word(R_AtomJmp, R_TapePtr, 0), LdSlot_
add_ui_self( R_TapePtr, S_(MipsCode)), add_ui_self( R_TapePtr, S_(MipsCode)),
jump_reg( R_AtomJmp), BdSlot_ nop, jump_reg( R_AtomJmp), BdSlot_ nop,
}; };
@@ -289,7 +277,7 @@ typedef Relative_(FArena) Struct_(AtomBuilder) { U4 start; U4 capacity; U4 used;
FI_ void atombuilder_push(AtomBuilder_R ab, Slice_MipsCode code) { FI_ void atombuilder_push(AtomBuilder_R ab, Slice_MipsCode code) {
assert(ab->capacity - ab->used - code.len); assert(ab->capacity - ab->used - code.len);
U4 dest = ab->start + ab->used * S_(MipsCode); U4 size = S_slice(code); U4 dest = ab->start + ab->used * S_(MipsCode); U4 size = S_slice(code);
mem_copy(dest, u4_(code.ptr), size); ab->used += size; mem_copy(b1_r(dest), b1_r(code.ptr), size); ab->used += size;
} }
#define atombuilder_push_mac(ab, mac) atombuilder_push(ab, slice_arg_from_array(Slice_MipsCode, mac)) #define atombuilder_push_mac(ab, mac) atombuilder_push(ab, slice_arg_from_array(Slice_MipsCode, mac))
@@ -313,7 +301,7 @@ FI_ AtomArena atomarena_make(Slice mem) { AtomArena a; atomarena_init(& a, mem);
FI_ MipsAtom* atomarena_push(AtomArena_R aa, Slice_MipsCode code) { FI_ MipsAtom* atomarena_push(AtomArena_R aa, Slice_MipsCode code) {
assert(aa->capacity - aa->used - code.len); assert(aa->capacity - aa->used - code.len);
U4 dest = atomarena_unused_start(aa[0]); U4 size = S_slice(code); U4 dest = atomarena_unused_start(aa[0]); U4 size = S_slice(code);
mem_copy(dest, u4_(code.ptr), size); aa->used += size; mem_copy(b1_r(dest), b1_r(code.ptr), size); aa->used += size;
return C_(MipsAtom*, dest); return C_(MipsAtom*, dest);
} }
FI_ void atomarena_reset(AtomArena_R aa) { aa->used = 0; } FI_ void atomarena_reset(AtomArena_R aa) { aa->used = 0; }
@@ -344,24 +332,14 @@ internal Reg const regfile_alloc_order[] = {
R_T8, R_T9, R_T8, R_T9,
}; };
typedef Struct_(RegFile) { typedef Struct_(RegFile) { A2_U2 GPR; };
A2_U2 GPR;
A2_U2 GTE;
};
#define regfile(pin_mask) {.GPR={u4_lo(pin_mask), u4_hi(pin_mask)} } #define regfile(pin_mask) {.GPR={u4_lo(pin_mask), u4_hi(pin_mask)} }
FI_ void regfile_init(RegFile_R rf) { FI_ void regfile_init(RegFile_R rf) {
/* pack the 32-bit ABI mask into the two U2s */ rf->GPR[0] = u4_lo(regfile_abi_mask); rf->GPR[1] = u4_hi(regfile_abi_mask);
rf->GPR[0] = u4_lo(regfile_abi_mask);
rf->GPR[1] = u4_hi(regfile_abi_mask);
rf->GTE[0] = rf->GTE[1] = 0;
} }
FI_ RegFile regfile_make(void) { RegFile rf; regfile_init(& rf); return rf; } FI_ RegFile regfile_make(void) { RegFile rf; regfile_init(& rf); return rf; }
typedef Struct_(RegFile_RInfo) { typedef Struct_(RegFile_RInfo) { U2_R section; U2 mask; B2 occupied; };
U2_R section;
U2 mask;
B2 occupied;
};
FI_ RegFile_RInfo regfile_rinfo(A2_U2 file, Reg r_id) { FI_ RegFile_RInfo regfile_rinfo(A2_U2 file, Reg r_id) {
U2 s_id = r_id >> 4; U2 s_id = r_id >> 4;
U2_R section = & file[s_id]; U2_R section = & file[s_id];
@@ -371,19 +349,11 @@ FI_ RegFile_RInfo regfile_rinfo(A2_U2 file, Reg r_id) {
} }
FI_ Reg regfile__alloc_helper(A2_U2 file, Reg r_id) { FI_ Reg regfile__alloc_helper(A2_U2 file, Reg r_id) {
Reg result = 0; RegFile_RInfo info = regfile_rinfo(file, r_id); Reg result = 0; RegFile_RInfo info = regfile_rinfo(file, r_id);
if (info.occupied == false) { if (info.occupied == false) { info.section[0] |= info.mask; result = r_id; }
info.section[0] |= info.mask;
result = r_id;
}
return result; return result;
} }
/* regfile_alloc picks the next free GPR from regfile_alloc_order.
* The table is the first-fit allocation order: T0..T7, V0..V1, A0..A3,
* S0..S7, T8..T9. The 24 entries leave room for the tape program to use
* any of them while R0, R1, R26-R31 remain reserved. */
I_ Reg regfile_alloc(RegFile_R rf) { I_ Reg regfile_alloc(RegFile_R rf) {
Reg allocated = 0; Reg allocated = 0; for index_iter(U4, r_id, R_V0, <, R_T9) {
for index_iter(U4, r_id, R_V0, <, R_T9) {
allocated = regfile__alloc_helper(rf->GPR, r_id); allocated = regfile__alloc_helper(rf->GPR, r_id);
Jmp_nZero_(allocated,resolved); Jmp_nZero_(allocated,resolved);
} }
@@ -397,8 +367,7 @@ FI_ Reg regfile_pin(RegFile_R rf, Reg r_id) {
return r_id; return r_id;
} }
FI_ void regfile_pin_mask(RegFile_R rf, U4 mask) { FI_ void regfile_pin_mask(RegFile_R rf, U4 mask) {
B4 occupied = u4_r(rf->GPR)[0] & mask; B4 occupied = u4_r(rf->GPR)[0] & mask; assert(occupied == false);
assert(occupied == false);
u4_r(rf->GPR)[0] |= mask; u4_r(rf->GPR)[0] |= mask;
} }
FI_ void regfile_free_mask(RegFile_R rf, U4 mask) { FI_ void regfile_free_mask(RegFile_R rf, U4 mask) {
@@ -406,21 +375,23 @@ FI_ void regfile_free_mask(RegFile_R rf, U4 mask) {
u4_r(rf->GPR)[0] &= ~mask; u4_r(rf->GPR)[0] &= ~mask;
} }
FI_ void regfile_free_reg(RegFile_R rf, Reg r_id) { FI_ void regfile_free_reg(RegFile_R rf, Reg r_id) {
/* never free the ABI set */ if (regfile_abi_mask & (1u << r_id)) return; // never free the ABI set
if (regfile_abi_mask & (1u << r_id)) return;
RegFile_RInfo info = regfile_rinfo(rf->GPR, r_id); RegFile_RInfo info = regfile_rinfo(rf->GPR, r_id);
info.section[0] &= ~info.mask; info.section[0] &= ~info.mask;
} }
FI_ void regfile_reset(RegFile_R rf) { FI_ void regfile_reset (RegFile_R rf) { rf->GPR[0] = u4_lo(regfile_abi_mask); rf->GPR[1] = u4_hi(regfile_abi_mask); }
rf->GPR[0] = u4_lo(regfile_abi_mask); FI_ void regfile_reset_to_mask(RegFile_R rf, U4 mask) { rf->GPR[0] = u4_lo(mask); rf->GPR[1] = u4_hi(mask); }
rf->GPR[1] = u4_hi(regfile_abi_mask);
}
FI_ void regfile_reset_to_mask(RegFile_R rf, U4 mask) {
rf->GPR[0] = u4_lo(mask);
rf->GPR[1] = u4_hi(mask);
}
#pragma endregion RegFileArena (Register File Allocator) #pragma endregion RegFileArena (Register File Allocator)
#pragma region Mips Atom Components (Procedures)
// For doing direct-chaining of "atoms or fragments".
FI_ Slice_MipsCode ac_yield_to(AtomBuilder_R ab, Reg code_ptr) atom_dbg_skip MipsAtomComp_Proc_(ab, {
jump_reg(code_ptr), BdSlot_ nop,
})
#pragma endregion Mips Atom Components (Procedures)
#pragma region Mips Atom Procs #pragma region Mips Atom Procs
/* RegUse structs are a convention to organize register allocations for a mips atom procedure. /* RegUse structs are a convention to organize register allocations for a mips atom procedure.
Unlike the usual enum-based declarations, they provide a namespaced scope and have view types via union declarations. */ Unlike the usual enum-based declarations, they provide a namespaced scope and have view types via union declarations. */
@@ -441,7 +412,6 @@ FI_ void regfile_reset_to_mask(RegFile_R rf, U4 mask) {
add_si(r.t1.view_3, r.usual_modifiable, 10), add_si(r.t1.view_3, r.usual_modifiable, 10),
mac_yield(), mac_yield(),
}) })
#pragma endregion Mips Atom Procs #pragma endregion Mips Atom Procs
#pragma region Baked Mips Atoms #pragma region Baked Mips Atoms
+4 -4
View File
@@ -8,7 +8,7 @@
#pragma region hello_camera #pragma region hello_camera
// --- atom: pad_input_cube_rotation (61 words) --- // --- atom: pad_input_cube_rotation (60 words) ---
#define _atom_offset_dpad_left_exit_dpad_left 6 #define _atom_offset_dpad_left_exit_dpad_left 6
#define _atom_offset_dpad_right_exit_dpad_right 6 #define _atom_offset_dpad_right_exit_dpad_right 6
@@ -26,7 +26,7 @@ enum {
atom_offset_end_low_exit_stick = _atom_offset_end_low_exit_stick, atom_offset_end_low_exit_stick = _atom_offset_end_low_exit_stick,
}; };
// --- atom: pad_input_cam (40 words) --- // --- atom: pad_input_cam (39 words) ---
#define _atom_offset_left_x_exit_left_x 3 #define _atom_offset_left_x_exit_left_x 3
#define _atom_offset_right_x_exit_right_x 3 #define _atom_offset_right_x_exit_right_x 3
@@ -44,7 +44,7 @@ enum {
atom_offset_circle_z_exit_circle_z = _atom_offset_circle_z_exit_circle_z, atom_offset_circle_z_exit_circle_z = _atom_offset_circle_z_exit_circle_z,
}; };
// --- atom: cube_g4_face (75 words) --- // --- atom: cube_g4_face (73 words) ---
#define _atom_offset_cull_cube_g4_face_exit 41 #define _atom_offset_cull_cube_g4_face_exit 41
#define _atom_offset_bounds_chk_cube_g4_face_exit 24 #define _atom_offset_bounds_chk_cube_g4_face_exit 24
@@ -54,7 +54,7 @@ enum {
atom_offset_bounds_chk_cube_g4_face_exit = _atom_offset_bounds_chk_cube_g4_face_exit, atom_offset_bounds_chk_cube_g4_face_exit = _atom_offset_bounds_chk_cube_g4_face_exit,
}; };
// --- atom: floor_f3_face (58 words) --- // --- atom: floor_f3_face (56 words) ---
#define _atom_offset_culling_floor_f3_face_exit 25 #define _atom_offset_culling_floor_f3_face_exit 25
#define _atom_offset_bounds_chk_floor_f3_face_exit 16 #define _atom_offset_bounds_chk_floor_f3_face_exit 16
+103 -173
View File
@@ -3,7 +3,7 @@
# include "duffle/gen/macs.h" # include "duffle/gen/macs.h"
# include "duffle/gen/offsets.h" # include "duffle/gen/offsets.h"
# include "duffle/dsl.atom.h" # include "duffle/dsl.atom.h"
# include "duffle/lottes_tape.h" # include "duffle/tape.h"
# include "duffle/mips.h" # include "duffle/mips.h"
# include "duffle/gte.h" # include "duffle/gte.h"
# include "duffle/gp.h" # include "duffle/gp.h"
@@ -11,8 +11,8 @@
# include "duffle/word_count.metadata.h" # include "duffle/word_count.metadata.h"
# include "duffle/psyq.h" # include "duffle/psyq.h"
# include "duffle/math.atom.h" # include "duffle/math.atom.h"
# include "duffle/mips.atom.c"
# include "duffle/gte.atom.c" # include "duffle/gte.atom.c"
# include "duffle/mips.atom.c"
# include "duffle/gp.atom.c" # include "duffle/gp.atom.c"
# include "duffle/psyq.atom.c" # include "duffle/psyq.atom.c"
# include "gen/offsets.h" # include "gen/offsets.h"
@@ -21,7 +21,7 @@
# include "hello_camera.h" # include "hello_camera.h"
#endif #endif
ATOM_FILE_DEBUGGER_LINE_MARKER(hello_joypad_atom_c); ATOM_FILE_DEBUGGER_LINE_MARKER(hello_camera_atom_c);
#pragma region MACs (Mips Atom components) #pragma region MACs (Mips Atom components)
@@ -92,11 +92,6 @@ MipsAtomComp_Proc_(ab, {
#pragma endregion MACs #pragma endregion MACs
#pragma region Atom Procs #pragma region Atom Procs
// Modular Atoms
#define AtomBundle_(name) Struct_(tmpl(AtomBundle,name))
#define AtomBundle_Len(name) S_(tmpl(AtomBundle,name))/S_(MipsAtom*)
#define AtomBundleEntry_(bundle,entry) tmpl(bundle,entry)
#pragma region resolve_look_at #pragma region resolve_look_at
/* ─── resolve_look_at bundle chain atoms ──────────────────────────── */ /* ─── resolve_look_at bundle chain atoms ──────────────────────────── */
@@ -108,7 +103,7 @@ typedef AtomBundle_(resolve_look_at) { MipsAtom
*normalize_right_ux, *normalize_right_ux,
*cross_to_up, *cross_to_up,
*normalize_up_uy, *normalize_up_uy,
*pop_mv_trans; *populate_mt3s4s2;
}; };
typedef Struct_(ResolveLookAtScratch) { typedef Struct_(ResolveLookAtScratch) {
@@ -123,71 +118,56 @@ typedef Struct_(ResolveLookAtScratch) {
V3_S4 up_in; V3_S4 up_in;
}; };
/* Binds_ResolveLookAtSub — what the C side pushes onto the tape before input_and_sub.
* The scratchpad base is no longer pushed because R_ScratchBase (= R_SP) is a tape carrier
* preserved across atoms; the atom body reads 0x1F800000 directly from R_SP. */
typedef Struct_(Binds_ResolveLookAt) {
MT3_S2S4* look_at;
P3_S4* eye;
P3_S4* target;
V3_S4* up_in;
};
typedef Struct_(Binds_ResolveLookAtSub) { typedef Struct_(Binds_ResolveLookAtSub) {
P3_S4* target; P3_S4* target;
P3_S4* eye; P3_S4* eye;
V3_S4* up_in; V3_S4* up_in;
}; };
typedef Struct_(RegUse_resolve_look_at_input_and_sub) { typedef Struct_(RegUse_resolve_look_at_input_and_sub) {
Reg target; Reg eye; Reg up_in; Reg target_ptr;
Reg t0; Reg t1; Reg t2; Reg t3; Reg t4; Reg eye_ptr;
Reg up_in_ptr;
union { Reg_(V3_S4) r012, up_in, eye; };
union { Reg_(V3_S4) r345, target, fwd; };
}; };
/* Atom 0 in the bundle: input_and_sub. Stages C-side inputs into the scratchpad and computes fwd = target - eye. */ /* Atom 0 in the bundle: input_and_sub. Stages C-side inputs into the scratchpad and computes fwd = target - eye. */
internal MipsAtom* AtomBundleEntry_(resolve_look_at,input_and_sub)(AtomArena_R aa, RegUse_resolve_look_at_input_and_sub r) internal MipsAtom* AtomBundleEntry_(resolve_look_at,input_and_sub)(AtomArena_R aa, RegUse_resolve_look_at_input_and_sub r)
atom_info(atom_bind(Binds_ResolveLookAtSub)) MipsAtom_Proc_(aa, { atom_info(atom_bind(Binds_ResolveLookAtSub)) MipsAtom_Proc_(aa, {
load_word(r.target, R_TapePtr, O_(Binds_ResolveLookAtSub,target)), load_word(r.target_ptr, R_TapePtr, O_(Binds_ResolveLookAtSub,target)),
load_word(r.eye, R_TapePtr, O_(Binds_ResolveLookAtSub,eye)), load_word(r.eye_ptr, R_TapePtr, O_(Binds_ResolveLookAtSub,eye)),
load_word(r.up_in, R_TapePtr, O_(Binds_ResolveLookAtSub,up_in)), load_word(r.up_in_ptr, R_TapePtr, O_(Binds_ResolveLookAtSub,up_in)),
LdSlot_ add_ui_self( R_TapePtr, S_(Binds_ResolveLookAtSub)), LdSlot_ add_ui_self( R_TapePtr, S_(Binds_ResolveLookAtSub)),
/* Stage up_in.x/y/z into the scratchpad. R_ScratchBase = R_SP = 0x1F800000. */ /* Stage up_in.x/y/z into the scratchpad. R_ScratchBase = R_SP = 0x1F800000. */
mac_load_word_v3( r.t0, r.t1, r.t2, r.up_in, 0), LdSlot_ mac_load_v3s4( r.up_in, r.up_in_ptr, 0), LdSlot_
mac_store_word_v3(r.t0, r.t1, r.t2, R_ScratchBase, O_(ResolveLookAtScratch,up_in)), mac_store_v3s4(r.up_in, R_ScratchBase, O_(ResolveLookAtScratch,up_in)),
// Stage eye.x/y/z into the scratchpad (atom 6 reads these for the translation column). // Stage eye.x/y/z into the scratchpad (atom 6 reads these for the translation column).
mac_load_word_v3( r.t0, r.t1, r.t2, r.eye, 0), LdSlot_ mac_load_v3s4( r.eye, r.eye_ptr, 0), LdSlot_
mac_store_word_v3(r.t0, r.t1, r.t2, R_ScratchBase, O_(ResolveLookAtScratch,eye)), mac_store_v3s4(r.eye, R_ScratchBase, O_(ResolveLookAtScratch,eye)),
/* Compute fwd = target - eye. */ /* Compute fwd = target - eye. */
// mac_load_p3s4(t3, R_AT, t4, r.eye, 0), mac_load_v3s4( r.target, r.target_ptr, 0), LdSlot_
mac_load_word_v3(r.t3, R_AT, r.t4, r.target, 0), LdSlot_ mac_sub_v3s4_self(r.fwd, r.eye),
mac_sub_s_v3_self( mac_store_v3s4( r.fwd, R_ScratchBase, O_(ResolveLookAtScratch,fwd)),
r.t3, R_AT, r.t4,
r.t0, r.t1, r.t2),
mac_store_word_v3(r.t3, R_AT, r.t4, R_ScratchBase, O_(ResolveLookAtScratch,fwd)),
mac_yield() mac_yield()
}) })
typedef Struct_(Binds_ResolveLookAt_PopulateMT3S4S2) {
typedef Struct_(Binds_ResolveLookAtPopMvTrans) { MT3_S2S4* look_at; /* MT3_S2S4* — destination matrix address */
U4 look_at; /* MT3_S2S4* — destination matrix address */
}; };
typedef Struct_(RegUse_resolve_look_at__pop_mv_trans) { typedef Struct_(RegUse_resolve_look_at_populate_mt3s4s2) {
Reg look_at; Reg look_at;
Reg_(V3_S4) row; /* populate phase: load ux/uy/uz */
union { Reg ux, v_x; } t6; /* populate addr (canonical) → matrix_vector v_x */
union { Reg uy, v_y; } t7; /* populate uy → matrix_vector v_y */
union { Reg uz, v_z; } t8; /* populate uz → matrix_vector v_z */
Reg eye; /* matrix_vector phase: load -eye */ Reg eye; /* matrix_vector phase: load -eye */
Reg_(V3_S4) row; /* populate phase: load ux/uy/uz */
union { Reg r0, ux, vx; }; /* populate addr → matrix_vector v_x */
union { Reg r1, uy, vy; }; /* populate uy → matrix_vector v_y */
union { Reg r2, uz, vz; }; /* populate uz → matrix_vector v_z */
}; };
/* Atom 6 (fused): write look_at->m[][] from ux/uy/uz as packed S2 (populate), /* write look_at->m[][] from ux/uy/uz as packed S2 (populate),
* ctc2 RT chain into C2[0..4] (matrix_vector), MVMVA RT*(-eye)>>12, store off * ctc2 RT chain into C2[0..4] (matrix_vector), MVMVA RT*(-eye)>>12, store off
* directly to look_at->t[] (trans_matrix). Replaces the previous 3 separate atoms * directly to look_at->t[] (trans_matrix).
* (populate + matrix_vector + trans_matrix).
*
* MT3_S2S4 { A3x3_S2 m; A3_S4 t; }
* m[][] is S2 packed (9 × 2 = 18 bytes at offset 0)
* t[0..2] is S4 (3 × 4 = 12 bytes at offset 18)
* *
* C11 ApplyMatrixLV semantics (gte.atom.c ac_apply_matrix_lv; libgte reference): * C11 ApplyMatrixLV semantics (gte.atom.c ac_apply_matrix_lv; libgte reference):
* 1. ctc2 RT matrix (5 ctc2s to C2[0..4]) * 1. ctc2 RT matrix (5 ctc2s to C2[0..4])
@@ -197,82 +177,51 @@ typedef Struct_(RegUse_resolve_look_at__pop_mv_trans) {
* 4. mtc2 to IR1/2/3, nop2, MVMVA pass2 (sf=1, mx=0, v=3, cv=3) * 4. mtc2 to IR1/2/3, nop2, MVMVA pass2 (sf=1, mx=0, v=3, cv=3)
* 5. mfc2 MACs → off * 5. mfc2 MACs → off
* 6. store off to look_at->t[] (skip scratch.eye intermediate) * 6. store off to look_at->t[] (skip scratch.eye intermediate)
*
* GPR codes (assigned by resolve_look_at_init):
* r_scratch : R_ResolveScratch (R_T4 carrier)
* r_look_at : ralloc() — also serves as the off-dst in the trans_matrix phase
* r_row : V3_S4, reused for ux/uy/uz loads in populate phase
* r_eye : ralloc() — &scratch.eye, used for -eye load in matrix_vector phase
* r_v_x/v_y/v_z : ralloc() — populate scratch addrs (ux/uy/uz), reused as
* ctc2 transfer + MVMVA -eye temp in matrix_vector phase
* (v_x/v_y/v_z alias ux/uy/uz via the union; lifetime ends for ux/uy/uz after
* populate's mac_load_v3s4, so reusing for v.x/v.y/v.z is safe)
* Pool cost: 1 carrier + 1 look_at + 3 row + 1 eye + 3 aliased = 9 GPRs
*
* Net word savings vs the previous 3-atom flow: ~15 words + 2 mac_yields + 1 tape pop.
* - 2 mac_yields (trans_matrix's + matrix_vector's) → fused into one yield
* - 1 redundant tb_data (look_at was pushed 2x; now once)
* - mac_trans_mt3s3s4 (6 words) → replaced by direct mac_store_v3s4
* - mac_store_v3s4 to scratch.eye (3 words intermediate) → eliminated
* - add_si for r_off_ptr (2 words) → eliminated
* - mac_store_v3s4 zero-store of t[] (3 words) → eliminated (matrix_vector writes
* off directly; no consumer needed the zero first)
* - 1 set_gte_mt3s2s4 ctc2 chain (13 baked words) → eliminated (matrix_vector
* has its own ctc2 RT chain; cube rendering atoms reload C2 state themselves)
*/ */
internal MipsAtom* resolve_look_at__pop_mv_trans(AtomArena_R aa, internal MipsAtom* AtomBundleEntry_(resolve_look_at,populate_mt3s4s2)(AtomArena_R aa, RegUse_resolve_look_at_populate_mt3s4s2 r)
RegUse_resolve_look_at__pop_mv_trans r atom_info(atom_bind(Binds_ResolveLookAt_PopulateMT3S4S2)) MipsAtom_Proc_(aa, {
) MipsAtom_Proc_(aa, {
/* --- Tape pop: look_at pointer --- */ /* --- Tape pop: look_at pointer --- */
load_word(r.look_at, R_TapePtr, O_(Binds_ResolveLookAtPopMvTrans,look_at)), load_word(r.look_at, R_TapePtr, O_(Binds_ResolveLookAt_PopulateMT3S4S2,look_at)),
LdSlot_ add_ui_self(R_TapePtr, S_(Binds_ResolveLookAtPopMvTrans)), LdSlot_ add_ui_self( R_TapePtr, S_(Binds_ResolveLookAt_PopulateMT3S4S2)),
/* --- Scratch addresses for ux/uy/uz/eye (populate phase; t6/t7/t8 alias ux/uy/uz). add_si(r.ux, R_ScratchBase, O_(ResolveLookAtScratch, ux)), LdSlot_
* R_ScratchBase (= R_SP) holds 0x1F800000; no per-atom bake is required because add_si(r.uy, R_ScratchBase, O_(ResolveLookAtScratch, uy)),
* R_SP is a tape carrier preserved across atoms. --- */ add_si(r.uz, R_ScratchBase, O_(ResolveLookAtScratch, uz)),
add_si(r.t6.ux, R_ScratchBase, O_(ResolveLookAtScratch, ux)), LdSlot_
add_si(r.t7.uy, R_ScratchBase, O_(ResolveLookAtScratch, uy)),
add_si(r.t8.uz, R_ScratchBase, O_(ResolveLookAtScratch, uz)),
add_si(r.eye, R_ScratchBase, O_(ResolveLookAtScratch, eye)), add_si(r.eye, R_ScratchBase, O_(ResolveLookAtScratch, eye)),
/* --- POPULATE phase: write look_at->m[][] from ux/uy/uz as packed S2 --- */ /* write look_at->m[][] from ux/uy/uz as packed S2 */
mac_load_v3s4(r.row, r.t6.ux, 0), LdSlot_ mac_store_v3s2(r.row, r.look_at, O_(MT3_S2S4, m[0])), mac_load_v3s4(r.row, r.ux, 0), LdSlot_ mac_store_v3s2(r.row, r.look_at, O_(MT3_S2S4, m[0])),
mac_load_v3s4(r.row, r.t7.uy, 0), LdSlot_ mac_store_v3s2(r.row, r.look_at, O_(MT3_S2S4, m[1])), mac_load_v3s4(r.row, r.uy, 0), LdSlot_ mac_store_v3s2(r.row, r.look_at, O_(MT3_S2S4, m[1])),
mac_load_v3s4(r.row, r.t8.uz, 0), LdSlot_ mac_store_v3s2(r.row, r.look_at, O_(MT3_S2S4, m[2])), mac_load_v3s4(r.row, r.uz, 0), LdSlot_ mac_store_v3s2(r.row, r.look_at, O_(MT3_S2S4, m[2])),
/* --- MATRIX-VECTOR phase: ctc2 RT chain + MVMVA RT*(-eye)>>12 --- */ /* ctc2 RT chain + MVMVA RT * (-eye) >> 12 */
/* RT packing (per libgte ApplyMatrixLV convention; see gte.h:217-220 + /* C2[0] = (RT12<<16)|RT11 ← ctc2 RT11 from m[0][0..1]
* atom_6b_disasm_comparison.md:28-32): * C2[1] = (RT21<<16)|RT13 ← ctc2 RT12 from m[0][2..3]
* C2[0] = (RT12<<16)|RT11 ← ctc2 RT11 from m[0][0..1] packed word * C2[2] = (RT23<<16)|RT22 ← ctc2 RT13 from m[1][1..2]
* C2[1] = (RT21<<16)|RT13 ← ctc2 RT12 from m[0][2..3] packed word * C2[3] = (RT32<<16)|RT31 ← ctc2 RT21 from m[2][0..1]
* C2[2] = (RT23<<16)|RT22 ← ctc2 RT13 from m[1][1..2] packed word * C2[4] = (RT33<<16)|junk ← ctc2 RT22 from m[2][2] (half) */
* C2[3] = (RT32<<16)|RT31 ← ctc2 RT21 from m[2][0..1] packed word load_word( r.vx, r.look_at, O_(MT3_S2S4, m[0][0])), /* RT11|RT12 */ LdSlot_
* C2[4] = (RT33<<16)|junk ← ctc2 RT22 from m[2][2] (half) load_word( r.vy, r.look_at, O_(MT3_S2S4, m[0][2])), /* RT13|RT21 */ LdSlot_ gte_mv_to_ctrl_r(r.vx, gte_cr_RT11),
* Each ctc2 writes a WHOLE 32-bit C2 slot; the "macro name" identifies load_word( r.vz, r.look_at, O_(MT3_S2S4, m[1][1])), /* RT22|RT23 */ LdSlot_ gte_mv_to_ctrl_r(r.vy, gte_cr_RT12),
* which C2 register, not which 16-bit half. */ load_word( r.vx, r.look_at, O_(MT3_S2S4, m[2][0])), /* RT31|RT32 */ LdSlot_ gte_mv_to_ctrl_r(r.vz, gte_cr_RT13),
load_word( r.t6.v_x, r.look_at, O_(MT3_S2S4, m[0][0])), /* RT11|RT12 */ LdSlot_ load_half_u(r.vy, r.look_at, O_(MT3_S2S4, m[2][2])), /* RT33 */ LdSlot_ gte_mv_to_ctrl_r(r.vx, gte_cr_RT21),
load_word( r.t7.v_y, r.look_at, O_(MT3_S2S4, m[0][2])), /* RT13|RT21 */ LdSlot_ gte_mv_to_ctrl_r(r.t6.v_x, gte_cr_RT11),
load_word( r.t8.v_z, r.look_at, O_(MT3_S2S4, m[1][1])), /* RT22|RT23 */ LdSlot_ gte_mv_to_ctrl_r(r.t7.v_y, gte_cr_RT12), GteDelay_ mac_load_word_v3(r.vx, r.vy, r.vz, r.eye, 0), LdSlot_
load_word( r.t6.v_x, r.look_at, O_(MT3_S2S4, m[2][0])), /* RT31|RT32 */ LdSlot_ gte_mv_to_ctrl_r(r.t8.v_z, gte_cr_RT13), mac_sub_s_v3(r.vx, r.vy, r.vz, R_0, R_0, R_0, r.vx, r.vy, r.vz),
load_half_u(r.t7.v_y, r.look_at, O_(MT3_S2S4, m[2][2])), /* RT33 */ LdSlot_ gte_mv_to_ctrl_r(r.t6.v_x, gte_cr_RT21),
/* pos = -eye. The three loads also retire the last CTC2. */ gte_mv_to_ctrl_r(r.t7.v_y, gte_cr_RT22), gte_mv_to_data_r(r.vx, C2_IR1),
GteDelay_ mac_load_word_v3(r.t6.v_x, r.t7.v_y, r.t8.v_z, r.eye, 0), LdSlot_ gte_mv_to_data_r(r.vy, C2_IR2),
mac_sub_s_v3(r.t6.v_x, r.t7.v_y, r.t8.v_z, R_0, R_0, R_0, r.t6.v_x, r.t7.v_y, r.t8.v_z), gte_mv_to_data_r(r.vz, C2_IR3),
/* mtc2 pos (as S16) to IR1/2/3. The GTE takes low 16 bits. pos fits in S16. For negative pos, the 32-bit sign-extended value's low 16 bits = correct S16. */
gte_mv_to_data_r(r.t6.v_x, C2_IR1),
gte_mv_to_data_r(r.t7.v_y, C2_IR2),
gte_mv_to_data_r(r.t8.v_z, C2_IR3),
GteDelay_ nop2, GteDelay_ nop2,
/* MVMVA pass 2 — C11 ApplyMatrixLV command. /* MVMVA pass 2 — C11 ApplyMatrixLV command. sf=1, mx=0 (RT), v=3 (IR), cv=3. Reads RT × IR >> 12. */
* sf=1, mx=0 (RT), v=3 (IR), cv=3. Reads RT × IR >> 12. */ gte_cmdw_mvmva_c11_pass2, GteDelay_ load_word(R_AtomJmp, R_TapePtr, 0), // ac_yield: word 1
gte_cmdw_mvmva_c11_pass2, GteDelay_ nop, mac_gte_mv_from_data_r_mac123(r.vx, r.vy, r.vz), GteDelay_ add_ui_self( R_TapePtr, S_(MipsCode)), // ac_yield: word 2
mac_gte_mv_from_data_r_mac123(r.t6.v_x, r.t7.v_y, r.t8.v_z), GteDelay_ nop,
/* --- TRANS-MATRIX phase: store off directly to look_at->t[] (skip scratch.eye intermediate) --- */ /* store off directly to look_at->t[] (skip scratch.eye intermediate) */
mac_store_word_v3(r.t6.v_x, r.t7.v_y, r.t8.v_z, r.look_at, O_(MT3_S2S4, t)), mac_store_word_v3(r.vx, r.vy, r.vz, r.look_at, O_(MT3_S2S4, t)),
mac_yield() jump_reg(R_AtomJmp), BdSlot_ nop, // ac_yield: word 3-4
}) })
#pragma endregion resolve_look_at #pragma endregion resolve_look_at
@@ -288,8 +237,8 @@ enum {
}; };
//screen_env_init. Mirrors the libpsyx's SetDefDispEnv + SetDefDrawEnv + the manual enable_auto_clear / initial_bg_color writes. //screen_env_init. Mirrors the libpsyx's SetDefDispEnv + SetDefDrawEnv + the manual enable_auto_clear / initial_bg_color writes.
internal MipsAtom_(screen_env_init) atom_info(atom_phase(screen_init) internal MipsAtom_(screen_env_init) atom_info(atom_phase(screen_init)
, atom_reads(R_T0, R_ScreenX, R_ScreenY, R_ScreenBuf) , atom_reads(R_ScreenBuf)
, atom_writes(R_T0, R_ScreenX, R_ScreenY) , atom_writes(R_ScreenBuf)
) { ) {
/* display[0] = (0, 0, 320, 240); rest of struct zeroed. */ /* display[0] = (0, 0, 320, 240); rest of struct zeroed. */
add_ui(R_ScreenX, R_0, ScreenRes_X), add_ui(R_ScreenY, R_0, ScreenRes_Y), add_ui(R_ScreenX, R_0, ScreenRes_X), add_ui(R_ScreenY, R_0, ScreenRes_Y),
@@ -339,23 +288,6 @@ internal MipsAtom_(screen_env_init) atom_info(atom_phase(screen_init)
mac_yield(), mac_yield(),
}; };
/* gp_screen_init's GPR setup. Tests the mixed user-pinning + auto-reg pattern:
* - R_IO_BaseAddr = R_T4 (user-pinned via atom_reg; pre-existing)
* - R_GP1_Offset = R_T2 (user-pinned via atom_reg; NEW -- for GPIO_PORT1_OFFSET)
* - R_ScreenX = R_T5 (user-pinned via atom_reg; used as a transfer and GTE setup reg)
* - R_GpTmp = auto-allocated by the lua pass and used for several GPU transfers;
* the C preprocessor resolves it to the chosen free pool GPR.
*
* For gp_screen_init, the auto-reg pool exclusions are:
* user_pinned (from the corpus register_alias_registry) : R_T0..R_T7 (all 8 user-pinned across hello_camera.atom.c)
* body-parsed physical registers : aliases resolve through the registry;
* the body uses R_ScreenX, not raw R_T5
* source_pool after both subtractions : {R_V0, R_V1} only
* R_GpTmp gets R_V0 (the first-fit choice). Its repeated GPU-transfer use proves that the
* auto-reg allocation is active while the R_ScreenX references prove the pinned alias is used.
* R_TapePtr (R_T9), R_AtomJmp (R_T8), R_AT are excluded from the POOL by construction in
* passes/auto_reg.lua -- see the "obvious exclusions" comment block at the top of that file.
*/
enum { enum {
R_IO_BaseAddr = R_T4 atom_reg, /* Caller-pinned: IO_BASE_ADDR = 0x1F800000 */ R_IO_BaseAddr = R_T4 atom_reg, /* Caller-pinned: IO_BASE_ADDR = 0x1F800000 */
R_GP1_Offset = R_T2 atom_reg, /* Caller-pinned: GPIO_PORT1_OFFSET = 0x10 */ R_GP1_Offset = R_T2 atom_reg, /* Caller-pinned: GPIO_PORT1_OFFSET = 0x10 */
@@ -509,7 +441,7 @@ typedef Struct_(Binds_PadInputCam) {
Camera* cam; Camera* cam;
}; };
internal MipsAtom_(pad_input_cam) atom_info(atom_bind(Binds_PadInputCam) internal MipsAtom_(pad_input_cam) atom_info(atom_bind(Binds_PadInputCam)
, atom_reads( R_Cam, R_CamPadState, R_TapePtr) , atom_reads( R_Cam, R_CamPadState)
, atom_writes(R_Cam) , atom_writes(R_Cam)
) { ) {
/* Bind pop: state → R_CamPadState (R_T5), cam → R_Cam (R_T4), advance R_TapePtr by 8. */ /* Bind pop: state → R_CamPadState (R_T5), cam → R_Cam (R_T4), advance R_TapePtr by 8. */
@@ -522,7 +454,7 @@ internal MipsAtom_(pad_input_cam) atom_info(atom_bind(Binds_PadInputCam)
load_word(R_T1, R_Cam, O_(Camera,pos.x)), load_word(R_T1, R_Cam, O_(Camera,pos.x)),
// D-pad Left → cam.pos.x -= 50. and_i fulfills BD-slot for load on R_Cam. // D-pad Left → cam.pos.x -= 50. and_i fulfills BD-slot for load on R_Cam.
LdSlot_ and_i(R_T3, R_T0, Pad_Left), branch_le_zero(R_T3, atom_offset(left_x, exit_left_x)), BdSlot_ mac_yield_load(), LdSlot_ LdSlot_ and_i(R_T3, R_T0, Pad_Left), branch_le_zero(R_T3, atom_offset(left_x, exit_left_x)), BdSlot_ nop,
add_si(R_T1, R_T1, -50), store_word(R_T1, R_Cam, O_(Camera,pos.x)), add_si(R_T1, R_T1, -50), store_word(R_T1, R_Cam, O_(Camera,pos.x)),
atom_label(exit_left_x) atom_label(exit_left_x)
@@ -544,16 +476,16 @@ internal MipsAtom_(pad_input_cam) atom_info(atom_bind(Binds_PadInputCam)
/* D-pad Cross → cam.pos.z -= 50. Load pos.z BEFORE the andi. */ /* D-pad Cross → cam.pos.z -= 50. Load pos.z BEFORE the andi. */
load_word(R_T1, R_Cam, O_(Camera,pos.z)), LdSlot_ load_word(R_T1, R_Cam, O_(Camera,pos.z)), LdSlot_
and_i(R_T3, R_T0, Pad_Cross), branch_le_zero(R_T3, atom_offset(cross_z, exit_cross_z)), BdSlot_ nop, and_i(R_T3, R_T0, Pad_Cross), branch_le_zero(R_T3, atom_offset(cross_z, exit_cross_z)), BdSlot_ load_word(R_AtomJmp, R_TapePtr, 0), LdSlot_ // ac_yield: word 1
add_si(R_T1, R_T1, -50), store_word(R_T1, R_Cam, O_(Camera,pos.z)), add_si(R_T1, R_T1, -50), store_word(R_T1, R_Cam, O_(Camera,pos.z)),
atom_label(exit_cross_z) atom_label(exit_cross_z)
/* D-pad Circle → cam.pos.z += 50. Reuses R_T1 from Cross. */ /* D-pad Circle → cam.pos.z += 50. Reuses R_T1 from Cross. */
and_i(R_T3, R_T0, Pad_Circle), branch_le_zero(R_T3, atom_offset(circle_z, exit_circle_z)), BdSlot_ nop, and_i(R_T3, R_T0, Pad_Circle), branch_le_zero(R_T3, atom_offset(circle_z, exit_circle_z)), BdSlot_ add_ui_self(R_TapePtr, S_(MipsCode)), // ac_yield: word 2
add_si(R_T1, R_T1, 50), store_word(R_T1, R_Cam, O_(Camera,pos.z)), add_si(R_T1, R_T1, 50), store_word(R_T1, R_Cam, O_(Camera,pos.z)),
atom_label(exit_circle_z) atom_label(exit_circle_z)
mac_yield_tail(), jump_reg(R_AtomJmp), BdSlot_ nop // ac_yield: word 3-4
}; };
enum { enum {
@@ -567,20 +499,17 @@ enum {
#define R_OtBase_Code R_T6_Code #define R_OtBase_Code R_T6_Code
}; };
typedef Struct_(Binds_CubeTri) { typedef Struct_(Binds_CubeTri) {
U4 PrimCursor; U1* prim_cursor;
V4_S2* FaceCursor; V4_S2* face_cursor;
V3_S2* VertBase; V3_S2* vert_base;
U4* OtBase; U4* ot_base;
}; };
internal MipsAtom_(rbind_cube_g4_face) atom_info(atom_bind(Binds_CubeTri), atom_phase(cube_g4) internal MipsAtom_(rbind_cube_g4_face) atom_info(atom_bind(Binds_CubeTri), atom_phase(cube_g4)){
, atom_reads(R_TapePtr)
, atom_writes(R_PrimCursor, R_FaceCursor, R_VertBase, R_OtBase, R_TapePtr)
){
/* Pop 4 arguments from the tape directly into the workspace registers */ /* Pop 4 arguments from the tape directly into the workspace registers */
load_word(R_PrimCursor, R_TapePtr, O_(Binds_CubeTri,PrimCursor)), load_word(R_PrimCursor, R_TapePtr, O_(Binds_CubeTri,prim_cursor)),
load_word(R_FaceCursor, R_TapePtr, O_(Binds_CubeTri,FaceCursor)), load_word(R_FaceCursor, R_TapePtr, O_(Binds_CubeTri,face_cursor)),
load_word(R_VertBase, R_TapePtr, O_(Binds_CubeTri,VertBase)), load_word(R_VertBase, R_TapePtr, O_(Binds_CubeTri,vert_base)),
load_word(R_OtBase, R_TapePtr, O_(Binds_CubeTri,OtBase)), load_word(R_OtBase, R_TapePtr, O_(Binds_CubeTri,ot_base)),
LdSlot_ add_ui_self( R_TapePtr, S_(Binds_CubeTri)), LdSlot_ add_ui_self( R_TapePtr, S_(Binds_CubeTri)),
mac_yield() mac_yield()
}; };
@@ -596,11 +525,13 @@ MipsAtom_(cube_g4_face) atom_info(atom_phase(cube_g4),
load_half_u(R_T2, R_FaceCursor, 2 * S_(S2)), load_half_u(R_T2, R_FaceCursor, 2 * S_(S2)),
// load_half_u(R_T3, R_FaceCursor, 3 * S_(S2)), // load_half_u(R_T3, R_FaceCursor, 3 * S_(S2)),
LdSlot_ mac_gte_load_tri_verts(R_VertBase, R_T0, R_T1, R_T2), GteDelay_ load_half_u(R_T3, R_FaceCursor, 3 * S_(S2)), LdSlot_ LdSlot_ mac_gte_load_tri_verts(R_VertBase, R_T0, R_T1, R_T2),
GteDelay_ nop, gte_cmdw_rotate_translate_perspective_triple, GteDelay_ load_half_u(R_T3, R_FaceCursor, 3 * S_(S2)), LdSlot_
GteDelay_ load_word(R_AtomJmp, R_TapePtr, 0), LdSlot_ //ac_yield: word 1,
gte_cmdw_rotate_translate_perspective_triple,
gte_cmdw_nclip, gte_cmdw_nclip,
gte_mv_from_data_r(R_T0, C2_MAC0), GteDelay_ nop, gte_mv_from_data_r(R_T0, C2_MAC0), GteDelay_ add_ui_self(R_TapePtr, S_(MipsCode)), // ac_yield: word 2
branch_le_zero(R_T0, atom_offset(cull, cube_g4_face_exit)), branch_le_zero(R_T0, atom_offset(cull, cube_g4_face_exit)),
/* BD-slot: Write the prim tag (R_0=0; overwrites the legacy tag word in the prim_buffer). /* BD-slot: Write the prim tag (R_0=0; overwrites the legacy tag word in the prim_buffer).
* If branch IS taken (face culled), the body is skipped and this 0-tag is stranded — * If branch IS taken (face culled), the body is skipped and this 0-tag is stranded —
@@ -630,16 +561,16 @@ MipsAtom_(cube_g4_face) atom_info(atom_phase(cube_g4),
// end: branch(cull) // end: branch(cull)
atom_label(cube_g4_face_exit) atom_label(cube_g4_face_exit)
add_ui_self(R_PrimCursor, S_(Poly_G4)), /* 9 words = Poly_G4 */ add_ui_self(R_PrimCursor, S_(Poly_G4)), // 9 words = Poly_G4
add_ui_self(R_FaceCursor, S_(S2) * 4), /* 4 × S2 = 8 bytes */ add_ui_self(R_FaceCursor, S_(S2) * 4), // 4 × S2 = 8 bytes
mac_yield() jump_reg(R_AtomJmp), BdSlot_ nop // ac_yield: word 3-4
}; };
typedef Struct_(Binds_FloorTri) { typedef Struct_(Binds_FloorTri) {
U4 PrimCursor; U1* prim_cursor;
V3_S2* FaceCursor; V3_S2* face_cursor;
V3_S2* VertBase; V3_S2* vert_base;
U4* OtBase; U4* ot_base;
}; };
internal internal
MipsAtom_(rbind_floor_f3_face) atom_info(atom_bind(Binds_FloorTri), atom_phase(floor_f3) MipsAtom_(rbind_floor_f3_face) atom_info(atom_bind(Binds_FloorTri), atom_phase(floor_f3)
@@ -647,10 +578,10 @@ MipsAtom_(rbind_floor_f3_face) atom_info(atom_bind(Binds_FloorTri), atom_phase(f
, atom_writes(R_PrimCursor, R_FaceCursor, R_VertBase, R_OtBase, R_TapePtr) , atom_writes(R_PrimCursor, R_FaceCursor, R_VertBase, R_OtBase, R_TapePtr)
){ ){
/* Pop 4 arguments from the tape directly into the workspace registers */ /* Pop 4 arguments from the tape directly into the workspace registers */
load_word(R_PrimCursor, R_TapePtr, O_(Binds_FloorTri,PrimCursor)), load_word(R_PrimCursor, R_TapePtr, O_(Binds_FloorTri,prim_cursor)),
load_word(R_FaceCursor, R_TapePtr, O_(Binds_FloorTri,FaceCursor)), load_word(R_FaceCursor, R_TapePtr, O_(Binds_FloorTri,face_cursor)),
load_word(R_VertBase, R_TapePtr, O_(Binds_FloorTri,VertBase)), load_word(R_VertBase, R_TapePtr, O_(Binds_FloorTri,vert_base)),
load_word(R_OtBase, R_TapePtr, O_(Binds_FloorTri,OtBase)), load_word(R_OtBase, R_TapePtr, O_(Binds_FloorTri,ot_base)),
LdSlot_ add_ui_self( R_TapePtr, S_(Binds_FloorTri)), LdSlot_ add_ui_self( R_TapePtr, S_(Binds_FloorTri)),
mac_yield() mac_yield()
}; };
@@ -662,13 +593,13 @@ MipsAtom_(floor_f3_face) atom_info(atom_phase(floor_f3)
, atom_writes(R_PrimCursor, R_FaceCursor) , atom_writes(R_PrimCursor, R_FaceCursor)
) { ) {
mac_load_tri_indices(R_FaceCursor, R_T0, R_T1, R_T2), mac_load_tri_indices(R_FaceCursor, R_T0, R_T1, R_T2),
mac_gte_load_tri_verts(R_VertBase, R_T0, R_T1, R_T2), mac_gte_load_tri_verts(R_VertBase, R_T0, R_T1, R_T2), GteDelay_ nop2,
nop2, gte_cmdw_rotate_translate_perspective_triple, // 2 nops retire the final cpu -> gte writes before RTPT gte_cmdw_rotate_translate_perspective_triple, // 2 nops retire the final cpu -> gte writes before RTPT
gte_cmdw_nclip, gte_cmdw_nclip,
/* Culling (Branch forward if Backface) */ /* Culling (Branch forward if Backface) */
gte_mv_from_data_r(R_T0, C2_MAC0), gte_mv_from_data_r(R_T0, C2_MAC0), GteDelay_ load_word(R_AtomJmp, R_TapePtr, 0), // ac_yield: word 1
nop, branch_le_zero(R_T0, atom_offset(culling, floor_f3_face_exit)), nop, // required gte -> cpu load-delay slot. branch_le_zero(R_T0, atom_offset(culling, floor_f3_face_exit)), BdSlot_ add_ui_self(R_TapePtr, S_(MipsCode)), // ac_yield: word 2
/* Format Primitive */ /* Format Primitive */
mac_gte_store_f3(R_PrimCursor), mac_gte_store_f3(R_PrimCursor),
@@ -678,7 +609,7 @@ MipsAtom_(floor_f3_face) atom_info(atom_phase(floor_f3)
/* Bounds Check OTZ < 2048 (Branch forward to skip insertion) */ /* Bounds Check OTZ < 2048 (Branch forward to skip insertion) */
add_ui( R_AT, R_0, OrderingTbl_Len), add_ui( R_AT, R_0, OrderingTbl_Len),
set_lt_u( R_AT, R_T1, R_AT), set_lt_u( R_AT, R_T1, R_AT),
branch_equal(R_AT, R_0, atom_offset(bounds_chk, floor_f3_face_exit)), nop, branch_equal(R_AT, R_0, atom_offset(bounds_chk, floor_f3_face_exit)), BdSlot_ nop,
mac_format_f3_color(R_PrimCursor, 0xFF, 0xFF, 0xFF), // RGB-form (R=FF, G=FF, B=FF = white) mac_format_f3_color(R_PrimCursor, 0xFF, 0xFF, 0xFF), // RGB-form (R=FF, G=FF, B=FF = white)
mac_insert_ot_tag(R_OtBase, R_PrimCursor, S_(Poly_F3)), /* Insert into Ordering Table Linked List */ mac_insert_ot_tag(R_OtBase, R_PrimCursor, S_(Poly_F3)), /* Insert into Ordering Table Linked List */
add_ui_self(R_PrimCursor, S_(Poly_F3)), /* Advance Prim Cursor (5 words) */ add_ui_self(R_PrimCursor, S_(Poly_F3)), /* Advance Prim Cursor (5 words) */
@@ -689,13 +620,12 @@ MipsAtom_(floor_f3_face) atom_info(atom_phase(floor_f3)
/* Advance Input Cursor & Yield (Both branch targets land here) */ /* Advance Input Cursor & Yield (Both branch targets land here) */
atom_label(floor_f3_face_exit) atom_label(floor_f3_face_exit)
add_ui_self(R_FaceCursor, S_(S2) * 4), /* Advance Face Cursor (4 * S2 = 8 bytes) */ add_ui_self(R_FaceCursor, S_(S2) * 4), /* Advance Face Cursor (4 * S2 = 8 bytes) */
mac_yield() jump_reg(R_AtomJmp), BdSlot_ nop // ac_yield: word 3-4
}; };
typedef Struct_(Binds_SyncPrimitiveArena) { U4 used; U4 cursor; }; typedef Struct_(Binds_SyncPrimitiveArena) { U4* used; U1* cursor; };
internal MipsAtom_(sync_primitive_arena) atom_info(atom_bind(Binds_SyncPrimitiveArena) internal MipsAtom_(sync_primitive_arena) atom_info(atom_bind(Binds_SyncPrimitiveArena)
, atom_reads( R_TapePtr, R_PrimCursor) , atom_reads(R_PrimCursor), atom_writes(R_AT)
, atom_writes(R_TapePtr)
){ ){
load_word(R_AT, R_TapePtr, O_(Binds_SyncPrimitiveArena,used)), load_word(R_AT, R_TapePtr, O_(Binds_SyncPrimitiveArena,used)),
load_word(R_T0, R_TapePtr, O_(Binds_SyncPrimitiveArena,cursor)), LdSlot_ load_word(R_T0, R_TapePtr, O_(Binds_SyncPrimitiveArena,cursor)), LdSlot_
+114 -146
View File
@@ -24,7 +24,7 @@
#include "duffle/pad.h" #include "duffle/pad.h"
#include "duffle/dsl.atom.h" #include "duffle/dsl.atom.h"
#include "duffle/lottes_tape.h" #include "duffle/tape.h"
#include "duffle/bios.h" #include "duffle/bios.h"
#include "duffle/psyq.h" #include "duffle/psyq.h"
@@ -80,9 +80,6 @@ typedef Struct_(SMemory) {
PadBiosRaw pad_raw[2]; PadBiosRaw pad_raw[2];
PadState pad[2]; PadState pad[2];
// TODO(Ed): We don't need this we can just cast at any point an address to a desired view of scratchpad, we have the address.
U4_V scratchpad; // d-cache
U1 ct_init_atom_mem[CT_InitAtomMem_Size]; U1 ct_init_atom_mem[CT_InitAtomMem_Size];
MipsAtom* normalize_v3s4; MipsAtom* normalize_v3s4;
MipsAtom* gte_cross_v3s4; MipsAtom* gte_cross_v3s4;
@@ -96,11 +93,11 @@ extern SMemory smem;
#define pad0_btn_(btn) btn & smem.pad[0].buttons #define pad0_btn_(btn) btn & smem.pad[0].buttons
#define pad1_btn_(btn) btn & smem.pad[1].buttons #define pad1_btn_(btn) btn & smem.pad[1].buttons
I_ B1* prim__alloc(U4 type_width, Str8 type_name) { I_ U1* prim__alloc(U4 type_width, Str8 type_name) {
gknown PrimitiveArena* pa = & smem.primitives; gknown PrimitiveArena* pa = & smem.primitives;
gknown B1* buf = (B1*) r_(smem.primitives.buf)[smem.active_buf_id]; gknown U1* buf = (U1*) r_(smem.primitives.buf)[smem.active_buf_id];
assert(pa->used + type_width < PrimitiveBuff_Len); assert(pa->used + type_width < PrimitiveBuff_Len);
B1* next = buf + pa->used; U1* next = buf + pa->used;
pa->used += type_width; pa->used += type_width;
return next; return next;
} }
@@ -114,10 +111,10 @@ I_ void resolve_look_at_c11(MT3_S2S4* look_at, P3_S4* eye, P3_S4* target, V3_S4*
V3_S4 pos, off; V3_S4 pos, off;
forward = target[0]; sub_v3s4(& forward, eye[0]); // RGA(Lengyel): Affine point - point = zero-weight direction. forward = target[0]; sub_v3s4(& forward, eye[0]); // RGA(Lengyel): Affine point - point = zero-weight direction.
normalize_v3s4(& forward, & uz); // RGA(Lengyel): Normalize the direction bulk. Not finite-point unitization. psy_normalize_v3s4(& forward, & uz); // RGA(Lengyel): Normalize the direction bulk. Not finite-point unitization.
cross_v3s4(& uz, up_in, & right); normalize_v3s4(& right, & ux); // RGA(Lengyel): Complement(Wedge(forward, up_in)) -> right axis. cross_v3s4(& uz, up_in, & right); psy_normalize_v3s4(& right, & ux); // RGA(Lengyel): Complement(Wedge(forward, up_in)) -> right axis.
cross_v3s4(& uz, & ux, & up); normalize_v3s4(& up, & uy); // RGA(Lengyel): Complement(Wedge(forward, right)) -> up axis. cross_v3s4(& uz, & ux, & up); psy_normalize_v3s4(& up, & uy); // RGA(Lengyel): Complement(Wedge(forward, right)) -> up axis.
// RGA(Lengyel): matrix expansion of the world-to-camera rotation (basis rows). // RGA(Lengyel): matrix expansion of the world-to-camera rotation (basis rows).
look_at->m[0][0] = ux.x; look_at->m[0][1] = ux.y; look_at->m[0][2] = ux.z; look_at->m[0][0] = ux.x; look_at->m[0][1] = ux.y; look_at->m[0][2] = ux.z;
@@ -143,25 +140,21 @@ internal void compile_init_atoms(void) {
RegUse_(gte_cross_v3s4) { RegUse_(gte_cross_v3s4) {
.a = ralloc_v3(), .a = ralloc_v3(),
.b = ralloc_v3(), .b = ralloc_v3(),
.x = ralloc(), .out = ralloc(),
.y = ralloc(), .src_a = ralloc(),
.z = ralloc(), .src_b = ralloc(),
}); });
regfile_reset(& rf); regfile_reset(& rf);
smem.normalize_v3s4 = build_normalize_v3s4(& ab, smem.normalize_v3s4 = normalize_v3s4(& ab,
RegUse_(build_normalize_v3s4) { RegUse_(normalize_v3s4) {
.scratch = ralloc(), .res = ralloc_v3(),
.src_ptr = ralloc(), .r0 = ralloc(),
.dst_ptr = ralloc(), .r1 = ralloc(),
.recip_est = ralloc(), .r2 = ralloc(),
.norm = ralloc(), .r3 = ralloc(),
.shift = ralloc(), .r4 = ralloc(),
.src_x = ralloc(), .r5 = ralloc(),
// .shift_count = ralloc(), /* dedicated slot for stage-3 → stage-4 shift count */
.t3 = ralloc(),
.t4 = ralloc(),
.t5 = ralloc(),
}); });
regfile_reset(& rf); regfile_reset(& rf);
@@ -171,25 +164,19 @@ internal void compile_init_atoms(void) {
} }
internal void compile_resolve_look_at(void) { internal void compile_resolve_look_at(void) {
AtomArena ab = atomarena_make(slice_ut_arr(smem.resolve_look_at_mem));
AtomBundle_resolve_look_at_R bundle = C_(void*, smem.resolve_look_at_bundle); AtomBundle_resolve_look_at_R bundle = C_(void*, smem.resolve_look_at_bundle);
AtomArena ab = atomarena_make(slice_ut_arr(smem.resolve_look_at_mem));
/* R_ScratchBase (= R_SP) is a tape carrier preserved across atoms; no carrier
* pin is needed in the regfile. The standard 24-register pool is sufficient. */
RegFile rf = regfile(regfile_abi_mask); RegFile rf = regfile(regfile_abi_mask);
#define ralloc() regfile_alloc(& rf) #define ralloc() regfile_alloc(& rf)
#define ralloc_v3() { ralloc(), ralloc(), ralloc() } #define ralloc_v3() { ralloc(), ralloc(), ralloc() }
bundle->input_and_sub = AtomBundleEntry_(resolve_look_at, input_and_sub)(& ab, bundle->input_and_sub = AtomBundleEntry_(resolve_look_at, input_and_sub)(& ab,
RegUse_(resolve_look_at_input_and_sub) { RegUse_(resolve_look_at_input_and_sub) {
.target = ralloc(), .target_ptr = ralloc(),
.eye = ralloc(), .eye_ptr = ralloc(),
.up_in = ralloc(), .up_in_ptr = ralloc(),
.t0 = ralloc(), .up_in = ralloc_v3(),
.t1 = ralloc(), .r012 = ralloc_v3(),
.t2 = ralloc(), .r345 = {ralloc(), R_AT, ralloc() },
.t3 = ralloc(),
.t4 = ralloc(),
}); });
regfile_reset(& rf); regfile_reset(& rf);
@@ -199,85 +186,70 @@ internal void compile_resolve_look_at(void) {
bundle->cross_to_up = smem.gte_cross_v3s4; bundle->cross_to_up = smem.gte_cross_v3s4;
bundle->normalize_up_uy = smem.normalize_v3s4; bundle->normalize_up_uy = smem.normalize_v3s4;
bundle->pop_mv_trans = resolve_look_at__pop_mv_trans(& ab, bundle->populate_mt3s4s2 = AtomBundleEntry_(resolve_look_at,populate_mt3s4s2)(& ab,
RegUse_(resolve_look_at__pop_mv_trans){ RegUse_(resolve_look_at_populate_mt3s4s2){
.look_at = ralloc(), .look_at = ralloc(),
.eye = ralloc(), .eye = ralloc(),
.row = ralloc_v3(), .row = ralloc_v3(),
.t6 = ralloc(), .r0 = ralloc(),
.t7 = ralloc(), .r1 = ralloc(),
.t8 = ralloc(), .r2 = ralloc(),
}); });
/* Sanity check: arena didn't overflow. */ assert(ab.used <= ResolveLookAtArena_Size); // Sanity check: arena didn't overflow.
assert(ab.used <= ResolveLookAtArena_Size);
#undef ralloc #undef ralloc
} }
/* Emit the resolve_look_at bundle into the tape. Called once per frame from update(). */ // Emit the resolve_look_at bundle into the tape. Called once per frame from update().
I_ void resolve_look_at(TapeBuilder_R tb I_ void resolve_look_at(TapeBuilder_R tb, MT3_S2S4* look_at, P3_S4* eye, P3_S4* target, V3_S4* up_in) {
, MT3_S2S4* look_at
, P3_S4* eye
, P3_S4* target
, V3_S4* up_in
){
/* Typed view of the scratchpad for field-address arithmetic. */ /* Typed view of the scratchpad for field-address arithmetic. */
ResolveLookAtScratch* sp = C_scratch(ResolveLookAtScratch*); ResolveLookAtScratch* sp = C_scratch(ResolveLookAtScratch*);
AtomBundle_resolve_look_at_R bundle = C_(void*, smem.resolve_look_at_bundle); AtomBundle_resolve_look_at_R bundle = C_(void*, smem.resolve_look_at_bundle);
tb_emit(tb, bundle->input_and_sub); tb_bind_(tb, Binds_ResolveLookAtSub,
.target = target,
.eye = eye,
.up_in = up_in,
);
tb_emit(tb, bundle->normalize_fwd_uz); tb_bind_(tb, Binds_normalize_v3s4,
.src_offset = O_(ResolveLookAtScratch,fwd),
.dst_offset = O_(ResolveLookAtScratch,uz),
);
tb_emit(tb, bundle->cross_to_right); tb_bind_(tb, Binds_gte_cross_v3s4,
.src_a = & sp->uz,
.src_b = & sp->up_in,
.out = & sp->right,
);
tb_emit(tb, bundle->normalize_right_ux); tb_bind_(tb, Binds_normalize_v3s4,
.src_offset = O_(ResolveLookAtScratch,right),
.dst_offset = O_(ResolveLookAtScratch,ux),
);
tb_emit(tb, bundle->cross_to_up); tb_bind_(tb, Binds_gte_cross_v3s4,
.src_a = & sp->uz,
.src_b = & sp->ux,
.out = & sp->up,
);
tb_emit(tb, bundle->normalize_up_uy); tb_bind_(tb, Binds_normalize_v3s4,
.src_offset = O_(ResolveLookAtScratch,up),
.dst_offset = O_(ResolveLookAtScratch,uy),
);
tb_emit(tb, bundle->populate_mt3s4s2); tb_bind_(tb, Binds_ResolveLookAt_PopulateMT3S4S2,
.look_at = look_at,
);
}
FI_ void camera_look_at(TapeBuilder_R tb, Camera* c, P3_S4* target, V3_S4* up_in) { resolve_look_at(tb, & c->look_at, & c->pos, target, up_in); }
tb_emit(tb, bundle->input_and_sub); {
tb_data(tb, u4_(target));
tb_data(tb, u4_(eye));
tb_data(tb, u4_(up_in));
}
tb_emit(tb, bundle->normalize_fwd_uz); {
tb_data(tb, u4_(O_(ResolveLookAtScratch, fwd) | (O_(ResolveLookAtScratch, uz) << 16)));
}
tb_emit(tb, bundle->cross_to_right); {
tb_data(tb, u4_(& sp->uz));
tb_data(tb, u4_(& sp->up_in));
tb_data(tb, u4_(& sp->right));
}
tb_emit(tb, bundle->normalize_right_ux); {
tb_data(tb, u4_(O_(ResolveLookAtScratch, right) | (O_(ResolveLookAtScratch, ux) << 16)));
}
tb_emit(tb, bundle->cross_to_up); {
tb_data(tb, u4_(& sp->uz));
tb_data(tb, u4_(& sp->ux));
tb_data(tb, u4_(& sp->up));
}
tb_emit(tb, bundle->normalize_up_uy); {
tb_data(tb, u4_(O_(ResolveLookAtScratch, up) | (O_(ResolveLookAtScratch, uy) << 16)));
}
tb_emit(tb, bundle->pop_mv_trans); {
tb_data(tb, u4_(look_at));
}
}
GCC_OPTIMIZATION_DISABLE
void update(PrimitiveArena* pa, U4* ordering_buf) void update(PrimitiveArena* pa, U4* ordering_buf)
{ {
TapeBuilder tb = tb_make(slice_ut_arr(smem.MemTape)); TapeBuilder tb = tb_make(slice_ut_arr(smem.MemTape));
/*Pad Input*/ {
// Pad Input
{
tb.used = 0; tb_scope_run(& tb) { tb.used = 0; tb_scope_run(& tb) {
// Grab latest state from bios. // Grab latest state from bios.
tb_emit_(pad_bios_snapshot); tb_emit_(pad_bios_snapshot);
tb_data_(raw, & smem.pad_raw[0]); tb_data(& tb, u4_(& smem.pad_raw[0]));
tb_data_(state, & smem.pad[0]); tb_data(& tb, u4_(& smem.pad[0]));
// tb_emit_(pad_bios_snapshot);
// tb_data_(raw, & smem.pad_raw[1]);
// tb_data_(state, & smem.pad[1]);
tb_emit_(pad_input_cam); tb_emit_(pad_input_cam);
tb_data_(state, & smem.pad[0]); tb_data(& tb, u4_(& smem.pad[0]));
tb_data_(cam, & smem.cam); tb_data(& tb, u4_(& smem.cam));
// tb_emit_(pad_input_cube_rotation);
// tb_data_(state, & smem.pad[0]);
// tb_data_(cube_rot, & smem.cube.rot);
// tb_data_(floor_rot, & smem.floor.rot);
} }
} }
@@ -305,7 +277,7 @@ void update(PrimitiveArena* pa, U4* ordering_buf)
if (use_c11_path == false) if (use_c11_path == false)
{ {
tb.used = 0; tb_scope_run(& tb) { tb.used = 0; tb_scope_run(& tb) {
resolve_look_at(& tb, & smem.cam.look_at, & smem.cam.pos, & smem.cube.pos, & v3s4(0, -fp_one, 0)); camera_look_at(& tb, & smem.cam, & smem.cube.pos, & v3s4(0, -fp_one, 0));
} }
} }
@@ -315,33 +287,30 @@ void update(PrimitiveArena* pa, U4* ordering_buf)
mt3s2s4_rotation (& smem.cube.rot, & smem.tform_world); mt3s2s4_rotation (& smem.cube.rot, & smem.tform_world);
mt3s2s4_translation(& smem.tform_world, & smem.cube.pos); mt3s2s4_translation(& smem.tform_world, & smem.cube.pos);
mt3s2s4_scale (& smem.tform_world, & smem.cube.scale); mt3s2s4_scale (& smem.tform_world, & smem.cube.scale);
// Combine world and look_at matrix. // Combine world and look_at matrix.
gte_comp_coord_m3s2(& smem.cam.look_at, & smem.tform_world, & smem.tform_view); gte_comp_coord_m3s2(& smem.cam.look_at, & smem.tform_world, & smem.tform_view);
gte_matrix_set_rotation (& smem.tform_view); gte_matrix_set_rotation (& smem.tform_view);
gte_matrix_set_translation(& smem.tform_view); gte_matrix_set_translation(& smem.tform_view);
U4 prim_base = u4_(pa->buf[smem.active_buf_id]); // TODO(Ed): We should do a bounds check beforehand to confirm pa can hold all tris?
U4 prim_cursor = prim_base + pa->used; // The tape atoms in-flight should not need to care.
U1* prim_base = u1_r(pa->buf[smem.active_buf_id]);
tb.used = 0; tb_scope(& tb) { U1* prim_cursor = prim_base + pa->used;
tb_emit(& tb, rbind_cube_g4_face); tb.used = 0; tb_scope_run(& tb) {
tb_data(& tb, prim_cursor); tb_emit(& tb, rbind_cube_g4_face); tb_bind_(& tb, Binds_CubeTri,
tb_data(& tb, u4_(smem.cube.faces)); .prim_cursor = prim_cursor,
tb_data(& tb, u4_(smem.cube.verts)); .face_cursor = smem.cube.faces,
tb_data(& tb, u4_(ordering_buf)); .vert_base = smem.cube.verts,
.ot_base = ordering_buf,
);
for (U4 i = 0; i < Cube_num_faces; i++) { for (U4 i = 0; i < Cube_num_faces; i++) {
// Two triangles per quad face: (x,y,z) and (x,z,w) tb_emit(& tb, cube_g4_face); // Two triangles per quad face: (x,y,z) and (x,z,w)
tb_emit(& tb, cube_g4_face);
} }
tb_emit(& tb, sync_primitive_arena); tb_bind_(& tb, Binds_SyncPrimitiveArena,
tb_emit(& tb, sync_primitive_arena); .used = & pa->used,
tb_data(& tb, u4_(& pa->used)); .cursor = prim_base,
tb_data(& tb, prim_base); );
} }
tape_run(tb_slice(tb));// Fire off the tape (bigger-clobber variant).
// smem.cube.rot.y += 30; // smem.cube.rot.y += 30;
} }
// Draw floor // Draw floor
@@ -350,45 +319,35 @@ void update(PrimitiveArena* pa, U4* ordering_buf)
mt3s2s4_rotation (& smem.floor.rot, & smem.tform_world); mt3s2s4_rotation (& smem.floor.rot, & smem.tform_world);
mt3s2s4_translation(& smem.tform_world, & smem.floor.pos); mt3s2s4_translation(& smem.tform_world, & smem.floor.pos);
mt3s2s4_scale (& smem.tform_world, & smem.floor.scale); mt3s2s4_scale (& smem.tform_world, & smem.floor.scale);
// Combine world and look_at matrix. // Combine world and look_at matrix.
gte_comp_coord_m3s2(& smem.cam.look_at, & smem.tform_world, & smem.tform_view); gte_comp_coord_m3s2(& smem.cam.look_at, & smem.tform_world, & smem.tform_view);
gte_matrix_set_rotation (& smem.tform_view); gte_matrix_set_rotation (& smem.tform_view);
gte_matrix_set_translation(& smem.tform_view); gte_matrix_set_translation(& smem.tform_view);
U4 prim_base = u4_(pa->buf[smem.active_buf_id]);
U4 prim_cursor = prim_base + pa->used;
// TODO(Ed): We should do a bounds check beforehand to confirm pa can hold all tris? // TODO(Ed): We should do a bounds check beforehand to confirm pa can hold all tris?
// The tape atoms in-flight should not need to care. // The tape atoms in-flight should not need to care.
U1_R prim_base = u1_r(pa->buf[smem.active_buf_id]);
// Prepare the tape. (Push protocol to tape) U1_R prim_cursor = prim_base + pa->used;
tb.used = 0; tb_scope(& tb) { tb.used = 0; tb_scope_run(& tb) { // Prepare the tape. (Push protocol to tape)
// tb_emit(& tb, set_gte_mt3s2s4); tb_emit(& tb, rbind_floor_f3_face); tb_bind_(& tb, Binds_FloorTri,
// tb_data(& tb, u4_(& smem.tform_view)); .prim_cursor = prim_cursor,
.face_cursor = smem.floor.faces,
tb_emit(& tb, rbind_floor_f3_face); .vert_base = smem.floor.verts,
// TODO(Ed): Just use a single context struct ref? .ot_base = ordering_buf,
tb_data(& tb, prim_cursor); );
tb_data(& tb, u4_(smem.floor.faces));
tb_data(& tb, u4_(smem.floor.verts));
tb_data(& tb, u4_(ordering_buf));
for (U4 i = 0; i < Floor_num_faces; i++) { for (U4 i = 0; i < Floor_num_faces; i++) {
tb_emit(& tb, floor_f3_face); tb_emit(& tb, floor_f3_face);
} }
// After floor_f3_face iterations complete, the primitive arena's used counter needs updating. // After floor_f3_face iterations complete, the primitive arena's used counter needs updating.
tb_emit(& tb, sync_primitive_arena); tb_emit(& tb, sync_primitive_arena); tb_bind_(& tb, Binds_SyncPrimitiveArena,
tb_data(& tb, u4_(& pa->used)); .used = & pa->used,
tb_data(& tb, prim_base); .cursor = prim_base,
);
} }
tape_run(tb_slice(tb));// Fire off the tape (bigger-clobber variant).
// C-side state (pa->used) has already been updated by the tape! // C-side state (pa->used) has already been updated by the tape!
// smem.floor.rot.y += 5; // smem.floor.rot.y += 5;
} }
} }
GCC_OPTIMIZATION_ENABLE
void render(void) { void render(void) {
} }
@@ -405,12 +364,22 @@ void gp_display_frame(DoubleBuffer* screen_buf, S4* active_buf_id, U4* ordering_
active_buf_id[0] = ! active_buf_id[0]; // Swap current buffer active_buf_id[0] = ! active_buf_id[0]; // Swap current buffer
} }
GCC_OPTIMIZATION_DISABLE
int main(void) int main(void)
{ {
smem = (SMemory){0}; smem = (SMemory){0};
// TODO(Ed): remove this field we don't need it in smem.
smem.scratchpad = C_(U4_V, Scratchpad_Loc); B4 basic_sample = false; if (basic_sample) {
// We will be defining the tape here along with its atom, then running the tape after.
TapeBuilder tb = tb_make(slice_ut_arr(smem.MemTape));
MipsCode add_one_to_R_T1[] = {
add_ui_self(R_T1, 1),
mac_yield(),
};
tb_emit(& tb, C_(MipsAtom*, add_one_to_R_T1));
Tape tape = tb_end(& tb);
tape_run(tape);
}
// smem.primitives.used = 0; // smem.primitives.used = 0;
// smem.active_buf_id = 0; // smem.active_buf_id = 0;
smem.cam.pos = v3s4(500, -1000, -1500); smem.cam.pos = v3s4(500, -1000, -1500);
@@ -455,4 +424,3 @@ int main(void)
}; };
return 0; return 0;
} }
GCC_OPTIMIZATION_ENABLE
+8 -8
View File
@@ -24,7 +24,7 @@ enum {
typedef U4 OrderingTable_Buffer[OrderingTbl_Len]; typedef U4 OrderingTable_Buffer[OrderingTbl_Len];
typedef Array_(OrderingTable_Buffer, 2); typedef Array_(OrderingTable_Buffer, 2);
typedef B1 PrimitiveBuffer[PrimitiveBuff_Len]; typedef U1 PrimitiveBuffer[PrimitiveBuff_Len];
typedef Array_(PrimitiveBuffer, 2); typedef Array_(PrimitiveBuffer, 2);
typedef Struct_(PrimitiveArena) { typedef Struct_(PrimitiveArena) {
A2_PrimitiveBuffer buf; A2_PrimitiveBuffer buf;
@@ -54,14 +54,14 @@ I_ void ent_cube128_init(A8_V3_S2* verts, A6_V4_S2* faces) {
{ 2, 3, 6, 7 }, { 2, 3, 6, 7 },
{ 3, 0, 7, 4 }, { 3, 0, 7, 4 },
}; };
mem_copy(u4_(verts), u4_(& baked_verts), S_(A8_V3_S2) ); mem_copy(b1_r(verts), b1_r(& baked_verts), S_(A8_V3_S2) );
mem_copy(u4_(faces), u4_(& baked_faces), S_(A6_V4_S2) ); mem_copy(b1_r(faces), b1_r(& baked_faces), S_(A6_V4_S2) );
return; return;
} }
typedef Struct_(Ent_Cube) { typedef Struct_(Ent_Cube) {
V3_S4 accel; V3_S4 accel;
V3_S4 vel; V3_S4 vel;
V3_S4 pos; // RGA(Lengyel): affine point with implicit weight one. Storage alias of V3_S4. V3_S4 pos;
V3_S4 scale; V3_S4 scale;
V3_S2 rot; V3_S2 rot;
A8_V3_S2 verts; A8_V3_S2 verts;
@@ -83,12 +83,12 @@ I_ void ent_floor_init(A4_V3_S2* verts, A2_V3_S2* faces) {
{ 0, 1, 2 }, { 0, 1, 2 },
{ 1, 3, 2 }, { 1, 3, 2 },
}; };
mem_copy(u4_(verts), u4_(& baked_verts), S_(A4_V3_S2)); mem_copy(b1_r(verts), b1_r(& baked_verts), S_(A4_V3_S2));
mem_copy(u4_(faces), u4_(& baked_faces), S_(A2_V3_S2)); mem_copy(b1_r(faces), b1_r(& baked_faces), S_(A2_V3_S2));
}; };
typedef Struct_(Ent_Floor) { typedef Struct_(Ent_Floor) {
V3_S4 accel; V3_S4 accel;
V3_S4 pos; // RGA(Lengyel): affine point with implicit weight one. Storage alias of V3_S4. V3_S4 pos;
V3_S4 scale; V3_S4 scale;
V3_S2 rot; V3_S2 rot;
A4_V3_S2 verts; A4_V3_S2 verts;
@@ -96,7 +96,7 @@ typedef Struct_(Ent_Floor) {
}; };
typedef Struct_(Camera) { typedef Struct_(Camera) {
P3_S4 pos; // RGA(Lengyel): affine point with implicit weight one. Storage alias of V3_S4. P3_S4 pos;
V3_S2 rot; V3_S2 rot;
MT3_S2S4 look_at; MT3_S2S4 look_at;
}; };
+52 -9
View File
@@ -1,11 +1,47 @@
--- duffle.lua — facade over duffle_scan / duffle_isa / duffle_emit. --- duffle.lua — facade over duffle_scan / duffle_isa / duffle_emit.
local scan = require("duffle_scan")
local isa = require("duffle_isa")
local emit = require("duffle_emit")
local M = {}
--- @class DuffleExport
--- bag: open module-export keys from duffle_scan / duffle_isa / duffle_emit
local scan = require("duffle_scan") ---@type DuffleExport
local isa = require("duffle_isa") ---@type DuffleExport
local emit = require("duffle_emit") ---@type DuffleExport
local M = {} ---@type DuffleExport
--- @alias Path string
--- @alias LineNum integer
--- @alias ByteOff integer
--- @alias MacroName string
--- @alias AtomName string
--- @alias Severity string
--- @class SourceFile
--- @field path Path
--- @field text string
--- @field dir string
--- @field basename string
--- @field scan SourceScan|nil
--- @class CorpusView
--- @field register_alias_registry table<string, AliasEntry>
--- @field type_name_registry table<string, TypeNameEntry>
--- @field atom_views table<AtomName, AtomViewEntry>
--- @field atom_ctxs table<AtomName, AtomCtxEntry>
--- @field atom_phases table<string, AtomPhaseGroup>
--- @field binds_by_name table<string, BindsEntry>
--- @field atoms_by_name table<AtomName, AtomEntry>
--- @field atom_infos AtomInfoEntry[]
--- @field components table<string, Component>
--- @field component_atom_infos AtomInfoEntry[]|nil
--- @field tape_chains table<string, TapeChain>|nil
--- @field source_order SourceFile[]
--- @field collisions CorpusCollision[]
--- @param src DuffleExport
--- @param label string
--- @return nil
local function merge(src, label) local function merge(src, label)
for k, v in pairs(src) do for k, v in pairs(src) do ---@type string, any
if M[k] ~= nil and M[k] ~= v then if M[k] ~= nil and M[k] ~= v then
error("duffle facade name collision on " .. tostring(k) .. " from " .. label, 0) error("duffle facade name collision on " .. tostring(k) .. " from " .. label, 0)
end end
@@ -17,8 +53,10 @@ merge(scan, "duffle_scan")
merge(isa, "duffle_isa") merge(isa, "duffle_isa")
merge(emit, "duffle_emit") merge(emit, "duffle_emit")
--- @param ctx PassCtx
--- @return CorpusView
function M.corpus_view(ctx) function M.corpus_view(ctx)
local corpus = ctx and ctx.shared and ctx.shared.corpus local corpus = ctx and ctx.shared and ctx.shared.corpus ---@type Corpus
if not corpus then error("requires ctx.shared.corpus", 0) end if not corpus then error("requires ctx.shared.corpus", 0) end
return { return {
register_alias_registry = corpus.register_alias_registry or {}, register_alias_registry = corpus.register_alias_registry or {},
@@ -31,16 +69,21 @@ function M.corpus_view(ctx)
atom_infos = corpus.atom_infos or {}, atom_infos = corpus.atom_infos or {},
components = corpus.components or {}, components = corpus.components or {},
component_atom_infos = corpus.component_atom_infos or {}, component_atom_infos = corpus.component_atom_infos or {},
component_body_index = corpus.component_body_index or {},
tape_chains = corpus.tape_chains or {}, tape_chains = corpus.tape_chains or {},
source_order = corpus.source_order or {}, source_order = corpus.source_order or {},
collisions = corpus.collisions or {}, collisions = corpus.collisions or {},
} }
end end
--- @param rules CheckRule[]
--- @param phase string
--- @param item AtomEntry|SourceFile
--- @param pipe_ctx PassScratch
--- @param findings Finding[]
--- @return nil
function M.run_check_rules(rules, phase, item, pipe_ctx, findings) function M.run_check_rules(rules, phase, item, pipe_ctx, findings)
for _, rule in ipairs(rules) do for _, rule in ipairs(rules) do ---@type integer, CheckRule
local fn = rule[phase] local fn = rule[phase] ---@type (fun(item: AtomEntry|SourceFile, pipe_ctx: PassScratch, findings: Finding[]): nil)|nil
if fn then fn(item, pipe_ctx, findings) end if fn then fn(item, pipe_ctx, findings) end
end end
end end
+356 -203
View File
@@ -1,9 +1,9 @@
--- duffle_emit.lua — project_emission + decl finders. --- duffle_emit.lua — project_emission + decl finders.
local scan = require("duffle_scan") local scan = require("duffle_scan") ---@type DuffleScan
local isa = require("duffle_isa") local isa = require("duffle_isa") ---@type DuffleIsa
local M = {} local M = {} ---@type DuffleEmit
for k, v in pairs(scan) do M[k] = v end for k, v in pairs(scan) do M[k] = v end ---@type string, any
for k, v in pairs(isa) do M[k] = v end for k, v in pairs(isa) do M[k] = v end ---@type string, any
-- Section 8: Cross-source component-body index + word-event expansion -- Section 8: Cross-source component-body index + word-event expansion
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
@@ -11,27 +11,41 @@ for k, v in pairs(isa) do M[k] = v end
-- Shared, memoized helpers: a single emitted-word event stream that every downstream pass reads from, -- Shared, memoized helpers: a single emitted-word event stream that every downstream pass reads from,
-- built once from the pre-tokenized bodies. -- built once from the pre-tokenized bodies.
--- @class ComponentBodyEntry --- @class EmissionWalkCtx
--- @field body_tokens table -- pre-tokenized {{tok=string, rel=integer}, ...} --- @field components table<string, Component>
--- @field body_off integer -- byte offset of body[1] in `source` --- @field word_counts WordCounts
--- @field line_of fun(pos:integer):integer -- byte-offset → 1-based line number in `source` --- @field reg_use_schema RegUseSchema|nil
--- @field source string -- absolute path of the source containing the declaration --- @field reg_use_param string|nil
--- @field declaration integer -- 1-based line number of the MipsAtomComp_(ac_X) declaration --- @field atom_name AtomName|nil
--- @field kind string -- "comp_bare" | "comp_proc" --- @field schema_name string|nil
--- @field visiting table<string, boolean>|nil -- bag: component name on DFS stack
--- @field root_call_path string|nil
--- @field root_call_line integer|nil
-- The cross-source component-body index is owned by the corpus (`corpus.component_body_index`, populated by `passes/components.lua`). --- @class RegUseCtx
--- @field reg_use_schema RegUseSchema|nil
--- @field reg_use_param string|nil
--- @field atom_name AtomName|nil
--- @field schema_name string|nil
-- Finding: see ps1_meta.lua
--- @class DuffleEmit
-- The cross-source component row is owned by the corpus (`corpus.components`, populated by `passes/components.lua`).
-- Consumers (`passes/static_analysis.lua`, `passes/emission_model.lua`) read it directly; per-pass memoization helpers stay out of scope. -- Consumers (`passes/static_analysis.lua`, `passes/emission_model.lua`) read it directly; per-pass memoization helpers stay out of scope.
-- ASCII byte constants used by split_call_args (kept local to keep Section 8 self-contained). -- ASCII byte constants used by split_call_args (kept local to keep Section 8 self-contained).
local E_BYTE_OPEN_PAREN = 0x28 local E_BYTE_OPEN_PAREN = 0x28 ---@type integer
local E_BYTE_OPEN_BRACE = 0x7B local E_BYTE_OPEN_BRACE = 0x7B ---@type integer
local E_BYTE_OPEN_BRACK = 0x5B local E_BYTE_OPEN_BRACK = 0x5B ---@type integer
local E_BYTE_DQUOTE = 0x22 local E_BYTE_DQUOTE = 0x22 ---@type integer
local E_BYTE_SQUOTE = 0x27 local E_BYTE_SQUOTE = 0x27 ---@type integer
local E_BYTE_COMMA = 0x2C local E_BYTE_COMMA = 0x2C ---@type integer
-- Map an open-delimiter byte to its matching close string for read_balanced. -- Map an open-delimiter byte to its matching close string for read_balanced.
local E_OPEN_CLOSE = { local E_OPEN_CLOSE = { ---@type table<integer, string> -- bag: open-delimiter byte -> close string
[E_BYTE_OPEN_PAREN] = ")", [E_BYTE_OPEN_PAREN] = ")",
[E_BYTE_OPEN_BRACE] = "}", [E_BYTE_OPEN_BRACE] = "}",
[E_BYTE_OPEN_BRACK] = "]", [E_BYTE_OPEN_BRACK] = "]",
@@ -44,16 +58,16 @@ local E_OPEN_CLOSE = {
--- @param inner string --- @param inner string
--- @return string[] --- @return string[]
local function split_call_args(inner) local function split_call_args(inner)
local args = {} local args = {} ---@type string[]
if not inner or inner == "" then return args end if not inner or inner == "" then return args end
local pos = 1 local pos = 1 ---@type integer
local len = #inner local len = #inner ---@type integer
local start = 1 local start = 1 ---@type integer
while pos <= len do while pos <= len do
local c = inner:byte(pos) local c = inner:byte(pos) ---@type integer
local close = E_OPEN_CLOSE[c] local close = E_OPEN_CLOSE[c] ---@type string|nil
if close then if close then
local _, after = M.read_balanced(inner, string.char(c), close, pos) local _, after = M.read_balanced(inner, string.char(c), close, pos) ---@type integer, integer
pos = after pos = after
elseif c == E_BYTE_DQUOTE or c == E_BYTE_SQUOTE then elseif c == E_BYTE_DQUOTE or c == E_BYTE_SQUOTE then
pos = M.skip_str_or_cmt(inner, pos) pos = M.skip_str_or_cmt(inner, pos)
@@ -74,18 +88,18 @@ end
--- @param tok string --- @param tok string
--- @return string, string[] --- @return string, string[]
local function token_ident_and_args(tok) local function token_ident_and_args(tok)
local ident, after = M.read_ident(tok, 1) local ident, after = M.read_ident(tok, 1) ---@type string|nil, integer
if not ident then return "?", {} end if not ident then return "?", {} end
local paren_pos = M.skip_ws_and_cmt(tok, after) local paren_pos = M.skip_ws_and_cmt(tok, after) ---@type integer
if tok:sub(paren_pos, paren_pos) ~= "(" then return ident, {} end if tok:sub(paren_pos, paren_pos) ~= "(" then return ident, {} end
local inner = M.read_parens(tok, paren_pos) local inner = M.read_parens(tok, paren_pos) ---@type string|nil
if not inner then return ident, {} end if not inner then return ident, {} end
return ident, split_call_args(inner) return ident, split_call_args(inner)
end end
-- The macro-name prefix that marks a `mac_X(...)` component invocation. -- The macro-name prefix that marks a `mac_X(...)` component invocation.
local E_MAC_PREFIX = "mac_" local E_MAC_PREFIX = "mac_" ---@type string
local E_MAC_PREFIX_LEN = 4 local E_MAC_PREFIX_LEN = 4 ---@type integer
--- Expand a body entry into the flat sequence of emitted machine-word events. --- Expand a body entry into the flat sequence of emitted machine-word events.
--- ---
@@ -96,18 +110,18 @@ local E_MAC_PREFIX_LEN = 4
--- * Known `mac_X(...)` calls: Recursively expand the indexed component body, including nested components. Every event from the expansion carries: --- * Known `mac_X(...)` calls: Recursively expand the indexed component body, including nested components. Every event from the expansion carries:
--- - `source` / `line` = the COMPONENT'S source path + the line of the token within the component body (i.e. "definition site"). --- - `source` / `line` = the COMPONENT'S source path + the line of the token within the component body (i.e. "definition site").
--- - `call_source` / `call_line` = the ROOT atom's source path + call-site line, PRESERVED across recursion so nested events still point at the original root. --- - `call_source` / `call_line` = the ROOT atom's source path + call-site line, PRESERVED across recursion so nested events still point at the original root.
--- * Unknown `mac_X` (not in `component_index`): fall back to `word_counts[ident]` if present; otherwise emit one opaque event so the cycle budget accounts for the word. --- * Unknown `mac_X` (not in `components`): fall back to `word_counts[ident]` if present; otherwise emit one opaque event so the cycle budget accounts for the word.
--- * Marker Tokens (`atom_label(...)` / `atom_offset(...)`): Zero events (they are pure metaprogram hints). --- * Marker Tokens (`atom_label(...)` / `atom_offset(...)`): Zero events (they are pure metaprogram hints).
--- ---
--- Cycle protection: a per-expansion `visiting` set tracks components currently on the expansion stack; --- Cycle protection: a per-expansion `visiting` set tracks components currently on the expansion stack;
--- a re-entry produces a deterministic `{kind = "cycle", ...}` error and aborts that branch (does NOT hang, does NOT recurse). --- a re-entry produces a deterministic `{check = "cycle", ...}` error and aborts that branch (does NOT hang, does NOT recurse).
--- ---
--- Pure: reads `body_entry` / `component_index` / `word_counts`. Memoization is the caller's responsibility. --- Pure: reads `body_entry` / `components` / `word_counts`. Memoization is the caller's responsibility.
--- Callers wanting `word_events` / `word_event_errors` precomputed for many atoms should memoize them per atom. --- Callers wanting `word_events` / `word_event_errors` precomputed for many atoms should memoize them per atom.
--- @param body_entry table -- `{body_tokens, body_off, line_of, source, declaration}` (declaration = root atom's atom.line) --- @param body_entry Component
--- @param component_index table -- the bare-name → ComponentBodyEntry map from M.get_component_body_index --- @param components table<string, Component>
--- @param word_counts table -- macro name → emitted-word count (from `ctx.shared.word_counts`) --- @param word_counts WordCounts
--- @return WordEvent[], WordEventError[] --- @return WordEvent[], Finding[]
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
-- Section 11: project_emission (per-atom emission projection) -- Section 11: project_emission (per-atom emission projection)
@@ -118,16 +132,16 @@ local E_MAC_PREFIX_LEN = 4
-- The items stream is the single ordered source of truth; `word_events` and `markers` are dense views over it. -- The items stream is the single ordered source of truth; `word_events` and `markers` are dense views over it.
-- --
-- The helper below operates on a body string (not a body_entry) so the pass can call it without depending on the older SourceScan / body_off conventions. -- The helper below operates on a body string (not a body_entry) so the pass can call it without depending on the older SourceScan / body_off conventions.
-- component_index argument is reserved for recursive component expansion. -- `components` is the recursive expansion map (`corpus.components`).
-- word_counts table is authored-metadata + current-component count table. -- word_counts table is authored-metadata + current-component count table.
--- @class EmissionProjection --- @class EmissionProjection
--- @field items table[] -- Ordered stream of word|label|offset|invoke_begin|invoke_end --- @field items EmissionItem[]
--- @field word_events table[] -- Dense view of items where kind == "word" --- @field word_events WordEvent[]
--- @field markers table[] -- Dense view of items where kind == "label"|"offset" --- @field markers EmissionMarker[]
--- @field invocations InvocationRecord[] -- dense view of items where kind == "invoke_begin"|"invoke_end" --- @field invocations InvocationRecord[] -- dense view of items where kind == "invoke_begin"|"invoke_end"
--- @field errors table[] -- Token-resolution failures surfaced without fail-loud --- @field errors Finding[]
--- @field warnings table[] -- Opaque warnings (e.g. unknown uncounted macro) --- @field warnings Finding[]
--- @class InvocationRecord --- @class InvocationRecord
--- Lives at `atom.paths.invocations[*]`. Constructed once at the single invocation-construction site --- Lives at `atom.paths.invocations[*]`. Constructed once at the single invocation-construction site
@@ -148,7 +162,7 @@ local E_MAC_PREFIX_LEN = 4
--- @field end_word integer -- 1-based items index of the `invoke_end` item (set by `emit_invoke_end`) --- @field end_word integer -- 1-based items index of the `invoke_end` item (set by `emit_invoke_end`)
--- @field word_count integer -- Number of `word` items emitted between `start_word` and `end_word` (inclusive) --- @field word_count integer -- Number of `word` items emitted between `start_word` and `end_word` (inclusive)
--- @field debug_skip boolean -- `debug_skip` stamp; true iff `corpus.components[name].debug_skip` is true at construction. Always boolean (never `nil`). --- @field debug_skip boolean -- `debug_skip` stamp; true iff `corpus.components[name].debug_skip` is true at construction. Always boolean (never `nil`).
--- @field errors table[] -- Per-invocation construction errors (cycle / count_mismatch); does not include pass-level errors --- @field errors Finding[]
-- Internal recursive walker. The items stream holds every emitted event in order; `word_events`, `markers`, -- Internal recursive walker. The items stream holds every emitted event in order; `word_events`, `markers`,
-- `invocations`, `errors`, `warnings` are dense views / side outputs appended alongside. -- `invocations`, `errors`, `warnings` are dense views / side outputs appended alongside.
@@ -164,38 +178,44 @@ local E_MAC_PREFIX_LEN = 4
-- * Unknown uncounted macros emit one opaque word + one warning. Unknown metadata-backed macros (entry in `word_counts`) emit the declared word count, no warning. -- * Unknown uncounted macros emit one opaque word + one warning. Unknown metadata-backed macros (entry in `word_counts`) emit the declared word count, no warning.
-- * Cycle detection uses an active DFS stack (`visiting`); a cycle appends a construction error to BOTH the projection errors and the cycle invocation's own errors, -- * Cycle detection uses an active DFS stack (`visiting`); a cycle appends a construction error to BOTH the projection errors and the cycle invocation's own errors,
-- then breaks out without recursing (the cycle entry still receives an invocation ID + paired `invoke_begin` / `invoke_end` items, so the boundary invariant is preserved). -- then breaks out without recursing (the cycle entry still receives an invocation ID + paired `invoke_begin` / `invoke_end` items, so the boundary invariant is preserved).
-- * Component declared-count mismatch (declared vs. measured) is a construction error (kind = "count_mismatch"); recorded on the invocation record and pass-level errors list. -- * Component declared-count mismatch (declared vs. measured) is a construction error (check = "count_mismatch"); recorded on the invocation record and pass-level errors list.
-- * Final boundary check: if any invocation is still open at end of walk, surface a "unbalanced" construction error. -- * Final boundary check: if any invocation is still open at end of walk, surface a "unbalanced" construction error.
--- @param root_body_entry Component
--- @param ctx_table EmissionWalkCtx
--- @return EmissionProjection
local function _project_emission_inner(root_body_entry, ctx_table) local function _project_emission_inner(root_body_entry, ctx_table)
local items = {} local items = {} ---@type EmissionItem[]
local word_events = {} local word_events = {} ---@type WordEvent[]
local markers = {} local markers = {} ---@type EmissionMarker[]
local invocations = {} local invocations = {} ---@type InvocationRecord[]
local errors = {} local errors = {} ---@type Finding[]
local warnings = {} local warnings = {} ---@type Finding[]
local word_idx = 0 local word_idx = 0 ---@type integer
local invocation_stack = {} -- stack of currently-open invocation records local invocation_stack = {} ---@type InvocationRecord[] -- stack of currently-open invocation records
local next_inv_id = 0 local next_inv_id = 0 ---@type integer
local reg_use_schema = ctx_table.reg_use_schema local reg_use_schema = ctx_table.reg_use_schema ---@type RegUseSchema|nil
local reg_use_param = ctx_table.reg_use_param local reg_use_param = ctx_table.reg_use_param ---@type string|nil
local atom_name = ctx_table.atom_name local atom_name = ctx_table.atom_name ---@type AtomName|nil
local slot_readonly = {} local slot_readonly = {} ---@type table<string, boolean> -- bag: slot name -> readonly
if reg_use_schema then if reg_use_schema then
for _, slot in ipairs(reg_use_schema.slots or {}) do for _, slot in ipairs(reg_use_schema.slots or {}) do ---@type integer, RegUseSlot
slot_readonly[slot.name] = slot.readonly == true slot_readonly[slot.name] = slot.readonly == true
end end
end end
--- @param sub_map table<string, string>|nil
--- @param operand string|any
--- @return any
local function apply_sub(sub_map, operand) local function apply_sub(sub_map, operand)
if not (sub_map and type(operand) == "string") then return operand end if not (sub_map and type(operand) == "string") then return operand end
if sub_map[operand] then return sub_map[operand] end if sub_map[operand] then return sub_map[operand] end
local dot = operand:find(".", 1, true) local dot = operand:find(".", 1, true) ---@type integer|nil
if dot then if dot then
local head = operand:sub(1, dot - 1) local head = operand:sub(1, dot - 1) ---@type string
local mapped = sub_map[head] local mapped = sub_map[head] ---@type string|nil
if type(mapped) == "string" then if type(mapped) == "string" then
return mapped .. operand:sub(dot) return mapped .. operand:sub(dot)
end end
@@ -203,58 +223,71 @@ local function _project_emission_inner(root_body_entry, ctx_table)
return operand return operand
end end
--- @param operand string|any
--- @return string|nil, string|nil, string|nil
local function resolve_gpr_key(operand) local function resolve_gpr_key(operand)
if type(operand) ~= "string" then return nil end if type(operand) ~= "string" then return nil end
if operand:sub(1, 2) == "R_" then return operand end if operand:sub(1, 2) == "R_" then return operand end
if not (reg_use_schema and reg_use_param) then return nil end if not (reg_use_schema and reg_use_param) then return nil end
local prefix = reg_use_param .. "." local prefix = reg_use_param .. "." ---@type string
if operand:sub(1, #prefix) ~= prefix then return nil end if operand:sub(1, #prefix) ~= prefix then return nil end
local member_path = operand:sub(#prefix + 1) local member_path = operand:sub(#prefix + 1) ---@type string
local slot = reg_use_schema.alias_to_slot[member_path] local slot = reg_use_schema.alias_to_slot[member_path] ---@type string|nil
if not slot then return nil, member_path end if not slot then return nil, member_path end
return "reguse:" .. atom_name .. ":" .. slot, nil, slot return "reguse:" .. atom_name .. ":" .. slot, nil, slot
end end
--- @return integer[]
local function open_invocation_ids_snapshot() local function open_invocation_ids_snapshot()
local ids = {} local ids = {} ---@type integer[]
for _, inv in ipairs(invocation_stack) do for _, inv in ipairs(invocation_stack) do ---@type integer, InvocationRecord
ids[#ids + 1] = inv.id ids[#ids + 1] = inv.id
end end
return ids return ids
end end
local function emit_word(encoder, args, line, word_call_text, --- @param encoder string
def_source_now, def_line_now, --- @param args string[]|nil
immediate_call_text, root_call_text_w, sub_map) --- @param line integer
local inv_ids = open_invocation_ids_snapshot() --- @param word_call_text string|nil
local outermost = inv_ids[1] or 0 --- @param def_source_now string|nil
--- @param def_line_now integer|nil
--- @param immediate_call_text string|nil
--- @param root_call_text_w string|nil
--- @param sub_map table<string, string>|nil
--- @return nil
local function emit_word(encoder, args, line, word_call_text, def_source_now, def_line_now, immediate_call_text, root_call_text_w, sub_map)
local inv_ids = open_invocation_ids_snapshot() ---@type integer[]
local outermost = inv_ids[1] or 0 ---@type integer
-- 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. -- 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.
-- For words emitted inside a component expansion, `immediate_call_text` is the immediate outer `mac_X(...)` token text; -- For words emitted inside a component expansion, `immediate_call_text` is the immediate outer `mac_X(...)` token text;
-- The call that triggered the body expansion we're currently walking. -- The call that triggered the body expansion we're currently walking.
local eff_call_text = immediate_call_text or word_call_text local eff_call_text = immediate_call_text or word_call_text ---@type string|nil
local eff_root_call_text = root_call_text_w local eff_root_call_text = root_call_text_w ---@type string|nil
local gpr_keys = nil local gpr_keys = nil ---@type string[]|nil
if reg_use_schema or sub_map then if reg_use_schema or sub_map then
gpr_keys = {} gpr_keys = {}
for pos, arg in ipairs(args or {}) do for pos, arg in ipairs(args or {}) do ---@type integer, string
local effective = apply_sub(sub_map, arg) local effective = apply_sub(sub_map, arg) ---@type any
local key, unresolved, slot = resolve_gpr_key(effective) local key, unresolved, slot = resolve_gpr_key(effective) ---@type string|nil, string|nil, string|nil
gpr_keys[pos] = key gpr_keys[pos] = key
if unresolved then if unresolved then
errors[#errors + 1] = { errors[#errors + 1] = {
kind = "reguse_unresolved", kind = "error",
check = "reguse_unresolved",
line = line, line = line,
msg = string.format("RegUse operand %q does not resolve in schema %q", msg = string.format("RegUse operand %q does not resolve in schema %q",
effective, (reg_use_schema and reg_use_schema.name) or "?"), effective, (reg_use_schema and reg_use_schema.name) or "?"),
} }
end end
if key and slot and slot_readonly[slot] then if key and slot and slot_readonly[slot] then
local row = M.instr(encoder) local row = M.instr(encoder) ---@type InstructionRow|nil
if row and row.writes then if row and row.writes then
for _, wpos in ipairs(row.writes) do for _, wpos in ipairs(row.writes) do ---@type integer, integer
if wpos == pos then if wpos == pos then
errors[#errors + 1] = { errors[#errors + 1] = {
kind = "reguse_const_write", kind = "error",
check = "reguse_const_write",
line = line, line = line,
msg = string.format("RegUse slot %q is Reg const; %s writes it", msg = string.format("RegUse slot %q is Reg const; %s writes it",
slot, encoder), slot, encoder),
@@ -268,6 +301,17 @@ local function _project_emission_inner(root_body_entry, ctx_table)
if not reg_use_schema then if not reg_use_schema then
gpr_keys = nil gpr_keys = nil
end end
local isa = M.instr(encoder) ---@type InstructionRow|nil
local isa_kind = isa and isa.kind or "unknown" ---@type string
local nop_words = (encoder == "nop" and 1) or (encoder == "nop2" and 2) or 0 ---@type integer
local is_yield = (encoder == "mac_yield" or encoder == "mac_yield_tail") ---@type boolean
local gp0_shape = type(encoder) == "string" ---@type string|nil
and encoder:match("^mac_format_([%w_]+)_color$")
or nil
local is_load = (isa_kind == "load") ---@type boolean
local is_branch = (isa_kind == "branch") ---@type boolean
local is_unconditional_jump = (encoder == "jump" or encoder == "call_addr") ---@type boolean
local is_terminal_jump = (encoder == "jump_reg" or encoder == "call_reg" or encoder == "jump_link") ---@type boolean
items[#items + 1] = { items[#items + 1] = {
kind = "word", kind = "word",
encoder = encoder, encoder = encoder,
@@ -280,6 +324,15 @@ local function _project_emission_inner(root_body_entry, ctx_table)
invocation_ids = inv_ids, invocation_ids = inv_ids,
outermost_invocation_id = outermost, outermost_invocation_id = outermost,
gpr_keys = gpr_keys, gpr_keys = gpr_keys,
ident = encoder,
isa_kind = isa_kind,
nop_words = nop_words,
is_yield = is_yield,
is_load = is_load,
is_branch = is_branch,
is_unconditional_jump = is_unconditional_jump,
is_terminal_jump = is_terminal_jump,
gp0_shape = gp0_shape,
} }
word_events[#word_events + 1] = { word_events[#word_events + 1] = {
i = word_idx, i = word_idx,
@@ -293,21 +346,43 @@ local function _project_emission_inner(root_body_entry, ctx_table)
outermost_invocation_id = outermost, outermost_invocation_id = outermost,
word_count = 1, word_count = 1,
gpr_keys = gpr_keys, gpr_keys = gpr_keys,
ident = encoder,
kind = isa_kind,
nop_words = nop_words,
is_yield = is_yield,
is_load = is_load,
is_branch = is_branch,
is_unconditional_jump = is_unconditional_jump,
is_terminal_jump = is_terminal_jump,
gp0_shape = gp0_shape,
} }
word_idx = word_idx + 1 word_idx = word_idx + 1
end end
--- @param kind string
--- @param name string
--- @param target string|nil
--- @param line integer
--- @param immediate_call_text string|nil
--- @param root_call_text_w string|nil
--- @param consuming_encoder string|nil
--- @param consuming_arg_pos integer|nil
--- @return nil
local function emit_marker(kind, name, target, line, local function emit_marker(kind, name, target, line,
immediate_call_text, root_call_text_w, immediate_call_text, root_call_text_w,
consuming_encoder, consuming_arg_pos) consuming_encoder, consuming_arg_pos)
local inv_ids = open_invocation_ids_snapshot() local inv_ids = open_invocation_ids_snapshot() ---@type integer[]
local outermost = inv_ids[1] or 0 local outermost = inv_ids[1] or 0 ---@type integer
-- Markers carry the open invocation stack snapshot. `call_text` / `root_call_text` belong to words, not markers — markers are zero-width and skip per-word call-site attribution. -- Markers carry the open invocation stack snapshot. `call_text` / `root_call_text` belong to words, not markers — markers are zero-width and skip per-word call-site attribution.
-- `consuming_encoder` + `consuming_arg_pos` carry the surrounding control-transfer instruction context -- `consuming_encoder` + `consuming_arg_pos` carry the surrounding control-transfer instruction context
-- (e.g. `branch_le_zero` consuming its 3rd argument, or `jump` / `call_addr` consuming their only argument). -- (e.g. `branch_le_zero` consuming its 3rd argument, or `jump` / `call_addr` consuming their only argument).
-- `passes/offsets.lua` reads these to dispatch per-consuming-instruction offset encoding. -- `passes/offsets.lua` reads these to dispatch per-consuming-instruction offset encoding.
-- nil for top-level markers (where the marker is the entire token — no surrounding consuming instruction). -- Offset markers require a real consuming encoder. A lone top-level `atom_offset` is not emitted.
local it = { -- Label and delay markers may have a nil encoder (they are not consumed as immediates).
if kind == "offset" and (consuming_encoder == nil or consuming_encoder == "") then
return
end
local it = { ---@type EmissionItem
kind = kind, kind = kind,
name = name, name = name,
line = line, line = line,
@@ -333,22 +408,26 @@ local function _project_emission_inner(root_body_entry, ctx_table)
-- Count top-level commas in `tok` between position `from_pos` (inclusive) and `to_pos` (exclusive). -- Count top-level commas in `tok` between position `from_pos` (inclusive) and `to_pos` (exclusive).
-- Tracks paren depth so commas inside nested () don't count. Skips string literals + comments. -- Tracks paren depth so commas inside nested () don't count. Skips string literals + comments.
-- Used by `emit_embedded_markers` to compute `consuming_arg_pos` for each embedded marker. -- Used by `emit_embedded_markers` to compute `consuming_arg_pos` for each embedded marker.
--- @param tok string
--- @param from_pos integer
--- @param to_pos integer
--- @return integer
local function count_top_level_commas(tok, from_pos, to_pos) local function count_top_level_commas(tok, from_pos, to_pos)
local depth = 0 local depth = 0 ---@type integer
local count = 0 local count = 0 ---@type integer
local i = from_pos local i = from_pos ---@type integer
while i < to_pos do while i < to_pos do
local c = tok:sub(i, i) local c = tok:sub(i, i) ---@type string
if c == "'" or c == '"' then if c == "'" or c == '"' then
local next_pos = M.skip_str_or_cmt(tok, i) local next_pos = M.skip_str_or_cmt(tok, i) ---@type integer
i = (next_pos > i) and next_pos or (i + 1) i = (next_pos > i) and next_pos or (i + 1)
elseif c == "/" and tok:sub(i + 1, i + 1) == "/" then elseif c == "/" and tok:sub(i + 1, i + 1) == "/" then
-- line comment: skip to end of line -- line comment: skip to end of line
local nl = tok:find("\n", i, true) local nl = tok:find("\n", i, true) ---@type integer|nil
i = (nl and nl + 1) or (#tok + 1) i = (nl and nl + 1) or (#tok + 1)
elseif c == "/" and tok:sub(i + 1, i + 1) == "*" then elseif c == "/" and tok:sub(i + 1, i + 1) == "*" then
-- block comment: skip to matching */ -- block comment: skip to matching */
local close = tok:find("*/", i + 2, true) local close = tok:find("*/", i + 2, true) ---@type integer|nil
i = (close and close + 2) or (#tok + 1) i = (close and close + 2) or (#tok + 1)
elseif c == "(" then elseif c == "(" then
depth = depth + 1 depth = depth + 1
@@ -368,10 +447,12 @@ local function _project_emission_inner(root_body_entry, ctx_table)
-- Find the position of the consuming instruction's open paren (the `(` that starts the consuming instruction's argument list). -- Find the position of the consuming instruction's open paren (the `(` that starts the consuming instruction's argument list).
-- Returns nil if the token's leading text isn't an ident followed by `(` (e.g. the ident is at the start of a non-instruction token). -- Returns nil if the token's leading text isn't an ident followed by `(` (e.g. the ident is at the start of a non-instruction token).
--- @param tok string
--- @return integer|nil
local function find_consuming_paren(tok) local function find_consuming_paren(tok)
local i = 1 local i = 1 ---@type integer
while i <= #tok do while i <= #tok do
local c = tok:sub(i, i) local c = tok:sub(i, i) ---@type string
if c == "(" then return i end if c == "(" then return i end
if not c:match("[%w_]") and c ~= " " then return nil end if not c:match("[%w_]") and c ~= " " then return nil end
i = i + 1 i = i + 1
@@ -379,25 +460,29 @@ local function _project_emission_inner(root_body_entry, ctx_table)
return nil return nil
end end
--- @param tok string
--- @param tok_line integer
--- @param consuming_encoder string|nil
--- @return nil
local function emit_embedded_markers(tok, tok_line, consuming_encoder) local function emit_embedded_markers(tok, tok_line, consuming_encoder)
-- When called with a non-nil `consuming_encoder`, the marker is nested inside that instruction's argument list. -- When called with a non-nil `consuming_encoder`, the marker is nested inside that instruction's argument list.
-- We compute each marker's arg position by counting top-level commas between the consuming instruction's `(` and the marker's start. -- We compute each marker's arg position by counting top-level commas between the consuming instruction's `(` and the marker's start.
local consuming_paren = nil local consuming_paren = nil ---@type integer|nil
if consuming_encoder then consuming_paren = find_consuming_paren(tok) end if consuming_encoder then consuming_paren = find_consuming_paren(tok) end
local pos = 1 local pos = 1 ---@type integer
while pos <= #tok do while pos <= #tok do
-- Trim leading whitespace and comments before each scan. -- Trim leading whitespace and comments before each scan.
pos = M.skip_ws_and_cmt(tok, pos) pos = M.skip_ws_and_cmt(tok, pos)
if pos > #tok then break end if pos > #tok then break end
local ident, after = M.read_ident(tok, pos) local ident, after = M.read_ident(tok, pos) ---@type string|nil, integer
if not ident then if not ident then
-- Not an ident: token is a string or comment; skip or one-step. -- Not an ident: token is a string or comment; skip or one-step.
local next_pos = M.skip_str_or_cmt(tok, pos) local next_pos = M.skip_str_or_cmt(tok, pos) ---@type integer
pos = (next_pos > pos) and next_pos or (pos + 1) pos = (next_pos > pos) and next_pos or (pos + 1)
goto continue_loop goto continue_loop
end end
if M.DELAY_MARKERS[ident] then if M.DELAY_MARKERS[ident] then
local arg_pos = nil local arg_pos = nil ---@type integer|nil
if consuming_encoder and consuming_paren then if consuming_encoder and consuming_paren then
arg_pos = count_top_level_commas(tok, consuming_paren + 1, pos) + 1 arg_pos = count_top_level_commas(tok, consuming_paren + 1, pos) + 1
end end
@@ -411,8 +496,8 @@ local function _project_emission_inner(root_body_entry, ctx_table)
goto continue_loop goto continue_loop
end end
-- Marker ident: parse the (...) arguments. -- Marker ident: parse the (...) arguments.
local open = M.skip_ws_and_cmt(tok, after) local open = M.skip_ws_and_cmt(tok, after) ---@type integer
local inner, after_paren = M.read_parens(tok, open) local inner, after_paren = M.read_parens(tok, open) ---@type string|nil, integer
if not inner then if not inner then
-- (...) Unreadable: fall back to non-marker behavior. -- (...) Unreadable: fall back to non-marker behavior.
pos = after pos = after
@@ -421,21 +506,27 @@ local function _project_emission_inner(root_body_entry, ctx_table)
-- Commit: label takes 1 arg, offset takes 2. -- Commit: label takes 1 arg, offset takes 2.
-- For embedded markers, propagate the consuming_encoder + the marker's arg position -- For embedded markers, propagate the consuming_encoder + the marker's arg position
-- (1-based) so `passes/offsets.lua` can dispatch per-consuming-instruction offset encoding. -- (1-based) so `passes/offsets.lua` can dispatch per-consuming-instruction offset encoding.
-- Top-level markers (no consuming_encoder) get nil for both — the offsets pass treats -- Offset markers are emitted only when a consuming encoder is present.
-- them as branch-equivalent for backward compatibility. local arg_pos = nil ---@type integer|nil
local arg_pos = nil
if consuming_encoder and consuming_paren then if consuming_encoder and consuming_paren then
arg_pos = count_top_level_commas(tok, consuming_paren + 1, pos) + 1 arg_pos = count_top_level_commas(tok, consuming_paren + 1, pos) + 1
end end
local args = split_call_args(inner) local args = split_call_args(inner) ---@type string[]
if ident == "atom_label" then emit_marker("label", args[1] or "", nil, tok_line, nil, nil, consuming_encoder, arg_pos) if ident == "atom_label" then emit_marker("label", args[1] or "", nil, tok_line, nil, nil, consuming_encoder, arg_pos)
else emit_marker("offset", args[1] or "", args[2] or "", tok_line, nil, nil, consuming_encoder, arg_pos) elseif consuming_encoder then emit_marker("offset", args[1] or "", args[2] or "", tok_line, nil, nil, consuming_encoder, arg_pos)
end end
pos = after_paren pos = after_paren
::continue_loop:: ::continue_loop::
end end
end end
--- @param inv_kind string
--- @param component_name string
--- @param call_text string
--- @param root_call_text string|nil
--- @param call_path string
--- @param call_line integer
--- @return InvocationRecord
local function emit_invoke_begin(inv_kind, component_name, call_text, local function emit_invoke_begin(inv_kind, component_name, call_text,
root_call_text, call_path, call_line) root_call_text, call_path, call_line)
next_inv_id = next_inv_id + 1 next_inv_id = next_inv_id + 1
@@ -443,21 +534,20 @@ local function _project_emission_inner(root_body_entry, ctx_table)
-- The stamp is resolved from the `corpus.components[name]` registry (passed in via `ctx_table.components` by `emission_model.run`), -- The stamp is resolved from the `corpus.components[name]` registry (passed in via `ctx_table.components` by `emission_model.run`),
-- Unmarked components stamp `false` (not `nil`) so consumers can dispatch on the boolean without nil checks. -- Unmarked components stamp `false` (not `nil`) so consumers can dispatch on the boolean without nil checks.
-- --
-- The walker has already found the component body in `ctx_table.component_index[component_name]`, so the matching entry MUST exist in `ctx_table.components[component_name]` -- The walker has already found the component body in `ctx_table.components[component_name]`.
-- (both registries are populated from the same source by the components pass).
-- A missing entry is a corpus-plumbing bug; we fail loudly here rather than silently stamp `false` and mask the regression. -- A missing entry is a corpus-plumbing bug; we fail loudly here rather than silently stamp `false` and mask the regression.
local components = ctx_table.components local components = ctx_table.components ---@type table<string, Component>|nil
local component_def = components and components[component_name] or nil local component_def = components and components[component_name] or nil ---@type Component|nil
if not component_def then if not component_def then
error("duffle.emit_invoke_begin: component " .. string.format("%q", component_name) error("duffle.emit_invoke_begin: component " .. string.format("%q", component_name)
.. " is present in `component_index` (the walker matched a `mac_" .. component_name .. "()` call) but absent from `components` (the canonical corpus.components registry). " .. " is present in the walk (the walker matched a `mac_" .. component_name .. "()` call) but absent from `components` (the canonical corpus.components registry). "
.. "This is a corpus-plumbing bug — the components pass must populate corpus.components[name] for every component it puts in corpus.component_body_index[name]. " .. "This is a corpus-plumbing bug — the components pass must populate corpus.components[name] for every expanded component. "
.. "The emission pass refuses to silently stamp `debug_skip = false` for a missing registry entry." .. "The emission pass refuses to silently stamp `debug_skip = false` for a missing registry entry."
, 0 , 0
) )
end end
local debug_skip_stamp = component_def.debug_skip == true local debug_skip_stamp = component_def.debug_skip == true ---@type boolean
local inv = { local inv = { ---@type InvocationRecord
id = next_inv_id, id = next_inv_id,
parent_id = 0, -- patched below by caller parent_id = 0, -- patched below by caller
kind = inv_kind, kind = inv_kind,
@@ -491,6 +581,8 @@ local function _project_emission_inner(root_body_entry, ctx_table)
return inv return inv
end end
--- @param inv InvocationRecord
--- @return nil
local function emit_invoke_end(inv) local function emit_invoke_end(inv)
-- 0-based emitted-word position of the LAST word inside this invocation. -- 0-based emitted-word position of the LAST word inside this invocation.
-- After the last body word was emitted, `word_idx` was incremented past it, so `word_idx - 1` is the 0-based position of the last word. -- After the last body word was emitted, `word_idx` was incremented past it, so `word_idx - 1` is the 0-based position of the last word.
@@ -502,7 +594,7 @@ local function _project_emission_inner(root_body_entry, ctx_table)
word_index = word_idx, word_index = word_idx,
invocation_ids = open_invocation_ids_snapshot(), invocation_ids = open_invocation_ids_snapshot(),
} }
for i = #invocation_stack, 1, -1 do for i = #invocation_stack, 1, -1 do ---@type integer
if invocation_stack[i] == inv then if invocation_stack[i] == inv then
table.remove(invocation_stack, i) table.remove(invocation_stack, i)
break break
@@ -512,15 +604,19 @@ local function _project_emission_inner(root_body_entry, ctx_table)
-- Resolve the per-token word count. If unresolved, surface ONE warning -- Resolve the per-token word count. If unresolved, surface ONE warning
-- and fall back to 1 opaque word so the cycle budget still accounts for the slot. -- and fall back to 1 opaque word so the cycle budget still accounts for the slot.
--- @param ident string
--- @param tok_line integer
--- @return integer
local function resolve_count(ident, tok_line) local function resolve_count(ident, tok_line)
local wc = ctx_table.word_counts local wc = ctx_table.word_counts ---@type WordCounts|nil
if wc and wc[ident] then return wc[ident] end if wc and wc[ident] then return wc[ident] end
local canon = M.gte_canon(ident) local canon = M.gte_canon(ident) ---@type string
if canon ~= ident and wc and wc[canon] then return wc[canon] end if canon ~= ident and wc and wc[canon] then return wc[canon] end
warnings[#warnings + 1] = { warnings[#warnings + 1] = {
kind = "uncounted", kind = "warning",
check = "uncounted",
line = tok_line, line = tok_line,
msg = string.format("project_emission: opaque word emitted for %q (no entry in word_counts or component_index)", msg = string.format("project_emission: opaque word emitted for %q (no entry in word_counts or components)",
ident), ident),
} }
return 1 return 1
@@ -531,26 +627,58 @@ local function _project_emission_inner(root_body_entry, ctx_table)
-- walk_root_call_text: Outermost `mac_X(...)` token text (preserved across recursion). -- walk_root_call_text: Outermost `mac_X(...)` token text (preserved across recursion).
-- walk_immediate_call_text: IMMEDIATE outer `mac_X(...)` token text for words emitted in this body — nil for the root atom body. -- walk_immediate_call_text: IMMEDIATE outer `mac_X(...)` token text for words emitted in this body — nil for the root atom body.
-- Two trackers are propagated as separate parameters so words deep inside nested expansions correctly identify both their immediate call site and the outermost call site. -- Two trackers are propagated as separate parameters so words deep inside nested expansions correctly identify both their immediate call site and the outermost call site.
--- @param body_entry Component
--- @param walk_parent_inv_id integer
--- @param walk_root_call_text string|nil
--- @param walk_immediate_call_text string|nil
--- @param sub_map table<string, string>|nil
--- @return nil
local function walk_body_entry(body_entry, walk_parent_inv_id, local function walk_body_entry(body_entry, walk_parent_inv_id,
walk_root_call_text, walk_immediate_call_text) walk_root_call_text, walk_immediate_call_text, sub_map)
local tokens = body_entry.body_tokens or {} local tokens = body_entry.body_tokens or {} ---@type BodyToken[]
local body_off = body_entry.body_off or 0 local body_off = body_entry.body_off or 0 ---@type integer
local line_of = body_entry.line_of or M.LineIndex("") local line_of = body_entry.line_of or M.LineIndex("") ---@type LineIndexFn
local def_source = body_entry.source or "" local def_source = body_entry.source or "" ---@type string
local def_line = body_entry.declaration or 0 local def_line = body_entry.line or 0 ---@type integer
local sub_map = body_entry.sub_map
-- Per-token dispatch: each matched branch returns; only the fall-through -- Per-token dispatch: each matched branch returns; only the fall-through
-- "opaque word" emit handles direct encoders + mac_X-without-component. -- "opaque word" emit handles direct encoders + mac_X-without-component.
--- @param bt BodyToken
--- @return nil
local function process_token(bt) local function process_token(bt)
local tok = M.trim(bt.tok or "") local tok = M.trim(bt.tok or "") ---@type string
-- Substituted MipsCode args can carry // comments from the call site.
while tok ~= "" do
if tok:sub(1, 2) == "//" then
local nl = tok:find("\n") ---@type integer|nil
tok = M.trim(nl and tok:sub(nl + 1) or "")
elseif tok:sub(1, 2) == "/*" then
local close = tok:find("*/", 3, true) ---@type integer|nil
tok = M.trim(close and tok:sub(close + 2) or "")
else
break
end
end
if tok == "" then return end if tok == "" then return end
local ident, after = M.read_ident(tok, 1) local ident, after = M.read_ident(tok, 1) ---@type string|nil, integer
if not ident then ident = "?" end if not ident then ident = "?" end
local _, args = token_ident_and_args(tok) local _, args = token_ident_and_args(tok) ---@type string, string[]
local tok_line = line_of(body_off + bt.rel) or 0 local tok_line = line_of(body_off + bt.rel) or 0 ---@type integer
if M.DELAY_MARKERS[ident] then if M.DELAY_MARKERS[ident] then
emit_marker("delay", ident, nil, tok_line) 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)) ---@type string
while true do
rest = M.trim(rest)
if rest:sub(1, 2) == "//" then
local nl = rest:find("\n") ---@type integer|nil
rest = nl and rest:sub(nl + 1) or ""
elseif rest:sub(1, 2) == "/*" then
local close = rest:find("*/", 3, true) ---@type integer|nil
if not close then rest = ""; break end
rest = rest:sub(close + 2)
else
break
end
end
if rest ~= "" then if rest ~= "" then
process_token({ tok = rest, rel = bt.rel }) process_token({ tok = rest, rel = bt.rel })
end end
@@ -560,30 +688,38 @@ local function _project_emission_inner(root_body_entry, ctx_table)
-- Pass `ident` as the consuming instruction so `emit_embedded_markers` can compute each marker's arg position + record the consuming_encoder for the offsets pass. -- Pass `ident` as the consuming instruction so `emit_embedded_markers` can compute each marker's arg position + record the consuming_encoder for the offsets pass.
-- Canonicalize `jump_rel` to `branch_equal` (its preprocessor-expanded form) so the `consuming_encoder` metadata in marker records is canonical. -- Canonicalize `jump_rel` to `branch_equal` (its preprocessor-expanded form) so the `consuming_encoder` metadata in marker records is canonical.
-- `jump_rel`: unconditional jump alias from `code/duffle/mips.h`. -- `jump_rel`: unconditional jump alias from `code/duffle/mips.h`.
local consuming_encoder_for_markers = (ident == "jump_rel") and "branch_equal" or ident local consuming_encoder_for_markers = (ident == "jump_rel") and "branch_equal" or ident ---@type string
if ident ~= "atom_label" and ident ~= "atom_offset" then if ident ~= "atom_label" and ident ~= "atom_offset" then
emit_embedded_markers(tok, tok_line, consuming_encoder_for_markers) emit_embedded_markers(tok, tok_line, consuming_encoder_for_markers)
end end
-- atom_label / atom_offset: terminal markers, no further descent. -- atom_label / atom_offset: terminal markers, no further descent.
-- Top-level markers (the marker IS the entire token) have no consuming instruction; -- A lone top-level atom_label is an anchor and is emitted with no consuming encoder.
-- nil for both `consuming_encoder` and `consuming_arg_pos`. -- A lone top-level atom_offset has no consuming encoder and is not emitted.
-- The offsets pass treats these as branch-equivalent for backward compatibility.
-- TODO(Ed): Review this don't want legacy cruft here..
if ident == "atom_label" then emit_marker("label", args[1] or "", nil, tok_line); return if ident == "atom_label" then emit_marker("label", args[1] or "", nil, tok_line); return
elseif ident == "atom_offset" then emit_marker("offset", args[1] or "", args[2] or "", tok_line); return elseif ident == "atom_offset" then return
end
-- MipsCode formals (nop_slot1, …): the ident is a sub_map key.
-- Re-process the replacement token so load_word(...) becomes a real encoder.
if sub_map and type(sub_map[ident]) == "string" and sub_map[ident] ~= ident then
local repl = M.trim(sub_map[ident]) ---@type string
if repl ~= "" then
process_token({ tok = repl, rel = bt.rel })
return
end
end end
if ident:sub(1, 4) == "mac_" then if ident:sub(1, 4) == "mac_" then
local bare = ident:sub(5) local bare = ident:sub(5) ---@type string
local comp = ctx_table.component_index[bare] local comp = ctx_table.components[bare] ---@type Component|nil
if comp then if comp then
local invocation_root_call_text = walk_root_call_text or tok local invocation_root_call_text = walk_root_call_text or tok ---@type string
if ctx_table.visiting[bare] then if ctx_table.visiting[bare] then
-- Cycle: still allocate inv_id, emit zero-width begin/end, record the cycle error; do NOT recurse. -- Cycle: still allocate inv_id, emit zero-width begin/end, record the cycle error; do NOT recurse.
local inv = emit_invoke_begin(comp.kind or "comp_bare", bare, tok, invocation_root_call_text, def_source, tok_line) local inv = emit_invoke_begin(comp.kind or "comp_bare", bare, tok, invocation_root_call_text, def_source, tok_line) ---@type InvocationRecord
inv.parent_id = walk_parent_inv_id inv.parent_id = walk_parent_inv_id
inv.call_text = tok inv.call_text = tok
local err = { local err = { ---@type Finding
kind = "cycle", kind = "error",
check = "cycle",
msg = string.format("project_emission: component cycle detected: %q", bare), msg = string.format("project_emission: component cycle detected: %q", bare),
source = def_source, source = def_source,
line = tok_line, line = tok_line,
@@ -595,40 +731,34 @@ local function _project_emission_inner(root_body_entry, ctx_table)
end end
-- First visit: descend + count + count_mismatch-check below. -- First visit: descend + count + count_mismatch-check below.
ctx_table.visiting[bare] = true ctx_table.visiting[bare] = true
local inv = emit_invoke_begin(comp.kind or "comp_bare", bare, tok, invocation_root_call_text, def_source, tok_line) local inv = emit_invoke_begin(comp.kind or "comp_bare", bare, tok, invocation_root_call_text, def_source, tok_line) ---@type InvocationRecord
inv.parent_id = walk_parent_inv_id inv.parent_id = walk_parent_inv_id
inv.call_text = tok inv.call_text = tok
inv.def_path = comp.source inv.def_path = comp.source
inv.def_line = comp.declaration inv.def_line = comp.line
-- Propagate trackers into the recursive walk: -- Propagate trackers into the recursive walk:
-- immediate_call_text = this call's tok (the IMMEDIATE outer call for words emitted in this body) -- 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) -- root_call_text = the OUTERMOST call (immutable across the recursion)
local formal_names = ctx_table.component_index[bare] -- child_map = per-invocation formal substitution; stays on the walk stack
and ctx_table.component_index[bare].arg_names local formal_names = comp.arg_names ---@type string[]|nil
local child_map = nil local child_map = nil ---@type table<string, string>|nil
if formal_names then if formal_names then
child_map = {} child_map = {}
for i, fname in ipairs(formal_names) do for i, fname in ipairs(formal_names) do ---@type integer, string
child_map[fname] = apply_sub(sub_map, args[i]) child_map[fname] = apply_sub(sub_map, args[i])
end end
end end
walk_body_entry({ walk_body_entry(comp,
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, inv.id,
invocation_root_call_text, invocation_root_call_text,
tok) tok,
child_map)
ctx_table.visiting[bare] = nil ctx_table.visiting[bare] = nil
emit_invoke_end(inv) emit_invoke_end(inv)
-- Count `word` items inside [start_word, end_word]. -- Count `word` items inside [start_word, end_word].
local wc_inside = 0 local wc_inside = 0 ---@type integer
for i = inv.start_word, inv.end_word do for i = inv.start_word, inv.end_word do ---@type integer
local it = items[i] local it = items[i] ---@type EmissionItem|nil
if it and it.kind == "word" then if it and it.kind == "word" then
wc_inside = wc_inside + 1 wc_inside = wc_inside + 1
end end
@@ -636,10 +766,11 @@ local function _project_emission_inner(root_body_entry, ctx_table)
inv.word_count = wc_inside inv.word_count = wc_inside
-- count_mismatch is a construction error: word_counts["mac_X"] is the declared count populated by the components pass; -- count_mismatch is a construction error: word_counts["mac_X"] is the declared count populated by the components pass;
-- We compare against the measured word count. -- We compare against the measured word count.
local declared = ctx_table.word_counts["mac_" .. bare] local declared = ctx_table.word_counts["mac_" .. bare] ---@type integer|nil
if declared and wc_inside ~= declared then if declared and wc_inside ~= declared then
local err = { local err = { ---@type Finding
kind = "count_mismatch", kind = "error",
check = "count_mismatch",
msg = string.format("project_emission: mac_%s declared=%d measured=%d", bare, declared, wc_inside), msg = string.format("project_emission: mac_%s declared=%d measured=%d", bare, declared, wc_inside),
source = def_source, source = def_source,
line = tok_line, line = tok_line,
@@ -649,18 +780,18 @@ local function _project_emission_inner(root_body_entry, ctx_table)
end end
return return
end end
-- mac_X NOT in component_index: fall through to opaque emit. -- mac_X NOT in components: fall through to opaque emit.
end end
-- Direct encoder, or mac_X-without-component: resolve count + emit n words. -- Direct encoder, or mac_X-without-component: resolve count + emit n words.
-- Resolve_count may emit a warning if the count is unresolved. -- Resolve_count may emit a warning if the count is unresolved.
local n = resolve_count(ident, tok_line) local n = resolve_count(ident, tok_line) ---@type integer
local out_ident = (ident == "nop2") and "nop" or ident local out_ident = (ident == "nop2") and "nop" or ident ---@type string
for _ = 1, n do for _ = 1, n do ---@type integer
emit_word(out_ident, args, tok_line, tok, def_source, def_line, walk_immediate_call_text, walk_root_call_text, sub_map) emit_word(out_ident, args, tok_line, tok, def_source, def_line, walk_immediate_call_text, walk_root_call_text, sub_map)
end end
end end
for _, bt in ipairs(tokens) do for _, bt in ipairs(tokens) do ---@type integer, BodyToken
process_token(bt) process_token(bt)
end end
end end
@@ -674,13 +805,14 @@ local function _project_emission_inner(root_body_entry, ctx_table)
-- Walk first; the pass caller stamps the root call site for direct words after the projection returns. -- Walk first; the pass caller stamps the root call site for direct words after the projection returns.
-- For nested words the def_path / def_line already point at the component source and MUST be preserved (the stamping helper checks for that). -- For nested words the def_path / def_line already point at the component source and MUST be preserved (the stamping helper checks for that).
walk_body_entry(root_body_entry, 0, nil, nil) walk_body_entry(root_body_entry, 0, nil, nil, nil)
-- Boundary check: every invoke_begin must have a matching invoke_end. -- Boundary check: every invoke_begin must have a matching invoke_end.
-- If anything is still open, surface a hard error. -- If anything is still open, surface a hard error.
if #invocation_stack > 0 then if #invocation_stack > 0 then
errors[#errors + 1] = { errors[#errors + 1] = {
kind = "unbalanced", kind = "error",
check = "unbalanced",
msg = string.format("project_emission: invocation boundaries not balanced (%d unclosed invocation(s) at end of walk)", #invocation_stack), msg = string.format("project_emission: invocation boundaries not balanced (%d unclosed invocation(s) at end of walk)", #invocation_stack),
} }
end end
@@ -702,12 +834,13 @@ end
--- * Metadata-backed N-word tokens (`nop2`, `mask_upper`, ...): N `word` items, all sharing the same encoder + word_count = 1. --- * Metadata-backed N-word tokens (`nop2`, `mask_upper`, ...): N `word` items, all sharing the same encoder + word_count = 1.
--- `nop2` is normalized to encoder `nop` (per the spec). --- `nop2` is normalized to encoder `nop` (per the spec).
--- * `atom_label(F)` markers: one `label` item with `name = "F"`, `word_index = current word_idx`; zero-width (does NOT advance word_idx). --- * `atom_label(F)` markers: one `label` item with `name = "F"`, `word_index = current word_idx`; zero-width (does NOT advance word_idx).
--- * `atom_offset(B, T)` markers: one `offset` item with `name = "B"`, `target = "T"`, `word_index = current word_idx`; zero-width. --- * `atom_offset(B, T)` nested in a consuming instruction: one `offset` item with `name = "B"`, `target = "T"`, `word_index = current word_idx`; zero-width.
--- A lone top-level `atom_offset` is not emitted.
--- * Delay markers (`GteDelay_` / `LdSlot_` / `BdSlot_` / `DmaSlot_`): one `delay` item; zero-width. The following encoder is the next token. --- * Delay markers (`GteDelay_` / `LdSlot_` / `BdSlot_` / `DmaSlot_`): one `delay` item; zero-width. The following encoder is the next token.
--- * `mac_X(...)` calls: emit `invoke_begin` (zero-width), recurse into the component body, emit `invoke_end` (zero-width). --- * `mac_X(...)` calls: emit `invoke_begin` (zero-width), recurse into the component body, emit `invoke_end` (zero-width).
--- The component body's words land between the begin/end pair; one invocation record is allocated per call (monotonic ID per atom). --- The component body's words land between the begin/end pair; one invocation record is allocated per call (monotonic ID per atom).
--- * Unknown uncounted macros emit 1 opaque word + one warning per occurrence. --- * Unknown uncounted macros emit 1 opaque word + one warning per occurrence.
--- * Tokens whose count cannot be resolved (e.g. `mac_unknown` not in word_counts and not in component_index) surface one --- * Tokens whose count cannot be resolved (e.g. `mac_unknown` not in word_counts and not in components) surface one
--- warning; cycle + count-mismatch + boundary violations are construction errors on `pass.errors`. --- warning; cycle + count-mismatch + boundary violations are construction errors on `pass.errors`.
--- ---
--- Every emitted `word` carries: `i` (0-based word index), `encoder`, `args` (top-level args), `def_path`, `def_line`, --- Every emitted `word` carries: `i` (0-based word index), `encoder`, `args` (top-level args), `def_path`, `def_line`,
@@ -717,14 +850,15 @@ end
--- for the open invocation stack at that word. --- for the open invocation stack at that word.
--- ---
--- @param body_text string -- the raw atom body string --- @param body_text string -- the raw atom body string
--- @param component_index table -- bare-name → component record (corpus.component_body_index) --- @param component_index table<string, Component>
--- @param word_counts table -- macro name → emitted word count --- @param word_counts WordCounts
--- @param components table -- bare-name → component definition (corpus.components); REQUIRED — consumed at the invocation-construction site to stamp --- @param components table<string, Component>
--- `invocation.debug_skip`. A missing or non-table `components` raises a fail-loud error rather than silently falling back. --- `invocation.debug_skip`. A missing or non-table `components` raises a fail-loud error rather than silently falling back.
--- @return EmissionProjection --- @return EmissionProjection
--- @param reg_use_ctx RegUseCtx|nil
function M.project_emission(body_text, component_index, word_counts, components, reg_use_ctx) 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 -- The recursive walk delegates to `_project_emission_inner` so component bodies
-- `{body_tokens, body_off, line_of, source, declaration}` records from `corpus.component_body_index`) -- (the `corpus.components` row: body_tokens / body_off / line_of / source / line)
-- re-enter the same walker with the same shared output state. -- re-enter the same walker with the same shared output state.
-- --
-- The walker is body-relative: it builds `line_of` from `body_text` and stamps body-relative line numbers (1..N) -- The walker is body-relative: it builds `line_of` from `body_text` and stamps body-relative line numbers (1..N)
@@ -753,18 +887,18 @@ function M.project_emission(body_text, component_index, word_counts, components,
} }
end end
local tokens = M.tokenize_body(body_text) local tokens = M.tokenize_body(body_text) ---@type BodyToken[]
local comps = components or component_index or {} ---@type table<string, Component>
return _project_emission_inner({ return _project_emission_inner({
body_tokens = tokens, body_tokens = tokens,
body_off = 0, body_off = 0,
line_of = M.LineIndex(body_text), line_of = M.LineIndex(body_text),
source = "", source = "",
declaration = 0, line = 0,
}, },
{ {
component_index = component_index or {}, components = comps,
word_counts = word_counts or {}, word_counts = word_counts or {},
components = components,
reg_use_schema = reg_use_ctx and reg_use_ctx.reg_use_schema, 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, reg_use_param = reg_use_ctx and reg_use_ctx.reg_use_param,
atom_name = reg_use_ctx and reg_use_ctx.atom_name, atom_name = reg_use_ctx and reg_use_ctx.atom_name,
@@ -786,22 +920,26 @@ end
-- (FI_, atom_dbg_skip, comments) until it finds an ident followed by "(". -- (FI_, atom_dbg_skip, comments) until it finds an ident followed by "(".
-- That ident is the function name; the parens contents are the args. -- That ident is the function name; the parens contents are the args.
------------------------------------------------------------------------------- -------------------------------------------------------------------------------
--- @param source string
--- @param before_pos integer
--- @param slice_mips_code_len integer
--- @return string|nil, string|nil
function M.find_function_decl_for(source, before_pos, slice_mips_code_len) function M.find_function_decl_for(source, before_pos, slice_mips_code_len)
local search_pos = 1 local search_pos = 1 ---@type integer
local last_match = nil local last_match = nil ---@type integer|nil
while true do while true do
local found = source:find("Slice_MipsCode", search_pos, true) local found = source:find("Slice_MipsCode", search_pos, true) ---@type integer|nil
if not found or found >= before_pos then break end if not found or found >= before_pos then break end
last_match = found last_match = found
search_pos = found + slice_mips_code_len search_pos = found + slice_mips_code_len
end end
if not last_match then return nil, nil end if not last_match then return nil, nil end
local pos = last_match + slice_mips_code_len local pos = last_match + slice_mips_code_len ---@type integer
while pos < before_pos do while pos < before_pos do
-- skip whitespace -- skip whitespace
while pos <= #source do while pos <= #source do
local c = source:sub(pos, pos) local c = source:sub(pos, pos) ---@type string
if c == " " or c == "\t" or c == "\n" or c == "\r" then if c == " " or c == "\t" or c == "\n" or c == "\r" then
pos = pos + 1 pos = pos + 1
else else
@@ -817,18 +955,18 @@ function M.find_function_decl_for(source, before_pos, slice_mips_code_len)
end end
-- skip block comments -- skip block comments
if source:sub(pos, pos + 1) == "/*" then if source:sub(pos, pos + 1) == "/*" then
local close = source:find("*/", pos + 2, true) local close = source:find("*/", pos + 2, true) ---@type integer|nil
if not close then break end if not close then break end
pos = close + 2 pos = close + 2
goto continue goto continue
end end
-- try to read an ident -- try to read an ident
local ident, ident_end = M.read_ident(source, pos) local ident, ident_end = M.read_ident(source, pos) ---@type string|nil, integer
if not ident then break end if not ident then break end
-- check if the next non-ws char after ident is "(" -- check if the next non-ws char after ident is "("
local next_pos = M.skip_ws_and_cmt(source, ident_end) local next_pos = M.skip_ws_and_cmt(source, ident_end) ---@type integer
if source:sub(next_pos, next_pos) == "(" then if source:sub(next_pos, next_pos) == "(" then
local inner = M.read_parens(source, next_pos) local inner = M.read_parens(source, next_pos) ---@type string|nil
if inner then if inner then
return ident, inner return ident, inner
end end
@@ -854,23 +992,27 @@ end
-- The walk finds the LAST "MipsAtom*" before before_pos, then skips whitespace + qualifiers (internal, I_, FI_, comments) -- The walk finds the LAST "MipsAtom*" before before_pos, then skips whitespace + qualifiers (internal, I_, FI_, comments)
-- until it finds an ident followed by "(". -- until it finds an ident followed by "(".
------------------------------------------------------------------------------- -------------------------------------------------------------------------------
--- @param source string
--- @param before_pos integer
--- @param mips_atom_ptr_len integer
--- @return string|nil, string|nil, string|nil, integer|nil
function M.find_atom_proc_decl_for(source, before_pos, mips_atom_ptr_len) function M.find_atom_proc_decl_for(source, before_pos, mips_atom_ptr_len)
local search_pos = 1 local search_pos = 1 ---@type integer
local last_match = nil local last_match = nil ---@type integer|nil
while true do while true do
-- plain=true: "*" is literal, no escaping needed -- plain=true: "*" is literal, no escaping needed
local found = source:find("MipsAtom*", search_pos, true) local found = source:find("MipsAtom*", search_pos, true) ---@type integer|nil
if not found or found >= before_pos then break end if not found or found >= before_pos then break end
last_match = found last_match = found
search_pos = found + mips_atom_ptr_len search_pos = found + mips_atom_ptr_len
end end
if not last_match then return nil, nil end if not last_match then return nil, nil end
local pos = last_match + mips_atom_ptr_len local pos = last_match + mips_atom_ptr_len ---@type integer
while pos < before_pos do while pos < before_pos do
-- skip whitespace -- skip whitespace
while pos <= #source do while pos <= #source do
local c = source:sub(pos, pos) local c = source:sub(pos, pos) ---@type string
if c == " " or c == "\t" or c == "\n" or c == "\r" then if c == " " or c == "\t" or c == "\n" or c == "\r" then
pos = pos + 1 pos = pos + 1
else else
@@ -886,19 +1028,29 @@ function M.find_atom_proc_decl_for(source, before_pos, mips_atom_ptr_len)
end end
-- skip block comments -- skip block comments
if source:sub(pos, pos + 1) == "/*" then if source:sub(pos, pos + 1) == "/*" then
local close = source:find("*/", pos + 2, true) local close = source:find("*/", pos + 2, true) ---@type integer|nil
if not close then break end if not close then break end
pos = close + 2 pos = close + 2
goto continue goto continue
end end
-- try to read an ident -- try to read an ident
local ident, ident_end = M.read_ident(source, pos) local ident, ident_end = M.read_ident(source, pos) ---@type string|nil, integer
if not ident then break end if not ident then break end
-- check if the next non-ws char after ident is "(" -- check if the next non-ws char after ident is "("
local next_pos = M.skip_ws_and_cmt(source, ident_end) local next_pos = M.skip_ws_and_cmt(source, ident_end) ---@type integer
if source:sub(next_pos, next_pos) == "(" then if source:sub(next_pos, next_pos) == "(" then
local inner, after_paren = M.read_parens(source, next_pos) local inner, after_paren = M.read_parens(source, next_pos) ---@type string|nil, integer
if inner then if inner then
if ident == "AtomBundleEntry_" then
local tmpl = M.split_top_level_commas(inner) ---@type string[]
if #tmpl ~= 2 then return nil end
local name = M.trim(tmpl[1]) .. "_" .. M.trim(tmpl[2]) ---@type string
local formals_pos = M.skip_ws_and_cmt(source, after_paren) ---@type integer
if source:sub(formals_pos, formals_pos) ~= "(" then return nil end
local real_inner, after_real = M.read_parens(source, formals_pos) ---@type string|nil, integer
if not real_inner then return nil end
return name, real_inner, name, after_real
end
return ident, inner, ident, after_paren return ident, inner, ident, after_paren
end end
end end
@@ -910,3 +1062,4 @@ function M.find_atom_proc_decl_for(source, before_pos, mips_atom_ptr_len)
end end
return M return M
+187 -46
View File
@@ -1,16 +1,169 @@
--- duffle_isa.lua — encoder / GTE / hardware tables. --- duffle_isa.lua — encoder / GTE / hardware tables.
local M = {}
--- @class InstructionImm
--- @field arg integer
--- @field signed boolean|nil
--- @field width integer
--- @class InstructionValue
--- @field dest integer
--- @field op string
--- @field sources integer[]|nil
--- @field immediate integer|nil
--- @field source integer|nil
--- @class InstructionRow
--- @field cycles integer
--- @field kind string
--- @field reads integer[]|nil
--- @field writes integer[]|nil
--- @field imm InstructionImm[]|nil
--- @field value InstructionValue|nil
--- @field delay_slot boolean|nil
--- @field suppress_arg1 table<string, string>|nil -- bag: GPR ident -> reason
--- @class TapeAtomMacroRow
--- @field kind string
--- @field binds boolean
--- @class GteCommandPort
--- @field register string
--- @field role string
--- @class GteCommandLatch
--- @field register string
--- @field required integer
--- @class GteCommandRow
--- @field aliases string[]
--- @field cycles integer
--- @field inputs string[]
--- @field outputs GteCommandPort[]
--- @field latch GteCommandLatch[]
--- @class GteCrAliasGroup
--- @field [1] integer -- C2 control-register slot
--- @field [2] string[] -- aliases that share that slot
--- @class GtePackedSlotRelation
--- @field slot integer
--- @field first string
--- @field second string
--- @class HardwareRelationPort
--- @field domain string
--- @field arg integer
--- @class HardwareRelationVisibility
--- @field kind string
--- @field required integer
--- @class HardwareRelationEvidence
--- @field confidence string
--- @field source string
--- @class HardwareRelationRow
--- @field id string
--- @field semantic string
--- @field consumer string
--- @field token string
--- @field direction string
--- @field reads HardwareRelationPort
--- @field writes HardwareRelationPort
--- @field visibility HardwareRelationVisibility|nil
--- @field evidence HardwareRelationEvidence
--- @field violation_kind string
--- @field destination_match string|nil
--- @field fanout_to string[]|nil
--- @field required integer|nil
--- @field clear_on_consumer boolean|nil
--- @field stage boolean|nil
--- @field cu2_transition boolean|nil
--- @field status_register integer|nil
--- @class Cu2TransitionPolicy
--- @field status_register integer
--- @field enable_bit integer
--- @field required integer
--- @field visibility_kind string
--- @field evidence HardwareRelationEvidence
--- @class GprRole
--- @field name string
--- @field pool boolean
--- @field optional boolean
--- @field carrier boolean
--- @class DuffleIsa
--- @field GPR_ROLE table<string, GprRole>
--- @field TAPE_ATOM_MACROS table<string, TapeAtomMacroRow>
--- @field DELAY_MARKERS table<string, boolean>
--- @field INSTRUCTION table<string, InstructionRow>
--- @field GTE_COMMAND table<string, GteCommandRow>
--- @field ALIAS_TO_CANONICAL table<string, string>
--- @field instr fun(ident: string): InstructionRow|nil
--- @field gte_canon fun(ident: string): string
--- @field gte fun(ident: string): GteCommandRow|nil
--- @field GTE_CR_ALIAS_GROUPS GteCrAliasGroup[]
--- @field GTE_PACKED_SLOT_RELATIONS GtePackedSlotRelation[]
--- @field OPERAND_READ_POSITIONS table<string, integer[]>
--- @field GP0_CMD_SIZE table<integer, integer>
--- @field GP0_CMD_BY_SHAPE table<string, integer>
--- @field UNKNOWN_INSTRUCTION_CYCLES integer
--- @field HARDWARE_RELATIONS HardwareRelationRow[]
--- @field CU2_TRANSITION_POLICY Cu2TransitionPolicy
local M = {} ---@type DuffleIsa
-- Section 7: domain tables -- Section 7: domain tables
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
-- One GprRole row per name. Construction order is the auto_reg pool order,
-- then R_AT, then the three carriers. Index by name into M.GPR_ROLE.
--- @type table<string, GprRole>
M.GPR_ROLE = {
{ name = "R_V0", pool = true, optional = true, carrier = false },
{ name = "R_V1", pool = true, optional = true, carrier = false },
{ name = "R_T0", pool = true, optional = true, carrier = false },
{ name = "R_T1", pool = true, optional = true, carrier = false },
{ name = "R_T2", pool = true, optional = true, carrier = false },
{ name = "R_T3", pool = true, optional = true, carrier = false },
{ name = "R_T4", pool = true, optional = true, carrier = false },
{ name = "R_T5", pool = true, optional = true, carrier = false },
{ name = "R_T6", pool = true, optional = true, carrier = false },
{ name = "R_T7", pool = true, optional = true, carrier = false },
{ name = "R_A0", pool = true, optional = true, carrier = false },
{ name = "R_A1", pool = true, optional = true, carrier = false },
{ name = "R_A2", pool = true, optional = true, carrier = false },
{ name = "R_A3", pool = true, optional = true, carrier = false },
{ name = "R_S0", pool = true, optional = true, carrier = false },
{ name = "R_S1", pool = true, optional = true, carrier = false },
{ name = "R_S2", pool = true, optional = true, carrier = false },
{ name = "R_S3", pool = true, optional = true, carrier = false },
{ name = "R_S4", pool = true, optional = true, carrier = false },
{ name = "R_S5", pool = true, optional = true, carrier = false },
{ name = "R_S6", pool = true, optional = true, carrier = false },
{ name = "R_S7", pool = true, optional = true, carrier = false },
{ name = "R_T8", pool = true, optional = true, carrier = false },
{ name = "R_T9", pool = true, optional = true, carrier = false },
{ name = "R_AT", pool = false, optional = true, carrier = false },
{ name = "R_TapePtr", pool = false, optional = true, carrier = true },
{ name = "R_AtomJmp", pool = false, optional = true, carrier = true },
{ name = "R_ScratchBase", pool = false, optional = true, carrier = true },
}
for _, row in ipairs(M.GPR_ROLE) do ---@type integer, GprRole
M.GPR_ROLE[row.name] = row
end
-- atom_info sub-calls: atom_bind, atom_reads, atom_writes, atom_view, atom_reg_types, atom_ctx, atom_phase. -- atom_info sub-calls: atom_bind, atom_reads, atom_writes, atom_view, atom_reg_types, atom_ctx, atom_phase.
--- @type table<string, TapeAtomMacroRow>
M.TAPE_ATOM_MACROS = { M.TAPE_ATOM_MACROS = {
["atom_info"] = { kind = "info", binds = false }, ["atom_info"] = { kind = "info", binds = false },
} }
-- Empty C macros that prefix the next encoder. Zero words. -- Empty C macros that prefix the next encoder. Zero words.
-- BdSlot_ nop is one nop word. The marker is not the BD instruction. -- BdSlot_ nop is one nop word. The marker is not the BD instruction.
--- @type table<string, boolean> -- bag: marker prefix -> true
M.DELAY_MARKERS = { M.DELAY_MARKERS = {
["GteDelay_"] = true, ["GteDelay_"] = true,
["LdSlot_"] = true, ["LdSlot_"] = true,
@@ -18,7 +171,8 @@ M.DELAY_MARKERS = {
["DmaSlot_"] = true, ["DmaSlot_"] = true,
} }
-- One row per encoder. Old table names are load-time views (build_isa_views). -- One row per encoder. Read through duffle.instr.
--- @type table<string, InstructionRow>
M.INSTRUCTION = { M.INSTRUCTION = {
["BdSlot_"] = { cycles = 0, kind = "marker", }, ["BdSlot_"] = { cycles = 0, kind = "marker", },
["LdSlot_"] = { cycles = 0, kind = "marker", }, ["LdSlot_"] = { cycles = 0, kind = "marker", },
@@ -115,6 +269,7 @@ M.INSTRUCTION = {
} }
-- One row per GTE command. Alias cycle numbers live here, not on INSTRUCTION. -- One row per GTE command. Alias cycle numbers live here, not on INSTRUCTION.
--- @type table<string, GteCommandRow>
M.GTE_COMMAND = { M.GTE_COMMAND = {
["gte_cmdw_avsz3"] = { ["gte_cmdw_avsz3"] = {
aliases = { "gte_avg_sort_z3", "gte_avsz3", "gte_cmdw_avg_sort_z3" }, aliases = { "gte_avg_sort_z3", "gte_avsz3", "gte_cmdw_avg_sort_z3" },
@@ -296,60 +451,28 @@ M.GTE_COMMAND = {
}, },
} }
--- @param ident string
--- @return InstructionRow|nil
function M.instr (ident) return M.INSTRUCTION [ident] end function M.instr (ident) return M.INSTRUCTION [ident] end
--- @param ident string
--- @return string
function M.gte_canon(ident) return M.ALIAS_TO_CANONICAL [ident] or ident end function M.gte_canon(ident) return M.ALIAS_TO_CANONICAL [ident] or ident end
--- @param ident string
--- @return GteCommandRow|nil
function M.gte (ident) return M.GTE_COMMAND[M.gte_canon(ident)] end function M.gte (ident) return M.GTE_COMMAND[M.gte_canon(ident)] end
local function build_isa_views() --- @return nil
local function build_alias_map()
--- @type table<string, string> -- bag: alias or canon -> canon
M.ALIAS_TO_CANONICAL = {} M.ALIAS_TO_CANONICAL = {}
for canon, row in pairs(M.GTE_COMMAND) do for canon, row in pairs(M.GTE_COMMAND) do ---@type string, GteCommandRow
M.ALIAS_TO_CANONICAL[canon] = canon M.ALIAS_TO_CANONICAL[canon] = canon
for _, alias in ipairs(row.aliases or {}) do for _, alias in ipairs(row.aliases or {}) do ---@type integer, string
M.ALIAS_TO_CANONICAL[alias] = canon M.ALIAS_TO_CANONICAL[alias] = canon
end end
end end
M.INSTRUCTION_LATENCY = {}
M.INSTRUCTION_GPR_EFFECTS = {}
M.IMMEDIATE_FIELD_WIDTHS = {}
M.GPR_VALUE_RULES = {}
M.CONTROL_TRANSFER_DELAY_SLOT_POLICIES = {}
for name, row in pairs(M.INSTRUCTION) do
M.INSTRUCTION_LATENCY[name] = row.cycles
if row.reads or row.writes then
M.INSTRUCTION_GPR_EFFECTS[name] = {
reads = row.reads or {},
writes = row.writes or {},
}
end end
if row.imm then M.IMMEDIATE_FIELD_WIDTHS[name] = row.imm end build_alias_map()
if row.value then M.GPR_VALUE_RULES [name] = row.value end
if (row.kind == "branch" or row.kind == "jump" or row.kind == "call")
and row.delay_slot ~= false then
M.CONTROL_TRANSFER_DELAY_SLOT_POLICIES[name] = {
family = row.kind,
suppress_arg1 = row.suppress_arg1,
}
end
end
M.GTE_COMMAND_ALIASES = {}
M.GTE_COMMAND_INPUTS = {}
M.GTE_COMMAND_OUTPUTS = {}
M.GTE_COMMAND_LATCH_WINDOWS = {}
for canon, row in pairs(M.GTE_COMMAND) do
M.GTE_COMMAND_ALIASES [canon] = canon
M.INSTRUCTION_LATENCY [canon] = row.cycles
M.INSTRUCTION_GPR_EFFECTS[canon] = { reads = {}, writes = {} }
for _, alias in ipairs(row.aliases or {}) do
M.GTE_COMMAND_ALIASES [alias] = canon
M.INSTRUCTION_LATENCY [alias] = row.cycles
M.INSTRUCTION_GPR_EFFECTS[alias] = { reads = {}, writes = {} }
end
M.GTE_COMMAND_INPUTS [canon] = row.inputs
M.GTE_COMMAND_OUTPUTS [canon] = row.outputs
M.GTE_COMMAND_LATCH_WINDOWS[canon] = row.latch
end
end
build_isa_views()
--- GTE control-register alias groups. --- GTE control-register alias groups.
@@ -359,6 +482,7 @@ build_isa_views()
--- Cross-alias writes inside one atom body, or across the wave-context boundary, silently clobber each other. --- Cross-alias writes inside one atom body, or across the wave-context boundary, silently clobber each other.
--- The `check_gte_cr_alias_writes` check warns about each pair per source. See `docs/gte_reference.md` §"Control-register alias table" --- The `check_gte_cr_alias_writes` check warns about each pair per source. See `docs/gte_reference.md` §"Control-register alias table"
--- for the HW rationale and the libgte outer-product convention. --- for the HW rationale and the libgte outer-product convention.
--- @type GteCrAliasGroup[]
M.GTE_CR_ALIAS_GROUPS = { M.GTE_CR_ALIAS_GROUPS = {
{ 24, { "gte_cr_RBK", "gte_cr_OFX" } }, -- background R vs screen offset X { 24, { "gte_cr_RBK", "gte_cr_OFX" } }, -- background R vs screen offset X
{ 25, { "gte_cr_GBK", "gte_cr_OFY" } }, -- background G vs screen offset Y { 25, { "gte_cr_GBK", "gte_cr_OFY" } }, -- background G vs screen offset Y
@@ -366,6 +490,7 @@ M.GTE_CR_ALIAS_GROUPS = {
} }
-- Packed RT slots named by the gte.h packed-slot comment. First must be written before second. -- Packed RT slots named by the gte.h packed-slot comment. First must be written before second.
--- @type GtePackedSlotRelation[]
M.GTE_PACKED_SLOT_RELATIONS = { M.GTE_PACKED_SLOT_RELATIONS = {
{ slot = 2, first = "gte_cr_RT13", second = "gte_cr_RT22" }, { slot = 2, first = "gte_cr_RT13", second = "gte_cr_RT22" },
} }
@@ -380,6 +505,7 @@ M.GTE_PACKED_SLOT_RELATIONS = {
-- * The check tracks one entry per destination GPR per MFC2 / CFC2 event. -- * The check tracks one entry per destination GPR per MFC2 / CFC2 event.
-- A subsequent event counts as a "use" iff any of its read operand positions reference that destination GPR's ident (e.g. `R_T0`). -- A subsequent event counts as a "use" iff any of its read operand positions reference that destination GPR's ident (e.g. `R_T0`).
-- * Branch delay slots are out of scope (MIPS control-flow; tracked separately). -- * Branch delay slots are out of scope (MIPS control-flow; tracked separately).
--- @type table<string, integer[]> -- bag: encoder ident -> GPR operand positions
M.OPERAND_READ_POSITIONS = { M.OPERAND_READ_POSITIONS = {
-- CPU ALU with one or two GPR operands. Reads every GPR operand. -- CPU ALU with one or two GPR operands. Reads every GPR operand.
["add_ui"] = {1, 2}, ["add_ui"] = {1, 2},
@@ -479,6 +605,7 @@ M.OPERAND_READ_POSITIONS = {
-- set_poly_gt3(p) -> set_len(p, 9) -> 10 total GP0 0x34 -- set_poly_gt3(p) -> set_len(p, 9) -> 10 total GP0 0x34
-- set_poly_g4(p) -> set_len(p, 8) -> 9 total GP0 0x38 -- set_poly_g4(p) -> set_len(p, 8) -> 9 total GP0 0x38
-- set_poly_gt4(p) -> set_len(p, 12) -> 13 total GP0 0x3C -- set_poly_gt4(p) -> set_len(p, 12) -> 13 total GP0 0x3C
--- @type table<integer, integer> -- bag: GP0 cmd byte -> word count
M.GP0_CMD_SIZE = { M.GP0_CMD_SIZE = {
[0x20] = 5, -- Poly_F3 [0x20] = 5, -- Poly_F3
[0x24] = 8, -- Poly_FT3 [0x24] = 8, -- Poly_FT3
@@ -492,6 +619,7 @@ M.GP0_CMD_SIZE = {
-- Shape suffix (after `ac_format_` / `mac_format_` prefix) -> GP0 cmd byte. -- Shape suffix (after `ac_format_` / `mac_format_` prefix) -> GP0 cmd byte.
-- Lets the static-analysis check derive the cmd byte from a macro name like `mac_format_g4_color` -> `g4` -> 0x38 -> 9 expected words. -- Lets the static-analysis check derive the cmd byte from a macro name like `mac_format_g4_color` -> `g4` -> 0x38 -> 9 expected words.
--- @type table<string, integer> -- bag: shape suffix -> GP0 cmd byte
M.GP0_CMD_BY_SHAPE = { M.GP0_CMD_BY_SHAPE = {
["f3"] = 0x20, ["ft3"] = 0x24, ["f3"] = 0x20, ["ft3"] = 0x24,
["f4"] = 0x28, ["ft4"] = 0x2C, ["f4"] = 0x28, ["ft4"] = 0x2C,
@@ -499,6 +627,7 @@ M.GP0_CMD_BY_SHAPE = {
["g4"] = 0x38, ["gt4"] = 0x3C, ["g4"] = 0x38, ["gt4"] = 0x3C,
} }
--- @type integer
M.UNKNOWN_INSTRUCTION_CYCLES = 1 M.UNKNOWN_INSTRUCTION_CYCLES = 1
-- Hardware-relation policy table. -- Hardware-relation policy table.
@@ -531,11 +660,13 @@ M.UNKNOWN_INSTRUCTION_CYCLES = 1
-- * passes/static_analysis.lua::analyze_hardware_relations (forward walker). -- * passes/static_analysis.lua::analyze_hardware_relations (forward walker).
-- * passes/static_analysis.lua::transfer_hazards CHECK_RULES reader (renders hazards onto `findings`). -- * passes/static_analysis.lua::transfer_hazards CHECK_RULES reader (renders hazards onto `findings`).
-- This table is consumed by the hardware-relation analyzer and hazard renderer. -- This table is consumed by the hardware-relation analyzer and hazard renderer.
--- @type HardwareRelationRow[]
M.HARDWARE_RELATIONS = { M.HARDWARE_RELATIONS = {
-- CPU → COP2 data register (MTC2). The ordinary default is 2 cached words between producer and consumer (cpuspecifications.md:407-419). -- CPU → COP2 data register (MTC2). The ordinary default is 2 cached words between producer and consumer (cpuspecifications.md:407-419).
{ {
id = "mtc2_gpr_visibility", id = "mtc2_gpr_visibility",
semantic = "MTC2", semantic = "MTC2",
consumer = "cop2_input",
token = "gte_mv_to_data_r", token = "gte_mv_to_data_r",
direction = "gpr_to_cop2_data", direction = "gpr_to_cop2_data",
reads = { domain = "gpr", arg = 1 }, reads = { domain = "gpr", arg = 1 },
@@ -554,6 +685,7 @@ M.HARDWARE_RELATIONS = {
{ {
id = "mtc2_irgb_visibility", id = "mtc2_irgb_visibility",
semantic = "MTC2", semantic = "MTC2",
consumer = "cop2_input",
token = "gte_mv_to_data_r", token = "gte_mv_to_data_r",
direction = "gpr_to_cop2_data", direction = "gpr_to_cop2_data",
reads = { domain = "gpr", arg = 1 }, reads = { domain = "gpr", arg = 1 },
@@ -572,6 +704,7 @@ M.HARDWARE_RELATIONS = {
{ {
id = "ctc2_gpr_visibility", id = "ctc2_gpr_visibility",
semantic = "CTC2", semantic = "CTC2",
consumer = "cop2_input",
token = "gte_mv_to_ctrl_r", token = "gte_mv_to_ctrl_r",
direction = "gpr_to_cop2_control", direction = "gpr_to_cop2_control",
reads = { domain = "gpr", arg = 1 }, reads = { domain = "gpr", arg = 1 },
@@ -588,6 +721,7 @@ M.HARDWARE_RELATIONS = {
{ {
id = "mfc2_gpr_visibility", id = "mfc2_gpr_visibility",
semantic = "MFC2", semantic = "MFC2",
consumer = "gpr_read",
token = "gte_mv_from_data_r", token = "gte_mv_from_data_r",
direction = "cop2_data_to_gpr", direction = "cop2_data_to_gpr",
reads = { domain = "cop2.data", arg = 2 }, reads = { domain = "cop2.data", arg = 2 },
@@ -603,6 +737,7 @@ M.HARDWARE_RELATIONS = {
{ {
id = "cfc2_gpr_visibility", id = "cfc2_gpr_visibility",
semantic = "CFC2", semantic = "CFC2",
consumer = "gpr_read",
token = "gte_mv_from_ctrl_r", token = "gte_mv_from_ctrl_r",
direction = "cop2_control_to_gpr", direction = "cop2_control_to_gpr",
reads = { domain = "cop2.ctrl", arg = 2 }, reads = { domain = "cop2.ctrl", arg = 2 },
@@ -621,6 +756,7 @@ M.HARDWARE_RELATIONS = {
{ {
id = "mfc0_gpr_visibility", id = "mfc0_gpr_visibility",
semantic = "MFC0", semantic = "MFC0",
consumer = "gpr_read",
token = "sys_mov_from_cop0", token = "sys_mov_from_cop0",
direction = "cop0_control_to_gpr", direction = "cop0_control_to_gpr",
reads = { domain = "cop0.ctrl", arg = 2 }, reads = { domain = "cop0.ctrl", arg = 2 },
@@ -643,6 +779,7 @@ M.HARDWARE_RELATIONS = {
{ {
id = "lwc2_to_gte_command", id = "lwc2_to_gte_command",
semantic = "LWC2_to_GTE", semantic = "LWC2_to_GTE",
consumer = "cop2_input",
token = "gte_lw", token = "gte_lw",
direction = "memory_to_cop2_data", direction = "memory_to_cop2_data",
reads = { domain = "memory", arg = 2 }, reads = { domain = "memory", arg = 2 },
@@ -658,6 +795,7 @@ M.HARDWARE_RELATIONS = {
{ {
id = "lwc2_to_other_consumer", id = "lwc2_to_other_consumer",
semantic = "LWC2_to_other", semantic = "LWC2_to_other",
consumer = "cop2_input",
token = "gte_lw", token = "gte_lw",
direction = "memory_to_cop2_data", direction = "memory_to_cop2_data",
reads = { domain = "memory", arg = 2 }, reads = { domain = "memory", arg = 2 },
@@ -675,6 +813,7 @@ M.HARDWARE_RELATIONS = {
{ {
id = "swc2_memory_write", id = "swc2_memory_write",
semantic = "SWC2", semantic = "SWC2",
consumer = "gpr_read",
token = "gte_sw", token = "gte_sw",
direction = "cop2_data_to_memory", direction = "cop2_data_to_memory",
reads = { domain = "cop2.data", arg = 1 }, reads = { domain = "cop2.data", arg = 1 },
@@ -692,6 +831,7 @@ M.HARDWARE_RELATIONS = {
{ {
id = "mtc0_cu2_visibility", id = "mtc0_cu2_visibility",
semantic = "MTC0", semantic = "MTC0",
consumer = "gpr_read",
token = "sys_mov_to_cop0", token = "sys_mov_to_cop0",
direction = "gpr_to_cop0_status", direction = "gpr_to_cop0_status",
reads = { domain = "gpr", arg = 1 }, reads = { domain = "gpr", arg = 1 },
@@ -711,6 +851,7 @@ M.HARDWARE_RELATIONS = {
-- Bounded Status/SR.CU2 transition policy. -- Bounded Status/SR.CU2 transition policy.
-- The value lattice and the transition consumer both read this immutable row; no second value pass is permitted. -- The value lattice and the transition consumer both read this immutable row; no second value pass is permitted.
-- The source says the enable/disable transition takes "2 clock cycles or so", so the boundary is conservative rather than exact. -- The source says the enable/disable transition takes "2 clock cycles or so", so the boundary is conservative rather than exact.
--- @type Cu2TransitionPolicy
M.CU2_TRANSITION_POLICY = { M.CU2_TRANSITION_POLICY = {
status_register = 12, status_register = 12,
enable_bit = 0x40000000, enable_bit = 0x40000000,
+14 -10
View File
@@ -16,10 +16,13 @@
--- Net effect: the caller gets the duffle module in one statement; no separate `dofile(...)` + `require("duffle")` dance. --- Net effect: the caller gets the duffle module in one statement; no separate `dofile(...)` + `require("duffle")` dance.
--- ---
local M = {} --- @class DufflePaths
--- @field setup fun(): nil
local M = {} ---@type DufflePaths
-- Cache key for the repo root. Stored in `package.loaded` (process-global) so all 8 entry scripts + passes scripts share one resolution. -- Cache key for the repo root. Stored in `package.loaded` (process-global) so all 8 entry scripts + passes scripts share one resolution.
local CACHE_KEY = "__duffle_repo_root__" local CACHE_KEY = "__duffle_repo_root__" ---@type string
--- Resolve the repo root from this script's own path. Zero shell spawn. --- Resolve the repo root from this script's own path. Zero shell spawn.
--- `duffle_paths.lua` always lives at `<repo>/scripts/duffle_paths.lua`, so the repo root is the parent of the directory containing this script. --- `duffle_paths.lua` always lives at `<repo>/scripts/duffle_paths.lua`, so the repo root is the parent of the directory containing this script.
@@ -30,14 +33,14 @@ local CACHE_KEY = "__duffle_repo_root__"
local function find_repo_root() local function find_repo_root()
if package.loaded[CACHE_KEY] then return package.loaded[CACHE_KEY] end if package.loaded[CACHE_KEY] then return package.loaded[CACHE_KEY] end
local source = debug.getinfo(1, "S").source local source = debug.getinfo(1, "S").source ---@type string
-- Strip the leading `@` (Lua's dofile marker) and the trailing `/duffle_paths.lua` filename. -- Strip the leading `@` (Lua's dofile marker) and the trailing `/duffle_paths.lua` filename.
-- What remains is the directory containing this script, i.e. `<repo>/scripts/`. -- What remains is the directory containing this script, i.e. `<repo>/scripts/`.
local scripts_dir = source and source:match("^@?(.*)[/\\]duffle_paths%.lua$") local scripts_dir = source and source:match("^@?(.*)[/\\]duffle_paths%.lua$") ---@type string|nil
if not scripts_dir then return nil end if not scripts_dir then return nil end
-- The repo root is the parent of `scripts/`. Strip the trailing `scripts/` (with or without trailing slash). -- The repo root is the parent of `scripts/`. Strip the trailing `scripts/` (with or without trailing slash).
local root = scripts_dir:gsub("scripts[\\/]?$", "") local root = scripts_dir:gsub("scripts[\\/]?$", "") ---@type string
root = root:gsub("\\", "/") root = root:gsub("\\", "/")
if root == "" then root = "./" end if root == "" then root = "./" end
if not root:match("/$") then root = root .. "/" end if not root:match("/$") then root = root .. "/" end
@@ -50,8 +53,9 @@ end
--- This script does NOT touch the OS environment: no `os.setenv`, no `os.putenv`, no `$PATH` mods. --- This script does NOT touch the OS environment: no `os.setenv`, no `os.putenv`, no `$PATH` mods.
--- It just sets `package.path` and `package.cpath` (the standard Lua way to register module search dirs). --- It just sets `package.path` and `package.cpath` (the standard Lua way to register module search dirs).
--- lpeg is built by `update_deps.ps1` to `toolchain/lpeg/`, which we wire into `package.cpath` here (so `require("lpeg")` from `duffle.lua` resolves without any global state). --- lpeg is built by `update_deps.ps1` to `toolchain/lpeg/`, which we wire into `package.cpath` here (so `require("lpeg")` from `duffle.lua` resolves without any global state).
--- @return nil
function M.setup() function M.setup()
local repo_root = find_repo_root() local repo_root = find_repo_root() ---@type string|nil
if not repo_root then if not repo_root then
-- Unreachable in practice: find_repo_root() derives the repo root from this script's own source path via debug.getinfo(1, "S").source (no subprocess, no git CLI, <1ms). -- Unreachable in practice: find_repo_root() derives the repo root from this script's own source path via debug.getinfo(1, "S").source (no subprocess, no git CLI, <1ms).
-- A nil return means the source path did not match the expected <repo>/scripts/duffle_paths.lua layout — a packaging bug, not a "missing git repo" condition. -- A nil return means the source path did not match the expected <repo>/scripts/duffle_paths.lua layout — a packaging bug, not a "missing git repo" condition.
@@ -59,8 +63,8 @@ function M.setup()
os.exit(2) os.exit(2)
end end
local scripts_dir = repo_root .. "scripts/" local scripts_dir = repo_root .. "scripts/" ---@type string
local passes_dir = repo_root .. "scripts/passes/" local passes_dir = repo_root .. "scripts/passes/" ---@type string
package.path = scripts_dir .. "?.lua;" package.path = scripts_dir .. "?.lua;"
.. scripts_dir .. "?/init.lua;" .. scripts_dir .. "?/init.lua;"
.. passes_dir .. "?.lua;" .. passes_dir .. "?.lua;"
@@ -70,8 +74,8 @@ function M.setup()
-- lpeg: built by `update_deps.ps1` to `toolchain/lpeg/lpeg.dll`. -- lpeg: built by `update_deps.ps1` to `toolchain/lpeg/lpeg.dll`.
-- lfs: compiled from pcsx-redux's vendored luafilesystem source to `toolchain/lfs/lfs.dll`. -- lfs: compiled from pcsx-redux's vendored luafilesystem source to `toolchain/lfs/lfs.dll`.
-- Wire both directories into cpath so `require("lpeg")` and `require("lfs")` resolve. -- Wire both directories into cpath so `require("lpeg")` and `require("lfs")` resolve.
local lpeg_dir = repo_root .. "toolchain/lpeg/" local lpeg_dir = repo_root .. "toolchain/lpeg/" ---@type string
local lfs_dir = repo_root .. "toolchain/lfs/" local lfs_dir = repo_root .. "toolchain/lfs/" ---@type string
package.cpath = lpeg_dir .. "?.dll;" package.cpath = lpeg_dir .. "?.dll;"
.. lfs_dir .. "?.dll;" .. lfs_dir .. "?.dll;"
.. package.cpath .. package.cpath
+408 -209
View File
File diff suppressed because it is too large Load Diff
+159 -66
View File
@@ -22,7 +22,94 @@
-- spec: System V ABI gABI v1.2 §"ELF Header" (Table 1) + §"Section Header Table" -- spec: System V ABI gABI v1.2 §"ELF Header" (Table 1) + §"Section Header Table"
-- spec: System V ABI gABI v1.2 §"Symbol Table" (Elf32_Sym layout) -- spec: System V ABI gABI v1.2 §"Symbol Table" (Elf32_Sym layout)
local M = {} --- @class Elf32Adapter
--- @field read_u8_at fun(off: integer): integer|nil
--- @field read_u16_at fun(off: integer): integer|nil
--- @field read_u32_at fun(off: integer): integer|nil
--- @field read_size fun(): integer
--- @class Elf32Header
--- @field e_entry integer
--- @field e_shoff integer
--- @field e_shentsize integer
--- @field e_shnum integer
--- @field e_shstrndx integer
--- @field error string|nil
--- @class Elf32Section
--- @field sh_name integer
--- @field sh_type integer
--- @field sh_flags integer
--- @field sh_addr integer
--- @field sh_offset integer
--- @field sh_size integer
--- @field sh_link integer
--- @field name string
--- @class Elf32Sym
--- @field value integer
--- @field size integer
--- @field info integer
--- @field shndx integer
--- @class Elf32HeaderLayout
--- @field magic_offset integer
--- @field magic string
--- @field class_offset integer
--- @field endian_offset integer
--- @field header_bytes integer
--- @field e_entry_offset integer
--- @field e_shoff_offset integer
--- @field e_shentsize_offset integer
--- @field e_shnum_offset integer
--- @field e_shstrndx_offset integer
--- @class Elf32SectionLayout
--- @field sh_name_offset integer
--- @field sh_type_offset integer
--- @field sh_flags_offset integer
--- @field sh_addr_offset integer
--- @field sh_offset_offset integer
--- @field sh_size_offset integer
--- @field sh_link_offset integer
--- @field sh_entsize_bytes integer
--- @class Elf32SymLayout
--- @field st_name integer
--- @field st_value integer
--- @field st_size integer
--- @field st_info integer
--- @field sym_entry_bytes integer
--- @class Elf32Mod
--- @field ELFCLASS32 integer
--- @field ELFDATA2LSB integer
--- @field EM_MIPS integer
--- @field SHT_SYMTAB integer
--- @field SHT_STRTAB integer
--- @field SHT_NOBITS integer
--- @field SHF_WRITE integer
--- @field SHF_ALLOC integer
--- @field SHF_EXECINSTR integer
--- @field ELF32_HEADER Elf32HeaderLayout
--- @field ELF32_SECTION Elf32SectionLayout
--- @field ELF32_SYM Elf32SymLayout
--- @field dw_dwarf32_terminator integer
--- @field read_u32 fun(adapter: Elf32Adapter, off: integer): integer|nil
--- @field read_u16 fun(adapter: Elf32Adapter, off: integer): integer|nil
--- @field read_u8 fun(adapter: Elf32Adapter, off: integer): integer|nil
--- @field size fun(adapter: Elf32Adapter): integer
--- @field read_u32_le fun(buf: string, off: integer): integer
--- @field read_u16_le fun(buf: string, off: integer): integer
--- @field validate_adapter fun(adapter: any): boolean, string|nil
--- @field get_str fun(strtab: string, off: integer): string|nil
--- @field parse_elf32_headers fun(adapter: Elf32Adapter): Elf32Header|nil, string|nil
--- @field walk_sections fun(adapter: Elf32Adapter, hdr: Elf32Header): Elf32Section[]|nil, string|nil
--- @field read_section_bytes fun(adapter: Elf32Adapter, section: Elf32Section): string|nil
--- @field read_named_section fun(adapter: Elf32Adapter, sections: Elf32Section[], name: string): string|nil, string|nil
--- @field collect_symbols fun(adapter: Elf32Adapter, sections: Elf32Section[]): table<string, Elf32Sym>|nil, string|nil
local M = {} ---@type Elf32Mod
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
-- Little-endian readers (bit-weighted accumulator, math.floor only) -- Little-endian readers (bit-weighted accumulator, math.floor only)
@@ -39,7 +126,7 @@ local M = {}
--- **Call form:** explicit-pass. The reader receives `adapter` as the first positional argument and the offset as the second; no `self` is passed. --- **Call form:** explicit-pass. The reader receives `adapter` as the first positional argument and the offset as the second; no `self` is passed.
--- Test fixtures declare `function(offset) ... end` and the parsers call them via dot syntax `adapter.read_u8_at(off)`. --- Test fixtures declare `function(offset) ... end` and the parsers call them via dot syntax `adapter.read_u8_at(off)`.
--- The colon form `adapter:read_u8_at(off)` would prepend the adapter table as `offset` and break the contract. --- The colon form `adapter:read_u8_at(off)` would prepend the adapter table as `offset` and break the contract.
--- @param adapter table --- @param adapter Elf32Adapter
--- @param off integer -- zero-based wire offset --- @param off integer -- zero-based wire offset
--- @return integer|nil --- @return integer|nil
function M.read_u32(adapter, off) function M.read_u32(adapter, off)
@@ -50,7 +137,7 @@ function M.read_u32(adapter, off)
end end
--- Read a 2-byte little-endian unsigned integer from `adapter` at zero-based wire offset `off`. --- Read a 2-byte little-endian unsigned integer from `adapter` at zero-based wire offset `off`.
--- @param adapter table --- @param adapter Elf32Adapter
--- @param off integer -- zero-based wire offset --- @param off integer -- zero-based wire offset
--- @return integer|nil --- @return integer|nil
function M.read_u16(adapter, off) function M.read_u16(adapter, off)
@@ -59,7 +146,7 @@ function M.read_u16(adapter, off)
end end
--- Read a 1-byte unsigned integer from `adapter` at zero-based wire offset `off`. --- Read a 1-byte unsigned integer from `adapter` at zero-based wire offset `off`.
--- @param adapter table --- @param adapter Elf32Adapter
--- @param off integer -- zero-based wire offset --- @param off integer -- zero-based wire offset
--- @return integer|nil --- @return integer|nil
function M.read_u8(adapter, off) function M.read_u8(adapter, off)
@@ -67,7 +154,7 @@ function M.read_u8(adapter, off)
end end
--- Total adapter byte length. --- Total adapter byte length.
--- @param adapter table --- @param adapter Elf32Adapter
--- @return integer --- @return integer
function M.size(adapter) function M.size(adapter)
return adapter.read_size() return adapter.read_size()
@@ -76,8 +163,11 @@ end
--- Forwarders kept for backward compat with scripts/elf_dwarf.lua. --- Forwarders kept for backward compat with scripts/elf_dwarf.lua.
--- The metaprogram side keeps `read_u32_le` / `read_u16_le`; --- The metaprogram side keeps `read_u32_le` / `read_u16_le`;
--- both layers now use the same byte-level helpers under the hood. --- both layers now use the same byte-level helpers under the hood.
--- @param buf string
--- @param off integer
--- @return integer
function M.read_u32_le(buf, off) function M.read_u32_le(buf, off)
local byte_off = off + 1 local byte_off = off + 1 ---@type integer
return buf:byte(byte_off) return buf:byte(byte_off)
+ buf:byte(byte_off + 0x01) * 0x00000100 + buf:byte(byte_off + 0x01) * 0x00000100
+ buf:byte(byte_off + 0x02) * 0x00010000 + buf:byte(byte_off + 0x02) * 0x00010000
@@ -89,7 +179,7 @@ end
--- @param off integer -- zero-based wire offset --- @param off integer -- zero-based wire offset
--- @return integer --- @return integer
function M.read_u16_le(buf, off) function M.read_u16_le(buf, off)
local byte_off = off + 1 local byte_off = off + 1 ---@type integer
return buf:byte(byte_off) + buf:byte(byte_off + 0x01) * 0x00000100 return buf:byte(byte_off) + buf:byte(byte_off + 0x01) * 0x00000100
end end
@@ -116,6 +206,7 @@ M.SHF_EXECINSTR = 0x4 -- spec: gABI v1.2 §"Section Attributes" — executable
-- ELF32 header layout (System V ABI gABI v1.2 §"ELF Header" Table 1) -- ELF32 header layout (System V ABI gABI v1.2 §"ELF Header" Table 1)
-- --------------------------------------------------------------------------- -- ---------------------------------------------------------------------------
-- All offsets are zero-based wire offsets. The header is 52 bytes total (header_bytes = 0x34 = 52). -- All offsets are zero-based wire offsets. The header is 52 bytes total (header_bytes = 0x34 = 52).
--- @type Elf32HeaderLayout
M.ELF32_HEADER = { M.ELF32_HEADER = {
magic_offset = 0x00, -- 4 bytes; expected "\127ELF" magic_offset = 0x00, -- 4 bytes; expected "\127ELF"
magic = "\127ELF", magic = "\127ELF",
@@ -134,6 +225,7 @@ M.ELF32_HEADER = {
-- --------------------------------------------------------------------------- -- ---------------------------------------------------------------------------
-- Each entry is 40 bytes (sh_entsize_bytes = 0x28 = 40); -- Each entry is 40 bytes (sh_entsize_bytes = 0x28 = 40);
-- zero-based, field offsets relative to the start of the entry. -- zero-based, field offsets relative to the start of the entry.
--- @type Elf32SectionLayout
M.ELF32_SECTION = { M.ELF32_SECTION = {
sh_name_offset = 0x00, -- 4-byte LE; offset into .shstrtab sh_name_offset = 0x00, -- 4-byte LE; offset into .shstrtab
sh_type_offset = 0x04, -- 4-byte LE; section type (SHT_*) sh_type_offset = 0x04, -- 4-byte LE; section type (SHT_*)
@@ -150,6 +242,7 @@ M.ELF32_SECTION = {
-- --------------------------------------------------------------------------- -- ---------------------------------------------------------------------------
-- Each entry is 16 bytes (sym_entry_bytes = 0x10 = 16); -- Each entry is 16 bytes (sym_entry_bytes = 0x10 = 16);
-- zero-based, field offsets relative to the start of the entry. -- zero-based, field offsets relative to the start of the entry.
--- @type Elf32SymLayout
M.ELF32_SYM = { M.ELF32_SYM = {
st_name = 0x00, -- 4-byte LE; offset into the linked string table st_name = 0x00, -- 4-byte LE; offset into the linked string table
st_value = 0x04, -- 4-byte LE; symbol value (address / absolute) st_value = 0x04, -- 4-byte LE; symbol value (address / absolute)
@@ -190,7 +283,7 @@ end
--- @return string|nil --- @return string|nil
function M.get_str(strtab, off) function M.get_str(strtab, off)
if off < 0 or off >= #strtab then return nil end if off < 0 or off >= #strtab then return nil end
local end_pos = strtab:find("\0", off + 1, true) local end_pos = strtab:find("\0", off + 1, true) ---@type integer|nil
if not end_pos then return nil end if not end_pos then return nil end
return strtab:sub(off + 1, end_pos - 1) return strtab:sub(off + 1, end_pos - 1)
end end
@@ -205,39 +298,39 @@ end
--- On failure returns nil + a stable error code: --- On failure returns nil + a stable error code:
--- bad_magic, unsupported_elf_class, unsupported_elf_data, truncated_header --- bad_magic, unsupported_elf_class, unsupported_elf_data, truncated_header
--- The header's machine field is NOT validated here — callers (e.g. the helper's prime path) decide whether to require EM_MIPS before symbol reads. --- The header's machine field is NOT validated here — callers (e.g. the helper's prime path) decide whether to require EM_MIPS before symbol reads.
--- @param adapter table --- @param adapter Elf32Adapter
--- @return table|nil, string|nil --- @return Elf32Header|nil, string|nil
function M.parse_elf32_headers(adapter) function M.parse_elf32_headers(adapter)
local ok, err = M.validate_adapter(adapter) local ok, err = M.validate_adapter(adapter) ---@type boolean, string|nil
if not ok then return nil, err end if not ok then return nil, err end
-- 4-byte magic: 0x7F 'E' 'L' 'F'. -- 4-byte magic: 0x7F 'E' 'L' 'F'.
-- The byte readers take the adapter explicitly. -- The byte readers take the adapter explicitly.
-- The production `Support.File` adapter is wrapped by the caller to drop its implicit `self` so the parser shape is flat pass-style. -- The production `Support.File` adapter is wrapped by the caller to drop its implicit `self` so the parser shape is flat pass-style.
local b1 = M.read_u8(adapter, 0) local b1 = M.read_u8(adapter, 0) ---@type integer|nil
local b2 = M.read_u8(adapter, 1) local b2 = M.read_u8(adapter, 1) ---@type integer|nil
local b3 = M.read_u8(adapter, 2) local b3 = M.read_u8(adapter, 2) ---@type integer|nil
local b4 = M.read_u8(adapter, 3) local b4 = M.read_u8(adapter, 3) ---@type integer|nil
if not (b1 and b2 and b3 and b4) if not (b1 and b2 and b3 and b4)
or not (b1 == 0x7f and b2 == 0x45 and b3 == 0x4c and b4 == 0x46) then or not (b1 == 0x7f and b2 == 0x45 and b3 == 0x4c and b4 == 0x46) then
return nil, "bad_magic" return nil, "bad_magic"
end end
local class = M.read_u8(adapter, M.ELF32_HEADER.class_offset) local class = M.read_u8(adapter, M.ELF32_HEADER.class_offset) ---@type integer|nil
if class ~= M.ELFCLASS32 then if class ~= M.ELFCLASS32 then
return nil, "unsupported_elf_class" return nil, "unsupported_elf_class"
end end
local data = M.read_u8(adapter, M.ELF32_HEADER.endian_offset) local data = M.read_u8(adapter, M.ELF32_HEADER.endian_offset) ---@type integer|nil
if data ~= M.ELFDATA2LSB then if data ~= M.ELFDATA2LSB then
return nil, "unsupported_elf_data" return nil, "unsupported_elf_data"
end end
local e_entry = M.read_u32(adapter, M.ELF32_HEADER.e_entry_offset) local e_entry = M.read_u32(adapter, M.ELF32_HEADER.e_entry_offset) ---@type integer|nil
local e_shoff = M.read_u32(adapter, M.ELF32_HEADER.e_shoff_offset) local e_shoff = M.read_u32(adapter, M.ELF32_HEADER.e_shoff_offset) ---@type integer|nil
local e_shentsize = M.read_u16(adapter, M.ELF32_HEADER.e_shentsize_offset) local e_shentsize = M.read_u16(adapter, M.ELF32_HEADER.e_shentsize_offset) ---@type integer|nil
local e_shnum = M.read_u16(adapter, M.ELF32_HEADER.e_shnum_offset) local e_shnum = M.read_u16(adapter, M.ELF32_HEADER.e_shnum_offset) ---@type integer|nil
local e_shstrndx = M.read_u16(adapter, M.ELF32_HEADER.e_shstrndx_offset) local e_shstrndx = M.read_u16(adapter, M.ELF32_HEADER.e_shstrndx_offset) ---@type integer|nil
if not (e_entry and e_shoff and e_shentsize and e_shnum and e_shstrndx) then if not (e_entry and e_shoff and e_shentsize and e_shnum and e_shstrndx) then
return nil, "truncated_header" return nil, "truncated_header"
end end
@@ -254,11 +347,11 @@ end
--- Read one section-header entry from `adapter` at `sh_off`. --- Read one section-header entry from `adapter` at `sh_off`.
--- Returns a table with the wire fields plus a (yet-unresolved) `name` field. --- Returns a table with the wire fields plus a (yet-unresolved) `name` field.
--- @param adapter table --- @param adapter Elf32Adapter
--- @param sh_off integer --- @param sh_off integer
--- @return table|nil, string|nil -- entry, error --- @return Elf32Section|nil, string|nil
local function read_section_entry(adapter, sh_off) local function read_section_entry(adapter, sh_off)
local entry = { local entry = { ---@type Elf32Section
sh_name = M.read_u32(adapter, sh_off + M.ELF32_SECTION.sh_name_offset), sh_name = M.read_u32(adapter, sh_off + M.ELF32_SECTION.sh_name_offset),
sh_type = M.read_u32(adapter, sh_off + M.ELF32_SECTION.sh_type_offset), sh_type = M.read_u32(adapter, sh_off + M.ELF32_SECTION.sh_type_offset),
sh_flags = M.read_u32(adapter, sh_off + M.ELF32_SECTION.sh_flags_offset), sh_flags = M.read_u32(adapter, sh_off + M.ELF32_SECTION.sh_flags_offset),
@@ -279,22 +372,22 @@ end
--- (the section at logical index 0 is at array position 1, etc.). --- (the section at logical index 0 is at array position 1, etc.).
--- Each entry has the wire fields plus a resolved `name` derived from `.shstrtab`. --- Each entry has the wire fields plus a resolved `name` derived from `.shstrtab`.
--- Returns nil + a stable error code on failure: truncated_section_headers, missing_shstrtab, truncated_strtab --- Returns nil + a stable error code on failure: truncated_section_headers, missing_shstrtab, truncated_strtab
--- @param adapter table --- @param adapter Elf32Adapter
--- @param hdr table -- the table returned by parse_elf32_headers --- @param hdr Elf32Header
--- @return table|nil, string|nil --- @return Elf32Section[]|nil, string|nil
function M.walk_sections(adapter, hdr) function M.walk_sections(adapter, hdr)
if not hdr or hdr.error then return nil, hdr and hdr.error or "truncated_section_headers" end if not hdr or hdr.error then return nil, hdr and hdr.error or "truncated_section_headers" end
local file_size = M.size(adapter) local file_size = M.size(adapter) ---@type integer
if hdr.e_shoff + hdr.e_shnum * hdr.e_shentsize > file_size then if hdr.e_shoff + hdr.e_shnum * hdr.e_shentsize > file_size then
return nil, "truncated_section_headers" return nil, "truncated_section_headers"
end end
-- Read every section header first; we need .shstrtab to resolve names. -- Read every section header first; we need .shstrtab to resolve names.
local sections = {} local sections = {} ---@type Elf32Section[]
for i = 0, hdr.e_shnum - 1 do for i = 0, hdr.e_shnum - 1 do ---@type integer
local sh_off = hdr.e_shoff + i * hdr.e_shentsize local sh_off = hdr.e_shoff + i * hdr.e_shentsize ---@type integer
local entry, err = read_section_entry(adapter, sh_off) local entry, err = read_section_entry(adapter, sh_off) ---@type Elf32Section|nil, string|nil
if not entry then return nil, err end if not entry then return nil, err end
sections[i + 1] = entry sections[i + 1] = entry
end end
@@ -303,17 +396,17 @@ function M.walk_sections(adapter, hdr)
return nil, "missing_shstrtab" return nil, "missing_shstrtab"
end end
local shstrtab = sections[hdr.e_shstrndx + 1] local shstrtab = sections[hdr.e_shstrndx + 1] ---@type Elf32Section|nil
if not shstrtab or shstrtab.sh_type ~= M.SHT_STRTAB then if not shstrtab or shstrtab.sh_type ~= M.SHT_STRTAB then
return nil, "missing_shstrtab" return nil, "missing_shstrtab"
end end
if shstrtab.sh_offset + shstrtab.sh_size > file_size then if shstrtab.sh_offset + shstrtab.sh_size > file_size then
return nil, "truncated_section_headers" return nil, "truncated_section_headers"
end end
local shstrtab_bytes = M.read_section_bytes(adapter, shstrtab) local shstrtab_bytes = M.read_section_bytes(adapter, shstrtab) ---@type string|nil
if not shstrtab_bytes then return nil, "truncated_section_headers" end if not shstrtab_bytes then return nil, "truncated_section_headers" end
for _, s in ipairs(sections) do for _, s in ipairs(sections) do ---@type integer, Elf32Section
s.name = M.get_str(shstrtab_bytes, s.sh_name) or "" s.name = M.get_str(shstrtab_bytes, s.sh_name) or ""
end end
@@ -322,15 +415,15 @@ end
--- Read the bytes of one section. Returns a string, or nil if the adapter returns nil for any byte (out-of-bounds). --- Read the bytes of one section. Returns a string, or nil if the adapter returns nil for any byte (out-of-bounds).
--- The caller is responsible fors sizing the buffer (the section's sh_offset + sh_size must fit in adapter.size). --- The caller is responsible fors sizing the buffer (the section's sh_offset + sh_size must fit in adapter.size).
--- @param adapter table --- @param adapter Elf32Adapter
--- @param section table -- one entry from walk_sections --- @param section Elf32Section
--- @return string|nil --- @return string|nil
function M.read_section_bytes(adapter, section) function M.read_section_bytes(adapter, section)
local size = section.sh_size local size = section.sh_size ---@type integer
if size == 0 then return "" end if size == 0 then return "" end
local out = {} local out = {} ---@type string[]
for i = 0, size - 1 do for i = 0, size - 1 do ---@type integer
local b = M.read_u8(adapter, section.sh_offset + i) local b = M.read_u8(adapter, section.sh_offset + i) ---@type integer|nil
if b == nil then return nil end if b == nil then return nil end
out[#out + 1] = string.char(b) out[#out + 1] = string.char(b)
end end
@@ -339,15 +432,15 @@ end
--- Convenience: walk sections, then look up the named section, then read its bytes. --- Convenience: walk sections, then look up the named section, then read its bytes.
--- Returns nil + a stable error code if the section is absent or out-of-bounds. --- Returns nil + a stable error code if the section is absent or out-of-bounds.
--- @param adapter table --- @param adapter Elf32Adapter
--- @param sections table -- 1-based array from walk_sections --- @param sections Elf32Section[]
--- @param name string --- @param name string
--- @return string|nil, string|nil --- @return string|nil, string|nil
function M.read_named_section(adapter, sections, name) function M.read_named_section(adapter, sections, name)
if not sections then return nil, "missing_section" end if not sections then return nil, "missing_section" end
for _, s in ipairs(sections) do for _, s in ipairs(sections) do ---@type integer, Elf32Section
if s.name == name then if s.name == name then
local bytes = M.read_section_bytes(adapter, s) local bytes = M.read_section_bytes(adapter, s) ---@type string|nil
if not bytes then return nil, "truncated_section_data" end if not bytes then return nil, "truncated_section_data" end
return bytes, nil return bytes, nil
end end
@@ -359,47 +452,47 @@ end
--- Each stored entry is `{ value = st_value, size = st_size, info = st_info, shndx = st_shndx }`. --- Each stored entry is `{ value = st_value, size = st_size, info = st_info, shndx = st_shndx }`.
--- Both STB_LOCAL and STB_GLOBAL symbols are included; the live ELF stores `smem` as a local symbol. --- Both STB_LOCAL and STB_GLOBAL symbols are included; the live ELF stores `smem` as a local symbol.
--- Returns nil + a stable error code on failure: missing_symtab_strtab, truncated_section_headers --- Returns nil + a stable error code on failure: missing_symtab_strtab, truncated_section_headers
--- @param adapter table --- @param adapter Elf32Adapter
--- @param sections table --- @param sections Elf32Section[]
--- @return table|nil, string|nil --- @return table<string, Elf32Sym>|nil, string|nil
function M.collect_symbols(adapter, sections) function M.collect_symbols(adapter, sections)
if not sections then return nil, "missing_sections" end if not sections then return nil, "missing_sections" end
local symbols = {} local symbols = {} ---@type table<string, Elf32Sym> -- bag: symbol name -> Elf32Sym
local file_size = M.size(adapter) local file_size = M.size(adapter) ---@type integer
for _, s in ipairs(sections) do for _, s in ipairs(sections) do ---@type integer, Elf32Section
if s.sh_type == M.SHT_SYMTAB then if s.sh_type == M.SHT_SYMTAB then
local strtab = sections[s.sh_link + 1] local strtab = sections[s.sh_link + 1] ---@type Elf32Section|nil
if not strtab or strtab.sh_type ~= M.SHT_STRTAB then if not strtab or strtab.sh_type ~= M.SHT_STRTAB then
return nil, "missing_symtab_strtab" return nil, "missing_symtab_strtab"
end end
if strtab.sh_offset + strtab.sh_size > file_size then if strtab.sh_offset + strtab.sh_size > file_size then
return nil, "truncated_section_headers" return nil, "truncated_section_headers"
end end
local strtab_bytes = M.read_section_bytes(adapter, strtab) local strtab_bytes = M.read_section_bytes(adapter, strtab) ---@type string|nil
if not strtab_bytes then return nil, "truncated_section_headers" end if not strtab_bytes then return nil, "truncated_section_headers" end
if s.sh_offset + s.sh_size > file_size then if s.sh_offset + s.sh_size > file_size then
return nil, "truncated_section_headers" return nil, "truncated_section_headers"
end end
local symtab_bytes = M.read_section_bytes(adapter, s) local symtab_bytes = M.read_section_bytes(adapter, s) ---@type string|nil
if not symtab_bytes then return nil, "truncated_section_headers" end if not symtab_bytes then return nil, "truncated_section_headers" end
local n = #symtab_bytes / M.ELF32_SYM.sym_entry_bytes local n = #symtab_bytes / M.ELF32_SYM.sym_entry_bytes ---@type number
for j = 0, n - 1 do for j = 0, n - 1 do ---@type integer
local e = s.sh_offset + j * M.ELF32_SYM.sym_entry_bytes local e = s.sh_offset + j * M.ELF32_SYM.sym_entry_bytes ---@type integer
local st_name = M.read_u32(adapter, e + M.ELF32_SYM.st_name) local st_name = M.read_u32(adapter, e + M.ELF32_SYM.st_name) ---@type integer|nil
if st_name then if st_name then
local st_value = M.read_u32(adapter, e + M.ELF32_SYM.st_value) local st_value = M.read_u32(adapter, e + M.ELF32_SYM.st_value) ---@type integer|nil
local st_size = M.read_u32(adapter, e + M.ELF32_SYM.st_size) local st_size = M.read_u32(adapter, e + M.ELF32_SYM.st_size) ---@type integer|nil
local st_info = M.read_u8(adapter, e + M.ELF32_SYM.st_info) local st_info = M.read_u8(adapter, e + M.ELF32_SYM.st_info) ---@type integer|nil
-- st_shndx is at offset 14 (2 bytes) — derived from the layout -- st_shndx is at offset 14 (2 bytes) — derived from the layout
-- the metaprogram reads too. Inline the read to keep the -- the metaprogram reads too. Inline the read to keep the
-- adapter as the only I/O surface. -- adapter as the only I/O surface.
local b1 = M.read_u8(adapter, e + 14) local b1 = M.read_u8(adapter, e + 14) ---@type integer|nil
local b2 = M.read_u8(adapter, e + 15) local b2 = M.read_u8(adapter, e + 15) ---@type integer|nil
if not (b1 and b2) then if not (b1 and b2) then
return nil, "truncated_section_headers" return nil, "truncated_section_headers"
end end
local st_shndx = b1 + b2 * 0x100 local st_shndx = b1 + b2 * 0x100 ---@type integer
local name = M.get_str(strtab_bytes, st_name) or "" local name = M.get_str(strtab_bytes, st_name) or "" ---@type string
if name ~= "" then if name ~= "" then
symbols[name] = { symbols[name] = {
value = st_value, value = st_value,
+427 -182
View File
File diff suppressed because it is too large Load Diff
+124 -168
View File
@@ -10,8 +10,8 @@
-- Bootstrap follows the entry scripts; `scripts/duffle_paths.lua` sets package.path and package.cpath. See `ps1_meta.lua` for the rationale. -- Bootstrap follows the entry scripts; `scripts/duffle_paths.lua` sets package.path and package.cpath. See `ps1_meta.lua` for the rationale.
-- `debug.getinfo(1, "S").source` locates this file for standalone and orchestrated runs, then `duffle_paths.lua` returns the loaded `duffle` module. -- `debug.getinfo(1, "S").source` locates this file for standalone and orchestrated runs, then `duffle_paths.lua` returns the loaded `duffle` module.
local _bootstrap_dir = debug.getinfo(1, "S").source:match("^@?(.*[/\\])") or "./" local _bootstrap_dir = debug.getinfo(1, "S").source:match("^@?(.*[/\\])") or "./" ---@type string
local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua") local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua") ---@type DuffleExport
-- The annotation pass reads the source-derived registries from scan_source: -- The annotation pass reads the source-derived registries from scan_source:
-- * pipe_ctx.register_alias_registry — for atom_dbg_reg_default(R_X, ...) and atom_reg_types(R_X, ...) member-identity checks -- * pipe_ctx.register_alias_registry — for atom_dbg_reg_default(R_X, ...) and atom_reg_types(R_X, ...) member-identity checks
@@ -21,76 +21,40 @@ local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua")
-- Type declarations -- Type declarations
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
--- @class SourceFile -- SourceFile, PassCtx, PassResult, PassShared, Corpus, Finding: see ps1_meta.lua
--- @field path string -- Absolute path to the source file -- SourceScan, AtomEntry, AtomInfoEntry, BindsEntry, RegTypeDefault, AtomViewEntry: see scan_source.lua
--- @field text string -- Full source text
--- @field dir string -- Directory containing the source
--- @field basename string -- Filename without extension
--- @field scan table -- Pre-scanned SourceScan payload (from duffle.scan_source)
--- @class PassCtx --- @class RegTypeOccurrence
--- @field sources SourceFile[] --- @field reg string
--- @field metadata_path string --- @field type_name string
--- @field shared table --- @field source_line integer
--- @field shared.word_counts table<string, integer>
--- @field out_root string
--- @field project_root string
--- @field upstream table<string, table>
--- @field flags table
--- @field verbose boolean
--- @class PassResult
--- @field outputs table[]
--- @field errors table[]
--- @field warnings table[]
--- @class AtomAnnotation
--- @field line integer -- Source line of the atom_info call
--- @field macro string -- Macro name (always "atom_info" in the new shape)
--- @field name string -- Atom name
--- @field kind string -- Always "info"
--- @field binds string|nil -- Binds_X name if any
--- @field reads string[] -- R_* names (read targets)
--- @field writes string[] -- R_* names (write targets)
--- @field errors string[]|nil -- Parse-time errors from scan_source (atom_info body malformed)
--- @class DebugSkipMarker -- Sub-shape of scan_source.lua's @class DebugSkipMarker
--- @field marker_kind string -- Exact marker ident read from source. Only "atom_dbg_skip" (bare) is positive.
--- @field marker_line integer
--- @field args string|nil -- Trimmed text inside the parens (nil when has_parens is false)
--- @field has_parens boolean
--- @field is_bare boolean -- true iff marker_kind == "atom_dbg_skip" AND has_parens == false (the only positive form)
--- @field pending boolean -- true while awaiting the following declaration
--- @field superseded_by_marker_line integer|nil -- Set on a marker that was bumped out of the pending slot
--- @field target_kind string|nil -- "atom" | "comp_bare" | "comp_proc" | "unrelated" once observed
--- @class Finding
--- @field line integer -- Source line (or 0 for pass-level)
--- @field msg string -- Finding message
--- @class Findings --- @class Findings
--- @field errors Finding[] --- @field errors Finding[]
--- @field warnings Finding[] --- @field warnings Finding[]
--- @field info Finding[] --- @field info Finding[]
--- @class PipeCtx -- PassScratch: see ps1_meta.lua
--- @field atom_index table<string, AtomAnnotation> -- Name -> AtomAnnotation (only kind=="atom")
--- @field binds_index table<string, BindsStruct> -- Name -> BindsStruct
--- @field annot_counts table<string, integer> -- Name -> annotation count (for unique_annotation check)
--- @field types table<string, RegTypeDefault> -- From scan_source
--- @field atom_views table<string, AtomViewEntry> -- From scan_source
--- @field seen_defaults table<string, integer> -- Duplicate atom_dbg_reg_default detection
--- @field seen_field table<string, integer> -- Binds_* -> count of fields (set/checked by check_binds_no_duplicate_fields)
--- @field _scan SourceScan -- Full scan payload (typed-view sub-calls live here)
--- @class AnnotatedResult --- @class AnnotatedResult
--- @field atoms AtomEntry[] --- @field atoms AtomEntry[]
--- @field annots AtomAnnotation[] --- @field annots AtomInfoEntry[]
--- @field macros MacroEntry[] --- @field macros MacroEntry[]
--- @field binds BindsEntry[] --- @field binds BindsEntry[]
--- @field errors Finding[] --- @field errors Finding[]
--- @field warnings Finding[] --- @field warnings Finding[]
--- @field info Finding[] --- @field info Finding[]
--- @field source string|nil
--- @class CheckRule
--- @field per_annot (fun(item: AtomInfoEntry, pipe_ctx: PassScratch, findings: Findings): nil)|nil
--- @class SourceScan
--- @field type_occurrences RegTypeOccurrence[]|nil
--- @class AnnotationPass
--- @field validate fun(ctx: PassCtx, src: SourceFile, corpus_pipe_ctx: PassScratch|nil): AnnotatedResult
--- @field run fun(ctx: PassCtx): PassResult
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
-- Per-check functions (the CHECK_RULES table's payload) -- Per-check functions (the CHECK_RULES table's payload)
@@ -99,24 +63,27 @@ local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua")
--- `macro_word_drift` writes errors[] for missing or mismatched metadata and info[] for a match. --- `macro_word_drift` writes errors[] for missing or mismatched metadata and info[] for a match.
--- Check: Every annotated atom must have a matching MipsAtom_(name) declaration. --- Check: Every annotated atom must have a matching MipsAtom_(name) declaration.
--- @param a AtomAnnotation --- @param info AtomInfoEntry
--- @param pipe_ctx PipeCtx --- @param pipe_ctx PassScratch
--- @param findings Findings --- @param findings Findings
local function check_atom_decl_exists(a, pipe_ctx, findings) --- @return nil
if not pipe_ctx.atom_index[a.name] then local function check_atom_decl_exists(info, pipe_ctx, findings)
if not pipe_ctx.atom_index[info.atom_name] then
findings.errors[#findings.errors + 1] = { findings.errors[#findings.errors + 1] = {
line = a.line, line = info.info_line,
msg = string.format("annotation for '%s' has no matching MipsAtom_(%s) { ... }", a.name, a.name), msg = string.format("annotation for '%s' has no matching MipsAtom_(%s) { ... }", info.atom_name, info.atom_name),
} }
end end
end end
--- Check: Every atom may have AT MOST ONE annotation. --- Check: Every atom may have AT MOST ONE annotation.
--- Post-loop: Needs full-corpus `annot_counts` from pipe_ctx. --- Post-loop: Needs full-corpus `annot_counts` from pipe_ctx.
--- @param pipe_ctx PipeCtx --- @param _item AtomInfoEntry|nil
--- @param pipe_ctx PassScratch
--- @param findings Findings --- @param findings Findings
--- @return nil
local function check_unique_annotation(_item, pipe_ctx, findings) local function check_unique_annotation(_item, pipe_ctx, findings)
for name, n in pairs(pipe_ctx.annot_counts) do for name, n in pairs(pipe_ctx.annot_counts) do ---@type string, integer
if n > 1 then if n > 1 then
findings.errors[#findings.errors + 1] = { findings.errors[#findings.errors + 1] = {
line = pipe_ctx.atom_index[name] and pipe_ctx.atom_index[name].line or 0, line = pipe_ctx.atom_index[name] and pipe_ctx.atom_index[name].line or 0,
@@ -128,28 +95,30 @@ end
--- Check: BIND atoms must reference a real Binds_* struct. --- Check: BIND atoms must reference a real Binds_* struct.
--- I keep this as a warning so the annotation pass can report the common test-fixture case; `check_abi_handoff` in static analysis supplies the build-stopping error. --- I keep this as a warning so the annotation pass can report the common test-fixture case; `check_abi_handoff` in static analysis supplies the build-stopping error.
--- @param a AtomAnnotation --- @param info AtomInfoEntry
--- @param pipe_ctx PipeCtx --- @param pipe_ctx PassScratch
--- @param findings Findings --- @param findings Findings
local function check_binds_struct_exists(a, pipe_ctx, findings) --- @return nil
if not a.binds then return end local function check_binds_struct_exists(info, pipe_ctx, findings)
if pipe_ctx.binds_index[a.binds] then return end if not info.binds then return end
if pipe_ctx.binds_index[info.binds] then return end
findings.warnings[#findings.warnings + 1] = { findings.warnings[#findings.warnings + 1] = {
line = a.line, line = info.info_line,
msg = string.format("'%s' binds '%s' but no Struct_(%s) { ... } " msg = string.format("'%s' binds '%s' but no Struct_(%s) { ... } "
.. "declaration found (also flagged as an error by check_abi_handoff in the static-analysis pass)" .. "declaration found (also flagged as an error by check_abi_handoff in the static-analysis pass)"
, a.name, a.binds, a.binds), , info.atom_name, info.binds, info.binds),
} }
end end
--- Check: TAPE_WORDS(mac_X, N) ↔ WORD_COUNT(mac_X, N) drift. --- Check: TAPE_WORDS(mac_X, N) ↔ WORD_COUNT(mac_X, N) drift.
--- Three outcomes: missing (error), mismatch (error), match (info). --- Three outcomes: missing (error), mismatch (error), match (info).
--- @param m MacroEntry --- @param m MacroEntry
--- @param wc table<string, integer> -- Shared word-count table (from ctx.shared.word_counts) --- @param pipe_ctx PassScratch
--- @param findings Findings --- @param findings Findings
--- @return nil
local function check_macro_word_drift(m, pipe_ctx, findings) local function check_macro_word_drift(m, pipe_ctx, findings)
local wc = (pipe_ctx and pipe_ctx.word_counts) or {} local wc = (pipe_ctx and pipe_ctx.word_counts) or {} ---@type WordCounts
local declared = wc[m.name] local declared = wc[m.name] ---@type integer|nil
if not declared then if not declared then
findings.errors[#findings.errors + 1] = { findings.errors[#findings.errors + 1] = {
line = m.line, line = m.line,
@@ -173,12 +142,13 @@ end
--- Check: atom_dbg_reg_default(R_X, <type>) targets an alias in `pipe_ctx.register_alias_registry` and a type in `pipe_ctx.type_name_registry`. --- Check: atom_dbg_reg_default(R_X, <type>) targets an alias in `pipe_ctx.register_alias_registry` and a type in `pipe_ctx.type_name_registry`.
--- Pointer depth remains bounded to 0 or 1, and duplicate defaults remain errors. --- Pointer depth remains bounded to 0 or 1, and duplicate defaults remain errors.
--- @param _src SourceFile -- unused (kept for the per_source shape) --- @param _src SourceFile -- unused (kept for the per_source shape)
--- @param pipe_ctx PipeCtx --- @param pipe_ctx PassScratch
--- @param findings Findings --- @param findings Findings
--- @return nil
local function check_semantic_reg_defaults(_src, pipe_ctx, findings) local function check_semantic_reg_defaults(_src, pipe_ctx, findings)
-- Detect duplicate defaults using the ordered occurrence list (the out.types hash only retains the last declaration). -- Detect duplicate defaults using the ordered occurrence list (the out.types hash only retains the last declaration).
local seen_first_line = {} local seen_first_line = {} ---@type table<string, integer> -- bag: register ident -> first source line
for _, occ in ipairs(pipe_ctx.type_occurrences or {}) do for _, occ in ipairs(pipe_ctx.type_occurrences or {}) do ---@type integer, RegTypeOccurrence
if seen_first_line[occ.reg] == nil then if seen_first_line[occ.reg] == nil then
seen_first_line[occ.reg] = occ.source_line seen_first_line[occ.reg] = occ.source_line
else else
@@ -190,9 +160,9 @@ local function check_semantic_reg_defaults(_src, pipe_ctx, findings)
} }
end end
end end
local reg_registry = pipe_ctx.register_alias_registry or {} local reg_registry = pipe_ctx.register_alias_registry or {} ---@type table<string, AliasEntry>
local type_registry = pipe_ctx.type_name_registry or {} local type_registry = pipe_ctx.type_name_registry or {} ---@type table<string, TypeNameEntry>
for reg, def in pairs(pipe_ctx.types or {}) do for reg, def in pairs(pipe_ctx.types or {}) do ---@type string, RegTypeDefault
if not reg_registry[reg] then if not reg_registry[reg] then
findings.errors[#findings.errors + 1] = { findings.errors[#findings.errors + 1] = {
line = def.source_line, line = def.source_line,
@@ -223,14 +193,15 @@ end
--- Check: atom_reg_types(R_X, <type>) entries target an alias in `pipe_ctx.register_alias_registry` and a type in `pipe_ctx.type_name_registry`. --- Check: atom_reg_types(R_X, <type>) entries target an alias in `pipe_ctx.register_alias_registry` and a type in `pipe_ctx.type_name_registry`.
--- A bare `atom_reg` marker opts the `R_<n>` alias into GPR identity; references to R_T0..R_T3 require the same explicit marker. --- A bare `atom_reg` marker opts the `R_<n>` alias into GPR identity; references to R_T0..R_T3 require the same explicit marker.
--- @param _src SourceFile --- @param _src SourceFile
--- @param pipe_ctx PipeCtx --- @param pipe_ctx PassScratch
--- @param findings Findings --- @param findings Findings
--- @return nil
local function check_atom_reg_types(_src, pipe_ctx, findings) local function check_atom_reg_types(_src, pipe_ctx, findings)
local reg_registry = pipe_ctx.register_alias_registry or {} local reg_registry = pipe_ctx.register_alias_registry or {} ---@type table<string, AliasEntry>
local type_registry = pipe_ctx.type_name_registry or {} local type_registry = pipe_ctx.type_name_registry or {} ---@type table<string, TypeNameEntry>
for _, ai in ipairs(pipe_ctx.atom_infos_list or {}) do for _, ai in ipairs(pipe_ctx.atom_infos_list or {}) do ---@type integer, AtomInfoEntry
if ai.reg_type_overrides then if ai.reg_type_overrides then
for reg, ov in pairs(ai.reg_type_overrides) do for reg, ov in pairs(ai.reg_type_overrides) do ---@type string, RegTypeOverride
if not reg_registry[reg] then if not reg_registry[reg] then
findings.errors[#findings.errors + 1] = { findings.errors[#findings.errors + 1] = {
line = ai.info_line, line = ai.info_line,
@@ -254,14 +225,15 @@ end
--- Check: atom_view(Binds_X) entries reference a Binds_* struct with at least one field. --- Check: atom_view(Binds_X) entries reference a Binds_* struct with at least one field.
--- @param _src SourceFile --- @param _src SourceFile
--- @param pipe_ctx PipeCtx --- @param pipe_ctx PassScratch
--- @param findings Findings --- @param findings Findings
--- @return nil
local function check_atom_view_layout(_src, pipe_ctx, findings) local function check_atom_view_layout(_src, pipe_ctx, findings)
for atom_name, view in pairs(pipe_ctx.atom_views or {}) do for atom_name, view in pairs(pipe_ctx.atom_views or {}) do ---@type string, AtomViewEntry
if not view.binds_name then if not view.binds_name then
-- The atom had atom_reg_types but no atom_view; no layout check needed. -- The atom had atom_reg_types but no atom_view; no layout check needed.
else else
local bs = pipe_ctx.binds_index[view.binds_name] local bs = pipe_ctx.binds_index[view.binds_name] ---@type BindsEntry|nil
if not bs then if not bs then
findings.errors[#findings.errors + 1] = { findings.errors[#findings.errors + 1] = {
line = view.info_line, line = view.info_line,
@@ -283,15 +255,16 @@ end
--- Check: Binds_* structs require unique field names because atom_view uses those names for typed-field lookup in gdb. --- Check: Binds_* structs require unique field names because atom_view uses those names for typed-field lookup in gdb.
--- @param _src SourceFile --- @param _src SourceFile
--- @param pipe_ctx PipeCtx --- @param pipe_ctx PassScratch
--- @param findings Findings --- @param findings Findings
--- @return nil
local function check_binds_no_duplicate_fields(_src, pipe_ctx, findings) local function check_binds_no_duplicate_fields(_src, pipe_ctx, findings)
for _, bs in ipairs(pipe_ctx.binds_list or {}) do for _, bs in ipairs(pipe_ctx.binds_list or {}) do ---@type integer, BindsEntry
local seen = {} local seen = {} ---@type table<string, integer> -- bag: field name -> occurrence count
for _, f in ipairs(bs.fields or {}) do for _, f in ipairs(bs.fields or {}) do ---@type integer, TypeField
seen[f.name] = (seen[f.name] or 0) + 1 seen[f.name] = (seen[f.name] or 0) + 1
end end
for name, count in pairs(seen) do for name, count in pairs(seen) do ---@type string, integer
if count > 1 then if count > 1 then
findings.errors[#findings.errors + 1] = { findings.errors[#findings.errors + 1] = {
line = bs.line, line = bs.line,
@@ -315,11 +288,12 @@ end
--- 6. unsupported target_kind -> marker precedes an unrelated declaration --- 6. unsupported target_kind -> marker precedes an unrelated declaration
--- Valid markers stamp `debug_skip` on whole-atom, bare-component, and proc-component declaration records in scan_source.lua. --- Valid markers stamp `debug_skip` on whole-atom, bare-component, and proc-component declaration records in scan_source.lua.
--- @param marker DebugSkipMarker --- @param marker DebugSkipMarker
--- @param _pipe_ctx PipeCtx -- Unused; kept for consistency with per_annot // TODO(Ed): Remove? --- @param _pipe_ctx PassScratch -- Unused; kept for consistency with per_annot
--- @param findings Findings --- @param findings Findings
--- @return nil
local function check_skip_marker(marker, _pipe_ctx, findings) local function check_skip_marker(marker, _pipe_ctx, findings)
local kind = marker.marker_kind local kind = marker.marker_kind ---@type string
local line = marker.marker_line local line = marker.marker_line ---@type integer
-- Left `scan.debug_skip_markers` with production records for `atom_dbg_skip` only; other identifiers take the walker's unrelated branch. -- Left `scan.debug_skip_markers` with production records for `atom_dbg_skip` only; other identifiers take the walker's unrelated branch.
if marker.has_parens then if marker.has_parens then
@@ -372,15 +346,16 @@ end
--- Warn when a source references an unregistered alias. --- Warn when a source references an unregistered alias.
--- When a source uses an unregistered R_X, this check emits one pass-level info entry for that source and directs C-ABI register names to explicit alias registration. --- When a source uses an unregistered R_X, this check emits one pass-level info entry for that source and directs C-ABI register names to explicit alias registration.
--- @param _src SourceFile --- @param _src SourceFile
--- @param pipe_ctx PipeCtx --- @param pipe_ctx PassScratch
--- @param findings Findings --- @param findings Findings
--- @return nil
local function check_wave_context_migration(_src, pipe_ctx, findings) local function check_wave_context_migration(_src, pipe_ctx, findings)
if not (pipe_ctx.types and next(pipe_ctx.types)) then return end if not (pipe_ctx.types and next(pipe_ctx.types)) then return end
if not (pipe_ctx.atom_infos_list) then return end if not (pipe_ctx.atom_infos_list) then return end
local reg_registry = pipe_ctx.register_alias_registry or {} local reg_registry = pipe_ctx.register_alias_registry or {} ---@type table<string, AliasEntry>
for _, ai in ipairs(pipe_ctx.atom_infos_list) do for _, ai in ipairs(pipe_ctx.atom_infos_list) do ---@type integer, AtomInfoEntry
if ai.reg_type_overrides then if ai.reg_type_overrides then
for reg, _ in pairs(ai.reg_type_overrides) do for reg, _ in pairs(ai.reg_type_overrides) do ---@type string, RegTypeOverride
if not reg_registry[reg] then if not reg_registry[reg] then
findings.warnings[#findings.warnings + 1] = { findings.warnings[#findings.warnings + 1] = {
line = 0, line = 0,
@@ -400,14 +375,14 @@ end
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
-- --
-- Each rule entry picks one of four "shapes" of dispatch: -- Each rule entry picks one of four "shapes" of dispatch:
-- per_annot(annot, pipe_ctx, findings) -- runs once per AtomAnnotation -- per_annot(info, pipe_ctx, findings) -- runs once per scan.atom_infos row
-- post(pipe_ctx, findings) -- runs once after all per_annot calls complete (full-corpus aggregation) -- post(pipe_ctx, findings) -- runs once after all per_annot calls complete (full-corpus aggregation)
-- per_macro(macro, wc, findings) -- runs once per TAPE_WORDS / _Pragma macro declaration -- per_macro(macro, wc, findings) -- runs once per TAPE_WORDS / _Pragma macro declaration
-- per_skip_marker(marker, pipe_ctx, findings) -- runs once per src.scan.debug_skip_markers entry -- per_skip_marker(marker, pipe_ctx, findings) -- runs once per src.scan.debug_skip_markers entry
-- --
-- Adding a new check = 1 row here + 1 function above. The `validate()` dispatch loop never needs editing. -- Adding a new check = 1 row here + 1 function above. The `validate()` dispatch loop never needs editing.
local CHECK_RULES = { local CHECK_RULES = { ---@type CheckRule[]
{ name = "atom_decl_exists", per_annot = check_atom_decl_exists }, { name = "atom_decl_exists", per_annot = check_atom_decl_exists },
{ name = "binds_struct_exists", per_annot = check_binds_struct_exists }, { name = "binds_struct_exists", per_annot = check_binds_struct_exists },
{ name = "unique_annotation", post = check_unique_annotation }, { name = "unique_annotation", post = check_unique_annotation },
@@ -428,11 +403,11 @@ local CHECK_RULES = {
--- Builds one pass-wide pipe_ctx from the merged `corpus.*` registries and source-ordered `corpus.atom_infos`; per-source declarations and bodies remain in `src.scan`. --- Builds one pass-wide pipe_ctx from the merged `corpus.*` registries and source-ordered `corpus.atom_infos`; per-source declarations and bodies remain in `src.scan`.
--- The module ownership contract above requires callers to construct `ctx.shared.corpus` through `build_ctx`; the error message below enforces that gate. --- The module ownership contract above requires callers to construct `ctx.shared.corpus` through `build_ctx`; the error message below enforces that gate.
--- @param ctx PassCtx --- @param ctx PassCtx
--- @return PipeCtx --- @return PassScratch
local function build_corpus_pipe_ctx(ctx) local function build_corpus_pipe_ctx(ctx)
local view = duffle.corpus_view(ctx) local view = duffle.corpus_view(ctx) ---@type PassScratch
local annot_counts = {} local annot_counts = {} ---@type table<string, integer> -- bag: atom name -> annotation count
for _, info in ipairs(view.atom_infos) do for _, info in ipairs(view.atom_infos) do ---@type integer, AtomInfoEntry
if info and info.atom_name then if info and info.atom_name then
annot_counts[info.atom_name] = (annot_counts[info.atom_name] or 0) + 1 annot_counts[info.atom_name] = (annot_counts[info.atom_name] or 0) + 1
end end
@@ -446,40 +421,17 @@ end
--- Validate one source against its pre-scanned SourceScan payload + the corpus-wide pipe_ctx. --- Validate one source against its pre-scanned SourceScan payload + the corpus-wide pipe_ctx.
--- @param ctx PassCtx --- @param ctx PassCtx
--- @param src SourceFile --- @param src SourceFile
--- @param corpus_pipe_ctx PipeCtx|nil -- Built once per pass from corpus registries; nil builds the same projection here. --- @param corpus_pipe_ctx PassScratch|nil -- Built once per pass from corpus registries; nil builds the same projection here.
--- @return AnnotatedResult --- @return AnnotatedResult
local function validate(ctx, src, corpus_pipe_ctx) local function validate(ctx, src, corpus_pipe_ctx)
corpus_pipe_ctx = corpus_pipe_ctx or build_corpus_pipe_ctx(ctx) corpus_pipe_ctx = corpus_pipe_ctx or build_corpus_pipe_ctx(ctx)
local scan = src.scan local scan = src.scan ---@type SourceScan
-- Project the pre-scanned atoms to the AtomEntry shape this pass needs.
local atoms = {}
for _, a in ipairs(scan.atoms) do
if a.kind == "atom" or a.kind == "atom_proc" then
atoms[#atoms + 1] = { line = a.line, name = a.raw_name or a.name }
end
end
-- Project the pre-scanned atom_infos to AtomAnnotation shape.
local annots = {}
for _, info in ipairs(scan.atom_infos) do
annots[#annots + 1] = {
line = info.info_line,
macro = "atom_info",
name = info.atom_name,
kind = "info",
binds = info.binds,
reads = info.reads or {},
writes = info.writes or {},
errors = info.errors,
}
end
-- Build a per-source pipe_ctx: shared lookups come from `corpus_pipe_ctx`, while declarations, bodies, types, views, defaults, and occurrences come from `src.scan`. -- Build a per-source pipe_ctx: shared lookups come from `corpus_pipe_ctx`, while declarations, bodies, types, views, defaults, and occurrences come from `src.scan`.
local seen_defaults = {}; for reg, _ in pairs (scan.types or {}) do seen_defaults[reg] = (seen_defaults[reg] or 0) + 1 end local seen_defaults = {}; for reg, _ in pairs (scan.types or {}) do seen_defaults[reg] = (seen_defaults[reg] or 0) + 1 end ---@type table<string, integer> -- bag: register ident -> occurrence count
local atom_infos_list = {}; for _, ai in ipairs(scan.atom_infos or {}) do atom_infos_list[#atom_infos_list + 1] = ai end local atom_infos_list = {}; for _, ai in ipairs(scan.atom_infos or {}) do atom_infos_list[#atom_infos_list + 1] = ai end ---@type AtomInfoEntry[]
local pipe_ctx = { local pipe_ctx = { ---@type PassScratch
atom_index = {}, atom_index = {},
binds_index = {}, binds_index = {},
annot_counts = corpus_pipe_ctx.annot_counts, annot_counts = corpus_pipe_ctx.annot_counts,
@@ -493,28 +445,34 @@ local function validate(ctx, src, corpus_pipe_ctx)
register_alias_registry = corpus_pipe_ctx.register_alias_registry, register_alias_registry = corpus_pipe_ctx.register_alias_registry,
type_name_registry = corpus_pipe_ctx.type_name_registry, type_name_registry = corpus_pipe_ctx.type_name_registry,
} }
for _, a in ipairs(atoms) do pipe_ctx.atom_index [a.name] = a end local atoms = {} ---@type AtomEntry[]
for _, b in ipairs(scan.binds) do pipe_ctx.binds_index[b.name] = b end for _, a in ipairs(scan.atoms) do ---@type integer, AtomEntry
if a.kind == "atom" or a.kind == "atom_proc" then
atoms[#atoms + 1] = a
pipe_ctx.atom_index[a.raw_name or a.name] = a
end
end
for _, b in ipairs(scan.binds) do pipe_ctx.binds_index[b.name] = b end ---@type integer, BindsEntry
-- Findings live in a single struct with three lists (errors / warnings / info). -- Findings live in a single struct with three lists (errors / warnings / info).
-- Each check writes to the list appropriate for its severity. -- Each check writes to the list appropriate for its severity.
local findings = { errors = {}, warnings = {}, info = {} } local findings = { errors = {}, warnings = {}, info = {} } ---@type Findings
-- Lift parse-time errors already recorded in scan_source's atom_info payload into this pass's findings list. -- Lift parse-time errors already recorded in scan_source's atom_info payload into this pass's findings list.
for _, a in ipairs(annots) do for _, info in ipairs(scan.atom_infos) do ---@type integer, AtomInfoEntry
if a.errors then if info.errors then
for _, msg in ipairs(a.errors) do for _, msg in ipairs(info.errors) do ---@type integer, string
findings.errors[#findings.errors + 1] = { findings.errors[#findings.errors + 1] = {
line = a.line, line = info.info_line,
msg = string.format("'%s': %s", a.name, msg), msg = string.format("'%s': %s", info.atom_name, msg),
} }
end end
end end
end end
-- THE per-annotation pipeline. ONE loop. CHECK_RULES dispatches per_annot rules. -- THE per-annotation pipeline. ONE loop. CHECK_RULES dispatches per_annot rules.
for _, a in ipairs(annots) do for _, info in ipairs(scan.atom_infos) do ---@type integer, AtomInfoEntry
duffle.run_check_rules(CHECK_RULES, "per_annot", a, pipe_ctx, findings) duffle.run_check_rules(CHECK_RULES, "per_annot", info, pipe_ctx, findings)
end end
-- Post-loop rules (one-shot checks that need full-corpus aggregation in pipe_ctx). -- Post-loop rules (one-shot checks that need full-corpus aggregation in pipe_ctx).
@@ -522,14 +480,14 @@ local function validate(ctx, src, corpus_pipe_ctx)
-- scan_source records each marker in scan.debug_skip_markers; this loop validates each record independently and emits at most one error per marker. -- scan_source records each marker in scan.debug_skip_markers; this loop validates each record independently and emits at most one error per marker.
-- Valid markers stamp `debug_skip = true` on the following atom or component declaration, which downstream consumers read directly. -- Valid markers stamp `debug_skip = true` on the following atom or component declaration, which downstream consumers read directly.
local skip_markers = scan.debug_skip_markers or {} local skip_markers = scan.debug_skip_markers or {} ---@type DebugSkipMarker[]
for _, marker in ipairs(skip_markers) do for _, marker in ipairs(skip_markers) do ---@type integer, DebugSkipMarker
duffle.run_check_rules(CHECK_RULES, "per_skip_marker", marker, pipe_ctx, findings) duffle.run_check_rules(CHECK_RULES, "per_skip_marker", marker, pipe_ctx, findings)
end end
-- Per-macro rules (TAPE_WORDS vs WORD_COUNT drift). -- Per-macro rules (TAPE_WORDS vs WORD_COUNT drift).
pipe_ctx.word_counts = corpus_pipe_ctx.word_counts pipe_ctx.word_counts = corpus_pipe_ctx.word_counts
for _, m in ipairs(scan.macros) do for _, m in ipairs(scan.macros) do ---@type integer, MacroEntry
duffle.run_check_rules(CHECK_RULES, "per_macro", m, pipe_ctx, findings) duffle.run_check_rules(CHECK_RULES, "per_macro", m, pipe_ctx, findings)
end end
@@ -541,12 +499,12 @@ local function validate(ctx, src, corpus_pipe_ctx)
findings.info[#findings.info + 1] = { findings.info[#findings.info + 1] = {
line = 0, line = 0,
msg = string.format("scanned: %d atom(s), %d annotation(s), %d macro-word-decl(s), %d binds struct(s)" msg = string.format("scanned: %d atom(s), %d annotation(s), %d macro-word-decl(s), %d binds struct(s)"
, #atoms, #annots, #scan.macros, #scan.binds), , #atoms, #scan.atom_infos, #scan.macros, #scan.binds),
} }
return { return {
atoms = atoms, atoms = atoms,
annots = annots, annots = scan.atom_infos,
macros = scan.macros, macros = scan.macros,
binds = scan.binds, binds = scan.binds,
errors = findings.errors, errors = findings.errors,
@@ -559,9 +517,7 @@ end
-- M.run — orchestrator entry -- M.run — orchestrator entry
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
--- @class M local M = {} ---@type AnnotationPass
local M = {}
-- Expose `validate` for downstream passes (e.g. report.lua) that need to re-render the per-source results into a per-MODULE report. -- Expose `validate` for downstream passes (e.g. report.lua) that need to re-render the per-source results into a per-MODULE report.
M.validate = validate M.validate = validate
@@ -569,32 +525,32 @@ M.validate = validate
--- @param ctx PassCtx --- @param ctx PassCtx
--- @return PassResult --- @return PassResult
function M.run(ctx) function M.run(ctx)
local outputs = {} local outputs = {} ---@type PassOutputEntry[]
local errors = {} local errors = {} ---@type Finding[]
local warnings = {} local warnings = {} ---@type Finding[]
-- Build the shared pipe_ctx once for this run; every validate() call sees the same cross-source registries. -- Build the shared pipe_ctx once for this run; every validate() call sees the same cross-source registries.
-- The corpus owns the canonical cross-source registries; per-source scans retain body / declaration ownership. -- The corpus owns the canonical cross-source registries; per-source scans retain body / declaration ownership.
local corpus_pipe_ctx = build_corpus_pipe_ctx(ctx) local corpus_pipe_ctx = build_corpus_pipe_ctx(ctx) ---@type PassScratch
local corpus = ctx.shared.corpus local corpus = ctx.shared.corpus ---@type Corpus
-- Group `corpus.sources_by_dir` by module, validate every source in each bucket, and emit one errors.h per directory. -- Group `corpus.sources_by_dir` by module, validate every source in each bucket, and emit one errors.h per directory.
local by_dir = (corpus and corpus.sources_by_dir) or {} local by_dir = (corpus and corpus.sources_by_dir) or {} ---@type table<string, SourceFile[]>
for dir, dir_sources in pairs(by_dir) do for dir, dir_sources in pairs(by_dir) do ---@type string, SourceFile[]
local dir_basename = dir:match("([^/\\]+)$") or dir local dir_basename = dir:match("([^/\\]+)$") or dir ---@type string
local dir_atoms = 0 local dir_atoms = 0 ---@type integer
local dir_errors = {} local dir_errors = {} ---@type Finding[]
local dir_warnings = {} local dir_warnings = {} ---@type Finding[]
for _, src in ipairs(dir_sources) do for _, src in ipairs(dir_sources) do ---@type integer, SourceFile
local result = validate(ctx, src, corpus_pipe_ctx) local result = validate(ctx, src, corpus_pipe_ctx) ---@type AnnotatedResult
result.source = src.path -- tag for downstream rendering result.source = src.path -- tag for downstream rendering
dir_atoms = dir_atoms + #result.atoms dir_atoms = dir_atoms + #result.atoms
for _, e in ipairs(result.errors) do for _, e in ipairs(result.errors) do ---@type integer, Finding
dir_errors[#dir_errors + 1] = { line = e.line, msg = e.msg, source = src.path } dir_errors[#dir_errors + 1] = { line = e.line, msg = e.msg, source = src.path }
errors [#errors + 1] = { line = e.line, msg = e.msg } errors [#errors + 1] = { line = e.line, msg = e.msg }
end end
for _, w in ipairs(result.warnings) do for _, w in ipairs(result.warnings) do ---@type integer, Finding
dir_warnings[#dir_warnings + 1] = { line = w.line, msg = w.msg } dir_warnings[#dir_warnings + 1] = { line = w.line, msg = w.msg }
warnings [#warnings + 1] = { line = w.line, msg = w.msg } warnings [#warnings + 1] = { line = w.line, msg = w.msg }
end end
+151 -105
View File
@@ -37,9 +37,9 @@
-- Bootstrap: load `duffle_paths.lua` via `debug.getinfo(1, "S").source` -- Bootstrap: load `duffle_paths.lua` via `debug.getinfo(1, "S").source`
-- (works both standalone + when require'd). `duffle_paths.lua` sets package.path then returns `require("duffle")` -- (works both standalone + when require'd). `duffle_paths.lua` sets package.path then returns `require("duffle")`
-- at the bottom, so the dofile value IS the duffle module. -- at the bottom, so the dofile value IS the duffle module.
local _bootstrap_dir = debug.getinfo(1, "S").source:match("^@?(.*[/\\])") or "./" local _bootstrap_dir = debug.getinfo(1, "S").source:match("^@?(.*[/\\])") or "./" ---@type string
local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua") local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua") ---@type DuffleExport
local elf_dwarf = require("elf_dwarf") local elf_dwarf = require("elf_dwarf") ---@type ElfDwarfMod
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
-- Constants -- Constants
@@ -47,37 +47,72 @@ local elf_dwarf = require("elf_dwarf")
-- Format version emitted as the first line. Bump + add a migration test if the format changes; -- Format version emitted as the first line. Bump + add a migration test if the format changes;
-- the gdb runtime loader rejects mismatches (E2). -- the gdb runtime loader rejects mismatches (E2).
local FORMAT_VERSION = 1 local FORMAT_VERSION = 1 ---@type integer
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
-- Type declarations -- Type declarations
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
--- @class AtomSourceMapCtx --- @class AtomSourceMapCtx
--- @field shared table -- `ctx.shared` --- @field shared PassShared
--- @field shared.corpus table -- source-order registry; single writer is build_ctx --- @field out_root string
--- @field shared.word_counts table --- @field flags PassFlags
--- @field out_root string -- output root (e.g. "build/gen") --- @field project_root string|nil
--- @field flags table -- `ctx.flags`; reads `flags.gdb_runtime` + `flags.elf_path`
--- @class WordMapEntry
--- @field pos integer
--- @field line integer
--- @field text string
--- @field body_line integer
--- @field gpr_keys string[]|nil
--- @field invocation InvocationRecord|nil
--- @class NmAddr
--- @field [1] integer -- st_value
--- @field [2] integer -- st_size
--- @class GdbAtomRecord
--- @field idx integer|nil
--- @field name string
--- @field src_path string
--- @field file_base string
--- @field addr integer
--- @field size_bytes integer
--- @field words integer
--- @field entries WordMapEntry[]
--- @class ElfDwarfMod
--- @field read_nm fun(elf_path: Path): table<string, NmAddr>
--- @class AtomSourceMapPass
--- @field render_source_map fun(src: SourceFile): string
--- @field render_provenance fun(src: SourceFile, wc: WordCounts): string
--- @field render_atom_source_map fun(atom: AtomEntry): string
--- @field render_atom_provenance fun(atom: AtomEntry, wc: WordCounts, rel_path: string): string
--- @field run fun(ctx: PassCtx): PassResult
--- @class AtomEntry
--- @field paths AtomPaths|nil
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
-- Atom-path renderers -- Atom-path renderers
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
--- Join word boundaries (from `items`) to per-word call text + source lines (from `word_events`). --- Join word boundaries (from `items`) to per-word call text + source lines (from `word_events`).
--- @param atom table --- @param atom AtomEntry
--- @return table[], integer --- @return WordMapEntry[]
--- @return integer
local function canonical_word_entries(atom) local function canonical_word_entries(atom)
local paths = atom.paths or {} local paths = atom.paths or {} ---@type AtomPaths
local events = paths.word_events or {} local events = paths.word_events or {} ---@type WordEvent[]
local word_items = {} local word_items = {} ---@type EmissionItem[]
for _, item in ipairs(paths.items or {}) do for _, item in ipairs(paths.items or {}) do ---@type integer, EmissionItem
if item.kind == "word" then word_items[#word_items + 1] = item end if item.kind == "word" then word_items[#word_items + 1] = item end
end end
local entries = {} local entries = {} ---@type WordMapEntry[]
for index, event in ipairs(events) do for index, event in ipairs(events) do ---@type integer, WordEvent
local item = word_items[index] or {} local item = word_items[index] or {} ---@type EmissionItem
entries[#entries + 1] = { entries[#entries + 1] = {
pos = event.i or (index - 1), pos = event.i or (index - 1),
line = event.call_line or item.line or 0, line = event.call_line or item.line or 0,
@@ -97,19 +132,20 @@ end
--- `WORD N CALL <src-path>:<src-line> RAW` (raw `.word` outside any mac_* component) --- `WORD N CALL <src-path>:<src-line> RAW` (raw `.word` outside any mac_* component)
--- Component identity comes from the outermost invocation record; the count-table lookup confirms the component was declared in `corpus.word_counts` --- Component identity comes from the outermost invocation record; the count-table lookup confirms the component was declared in `corpus.word_counts`
--- (populated by word_count_eval + components passes). --- (populated by word_count_eval + components passes).
--- @param src table --- @param src SourceFile
--- @param atom table --- @param atom AtomEntry
--- @param wc table -- identity alias of corpus.word_counts --- @param wc WordCounts
--- @return string[], integer --- @return string[]
--- @return integer
local function emit_provenance_stanza(src, atom, wc) local function emit_provenance_stanza(src, atom, wc)
local lines = {} local lines = {} ---@type string[]
local rel_path = src.path:gsub("\\\\", "/") local rel_path = src.path:gsub("\\\\", "/") ---@type string
local entries, total = canonical_word_entries(atom) local entries, total = canonical_word_entries(atom) ---@type WordMapEntry[], integer
lines[#lines + 1] = string.format('ATOM %s "%s" 0', atom.raw_name or atom.name, rel_path) lines[#lines + 1] = string.format('ATOM %s "%s" 0', atom.raw_name or atom.name, rel_path)
for _, entry in ipairs(entries) do for _, entry in ipairs(entries) do ---@type integer, WordMapEntry
local inv = entry.invocation local inv = entry.invocation ---@type InvocationRecord|nil
local macro_count = inv and wc["mac_" .. inv.component_name] local macro_count = inv and wc["mac_" .. inv.component_name] ---@type integer|nil
if inv and macro_count ~= nil then if inv and macro_count ~= nil then
lines[#lines + 1] = string.format('WORD %d CALL %s:%d MACRO %s "%s:%d" BODY %d' lines[#lines + 1] = string.format('WORD %d CALL %s:%d MACRO %s "%s:%d" BODY %d'
, entry.pos, rel_path, entry.line, inv.component_name , entry.pos, rel_path, entry.line, inv.component_name
@@ -125,11 +161,11 @@ local function emit_provenance_stanza(src, atom, wc)
end end
--- Render the full provenance file content for one source. --- Render the full provenance file content for one source.
--- @param src table --- @param src SourceFile
--- @param wc table --- @param wc WordCounts
--- @return string --- @return string
local function render_provenance(src, wc) local function render_provenance(src, wc)
local lines = {} local lines = {} ---@type string[]
lines[#lines + 1] = "# FORMAT_VERSION 1" lines[#lines + 1] = "# FORMAT_VERSION 1"
lines[#lines + 1] = "# auto-generated by ps1_meta.lua (passes/atoms_source_map.lua) — DO NOT EDIT" lines[#lines + 1] = "# auto-generated by ps1_meta.lua (passes/atoms_source_map.lua) — DO NOT EDIT"
lines[#lines + 1] = "# Per-.word provenance: maps each emitted .word to its call site (atom body" lines[#lines + 1] = "# Per-.word provenance: maps each emitted .word to its call site (atom body"
@@ -138,14 +174,16 @@ local function render_provenance(src, wc)
lines[#lines + 1] = "# dwarf_injection to synthesize DW_TAG_inlined_subroutine instances + per-word" lines[#lines + 1] = "# dwarf_injection to synthesize DW_TAG_inlined_subroutine instances + per-word"
lines[#lines + 1] = "# line program rows for native source-level step into component bodies." lines[#lines + 1] = "# line program rows for native source-level step into component bodies."
--- @param atom AtomEntry
--- @return nil
local function append(atom) local function append(atom)
local stanza = emit_provenance_stanza(src, atom, wc) local stanza = emit_provenance_stanza(src, atom, wc) ---@type string[]
for _, line in ipairs(stanza) do lines[#lines + 1] = line end for _, line in ipairs(stanza) do lines[#lines + 1] = line end ---@type integer, string
end end
for _, atom in ipairs(src.scan.atoms or {}) do for _, atom in ipairs(src.scan.atoms or {}) do ---@type integer, AtomEntry
if atom.paths then append(atom) end if atom.paths then append(atom) end
end end
for _, atom in ipairs(src.scan.raw_atoms or {}) do for _, atom in ipairs(src.scan.raw_atoms or {}) do ---@type integer, AtomEntry
if atom.paths then append(atom) end if atom.paths then append(atom) end
end end
@@ -154,17 +192,17 @@ end
--- Render one atom's stanza for the sourcemap.txt form (ATOM header line, N WORD lines, ENDATOM marker). --- Render one atom's stanza for the sourcemap.txt form (ATOM header line, N WORD lines, ENDATOM marker).
--- Returns (lines, total_words). --- Returns (lines, total_words).
--- @param src table --- @param src SourceFile
--- @param atom table --- @param atom AtomEntry
--- @param wc table --- @return string[]
--- @return string[], integer --- @return integer
local function emit_atom_stanza(src, atom) local function emit_atom_stanza(src, atom)
local lines = {} local lines = {} ---@type string[]
local rel_path = src.path:gsub("\\\\", "/") local rel_path = src.path:gsub("\\\\", "/") ---@type string
local entries, total = canonical_word_entries(atom) local entries, total = canonical_word_entries(atom) ---@type WordMapEntry[], integer
lines[#lines + 1] = string.format('ATOM %s "%s" 0', atom.raw_name or atom.name, rel_path) lines[#lines + 1] = string.format('ATOM %s "%s" 0', atom.raw_name or atom.name, rel_path)
for _, entry in ipairs(entries) do for _, entry in ipairs(entries) do ---@type integer, WordMapEntry
lines[#lines + 1] = string.format("WORD %d LINE %d TEXT %s", lines[#lines + 1] = string.format("WORD %d LINE %d TEXT %s",
entry.pos, entry.line, entry.text) entry.pos, entry.line, entry.text)
end end
@@ -175,22 +213,23 @@ end
--- Render the full source map file content for one source (one .atoms.sourcemap.txt per source). Mirrors offsets.lua's --- Render the full source map file content for one source (one .atoms.sourcemap.txt per source). Mirrors offsets.lua's
--- `project_atoms` shape: scan.atoms + scan.raw_atoms, no kind filter. --- `project_atoms` shape: scan.atoms + scan.raw_atoms, no kind filter.
--- @param src table --- @param src SourceFile
--- @param wc table
--- @return string --- @return string
local function render_source_map(src) local function render_source_map(src)
local lines = {} local lines = {} ---@type string[]
lines[#lines + 1] = "# FORMAT_VERSION " .. FORMAT_VERSION lines[#lines + 1] = "# FORMAT_VERSION " .. FORMAT_VERSION
lines[#lines + 1] = "# auto-generated by ps1_meta.lua (passes/atoms_source_map.lua) — DO NOT EDIT" lines[#lines + 1] = "# auto-generated by ps1_meta.lua (passes/atoms_source_map.lua) — DO NOT EDIT"
--- @param atom AtomEntry
--- @return nil
local function append(atom) local function append(atom)
local stanza = emit_atom_stanza(src, atom) local stanza = emit_atom_stanza(src, atom) ---@type string[]
for _, line in ipairs(stanza) do lines[#lines + 1] = line end for _, line in ipairs(stanza) do lines[#lines + 1] = line end ---@type integer, string
end end
for _, atom in ipairs(src.scan.atoms or {}) do for _, atom in ipairs(src.scan.atoms or {}) do ---@type integer, AtomEntry
if atom.paths then append(atom) end if atom.paths then append(atom) end
end end
for _, atom in ipairs(src.scan.raw_atoms or {}) do for _, atom in ipairs(src.scan.raw_atoms or {}) do ---@type integer, AtomEntry
if atom.paths then append(atom) end if atom.paths then append(atom) end
end end
@@ -211,20 +250,22 @@ end
--- Build the list of atoms with addresses + word entries. Shared helper for the gdb-runtime file emission. --- Build the list of atoms with addresses + word entries. Shared helper for the gdb-runtime file emission.
--- @param ctx PassCtx --- @param ctx PassCtx
--- @return table[] -- list of {idx, name, src_path, file_base, addr, size_bytes, words, entries} --- @return GdbAtomRecord[]
local function build_atom_table(ctx) local function build_atom_table(ctx)
local addrs = elf_dwarf.read_nm(ctx.flags.elf_path) local addrs = elf_dwarf.read_nm(ctx.flags.elf_path) ---@type table<string, NmAddr>
local corpus = ctx.shared and ctx.shared.corpus local corpus = ctx.shared and ctx.shared.corpus ---@type Corpus|nil
local matched = {} local matched = {} ---@type GdbAtomRecord[]
for _, src in ipairs(corpus.source_order or {}) do for _, src in ipairs(corpus.source_order or {}) do ---@type integer, SourceFile
local file_base = src.path:match("([^/\\\\]+)$") or src.path local file_base = src.path:match("([^/\\\\]+)$") or src.path ---@type string
--- @param atom AtomEntry
--- @return nil
local function append(atom) local function append(atom)
if not atom.paths then return end if not atom.paths then return end
local name = atom.raw_name or atom.name local name = atom.raw_name or atom.name ---@type string
local info = addrs[name] local info = addrs[name] ---@type NmAddr|nil
if not info then return end if not info then return end
local entries, total = canonical_word_entries(atom) local entries, total = canonical_word_entries(atom) ---@type WordMapEntry[], integer
matched[#matched + 1] = { matched[#matched + 1] = {
name = name, name = name,
src_path = src.path, src_path = src.path,
@@ -235,13 +276,16 @@ local function build_atom_table(ctx)
entries = entries, entries = entries,
} }
end end
for _, atom in ipairs((src.scan or {}).atoms or {}) do append(atom) end for _, atom in ipairs((src.scan or {}).atoms or {}) do append(atom) end ---@type integer, AtomEntry
for _, atom in ipairs((src.scan or {}).raw_atoms or {}) do append(atom) end for _, atom in ipairs((src.scan or {}).raw_atoms or {}) do append(atom) end ---@type integer, AtomEntry
end end
-- Deterministic order: sort by address (matches `nm` output ordering). -- Deterministic order: sort by address (matches `nm` output ordering).
--- @param a GdbAtomRecord
--- @param b GdbAtomRecord
--- @return boolean
table.sort(matched, function(a, b) return a.addr < b.addr end) table.sort(matched, function(a, b) return a.addr < b.addr end)
for i, a in ipairs(matched) do a.idx = i - 1 end for i, a in ipairs(matched) do a.idx = i - 1 end ---@type integer, GdbAtomRecord
return matched return matched
end end
@@ -252,13 +296,14 @@ end
--- ---
--- Why hardcoded per-atom: gdb's `$` substitution doesn't concat inside var names — `$__atom_name_$__i` in a `while` --- Why hardcoded per-atom: gdb's `$` substitution doesn't concat inside var names — `$__atom_name_$__i` in a `while`
--- loop resolves to one literal identifier, not `name_i`. Compile-time emission is the only path. --- loop resolves to one literal identifier, not `name_i`. Compile-time emission is the only path.
--- @param lines table -- output line buffer (mutated in place) --- @param lines string[]
--- @param matched table -- list of atom records from `build_atom_table` --- @param matched GdbAtomRecord[]
--- @return nil
local function append_gdb_commands(lines, matched) local function append_gdb_commands(lines, matched)
-- ── tape_atoms ── -- ── tape_atoms ──
-- Hardcoded one printf per atom. No loop. -- Hardcoded one printf per atom. No loop.
lines[#lines + 1] = "define tape_atoms" lines[#lines + 1] = "define tape_atoms"
for _, a in ipairs(matched) do for _, a in ipairs(matched) do ---@type integer, GdbAtomRecord
-- gdb 12.1 quirk: literals in printf args require an attached target. -- gdb 12.1 quirk: literals in printf args require an attached target.
-- Use the per-atom convenience vars set above as printf args. -- Use the per-atom convenience vars set above as printf args.
lines[#lines + 1] = string.format(' printf " %%-32s @ 0x%%08x %%4d words\\n", $__atom_name_%d, $__atom_addr_%d, $__atom_words_%d', lines[#lines + 1] = string.format(' printf " %%-32s @ 0x%%08x %%4d words\\n", $__atom_name_%d, $__atom_addr_%d, $__atom_words_%d',
@@ -273,7 +318,7 @@ local function append_gdb_commands(lines, matched)
-- ── break_atom (generic) + per-atom break_atom_X ── -- ── break_atom (generic) + per-atom break_atom_X ──
lines[#lines + 1] = "define break_atom" lines[#lines + 1] = "define break_atom"
lines[#lines + 1] = ' echo "Usage: break_atom_<exact_name> (pick from the list below)"' lines[#lines + 1] = ' echo "Usage: break_atom_<exact_name> (pick from the list below)"'
for _, a in ipairs(matched) do for _, a in ipairs(matched) do ---@type integer, GdbAtomRecord
lines[#lines + 1] = string.format(' printf " break_atom_%%-32s\\n", $__atom_name_%d', a.idx) lines[#lines + 1] = string.format(' printf " break_atom_%%-32s\\n", $__atom_name_%d', a.idx)
end end
lines[#lines + 1] = "end" lines[#lines + 1] = "end"
@@ -282,7 +327,7 @@ local function append_gdb_commands(lines, matched)
lines[#lines + 1] = "end" lines[#lines + 1] = "end"
lines[#lines + 1] = "" lines[#lines + 1] = ""
for _, a in ipairs(matched) do for _, a in ipairs(matched) do ---@type integer, GdbAtomRecord
lines[#lines + 1] = string.format("define break_atom_%s", a.name) lines[#lines + 1] = string.format("define break_atom_%s", a.name)
lines[#lines + 1] = string.format(" break *$__atom_addr_%d", a.idx) lines[#lines + 1] = string.format(" break *$__atom_addr_%d", a.idx)
lines[#lines + 1] = string.format(' printf " Breakpoint set at %s (0x%%08x)\\n", $__atom_addr_%d', a.name, a.idx) lines[#lines + 1] = string.format(' printf " Breakpoint set at %s (0x%%08x)\\n", $__atom_addr_%d', a.name, a.idx)
@@ -296,7 +341,7 @@ local function append_gdb_commands(lines, matched)
-- ── step_atom / next_atom ── -- ── step_atom / next_atom ──
-- Hardcoded one tbreak per atom. No loop. -- Hardcoded one tbreak per atom. No loop.
lines[#lines + 1] = "define step_atom" lines[#lines + 1] = "define step_atom"
for _, a in ipairs(matched) do for _, a in ipairs(matched) do ---@type integer, GdbAtomRecord
lines[#lines + 1] = string.format(" tbreak *$__atom_addr_%d", a.idx) lines[#lines + 1] = string.format(" tbreak *$__atom_addr_%d", a.idx)
end end
lines[#lines + 1] = " continue" lines[#lines + 1] = " continue"
@@ -319,7 +364,7 @@ local function append_gdb_commands(lines, matched)
lines[#lines + 1] = "define where_in_atom" lines[#lines + 1] = "define where_in_atom"
lines[#lines + 1] = " set $__pc = (unsigned int)$pc" lines[#lines + 1] = " set $__pc = (unsigned int)$pc"
lines[#lines + 1] = " set $__matched = 0" lines[#lines + 1] = " set $__matched = 0"
for _, a in ipairs(matched) do for _, a in ipairs(matched) do ---@type integer, GdbAtomRecord
-- Precompute end_addr (gdb 12.1's expression evaluator chokes on `addr + words*4`). -- Precompute end_addr (gdb 12.1's expression evaluator chokes on `addr + words*4`).
lines[#lines + 1] = string.format(" set $__end_%d = $__atom_addr_%d + $__atom_words_%d * 4", a.idx, a.idx, a.idx) lines[#lines + 1] = string.format(" set $__end_%d = $__atom_addr_%d + $__atom_words_%d * 4", a.idx, a.idx, a.idx)
lines[#lines + 1] = string.format(" if $__pc >= $__atom_addr_%d && $__pc < $__end_%d", a.idx, a.idx) lines[#lines + 1] = string.format(" if $__pc >= $__atom_addr_%d && $__pc < $__end_%d", a.idx, a.idx)
@@ -328,15 +373,15 @@ local function append_gdb_commands(lines, matched)
lines[#lines + 1] = string.format(" set $__word = ($__pc - $__atom_addr_%d) / 4", a.idx) lines[#lines + 1] = string.format(" set $__word = ($__pc - $__atom_addr_%d) / 4", a.idx)
lines[#lines + 1] = string.format(' printf "word: %%d/%%d\\n", $__word, $__atom_words_%d', a.idx) lines[#lines + 1] = string.format(' printf "word: %%d/%%d\\n", $__word, $__atom_words_%d', a.idx)
-- One inner-if per WORD entry. Each word's line + text hardcoded. -- One inner-if per WORD entry. Each word's line + text hardcoded.
for _, we in ipairs(a.entries) do for _, we in ipairs(a.entries) do ---@type integer, WordMapEntry
lines[#lines + 1] = string.format(" if $__word == %d", we.pos) lines[#lines + 1] = string.format(" if $__word == %d", we.pos)
-- Escape TEXT for printf format string. -- Escape TEXT for printf format string.
local escaped_text = we.text:gsub("%%", "%%%%"):gsub('"', '\\"') local escaped_text = we.text:gsub("%%", "%%%%"):gsub('"', '\\"') ---@type string
lines[#lines + 1] = string.format(' printf "source: %%s:%%d %%s\\n", $__atom_file_%d, %d, "%s"', a.idx, we.line, escaped_text) lines[#lines + 1] = string.format(' printf "source: %%s:%%d %%s\\n", $__atom_file_%d, %d, "%s"', a.idx, we.line, escaped_text)
lines[#lines + 1] = " end" lines[#lines + 1] = " end"
end end
-- Fallback for words beyond the source map (shouldn't happen if nm matches). -- Fallback for words beyond the source map (shouldn't happen if nm matches).
local max_word = 0 local max_word = 0 ---@type integer
if #a.entries > 0 then max_word = a.entries[#a.entries].pos end if #a.entries > 0 then max_word = a.entries[#a.entries].pos end
lines[#lines + 1] = string.format(' if $__word > %d', max_word) lines[#lines + 1] = string.format(' if $__word > %d', max_word)
lines[#lines + 1] = ' printf "source: (no source-map entry for word %%d; map may be stale)\\n", $__word' lines[#lines + 1] = ' printf "source: (no source-map entry for word %%d; map may be stale)\\n", $__word'
@@ -361,7 +406,7 @@ local function append_gdb_commands(lines, matched)
lines[#lines + 1] = " set $__in_atom = 0" lines[#lines + 1] = " set $__in_atom = 0"
lines[#lines + 1] = " set $__did_step = 0" lines[#lines + 1] = " set $__did_step = 0"
lines[#lines + 1] = " set $__pc = (unsigned int)$pc" lines[#lines + 1] = " set $__pc = (unsigned int)$pc"
for _, a in ipairs(matched) do for _, a in ipairs(matched) do ---@type integer, GdbAtomRecord
-- Precompute end_addr in the convenience var (single expression gdb handles). -- Precompute end_addr in the convenience var (single expression gdb handles).
lines[#lines + 1] = string.format(" set $__end_%d = $__atom_addr_%d + $__atom_words_%d * 4", a.idx, a.idx, a.idx) lines[#lines + 1] = string.format(" set $__end_%d = $__atom_addr_%d + $__atom_words_%d * 4", a.idx, a.idx, a.idx)
lines[#lines + 1] = string.format(" if $__pc >= $__atom_addr_%d && $__pc < $__end_%d", a.idx, a.idx) lines[#lines + 1] = string.format(" if $__pc >= $__atom_addr_%d && $__pc < $__end_%d", a.idx, a.idx)
@@ -395,9 +440,10 @@ end
--- Emit the gdb-runtime file (post-link). Pure gdb scripting — addresses come from `mipsel-none-elf-nm -S`, get embedded --- Emit the gdb-runtime file (post-link). Pure gdb scripting — addresses come from `mipsel-none-elf-nm -S`, get embedded
--- in `<ctx.out_root>/gdb_tape_atoms_runtime.gdb`, and load via `set $var = ...` + `define ... end` blocks at gdb source-time. --- in `<ctx.out_root>/gdb_tape_atoms_runtime.gdb`, and load via `set $var = ...` + `define ... end` blocks at gdb source-time.
--- @param ctx PassCtx --- @param ctx PassCtx
--- @return nil
local function emit_gdb_runtime(ctx) local function emit_gdb_runtime(ctx)
if not (ctx.flags and ctx.flags.gdb_runtime) then return end if not (ctx.flags and ctx.flags.gdb_runtime) then return end
local elf_path = ctx.flags.elf_path local elf_path = ctx.flags.elf_path ---@type string|nil
if not elf_path or elf_path == "" then if not elf_path or elf_path == "" then
io.stderr:write("[atoms_source_map] --gdb-runtime requires --elf <elf>\n") io.stderr:write("[atoms_source_map] --gdb-runtime requires --elf <elf>\n")
return return
@@ -408,13 +454,13 @@ local function emit_gdb_runtime(ctx)
return return
end end
local matched = build_atom_table(ctx) local matched = build_atom_table(ctx) ---@type GdbAtomRecord[]
if #matched == 0 then if #matched == 0 then
io.stderr:write("[atoms_source_map] --gdb-runtime: no atoms matched against nm symbols (stale scan?).\n") io.stderr:write("[atoms_source_map] --gdb-runtime: no atoms matched against nm symbols (stale scan?).\n")
return return
end end
local lines = {} local lines = {} ---@type string[]
lines[#lines + 1] = "# Auto-generated by ps1_meta.lua (passes/atoms_source_map.lua)" lines[#lines + 1] = "# Auto-generated by ps1_meta.lua (passes/atoms_source_map.lua)"
lines[#lines + 1] = "# DO NOT EDIT — re-run ps1_meta.lua --atoms-source-map --gdb-runtime to regenerate" lines[#lines + 1] = "# DO NOT EDIT — re-run ps1_meta.lua --atoms-source-map --gdb-runtime to regenerate"
lines[#lines + 1] = "# Sourced by scripts/gdb/gdb_tape_atoms.gdb (the wrapper)." lines[#lines + 1] = "# Sourced by scripts/gdb/gdb_tape_atoms.gdb (the wrapper)."
@@ -435,7 +481,7 @@ local function emit_gdb_runtime(ctx)
-- Per-atom convenience vars (used as printf args; literals aren't accepted -- Per-atom convenience vars (used as printf args; literals aren't accepted
-- without an attached target on gdb 12.1). -- without an attached target on gdb 12.1).
for _, a in ipairs(matched) do for _, a in ipairs(matched) do ---@type integer, GdbAtomRecord
lines[#lines + 1] = string.format('set $__atom_name_%d = "%s"', a.idx, gdb_escape(a.name)) lines[#lines + 1] = string.format('set $__atom_name_%d = "%s"', a.idx, gdb_escape(a.name))
lines[#lines + 1] = string.format("set $__atom_addr_%d = 0x%x", a.idx, a.addr) lines[#lines + 1] = string.format("set $__atom_addr_%d = 0x%x", a.idx, a.addr)
lines[#lines + 1] = string.format("set $__atom_words_%d = %d", a.idx, a.words) lines[#lines + 1] = string.format("set $__atom_words_%d = %d", a.idx, a.words)
@@ -451,10 +497,12 @@ local function emit_gdb_runtime(ctx)
-- Confirmation line for the source operator. -- Confirmation line for the source operator.
lines[#lines + 1] = 'printf "[gdb_tape_atoms] runtime loaded %d atoms from %s\\n", $__atom_count, $__elf_path' lines[#lines + 1] = 'printf "[gdb_tape_atoms] runtime loaded %d atoms from %s\\n", $__atom_count, $__elf_path'
local out_path local out_path ---@type string
-- Move out of `<out_root>/gdb_tape_atoms_runtime.gdb` to `<out_root>/../gdb_tape_atoms_runtime.gdb` when the conventional `<out_root>` is `<build>/gen` -- Move out of `<out_root>/gdb_tape_atoms_runtime.gdb` to `<out_root>/../gdb_tape_atoms_runtime.gdb` when the conventional `<out_root>` is `<build>/gen`
-- (any equivalent spelling — relative, absolute backslash, absolute forward-slash, trailing-separator variants). -- (any equivalent spelling — relative, absolute backslash, absolute forward-slash, trailing-separator variants).
-- This puts the gdb runtime alongside the ELF at `build/` rather than under the report subdir. -- This puts the gdb runtime alongside the ELF at `build/` rather than under the report subdir.
--- @param p string
--- @return boolean
local function ends_with_gen_dir(p) local function ends_with_gen_dir(p)
if type(p) ~= "string" then return false end if type(p) ~= "string" then return false end
return p:match("[/\\]gen[/\\]?$") ~= nil or p == "build/gen" or p == "build\\gen" return p:match("[/\\]gen[/\\]?$") ~= nil or p == "build/gen" or p == "build\\gen"
@@ -462,7 +510,7 @@ local function emit_gdb_runtime(ctx)
if ends_with_gen_dir(ctx.out_root) then if ends_with_gen_dir(ctx.out_root) then
-- Strip the trailing `/gen` segment, then write the runtime script under `build/`. -- Strip the trailing `/gen` segment, then write the runtime script under `build/`.
-- e.g. "C:/projects/Pikuma/ps1/build/gen" -> "C:/projects/Pikuma/ps1/build". -- e.g. "C:/projects/Pikuma/ps1/build/gen" -> "C:/projects/Pikuma/ps1/build".
local parent = ctx.out_root:gsub("[/\\]gen[/\\]?$", "") local parent = ctx.out_root:gsub("[/\\]gen[/\\]?$", "") ---@type string
out_path = parent .. "/gdb_tape_atoms_runtime.gdb" out_path = parent .. "/gdb_tape_atoms_runtime.gdb"
else else
out_path = ctx.out_root .. "/gdb_tape_atoms_runtime.gdb" out_path = ctx.out_root .. "/gdb_tape_atoms_runtime.gdb"
@@ -476,27 +524,27 @@ end
-- M — module exports -- M — module exports
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
local M = {} local M = {} ---@type AtomSourceMapPass
-- Expose the pure render functions so `report.lua` and the focused tests can call them directly without triggering the file-emit path. -- Expose the pure render functions so `report.lua` and the focused tests can call them directly without triggering the file-emit path.
M.render_source_map = render_source_map M.render_source_map = render_source_map
M.render_provenance = render_provenance M.render_provenance = render_provenance
--- Render ONE atom's sourcemap stanza. --- Render ONE atom's sourcemap stanza.
--- @param atom table -- atom record (must have `atom.paths` populated) --- @param atom AtomEntry
--- @return string --- @return string
function M.render_atom_source_map(atom) function M.render_atom_source_map(atom)
assert(type(atom) == "table", "render_atom_source_map: atom must be a table") assert(type(atom) == "table", "render_atom_source_map: atom must be a table")
assert(type(atom.paths) == "table", "render_atom_source_map: atom.paths must be a table") assert(type(atom.paths) == "table", "render_atom_source_map: atom.paths must be a table")
local entries, total = canonical_word_entries(atom) local entries, total = canonical_word_entries(atom) ---@type WordMapEntry[], integer
local lines = {} local lines = {} ---@type string[]
lines[#lines + 1] = string.format("ATOM %s %d", (atom.raw_name or atom.name), total) lines[#lines + 1] = string.format("ATOM %s %d", (atom.raw_name or atom.name), total)
for _, entry in ipairs(entries) do for _, entry in ipairs(entries) do ---@type integer, WordMapEntry
local word_line = string.format("WORD %d LINE %d TEXT %s", local word_line = string.format("WORD %d LINE %d TEXT %s", ---@type string
entry.pos, entry.line, entry.text) entry.pos, entry.line, entry.text)
local keys = {} local keys = {} ---@type string[]
for pos = 1, 16 do for pos = 1, 16 do ---@type integer
local k = entry.gpr_keys and entry.gpr_keys[pos] local k = entry.gpr_keys and entry.gpr_keys[pos] ---@type string|nil
if type(k) == "string" and k:sub(1, 7) == "reguse:" then if type(k) == "string" and k:sub(1, 7) == "reguse:" then
keys[#keys + 1] = k keys[#keys + 1] = k
end end
@@ -514,25 +562,23 @@ end
--- ---
--- `rel_path` is the source path (forward-slashes) embedded in every `CALL` line. --- `rel_path` is the source path (forward-slashes) embedded in every `CALL` line.
--- The .md caller (report.lua) is expected to derive this once per `## <source>` heading and pass it down for each atom in that source. --- The .md caller (report.lua) is expected to derive this once per `## <source>` heading and pass it down for each atom in that source.
--- @param atom table -- atom record (must have `atom.paths` populated) --- @param atom AtomEntry
--- @param wc table -- identity alias of `corpus.word_counts` --- @param wc WordCounts
--- @param rel_path string -- source path (forward-slashes) for `CALL` fields --- @param rel_path string
--- @return string --- @return string
function M.render_atom_provenance(atom, wc, rel_path) function M.render_atom_provenance(atom, wc, rel_path)
assert(type(atom) == "table", "render_atom_provenance: atom must be a table") assert(type(atom) == "table", "render_atom_provenance: atom must be a table")
assert(type(atom.paths) == "table", "render_atom_provenance: atom.paths must be a table") assert(type(atom.paths) == "table", "render_atom_provenance: atom.paths must be a table")
assert(type(rel_path) == "string", "render_atom_provenance: rel_path must be a string") assert(type(rel_path) == "string", "render_atom_provenance: rel_path must be a string")
local entries, total = canonical_word_entries(atom) local entries, total = canonical_word_entries(atom) ---@type WordMapEntry[], integer
local lines = {} local lines = {} ---@type string[]
lines[#lines + 1] = string.format("ATOM %s %d", (atom.raw_name or atom.name), total) lines[#lines + 1] = string.format("ATOM %s %d", (atom.raw_name or atom.name), total)
for _, entry in ipairs(entries) do for _, entry in ipairs(entries) do ---@type integer, WordMapEntry
local inv = entry.invocation local inv = entry.invocation ---@type InvocationRecord|nil
local macro_count = inv and wc and wc["mac_" .. inv.component_name] local macro_count = inv and wc and wc["mac_" .. inv.component_name] ---@type integer|nil
if inv and macro_count ~= nil then if inv and macro_count ~= nil then
lines[#lines + 1] = string.format( lines[#lines + 1] = string.format('WORD %d CALL %s:%d MACRO %s "%s:%d" BODY %d'
'WORD %d CALL %s:%d MACRO %s "%s:%d" BODY %d', , entry.pos, rel_path, entry.line, inv.component_name, inv.def_path or "", inv.def_line or 0, entry.body_line)
entry.pos, rel_path, entry.line, inv.component_name,
inv.def_path or "", inv.def_line or 0, entry.body_line)
else else
lines[#lines + 1] = string.format( lines[#lines + 1] = string.format(
"WORD %d CALL %s:%d RAW", entry.pos, rel_path, entry.line) "WORD %d CALL %s:%d RAW", entry.pos, rel_path, entry.line)
@@ -548,17 +594,17 @@ end
--- @param ctx PassCtx --- @param ctx PassCtx
--- @return PassResult --- @return PassResult
function M.run(ctx) function M.run(ctx)
local outputs = {} local outputs = {} ---@type PassOutputEntry[]
local errors = {} local errors = {} ---@type Finding[]
local warnings = {} local warnings = {} ---@type Finding[]
local corpus = ctx.shared and ctx.shared.corpus local corpus = ctx.shared and ctx.shared.corpus ---@type Corpus|nil
if type(corpus) ~= "table" or type(corpus.source_order) ~= "table" then if type(corpus) ~= "table" or type(corpus.source_order) ~= "table" then
error("atoms_source_map.run requires ctx.shared.corpus.source_order (canonical corpus).", 0) error("atoms_source_map.run requires ctx.shared.corpus.source_order (canonical corpus).", 0)
end end
-- Word counts come from `corpus.word_counts` (populated by word_count_eval + components passes). -- Word counts come from `corpus.word_counts` (populated by word_count_eval + components passes).
local wc = corpus.word_counts or {} local wc = corpus.word_counts or {} ---@type WordCounts
if not next(wc) then if not next(wc) then
warnings[#warnings + 1] = { warnings[#warnings + 1] = {
line = 0, line = 0,
+135 -112
View File
@@ -9,8 +9,7 @@
--- These GPRs are unavailable to EVERY atom's source pool. --- These GPRs are unavailable to EVERY atom's source pool.
--- Carriers are preserved across atoms by context discipline and must never be reallocated. --- Carriers are preserved across atoms by context discipline and must never be reallocated.
--- Per-atom body parsing also catches alias references (R_<Alias>) and hardcoded R_Tn references, --- Per-atom body parsing also catches alias references (R_<Alias>) and hardcoded R_Tn references,
--- so the user can write either `R_T4` or `R_ResolveScratch` in an atom body and the pass will --- so the user can write either `R_T4` or `R_ResolveScratch` in an atom body and the pass will exclude R_T4 from that atom's pool.
--- exclude R_T4 from that atom's pool.
--- ---
--- Conflict detection: If the user hardcodes `R_Tn` in an atom body that shares a phase with an auto-reg that picked `R_Tn`, --- Conflict detection: If the user hardcodes `R_Tn` in an atom body that shares a phase with an auto-reg that picked `R_Tn`,
--- emit `phase_register_clash` as an info finding (no build stop). --- emit `phase_register_clash` as an info finding (no build stop).
@@ -19,50 +18,56 @@
--- Pool exhaustion: If a phase declares more `R_<Sym>` mappings than the 10-register pool can hold, --- Pool exhaustion: If a phase declares more `R_<Sym>` mappings than the 10-register pool can hold,
--- emit `phase_register_pool_exhausted` as a build-stopping error. --- emit `phase_register_pool_exhausted` as a build-stopping error.
--- @alias GprIdent string
--- @class GprAllocMap
--- @field [string] GprIdent -- bag: auto-reg symbol -> physical GPR
--- @class AutoRegOutput
--- @field auto_reg_h string
--- @class AutoRegResult --- @class AutoRegResult
--- @field outputs table[] -- {kind=, path=} entries --- @field outputs AutoRegOutput[]
--- @field errors table[] -- {line=, msg=} entries (build-stops) --- @field errors Finding[]
--- @field warnings table[] -- {line=, msg=} entries (build-continues) --- @field warnings Finding[]
local _bootstrap_dir = debug.getinfo(1, "S").source:match("^@?(.*[/\\])") or "./" --- @class AutoRegPass
local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua") --- @field run fun(ctx: PassCtx): AutoRegResult
--- @field POOL GprIdent[]
-- ════════════════════════════════════════════════════════════════════════════ local _bootstrap_dir = debug.getinfo(1, "S").source:match("^@?(.*[/\\])") or "./" ---@type string
-- THE GPR ALLOCATION POOL — what's allocatable, and (more importantly) WHY local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua") ---@type DuffleExport
-- ════════════════════════════════════════════════════════════════════════════ local isa = require("duffle_isa") ---@type DuffleIsa
--
-- The auto-reg pass picks physical GPRs for `atom_auto_reg(...)` / `phase_auto_reg(...)` markers. --- ════════════════════════════════════════════════════════════════════════════
-- The 24-register pool covers R2-R25 (the user/atom allocatable surface): --- THE GPR ALLOCATION POOL — what's allocatable, and (more importantly) WHY
-- R_T0..R_T7, R_V0..R_V1, R_A0..A3, R_S0..S7, R_T8..T9. --- ════════════════════════════════════════════════════════════════════════════
-- Excluded (and never added to the pool):
-- R_0 (code 0) — hardwired zero. Cannot be written.
-- R_AT (code 1) — assembler temporary. Reserved by the MIPS O32 ABI.
-- R_A0..A3 — explicitly omitted above even though their integer codes
-- map to POOL entries; the pool-construction loop below
-- only references the POOL string literals, never the
-- integer codes, so they are NOT auto-allocated by default.
-- (A0-A3 become available when the user adds them to
-- POOL or hardcodes an R_A0 reference in the atom body.)
-- R_K0/K1 (codes 26-27) — kernel / interrupt handler reserves. Never touched by user code.
-- R_GP/SP/FP/RA (codes 28-31) — R_SP/R_FP/R_RA are tape-runtime carriers between
-- tape_enter and tape_exit; R_GP stays the host global pointer.
--- ---
local POOL = { --- The auto-reg pass picks physical GPRs for `atom_auto_reg(...)` / `phase_auto_reg(...)` markers.
"R_T0", "R_T1", "R_T2", "R_T3", --- The 24-register pool covers R2-R25 (the user/atom allocatable surface):
"R_T4", "R_T5", "R_T6", "R_T7", --- R_T0..R_T7, R_V0..R_V1, R_A0..A3, R_S0..S7, R_T8..T9.
"R_V0", "R_V1", --- Excluded (and never added to the pool):
"R_A0", "R_A1", "R_A2", "R_A3", --- R_0 (code 0) — Hardwired zero. Cannot be written.
"R_S0", "R_S1", "R_S2", "R_S3", --- R_AT (code 1) — Assembler temporary. Reserved by the MIPS O32 ABI.
"R_S4", "R_S5", "R_S6", "R_S7", --- R_A0..A3 — Explicitly omitted above even though their integer codes map to POOL entries;
"R_T8", "R_T9", --- the pool-construction loop below only references the POOL string literals, never the integer codes, so they are NOT auto-allocated by default.
} --- (A0-A3 become available when the user adds them to POOL or hardcodes an R_A0 reference in the atom body.)
--- R_K0/K1 (codes 26-27) — Kernel / interrupt handler reserves. Never touched by user code.
--- R_GP/SP/FP/RA (codes 28-31) — R_SP/R_FP/R_RA are tape-runtime carriers between tape_enter and tape_exit; R_GP stays the host global pointer.
---
local POOL = {} ---@type GprIdent[]
for _, row in ipairs(isa.GPR_ROLE) do ---@type integer, GprRole
if row.pool then
POOL[#POOL + 1] = row.name
end
end
-- Map from integer MIPS GPR code (the `code` field on AliasEntry) to the physical GPR ident in POOL. -- Map from integer MIPS GPR code (the `code` field on AliasEntry) to the physical GPR ident in POOL.
-- The standard MIPS O32 ABI register numbering matches mips.h's R_*_Code #defines (mips.h). -- The standard MIPS O32 ABI register numbering matches mips.h's R_*_Code #defines (mips.h).
-- Only the POOL entries matter for auto_reg — non-pool aliases -- Only the POOL entries matter for auto_reg — non-pool aliases
-- (R_AT=1, R_A0..A3=4..7, R_T8=24, R_T9=25, R_K0/K1=26..27, R_GP/SP/FP/RA=28..31) -- (R_AT=1, R_A0..A3=4..7, R_T8=24, R_T9=25, R_K0/K1=26..27, R_GP/SP/FP/RA=28..31)
-- are deliberately omitted — see the comment block above for the WHY of each exclusion. -- are deliberately omitted — see the comment block above for the WHY of each exclusion.
local INT_CODE_TO_POOL_GPR = { local INT_CODE_TO_POOL_GPR = { ---@type table<integer, GprIdent> -- bag: MIPS GPR code -> POOL ident
[2] = "R_V0", [3] = "R_V1", [2] = "R_V0", [3] = "R_V1",
[4] = "R_A0", [5] = "R_A1", [6] = "R_A2", [7] = "R_A3", [4] = "R_A0", [5] = "R_A1", [6] = "R_A2", [7] = "R_A3",
[8] = "R_T0", [9] = "R_T1", [10] = "R_T2", [11] = "R_T3", [8] = "R_T0", [9] = "R_T1", [10] = "R_T2", [11] = "R_T3",
@@ -73,25 +78,31 @@ local INT_CODE_TO_POOL_GPR = {
} }
-- Stable sort for deterministic allocation order. -- Stable sort for deterministic allocation order.
--- @param tbl table<string, string> -- bag: key set only; values unused
--- @return string[]
local function stable_sort_keys(tbl) local function stable_sort_keys(tbl)
local keys = {} local keys = {} ---@type string[]
for k in pairs(tbl) do keys[#keys + 1] = k end for k in pairs(tbl) do keys[#keys + 1] = k end ---@type string
table.sort(keys) table.sort(keys)
return keys return keys
end end
-- Allocate one phase's auto-reg mappings. -- Allocate one phase's auto-reg mappings.
-- Returns (allocated_map, errors). On pool exhaustion, errors is populated and the function halts. -- Returns (allocated_map, errors). On pool exhaustion, errors is populated and the function halts.
--- @param phase_label string
--- @param decls table<string, string> -- bag: auto-reg symbol -> decl payload
--- @return GprAllocMap
--- @return Finding[]
local function allocate_phase(phase_label, decls) local function allocate_phase(phase_label, decls)
-- Deep-copy POOL into a fresh sequence table. The original `table.unpack and table.unpack(POOL) or { unpack(POOL) }` -- Deep-copy POOL into a fresh sequence table. The original `table.unpack and table.unpack(POOL) or { unpack(POOL) }`
-- idiom wraps the unpacked values in a single inner table under LuaJIT 5.1 (`table.unpack` is nil; the `or` returns one value), -- idiom wraps the unpacked values in a single inner table under LuaJIT 5.1 (`table.unpack` is nil; the `or` returns one value),
-- which corrupts the pool into `{ {R_T0, R_T1, ...} }` — making `table.remove(pool, 1)` return the inner table on iteration. -- which corrupts the pool into `{ {R_T0, R_T1, ...} }` — making `table.remove(pool, 1)` return the inner table on iteration.
local pool = {} local pool = {} ---@type GprIdent[]
for i = 1, #POOL do pool[i] = POOL[i] end for i = 1, #POOL do pool[i] = POOL[i] end ---@type integer
local result = {} local result = {} ---@type GprAllocMap
local errors = {} local errors = {} ---@type Finding[]
for _, sym in ipairs(stable_sort_keys(decls)) do for _, sym in ipairs(stable_sort_keys(decls)) do ---@type integer, string
local next_gpr = table.remove(pool, 1) local next_gpr = table.remove(pool, 1) ---@type GprIdent|nil
if not next_gpr then if not next_gpr then
errors[#errors + 1] = { errors[#errors + 1] = {
line = 0, line = 0,
@@ -115,13 +126,16 @@ end
-- Each entry's `code` is the integer MIPS GPR number (0..31); INT_CODE_TO_POOL_GPR translates it back to the physical GPR ident. -- Each entry's `code` is the integer MIPS GPR number (0..31); INT_CODE_TO_POOL_GPR translates it back to the physical GPR ident.
-- Aliases whose `code` points to a non-POOL GPR (e.g. R_S0, R_T8, R_K1) are ignored — -- Aliases whose `code` points to a non-POOL GPR (e.g. R_S0, R_T8, R_K1) are ignored —
-- they don't affect the auto_reg pool, and they're already excluded from POOL above. -- they don't affect the auto_reg pool, and they're already excluded from POOL above.
--- @param corpus Corpus
--- @return table<GprIdent, boolean>
--- @return table<string, GprIdent>
local function build_user_pins(corpus) local function build_user_pins(corpus)
local user_pinned = {} local user_pinned = {} ---@type table<GprIdent, boolean> -- bag: pinned physical GPR -> true
local alias_to_gpr = {} local alias_to_gpr = {} ---@type table<string, GprIdent> -- bag: alias ident -> physical GPR
if not corpus.register_alias_registry then return user_pinned, alias_to_gpr end if not corpus.register_alias_registry then return user_pinned, alias_to_gpr end
for alias_name, alias_entry in pairs(corpus.register_alias_registry) do for alias_name, alias_entry in pairs(corpus.register_alias_registry) do ---@type string, AliasEntry
if alias_entry.has_atom_reg and alias_entry.code then if alias_entry.has_atom_reg and alias_entry.code then
local gpr = INT_CODE_TO_POOL_GPR[alias_entry.code] local gpr = INT_CODE_TO_POOL_GPR[alias_entry.code] ---@type GprIdent|nil
if gpr then if gpr then
user_pinned[gpr] = true user_pinned[gpr] = true
alias_to_gpr[alias_name] = gpr alias_to_gpr[alias_name] = gpr
@@ -131,30 +145,33 @@ local function build_user_pins(corpus)
return user_pinned, alias_to_gpr return user_pinned, alias_to_gpr
end end
-- Find every physical GPR referenced in the atom body, via EITHER: --- Find every physical GPR referenced in the atom body, via EITHER:
-- (a) A hardcoded physical GPR ident (R_T\d+|R_V\d+|R_A\d+|R_S\d+) — the existing regex; --- (a) A hardcoded physical GPR ident (R_T\d+|R_V\d+|R_A\d+|R_S\d+) — the existing regex;
-- (b) An alias ident (R_<Alias>) resolved via alias_to_gpr back to its physical GPR ident. --- (b) An alias ident (R_<Alias>) resolved via alias_to_gpr back to its physical GPR ident.
-- Returns { [physical_gpr_ident] = count }. Clash-detection and source-pool-exclusion logic --- Returns { [physical_gpr_ident] = count }.
-- only needs the presence of each GPR (boolean test), but keeping count preserves the --- Clash-detection and source-pool-exclusion logic only needs the presence of each GPR (boolean test),
-- original find_hardcoded_rn shape so callers can switch without churn. --- but keeping count preserves the original find_hardcoded_rn shape so callers can switch without churn.
-- The alias pattern is sorted lexicographically to keep the regex deterministic. --- The alias pattern is sorted lexicographically to keep the regex deterministic.
--- @param body_text string
--- @param alias_to_gpr table<string, GprIdent> -- bag: alias ident -> physical GPR
--- @return table<GprIdent, integer>
local function find_used_gprs(body_text, alias_to_gpr) local function find_used_gprs(body_text, alias_to_gpr)
local found = {} local found = {} ---@type table<GprIdent, integer> -- bag: physical GPR -> hit count
-- (a) Hardcoded physical GPRs (R_T0..R_T7, R_V0..R_V1, R_A0..R_A3, R_S0..R_S7). -- (a) Hardcoded physical GPRs (R_T0..R_T7, R_V0..R_V1, R_A0..R_A3, R_S0..R_S7).
for gpr in body_text:gmatch("(R_T%d+|R_V%d+|R_A%d+|R_S%d+)") do for gpr in body_text:gmatch("(R_T%d+|R_V%d+|R_A%d+|R_S%d+)") do ---@type GprIdent
found[gpr] = (found[gpr] or 0) + 1 found[gpr] = (found[gpr] or 0) + 1
end end
-- (b) Alias references (R_<Alias>) resolved to physical GPRs via the registry. -- (b) Alias references (R_<Alias>) resolved to physical GPRs via the registry.
-- Sorted by name so the regex is byte-stable across runs. -- Sorted by name so the regex is byte-stable across runs.
if alias_to_gpr and next(alias_to_gpr) then if alias_to_gpr and next(alias_to_gpr) then
local aliases = {} local aliases = {} ---@type string[]
for alias_name in pairs(alias_to_gpr) do for alias_name in pairs(alias_to_gpr) do ---@type string
aliases[#aliases + 1] = alias_name aliases[#aliases + 1] = alias_name
end end
table.sort(aliases) table.sort(aliases)
local pattern = "(" .. table.concat(aliases, "|") .. ")" local pattern = "(" .. table.concat(aliases, "|") .. ")" ---@type string
for alias_name in body_text:gmatch(pattern) do for alias_name in body_text:gmatch(pattern) do ---@type string
local gpr = alias_to_gpr[alias_name] local gpr = alias_to_gpr[alias_name] ---@type GprIdent|nil
if gpr and not found[gpr] then if gpr and not found[gpr] then
found[gpr] = 1 found[gpr] = 1
end end
@@ -164,26 +181,31 @@ local function find_used_gprs(body_text, alias_to_gpr)
end end
-- Emit one gen/auto_reg.h header per directory. -- Emit one gen/auto_reg.h header per directory.
--- @param out_dir string
--- @param dir string
--- @param sources SourceFile[]
--- @param mappings GprAllocMap
--- @return string|nil
local function emit_auto_reg_h(out_dir, dir, sources, mappings) local function emit_auto_reg_h(out_dir, dir, sources, mappings)
if not mappings or next(mappings) == nil then return end if not mappings or next(mappings) == nil then return end
local out_path = out_dir .. "/" .. "auto_reg.h" local out_path = out_dir .. "/" .. "auto_reg.h" ---@type string
duffle.ensure_dir(out_dir) duffle.ensure_dir(out_dir)
local lines = { local lines = { ---@type string[]
"#ifdef INTELLISENSE_DIRECTIVES", "#ifdef INTELLISENSE_DIRECTIVES",
"#pragma once", "#pragma once",
"#endif", "#endif",
"// Auto-generated by ps1_meta.lua (passes/auto_reg.lua) — DO NOT EDIT", "// Auto-generated by ps1_meta.lua (passes/auto_reg.lua) — DO NOT EDIT",
"// Directory: " .. dir:gsub("/", "\\"), "// Directory: " .. dir:gsub("/", "\\"),
} }
for _, src in ipairs(sources) do for _, src in ipairs(sources) do ---@type integer, SourceFile
lines[#lines + 1] = "// source: " .. src.path lines[#lines + 1] = "// source: " .. src.path
end end
lines[#lines + 1] = "// Per-phase register allocations resolved by the lua pass." lines[#lines + 1] = "// Per-phase register allocations resolved by the lua pass."
lines[#lines + 1] = "// R_<Sym>_Code = <chosen GPR's _Code constant> for every marker in this directory." lines[#lines + 1] = "// R_<Sym>_Code = <chosen GPR's _Code constant> for every marker in this directory."
lines[#lines + 1] = "" lines[#lines + 1] = ""
for _, sym in ipairs(stable_sort_keys(mappings)) do for _, sym in ipairs(stable_sort_keys(mappings)) do ---@type integer, string
local gpr = mappings[sym] local gpr = mappings[sym] ---@type GprIdent
local gpr_code = gpr .. "_Code" local gpr_code = gpr .. "_Code" ---@type string
lines[#lines + 1] = "#define " .. sym .. "_Code " .. gpr_code lines[#lines + 1] = "#define " .. sym .. "_Code " .. gpr_code
end end
lines[#lines + 1] = "" lines[#lines + 1] = ""
@@ -196,16 +218,16 @@ end
-- Pass entry -- Pass entry
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
local M = {} local M = {} ---@type AutoRegPass
--- @param ctx PassCtx --- @param ctx PassCtx
--- @return AutoRegResult --- @return AutoRegResult
function M.run(ctx) function M.run(ctx)
local outputs = {} local outputs = {} ---@type AutoRegOutput[]
local errors = {} local errors = {} ---@type Finding[]
local warnings = {} local warnings = {} ---@type Finding[]
local corpus = ctx.shared and ctx.shared.corpus local corpus = ctx.shared and ctx.shared.corpus ---@type Corpus|nil
if type(corpus) ~= "table" then if type(corpus) ~= "table" then
error("auto_reg.run requires ctx.shared.corpus", 0) error("auto_reg.run requires ctx.shared.corpus", 0)
end end
@@ -215,17 +237,17 @@ function M.run(ctx)
-- MUST NOT be allocated to any auto-reg marker — they're preserved across atoms by the wave-context discipline. -- MUST NOT be allocated to any auto-reg marker — they're preserved across atoms by the wave-context discipline.
-- The corpus's register_alias_registry is the source of truth for these opt-in pins. -- The corpus's register_alias_registry is the source of truth for these opt-in pins.
-- Body references to those aliases (via alias_to_gpr) are also excluded on a per-atom basis in step 2 below. -- Body references to those aliases (via alias_to_gpr) are also excluded on a per-atom basis in step 2 below.
local user_pinned, alias_to_gpr = build_user_pins(corpus) local user_pinned, alias_to_gpr = build_user_pins(corpus) ---@type table<GprIdent, boolean>, table<string, GprIdent>
-- 1. Allocate phase pools first (phase declarations take precedence over per-atom declarations). -- 1. Allocate phase pools first (phase declarations take precedence over per-atom declarations).
local phase_allocations = {} local phase_allocations = {} ---@type table<string, GprAllocMap> -- bag: phase_label -> alloc map
for phase_label, decls in pairs(corpus.phase_auto_regs or {}) do for phase_label, decls in pairs(corpus.phase_auto_regs or {}) do ---@type string, table<string, string>
local mapping, errs = allocate_phase(phase_label, decls) local mapping, errs = allocate_phase(phase_label, decls) ---@type GprAllocMap, Finding[]
for sym, gpr in pairs(mapping) do for sym, gpr in pairs(mapping) do ---@type string, GprIdent
phase_allocations[phase_label] = phase_allocations[phase_label] or {} phase_allocations[phase_label] = phase_allocations[phase_label] or {}
phase_allocations[phase_label][sym] = gpr phase_allocations[phase_label][sym] = gpr
end end
for _, e in ipairs(errs) do for _, e in ipairs(errs) do ---@type integer, Finding
errors[#errors + 1] = e errors[#errors + 1] = e
end end
end end
@@ -234,16 +256,16 @@ function M.run(ctx)
-- Otherwise, allocate a private pool for the atom. -- Otherwise, allocate a private pool for the atom.
-- The phase membership is in `corpus.atom_phases[phase_label].atoms` (an array of atom names declared via `atom_phase(<phase>)` -- The phase membership is in `corpus.atom_phases[phase_label].atoms` (an array of atom names declared via `atom_phase(<phase>)`
-- in the atom's `atom_info` line). Build a reverse map `atom_name -> phase_label` so the lookup is O(1) per atom scope. -- in the atom's `atom_info` line). Build a reverse map `atom_name -> phase_label` so the lookup is O(1) per atom scope.
local atom_name_to_phase = {} local atom_name_to_phase = {} ---@type table<AtomName, string> -- bag: atom name -> phase label
for phase_label, entry in pairs(corpus.atom_phases or {}) do for phase_label, entry in pairs(corpus.atom_phases or {}) do ---@type string, AtomPhaseGroup
for _, atom_name in ipairs(entry.atoms or {}) do for _, atom_name in ipairs(entry.atoms or {}) do ---@type integer, AtomName
atom_name_to_phase[atom_name] = phase_label atom_name_to_phase[atom_name] = phase_label
end end
end end
local atom_allocations = {} local atom_allocations = {} ---@type table<AtomName, GprAllocMap> -- bag: atom scope -> alloc map
for atom_scope, decls in pairs(corpus.atom_auto_regs or {}) do for atom_scope, decls in pairs(corpus.atom_auto_regs or {}) do ---@type AtomName, table<string, string>
local phase_label = atom_name_to_phase[atom_scope] local phase_label = atom_name_to_phase[atom_scope] ---@type string|nil
-- Build the atom's source pool: start with the full POOL, subtract: -- Build the atom's source pool: start with the full POOL, subtract:
-- (a) every GPR already committed (phase allocations + prior atom allocations) -- (a) every GPR already committed (phase allocations + prior atom allocations)
-- (b) every USER-PINNED GPR (wave-context carriers + file-scope pinned aliases) -- (b) every USER-PINNED GPR (wave-context carriers + file-scope pinned aliases)
@@ -253,27 +275,26 @@ function M.run(ctx)
-- the original `source_pool = phase_allocations[phase_label]` form used the phase -- the original `source_pool = phase_allocations[phase_label]` form used the phase
-- allocation MAP as a pool, but that map has no array part, so `table.remove(source_pool, 1)` -- allocation MAP as a pool, but that map has no array part, so `table.remove(source_pool, 1)`
-- returned nil and every atom-with-phase marker errored with `phase_register_pool_exhausted`. -- returned nil and every atom-with-phase marker errored with `phase_register_pool_exhausted`.
local used = {} local used = {} ---@type table<GprIdent, boolean> -- bag: committed or body-referenced GPR -> true
for _, m in pairs(phase_allocations) do for _, gpr in pairs(m) do used[gpr] = true end end for _, m in pairs(phase_allocations) do for _, gpr in pairs(m) do used[gpr] = true end end ---@type integer, GprAllocMap
for _, m in pairs(atom_allocations) do for _, gpr in pairs(m) do used[gpr] = true end end for _, m in pairs(atom_allocations) do for _, gpr in pairs(m) do used[gpr] = true end end ---@type integer, GprAllocMap
-- (c) Body references — scan the atom body for hardcoded + alias-resolved GPRs. -- (c) Body references — scan the atom body for hardcoded + alias-resolved GPRs.
-- Folded into `used` so the source_pool exclusion is a single check. -- Folded into `used` so the source_pool exclusion is a single check.
local atom = corpus.atoms_by_name and corpus.atoms_by_name[atom_scope] local atom = corpus.atoms_by_name and corpus.atoms_by_name[atom_scope] ---@type AtomEntry|nil
if atom and atom.body then if atom and atom.body then
local body_used = find_used_gprs(atom.body, alias_to_gpr) local body_used = find_used_gprs(atom.body, alias_to_gpr) ---@type table<GprIdent, integer>
for gpr in pairs(body_used) do used[gpr] = true end for gpr in pairs(body_used) do used[gpr] = true end ---@type GprIdent
end end
local source_pool = {} local source_pool = {} ---@type GprIdent[]
for _, gpr in ipairs(POOL) do for _, gpr in ipairs(POOL) do ---@type integer, GprIdent
-- Exclude (a) prior commitments, (b) USER-PINNED GPRs (wave-context carriers -- Exclude (a) prior commitments, (b) USER-PINNED GPRs (wave-context carriers declared via atom_reg + _Code defs, preserved across atoms globally).
-- declared via atom_reg + _Code defs, preserved across atoms globally).
if not used[gpr] and not user_pinned[gpr] then if not used[gpr] and not user_pinned[gpr] then
source_pool[#source_pool + 1] = gpr source_pool[#source_pool + 1] = gpr
end end
end end
local result = {} local result = {} ---@type GprAllocMap
for _, sym in ipairs(stable_sort_keys(decls)) do for _, sym in ipairs(stable_sort_keys(decls)) do ---@type integer, string
local next_gpr = table.remove(source_pool, 1) local next_gpr = table.remove(source_pool, 1) ---@type GprIdent|nil
if not next_gpr then if not next_gpr then
errors[#errors + 1] = { errors[#errors + 1] = {
line = 0, line = 0,
@@ -295,11 +316,11 @@ function M.run(ctx)
-- This warning is kept as a defensive safety net for cases the body scanner might miss -- This warning is kept as a defensive safety net for cases the body scanner might miss
-- (e.g. macros that expand to register references the scanner cannot resolve). -- (e.g. macros that expand to register references the scanner cannot resolve).
-- For each resolved (scope, sym) -> R_Tn mapping, scan the atom body source for used GPRs. -- For each resolved (scope, sym) -> R_Tn mapping, scan the atom body source for used GPRs.
for atom_scope, decls in pairs(atom_allocations) do for atom_scope, decls in pairs(atom_allocations) do ---@type AtomName, GprAllocMap
local atom = corpus.atoms_by_name and corpus.atoms_by_name[atom_scope] local atom = corpus.atoms_by_name and corpus.atoms_by_name[atom_scope] ---@type AtomEntry|nil
if atom and atom.body then if atom and atom.body then
local used_in_body = find_used_gprs(atom.body, alias_to_gpr) local used_in_body = find_used_gprs(atom.body, alias_to_gpr) ---@type table<GprIdent, integer>
for sym, allocated_gpr in pairs(decls) do for sym, allocated_gpr in pairs(decls) do ---@type string, GprIdent
if used_in_body[allocated_gpr] and used_in_body[allocated_gpr] > 0 then if used_in_body[allocated_gpr] and used_in_body[allocated_gpr] > 0 then
warnings[#warnings + 1] = { warnings[#warnings + 1] = {
line = atom.line or 0, line = atom.line or 0,
@@ -314,30 +335,32 @@ function M.run(ctx)
-- 4. Emit per-directory gen/auto_reg.h. -- 4. Emit per-directory gen/auto_reg.h.
-- For each source directory that has atom_auto_regs or phase_auto_regs entries, emit one header. -- For each source directory that has atom_auto_regs or phase_auto_regs entries, emit one header.
local sources_by_dir = corpus.sources_by_dir or {} local sources_by_dir = corpus.sources_by_dir or {} ---@type table<string, SourceFile[]>
for dir, sources in pairs(sources_by_dir) do for dir, sources in pairs(sources_by_dir) do ---@type string, SourceFile[]
local per_dir_mappings = {} local per_dir_mappings = {} ---@type GprAllocMap
for _, src in ipairs(sources) do for _, src in ipairs(sources) do ---@type integer, SourceFile
-- Collect every (sym -> gpr) entry that originated from a source in this directory. -- Collect every (sym -> gpr) entry that originated from a source in this directory.
-- `src.scan.atom_auto_regs` is keyed by ATOM SCOPE NAME; `pairs(t)` iterates KEYS so `scope_name` here is the scope ident (e.g. "cube_g4_face"). -- `src.scan.atom_auto_regs` is keyed by ATOM SCOPE NAME; `pairs(t)` iterates KEYS so `scope_name` here is the scope ident (e.g. "cube_g4_face").
-- The previous `for _, scan_atom_auto` form silently assigned the VALUE (a `{sym = sym}` table) to the variable, -- The previous `for _, scan_atom_auto` form silently assigned the VALUE (a `{sym = sym}` table) to the variable,
-- which made `atom_allocations[scan_atom_auto]` a table-indexed lookup that never resolved. -- which made `atom_allocations[scan_atom_auto]` a table-indexed lookup that never resolved.
for scope_name in pairs(src.scan and src.scan.atom_auto_regs or {}) do for scope_name in pairs(src.scan and src.scan.atom_auto_regs or {}) do ---@type string
for sym, gpr in pairs(atom_allocations[scope_name] or {}) do for sym, gpr in pairs(atom_allocations[scope_name] or {}) do ---@type string, GprIdent
per_dir_mappings[sym] = gpr per_dir_mappings[sym] = gpr
end end
end end
for scope_name in pairs(src.scan and src.scan.phase_auto_regs or {}) do for scope_name in pairs(src.scan and src.scan.phase_auto_regs or {}) do ---@type string
for sym, gpr in pairs(phase_allocations[scope_name] or {}) do for sym, gpr in pairs(phase_allocations[scope_name] or {}) do ---@type string, GprIdent
per_dir_mappings[sym] = gpr per_dir_mappings[sym] = gpr
end end
end end
end end
local out_dir = dir .. "/gen" local out_dir = dir .. "/gen" ---@type string
local out_path = emit_auto_reg_h(out_dir, dir, sources, per_dir_mappings) local out_path = emit_auto_reg_h(out_dir, dir, sources, per_dir_mappings) ---@type string|nil
if out_path then outputs[#outputs + 1] = { auto_reg_h = out_path } end if out_path then outputs[#outputs + 1] = { auto_reg_h = out_path } end
end end
return { outputs = outputs, errors = errors, warnings = warnings } return { outputs = outputs, errors = errors, warnings = warnings }
end end
M.POOL = POOL
return M return M
+276 -297
View File
@@ -1,6 +1,6 @@
--- passes/components.lua — Component-macro header generator. --- passes/components.lua — Component-macro header generator.
--- ---
--- Ownership: `corpus.word_counts`, `corpus.components`, and `corpus.component_body_index`. --- Ownership: `corpus.word_counts` and `corpus.components`.
--- Scanner owns `declaration_comment` and `debug_skip` on each declaration record; this pass projects both forward. --- Scanner owns `declaration_comment` and `debug_skip` on each declaration record; this pass projects both forward.
--- ---
--- Reads the pre-scanned SourceScan payload from `duffle.scan_source` for `MipsAtomComp_(ac_X)` and `MipsAtomComp_Proc_(ac_X, { body })` declarations (kind="comp_bare" / "comp_proc"), --- Reads the pre-scanned SourceScan payload from `duffle.scan_source` for `MipsAtomComp_(ac_X)` and `MipsAtomComp_Proc_(ac_X, { body })` declarations (kind="comp_bare" / "comp_proc"),
@@ -22,71 +22,77 @@
-- Uses `debug.getinfo` to find this file's own directory, so it works both standalone and when require'd from the orchestrator. -- Uses `debug.getinfo` to find this file's own directory, so it works both standalone and when require'd from the orchestrator.
-- Bootstrap: load `duffle_paths.lua` via `debug.getinfo(1, "S").source` (works both standalone + when require'd). -- Bootstrap: load `duffle_paths.lua` via `debug.getinfo(1, "S").source` (works both standalone + when require'd).
-- duffle_paths.lua sets package.path then returns `require("duffle")` at the bottom, so the dofile value IS the duffle module. -- duffle_paths.lua sets package.path then returns `require("duffle")` at the bottom, so the dofile value IS the duffle module.
local _bootstrap_dir = debug.getinfo(1, "S").source:match("^@?(.*[/\\])") or "./" local _bootstrap_dir = debug.getinfo(1, "S").source:match("^@?(.*[/\\])") or "./" ---@type string
local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua") local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua") ---@type DuffleExport
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
-- Constants -- Constants
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
-- Atom component declaration identifiers. -- Atom component declaration identifiers.
local ATOM_COMP_PROC = "MipsAtomComp_Proc_" local ATOM_COMP_PROC = "MipsAtomComp_Proc_" ---@type string
local MIPS_ATOM = "Slice_MipsCode" -- prefix on the function declaration that wraps an AtomComp_Proc_ local MIPS_ATOM = "Slice_MipsCode" ---@type string -- prefix on the function declaration that wraps an AtomComp_Proc_
-- Component-name prefixes. -- Component-name prefixes.
local AC_PREFIX = "ac_" -- arg to MipsAtomComp_(ac_X); the X is the atom name local AC_PREFIX = "ac_" ---@type string -- arg to MipsAtomComp_(ac_X); the X is the atom name
local AC_PREFIX_LEN = 3 local AC_PREFIX_LEN = 3 ---@type integer
local MAC_PREFIX = "mac_" -- prefix on generated macros; the rest is the atom name local MAC_PREFIX = "mac_" ---@type string -- prefix on generated macros; the rest is the atom name
local MAC_PREFIX_LEN = 4 local MAC_PREFIX_LEN = 4 ---@type integer
-- ASCII byte values used in tokenization. -- ASCII byte values used in tokenization.
local BYTE_NEWLINE = 10 local BYTE_NEWLINE = 10 ---@type integer
local BYTE_SLASH = 47 local BYTE_SLASH = 47 ---@type integer
-- Output gen subdirectory + filename (per-directory aggregation; the directory name is the namespace). -- Output gen subdirectory + filename (per-directory aggregation; the directory name is the namespace).
local GEN_SUBDIR = "gen" local GEN_SUBDIR = "gen" ---@type string
local MACS_FILENAME = "macs.h" local MACS_FILENAME = "macs.h" ---@type string
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
-- Type declarations -- Type declarations
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
--- @class SourceFile -- SourceFile, PassCtx, PassResult: see ps1_meta.lua
--- @field path string -- Absolute path to the source file -- DuffleExport: see duffle.lua
--- @field text string -- Full source text -- SourceScan, AtomEntry, CorpusCollision, CollisionSite: see scan_source.lua
--- @field dir string -- Directory containing the source -- BodyToken: see emission_model.lua
--- @field basename string -- Filename without extension -- WordCounts: see word_count_eval.lua
--- @field scan table -- Pre-scanned SourceScan payload (from duffle.scan_source) -- InstructionRow, GteCommandRow: see duffle_isa.lua
--- @class PassCtx
--- @field sources SourceFile[] -- All source files in the build
--- @field metadata_path string -- Path to word_count.metadata.h
--- @field shared table -- Cross-pass shared state
--- @field out_root string -- Output root (e.g. "build/gen")
--- @field project_root string -- Project root (e.g. "code/")
--- @field upstream table<string, table> -- Per-pass upstream outputs
--- @field flags table -- CLI flags
--- @field verbose boolean -- Log diagnostic info
--- @class PassResult
--- @field outputs table[] -- {kind=, path=} entries describing emit files
--- @field errors table[] -- {line=, msg=} entries; build-stops
--- @field warnings table[] -- {line=, msg=} entries; build-succeeds
--- @class Component --- @class Component
--- @field name string -- Atom name (without `ac_` prefix) --- @field name string -- Atom name (without `ac_` prefix)
--- @field body string -- Brace-delimited body (without the braces) --- @field body string -- Brace-delimited body (without the braces)
--- @field body_off integer|nil -- Byte offset of body[1] in source
--- @field body_tokens BodyToken[]|nil
--- @field args string|nil -- Function-args string (function form only) --- @field args string|nil -- Function-args string (function form only)
--- @field arg_names string[]|nil -- Formal names with leading `ab` dropped
--- @field line integer -- Source line of the declaration --- @field line integer -- Source line of the declaration
--- @field comment string|nil -- Scanner-owned `declaration_comment`; the components pass reads it from the scanner record --- @field comment string|nil -- Scanner-owned `declaration_comment`; the components pass reads it from the scanner record
--- @field kind string -- "comp_bare" | "comp_proc" (atom_proc is NOT a component — see `project_components`) --- @field kind string -- "comp_bare" | "comp_proc" (atom_proc is NOT a component — see `project_components`)
--- @field debug_skip boolean -- Mirror of `a.debug_skip` (scanner-owned); true iff a bare `atom_dbg_skip` marker immediately preceded the declaration --- @field debug_skip boolean -- Mirror of `a.debug_skip` (scanner-owned); true iff a bare `atom_dbg_skip` marker immediately preceded the declaration
--- @field path string|nil -- Slash-normalized source path (collision sites)
--- @field source string|nil -- Absolute source path (emit)
--- @field line_of (fun(pos: integer): integer)|nil
--- @field cycle_cost integer|nil -- From metadata[c.name]; nil when the body was not costed
--- @field gp0_contrib integer|nil -- From metadata[c.name]; nil when the body was not costed
--- @class ComponentMeta
--- @field cycle_cost integer
--- @field gp0_contrib integer
--- @class ComponentMetaMap
--- @field [string] ComponentMeta -- bag: bare component name -> meta
--- @class MacsOutput
--- @field macs_h string
--- @class ComponentsPass
--- @field run fun(ctx: PassCtx): PassResult
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
-- Local helpers (file I/O + path normalization) -- Local helpers (file I/O + path normalization)
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
local M = {} local M = {} ---@type ComponentsPass
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
-- Back-walk helpers (composed into the entry point below: find_function_args_for) -- Back-walk helpers (composed into the entry point below: find_function_args_for)
@@ -100,16 +106,15 @@ local M = {}
--- Returns the args string (e.g., `"U4 off, U4 code, U1 r, U1 g, U1 b"`) or nil if no function declaration is found. --- Returns the args string (e.g., `"U4 off, U4 code, U1 r, U1 g, U1 b"`) or nil if no function declaration is found.
--- ---
--- After the `sym` arg was dropped from MipsAtomComp_Proc_, the component name --- After the `sym` arg was dropped from MipsAtomComp_Proc_, the component name
--- and the args both come from the preceding `FI_ Slice_MipsCode ac_X(args)` --- and the args both come from the preceding `FI_ Slice_MipsCode ac_X(args)` declaration.
--- declaration. The shared `duffle.find_function_decl_for` helper does the --- The shared `duffle.find_function_decl_for` helper does the backward walk; this function returns just the args.
--- backward walk; this function returns just the args.
--- ---
--- @param source string --- @param source string
--- @param name string (retained for signature stability; unused — the walk derives the name) --- @param name string (retained for signature stability; unused — the walk derives the name)
--- @param before_pos integer --- @param before_pos integer
--- @return string|nil --- @return string|nil
local function find_function_args_for(source, name, before_pos) local function find_function_args_for(source, name, before_pos)
local _, args_inner = duffle.find_function_decl_for(source, before_pos, #MIPS_ATOM) local _, args_inner = duffle.find_function_decl_for(source, before_pos, #MIPS_ATOM) ---@type string|nil, string|nil
return args_inner return args_inner
end end
@@ -125,24 +130,24 @@ end
--- @return string[]|nil --- @return string[]|nil
local function extract_arg_names(args_str) local function extract_arg_names(args_str)
if not args_str or args_str == "" then return nil end if not args_str or args_str == "" then return nil end
local names = {} local names = {} ---@type string[]
local tokens = duffle.split_top_level_commas(args_str) local tokens = duffle.split_top_level_commas(args_str) ---@type string[]
for _, tok in ipairs(tokens) do for _, tok in ipairs(tokens) do ---@type integer, string
local trimmed = duffle.trim(tok) local trimmed = duffle.trim(tok) ---@type string
if trimmed ~= "" then if trimmed ~= "" then
-- Strip trailing block comment (/* ... */) from the token, if present. -- Strip trailing block comment (/* ... */) from the token, if present.
-- split_top_level_commas only skips block comments at TOP LEVEL (between commas), -- split_top_level_commas only skips block comments at TOP LEVEL (between commas),
-- not block comments embedded WITHIN a token between a parameter and a trailing comma. -- not block comments embedded WITHIN a token between a parameter and a trailing comma.
-- Without this strip, the identifier-walk below stops at the `/` of `*/` and returns -- Without this strip, the identifier-walk below stops at the `/` of `*/` and returns
-- the wrong name (or nothing). See `test_extract_arg_names_handles_trailing_block_comments`. -- the wrong name (or nothing). See `test_extract_arg_names_handles_trailing_block_comments`.
local trimmed_end = #trimmed local trimmed_end = #trimmed ---@type integer
if trimmed_end >= 2 and trimmed:sub(trimmed_end - 1, trimmed_end) == "*/" then if trimmed_end >= 2 and trimmed:sub(trimmed_end - 1, trimmed_end) == "*/" then
-- Find the matching `/*` that opens the trailing comment. -- Find the matching `/*` that opens the trailing comment.
-- Walk back from the `*/` looking for `/*` (whitespace + `/*`). -- Walk back from the `*/` looking for `/*` (whitespace + `/*`).
local close_pos = trimmed_end - 1 -- position of the second-to-last char local close_pos = trimmed_end - 1 ---@type integer -- position of the second-to-last char
-- Walk back: skip trailing whitespace, then look for the `/*` opener. -- Walk back: skip trailing whitespace, then look for the `/*` opener.
while close_pos > 1 do while close_pos > 1 do
local ch = trimmed:sub(close_pos, close_pos) local ch = trimmed:sub(close_pos, close_pos) ---@type string
if ch == " " or ch == "\t" or ch == "\n" or ch == "\r" then if ch == " " or ch == "\t" or ch == "\n" or ch == "\r" then
close_pos = close_pos - 1 close_pos = close_pos - 1
else else
@@ -150,8 +155,8 @@ local function extract_arg_names(args_str)
end end
end end
-- Now scan back from close_pos for the `/*` opener (slashes are at close_pos-1 and close_pos-2). -- Now scan back from close_pos for the `/*` opener (slashes are at close_pos-1 and close_pos-2).
local opener_pos = nil local opener_pos = nil ---@type integer|nil
local scan = close_pos - 3 local scan = close_pos - 3 ---@type integer
while scan >= 1 do while scan >= 1 do
if trimmed:sub(scan, scan + 1) == "/*" then if trimmed:sub(scan, scan + 1) == "/*" then
opener_pos = scan opener_pos = scan
@@ -170,9 +175,9 @@ local function extract_arg_names(args_str)
trimmed_end = #trimmed trimmed_end = #trimmed
if trimmed_end >= 4 and trimmed:sub(trimmed_end, trimmed_end) == "]" then if trimmed_end >= 4 and trimmed:sub(trimmed_end, trimmed_end) == "]" then
-- Walk back: skip digits, expect `[`. -- Walk back: skip digits, expect `[`.
local bracket_pos = trimmed_end - 1 local bracket_pos = trimmed_end - 1 ---@type integer
while bracket_pos > 1 do while bracket_pos > 1 do
local ch = trimmed:sub(bracket_pos, bracket_pos) local ch = trimmed:sub(bracket_pos, bracket_pos) ---@type string
if ch >= "0" and ch <= "9" then if ch >= "0" and ch <= "9" then
bracket_pos = bracket_pos - 1 bracket_pos = bracket_pos - 1
else else
@@ -186,18 +191,18 @@ local function extract_arg_names(args_str)
if trimmed == "" then goto continue end if trimmed == "" then goto continue end
-- Find the identifier at the end: walk back over trailers (whitespace + `*` + `[]`), -- Find the identifier at the end: walk back over trailers (whitespace + `*` + `[]`),
-- then walk back over the identifier chars (alnum + `_`). -- then walk back over the identifier chars (alnum + `_`).
local ident_end = #trimmed local ident_end = #trimmed ---@type integer
while ident_end > 0 do while ident_end > 0 do
local ch = trimmed:sub(ident_end, ident_end) local ch = trimmed:sub(ident_end, ident_end) ---@type string
if ch == " " or ch == "\t" or ch == "*" or ch == "]" or ch == "[" then if ch == " " or ch == "\t" or ch == "*" or ch == "]" or ch == "[" then
ident_end = ident_end - 1 ident_end = ident_end - 1
else else
break break
end end
end end
local ident_start = ident_end local ident_start = ident_end ---@type integer
while ident_start > 0 do while ident_start > 0 do
local ch = trimmed:sub(ident_start, ident_start) local ch = trimmed:sub(ident_start, ident_start) ---@type string
if duffle.is_alnum_byte(string.byte(ch)) or ch == "_" then if duffle.is_alnum_byte(string.byte(ch)) or ch == "_" then
ident_start = ident_start - 1 ident_start = ident_start - 1
else else
@@ -205,7 +210,7 @@ local function extract_arg_names(args_str)
end end
end end
ident_start = ident_start + 1 ident_start = ident_start + 1
local name = trimmed:sub(ident_start, ident_end) local name = trimmed:sub(ident_start, ident_end) ---@type string
if name ~= "" then names[#names + 1] = name end if name ~= "" then names[#names + 1] = name end
::continue:: ::continue::
end end
@@ -214,8 +219,10 @@ local function extract_arg_names(args_str)
return names return names
end end
--- @param args_str string|nil
--- @return string[]|nil
local function formal_arg_names(args_str) local function formal_arg_names(args_str)
local names = extract_arg_names(args_str) local names = extract_arg_names(args_str) ---@type string[]|nil
if not names then return nil end if not names then return nil end
if names[1] == "ab" then table.remove(names, 1) end if names[1] == "ab" then table.remove(names, 1) end
if #names == 0 then return nil end if #names == 0 then return nil end
@@ -234,11 +241,11 @@ end
--- Carries the scanner-owned `debug_skip` flag forward so the generated projection can emit `/* atom_dbg_skip */` --- Carries the scanner-owned `debug_skip` flag forward so the generated projection can emit `/* atom_dbg_skip */`
--- before the authored comment and so `update_canonical_components` can mirror the same field onto `corpus.components[name]`. --- before the authored comment and so `update_canonical_components` can mirror the same field onto `corpus.components[name]`.
--- @param source string -- the full source text (needed for backward lookups) --- @param source string -- the full source text (needed for backward lookups)
--- @param scan table -- SourceScan from duffle.scan_source --- @param scan SourceScan
--- @return Component[] --- @return Component[]
local function project_components(source, scan) local function project_components(source, scan)
local out = {} local out = {} ---@type Component[]
for _, a in ipairs(scan.atoms) do for _, a in ipairs(scan.atoms) do ---@type integer, AtomEntry
-- Only `MipsAtomComp_(ac_X)` (kind="comp_bare") and `MipsAtomComp_Proc_(ac_X, ...)` (kind="comp_proc") -- Only `MipsAtomComp_(ac_X)` (kind="comp_bare") and `MipsAtomComp_Proc_(ac_X, ...)` (kind="comp_proc")
-- are COMPONENTS — they get inlined via `mac_<name>` aliases inside atom bodies. -- are COMPONENTS — they get inlined via `mac_<name>` aliases inside atom bodies.
-- `MipsAtom_Proc_` (kind="atom_proc") is an ATOM (ends with `mac_yield()`); it gets emitted via -- `MipsAtom_Proc_` (kind="atom_proc") is an ATOM (ends with `mac_yield()`); it gets emitted via
@@ -249,10 +256,10 @@ local function project_components(source, scan)
-- Function-args lookup is meaningful for `MipsAtomComp_Proc_` components -- Function-args lookup is meaningful for `MipsAtomComp_Proc_` components
-- (the macro sits inside `FI_ Slice_MipsCode ac_X(...)`); the alias expansion -- (the macro sits inside `FI_ Slice_MipsCode ac_X(...)`); the alias expansion
-- discards the `ab` (atom-builder) arg the same way both forms do. -- discards the `ab` (atom-builder) arg the same way both forms do.
local args = find_function_args_for(source, a.raw_name, a.ident_pos) local args = find_function_args_for(source, a.raw_name, a.ident_pos) ---@type string|nil
-- Comment ownership: scan_source.lua stamps `declaration_comment` on the record by walking backward past any associated bare marker. -- Comment ownership: scan_source.lua stamps `declaration_comment` on the record by walking backward past any associated bare marker.
-- The pass reads `declaration_comment` directly. -- The pass reads `declaration_comment` directly.
local comment = a.declaration_comment or "" local comment = a.declaration_comment or "" ---@type string
out[#out + 1] = { out[#out + 1] = {
line = a.line, line = a.line,
name = a.name, name = a.name,
@@ -283,23 +290,23 @@ end
--- @param s string --- @param s string
--- @return string --- @return string
local function convert_line_comments_to_block(s) local function convert_line_comments_to_block(s)
local result = s local result = s ---@type string
local pos = 1 local pos = 1 ---@type integer
local len = #result local len = #result ---@type integer
while pos <= len do while pos <= len do
local is_double_slash = result:byte(pos) == BYTE_SLASH local is_double_slash = result:byte(pos) == BYTE_SLASH ---@type boolean
and pos + 1 <= len and result:byte(pos + 1) == BYTE_SLASH and pos + 1 <= len and result:byte(pos + 1) == BYTE_SLASH
if not is_double_slash then if not is_double_slash then
pos = pos + 1 pos = pos + 1
else else
-- Find end of line. -- Find end of line.
local eol = pos local eol = pos ---@type integer
while eol <= len and result:byte(eol) ~= BYTE_NEWLINE do while eol <= len and result:byte(eol) ~= BYTE_NEWLINE do
eol = eol + 1 eol = eol + 1
end end
local before = result:sub(1, pos - 1) local before = result:sub(1, pos - 1) ---@type string
local comment = result:sub(pos + 2, eol - 1) -- skip the `//` local comment = result:sub(pos + 2, eol - 1) ---@type string -- skip the `//`
local after local after ---@type string
if eol <= len and result:byte(eol) == BYTE_NEWLINE then if eol <= len and result:byte(eol) == BYTE_NEWLINE then
after = " */" .. result:sub(eol) -- keep the newline after = " */" .. result:sub(eol) -- keep the newline
else else
@@ -329,25 +336,54 @@ local function strip_mac_prefix(ident)
return ident return ident
end 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) ---@type string|nil
if not ident or not duffle.DELAY_MARKERS[ident] then return tok end
local rest = tok:sub(#ident + 1):match("^%s*(.*)$") or "" ---@type string
while rest:sub(1, 2) == "/*" do
local close = rest:find("*/", 3, true) ---@type integer|nil
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 --- (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). --- 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_`) --- @param name string -- the component name (without `mac_`)
--- @param comp_by_name table<string, Component> --- @param comp_by_name table<string, Component>
--- @param wc table<string, integer> --- @param wc WordCounts
--- @param cache table<string, integer> --- @param cache table<string, integer> -- bag: name -> count; -1 in-progress sentinel
--- @return integer --- @return integer
local function word_count_rec(name, comp_by_name, wc, cache) local function word_count_rec(name, comp_by_name, wc, cache)
if cache[name] ~= nil then return cache[name] end if cache[name] ~= nil then return cache[name] end
cache[name] = -1 -- mark in-progress (cycle detection) cache[name] = -1 -- mark in-progress (cycle detection)
local cc = comp_by_name[name] local cc = comp_by_name[name] ---@type Component|nil
local n local n ---@type integer
if cc then if cc then
n = 0 n = 0
local tokens = cc.body_tokens local tokens = cc.body_tokens ---@type BodyToken[]
for _, t in ipairs(tokens) do for _, t in ipairs(tokens) do ---@type integer, BodyToken
local trimmed = t.tok local trimmed = t.tok ---@type string
if trimmed ~= "" then if trimmed ~= "" then
local lookup = strip_mac_prefix(duffle.read_ident(trimmed, 1)) local work = trimmed ---@type string
while true do
local marker = duffle.read_ident(work, 1) ---@type string|nil
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)) ---@type string|nil
if lookup == "atom_label" or lookup == "atom_offset" then if lookup == "atom_label" or lookup == "atom_offset" then
-- Pure metaprogram anchors; emit zero words. -- Pure metaprogram anchors; emit zero words.
elseif lookup and comp_by_name[lookup] then elseif lookup and comp_by_name[lookup] then
@@ -362,6 +398,7 @@ local function word_count_rec(name, comp_by_name, wc, cache)
end end
end end
end end
end
else else
-- Not a known component: assume 1 word (regular instruction). -- Not a known component: assume 1 word (regular instruction).
n = 1 n = 1
@@ -376,14 +413,14 @@ end
--- references hit memoized values instead of re-walking the body. --- references hit memoized values instead of re-walking the body.
--- Cycle detection (A -> B -> A) is preserved via the in-progress `-1` sentinel in `cache`. --- Cycle detection (A -> B -> A) is preserved via the in-progress `-1` sentinel in `cache`.
--- @param components Component[] --- @param components Component[]
--- @param wc table<string, integer> --- @param wc WordCounts
--- @return table<string, integer> -- map of component name (without `mac_`) -> word count --- @return table<string, integer> -- bag: bare component name -> word count
local function count_all_components(components, wc) local function count_all_components(components, wc)
local comp_by_name = {} local comp_by_name = {} ---@type table<string, Component>
for _, cc in ipairs(components) do comp_by_name[cc.name] = cc end for _, cc in ipairs(components) do comp_by_name[cc.name] = cc end ---@type integer, Component
local cache = {} local cache = {} ---@type table<string, integer> -- bag: memo; -1 in-progress sentinel
local counts = {} local counts = {} ---@type table<string, integer> -- bag: bare name -> word count
for _, c in ipairs(components) do for _, c in ipairs(components) do ---@type integer, Component
counts[c.name] = word_count_rec(c.name, comp_by_name, wc, cache) counts[c.name] = word_count_rec(c.name, comp_by_name, wc, cache)
end end
return counts return counts
@@ -398,79 +435,52 @@ end
-- Always walk the original `MipsAtomComp_` body via `cc.body_tokens`. -- Always walk the original `MipsAtomComp_` body via `cc.body_tokens`.
-- ═══════════════════════════════════════════ -- ═══════════════════════════════════════════
--- (internal) Recursive cycle-cost derivation. Sum `latency[ident]` per emitted instruction in the component body, --- (internal) One walk of a component body that fills both `cycle_cost` and `gp0_contrib`.
--- recursing through nested `mac_*` calls (so `mac_format_g4_color`'s cost = 4 × `mac_pack_color_word`'s cost). --- Cycle: sum `isa.cycles` / `gte.cycles` / `latency[ident]` / 1 per leaf, recurse `mac_*`.
--- Special rule: `mac_yield`'s cost = 0 (per `lottes_tape.h:125-130` "the runtime cost lands in the next atom's prologue"). --- `mac_yield` cycle_cost is 0 (runtime cost lands in the next atom's prologue); its gp0 still comes from the token walk.
--- GP0: count `gte_sw` and `store_word` / `store_half` / `store_byte` that target `R_PrimCursor` / `O_(Poly_` / `r_prim_cursor` / `r_primitive_cursor` / `r_base`.
--- `insert_ot_tag*` gp0_contrib is 0; cycle still comes from the body walk.
--- Missing component: cycle 1, gp0 0.
--- @param name string -- component bare name (e.g. "yield", "pack_color_word") --- @param name string -- component bare name (e.g. "yield", "pack_color_word")
--- @param comp_by_name table<string, Component> --- @param comp_by_name table<string, Component>
--- @param latency table<string, integer> --- @param latency table<string, integer> -- bag: ident -> cycle cost
--- @param cache table<string, integer> -- shared memoization; `-1` sentinel detects cycles --- @param cache ComponentMetaMap
--- @return integer --- @return ComponentMeta
local function cycle_cost_rec(name, comp_by_name, latency, cache) local function component_meta_rec(name, comp_by_name, latency, cache)
if cache[name] ~= nil then return cache[name] end if cache[name] ~= nil then return cache[name] end
cache[name] = -1 cache[name] = { cycle_cost = -1, gp0_contrib = -1 }
local cc = comp_by_name[name] local cc = comp_by_name[name] ---@type Component|nil
local n local cycle_cost ---@type integer
local gp0_contrib ---@type integer
if cc then if cc then
if name == "yield" then local skip_cycle = (name == "yield") ---@type boolean
-- mac_yield's cost is 0 by convention (the runtime cost lands in the next atom's prologue). local skip_gp0 = name:match("^insert_ot_tag") ~= nil ---@type boolean
n = 0 cycle_cost = 0
else gp0_contrib = 0
n = 0 if not skip_cycle or not skip_gp0 then
local tokens = cc.body_tokens local tokens = cc.body_tokens ---@type BodyToken[]
for _, t in ipairs(tokens) do for _, t in ipairs(tokens) do ---@type integer, BodyToken
local trimmed = t.tok local trimmed = t.tok ---@type string
if trimmed ~= "" then if trimmed ~= "" then
local ident = duffle.read_ident(trimmed, 1) local ident = duffle.read_ident(trimmed, 1) ---@type string|nil
if ident and ident:sub(1, MAC_PREFIX_LEN) == MAC_PREFIX then if ident and ident:sub(1, MAC_PREFIX_LEN) == MAC_PREFIX then
-- Nested `mac_X(...)` call: recurse. local nested = ident:sub(MAC_PREFIX_LEN + 1) ---@type string
local nested = ident:sub(MAC_PREFIX_LEN + 1) local nested_meta = component_meta_rec(nested, comp_by_name, latency, cache) ---@type ComponentMeta
n = n + cycle_cost_rec(nested, comp_by_name, latency, cache) if not skip_cycle then
else cycle_cost = cycle_cost + nested_meta.cycle_cost
-- Leaf instruction or pseudo-macro.
local isa = duffle.instr(ident)
local gte = duffle.gte(ident)
n = n + ((isa and isa.cycles) or (gte and gte.cycles) or latency[ident] or 1)
end
end
end end
if not skip_gp0 then
gp0_contrib = gp0_contrib + nested_meta.gp0_contrib
end end
else else
n = 1 if not skip_cycle then
local isa = duffle.instr(ident) ---@type InstructionRow|nil
local gte = duffle.gte(ident) ---@type GteCommandRow|nil
cycle_cost = cycle_cost + ((isa and isa.cycles) or (gte and gte.cycles) or latency[ident] or 1)
end end
cache[name] = n if not skip_gp0 then
return n if ident == "gte_sw" then
end gp0_contrib = gp0_contrib + 1
--- (internal) Recursive GP0 prim-buffer contribution. Count `store_word` / `store_half` / `store_byte`
--- calls in the component body that target `R_PrimCursor` (these are the RAM-side prim-buffer words the macro contributes), recursing through nested `mac_*` calls.
--- Only `R_PrimCursor`-targeting stores count. Stores targeting other registers (e.g. `R_OtBase`, heap pointers) are not prim-buffer contributions.
--- @param name string
--- @param comp_by_name table<string, Component>
--- @param cache table<string, integer>
--- @return integer
local function gp0_contrib_rec(name, comp_by_name, cache)
if name:match("^insert_ot_tag") then
cache[name] = 0
return 0
end
if cache[name] ~= nil then return cache[name] end
cache[name] = -1
local cc = comp_by_name[name]
local n
if cc then
n = 0
local tokens = cc.body_tokens
for _, t in ipairs(tokens) do
local trimmed = t.tok
if trimmed ~= "" then
local ident = duffle.read_ident(trimmed, 1)
if ident and ident:sub(1, MAC_PREFIX_LEN) == MAC_PREFIX then
-- Nested `mac_X(...)` call: recurse.
local nested = ident:sub(MAC_PREFIX_LEN + 1)
n = n + gp0_contrib_rec(nested, comp_by_name, cache)
elseif ident == "gte_sw" then
n = n + 1
elseif ident == "store_word" or ident == "store_half" or ident == "store_byte" then elseif ident == "store_word" or ident == "store_half" or ident == "store_byte" then
if trimmed:find("R_PrimCursor", 1, true) if trimmed:find("R_PrimCursor", 1, true)
or trimmed:find("O_(Poly_", 1, true) or trimmed:find("O_(Poly_", 1, true)
@@ -478,35 +488,34 @@ local function gp0_contrib_rec(name, comp_by_name, cache)
or trimmed:find("r_primitive_cursor", 1, true) or trimmed:find("r_primitive_cursor", 1, true)
or trimmed:find("r_base", 1, true) or trimmed:find("r_base", 1, true)
then then
n = n + 1 gp0_contrib = gp0_contrib + 1
end
end
end
end end
end end
end end
end end
else else
n = 0 cycle_cost = 1
gp0_contrib = 0
end end
cache[name] = n cache[name] = { cycle_cost = cycle_cost, gp0_contrib = gp0_contrib }
return n return cache[name]
end end
--- Compute `cycle_cost` + `gp0_contrib` for every component in `components` in a single pass. --- Compute `cycle_cost` + `gp0_contrib` for every component in `components` in a single pass.
--- Memoization cache is built ONCE (per source) and shared across both helpers so that --- One memoization cache; a nested `mac_Y` inside a `mac_X` body computes both fields once.
--- a nested `mac_Y` reference inside a `mac_X` body computes its values once.
--- @param components Component[] --- @param components Component[]
--- @param latency table<string, integer> --- @param latency table<string, integer> -- bag: ident -> cycle cost
--- @return table<string, {cycle_cost=integer, gp0_contrib=integer}> --- @return ComponentMetaMap
local function compute_components_metadata(components, latency) local function compute_components_metadata(components, latency)
local comp_by_name = {} local comp_by_name = {} ---@type table<string, Component>
for _, cc in ipairs(components) do comp_by_name[cc.name] = cc end for _, cc in ipairs(components) do comp_by_name[cc.name] = cc end ---@type integer, Component
local cc_cache = {} local cache = {} ---@type ComponentMetaMap
local gc_cache = {} local out = {} ---@type ComponentMetaMap
local out = {} for _, c in ipairs(components) do ---@type integer, Component
for _, c in ipairs(components) do out[c.name] = component_meta_rec(c.name, comp_by_name, latency, cache)
out[c.name] = {
cycle_cost = cycle_cost_rec(c.name, comp_by_name, latency, cc_cache),
gp0_contrib = gp0_contrib_rec(c.name, comp_by_name, gc_cache),
}
end end
return out return out
end end
@@ -520,11 +529,11 @@ end
--- @param s string --- @param s string
--- @return string[] --- @return string[]
local function split_comment_lines(s) local function split_comment_lines(s)
local out = {} local out = {} ---@type string[]
local pos = 1 local pos = 1 ---@type integer
local s_len = #s local s_len = #s ---@type integer
while pos <= s_len do while pos <= s_len do
local nl = s:find("\n", pos, true) local nl = s:find("\n", pos, true) ---@type integer|nil
if not nl then if not nl then
out[#out + 1] = s:sub(pos) out[#out + 1] = s:sub(pos)
break break
@@ -543,7 +552,7 @@ end
--- @param args_str string|nil --- @param args_str string|nil
--- @return string --- @return string
local function signature_from_args(args_str) local function signature_from_args(args_str)
local names = formal_arg_names(args_str) local names = formal_arg_names(args_str) ---@type string[]|nil
if names then if names then
return table.concat(names, ", ") return table.concat(names, ", ")
end end
@@ -552,63 +561,59 @@ end
--- Strip the trailing `" \"` (space + backslash) line continuation from the last body line. --- Strip the trailing `" \"` (space + backslash) line continuation from the last body line.
--- The last 2 chars are always that pair. --- The last 2 chars are always that pair.
--- @param lines string[]
--- @return nil
local function strip_trailing_continuation(lines) local function strip_trailing_continuation(lines)
local last = lines[#lines] local last = lines[#lines] ---@type string
if last:sub(-2) == " \\" then if last:sub(-2) == " \\" then
lines[#lines] = last:sub(1, -3) lines[#lines] = last:sub(1, -3)
end end
end end
--- Classify a token as a "pure delay marker token" (a delay-marker identifier --- Classify a token as a "pure delay marker token" (a delay-marker identifier with no following instruction — only whitespace and/or block comments).
--- with no following instruction — only whitespace and/or block comments).
--- Examples that match: --- Examples that match:
--- * `GteDelay_` → marker alone --- * `GteDelay_` → marker alone
--- * `GteDelay_ /* RT diagonal: D1 = a.x... */` → marker + block comment --- * `GteDelay_ /* RT diagonal: D1 = a.x... */` → marker + block comment
--- * `GteDelay_ /* RT diagonal: ... */\n\t` → marker + comment + trailing whitespace --- * `GteDelay_ /* RT diagonal: ... */\n\t` → marker + comment + trailing whitespace
--- Examples that DO NOT match (these contain a real instruction after the marker --- Examples that DO NOT match (these contain a real instruction after the marker and must be preserved verbatim so the instruction still gets emitted):
--- and must be preserved verbatim so the instruction still gets emitted):
--- * `GteDelay_ nop2` --- * `GteDelay_ nop2`
--- * `GteDelay_ add_si(r.dst_ptr, r.scratch, dst_offset)` --- * `GteDelay_ add_si(r.dst_ptr, r.scratch, dst_offset)`
--- ---
--- Why this classification matters: the metaprogram emits tokens separated by `,` --- Why this classification matters: the metaprogram emits tokens separated by `,` and joins them with `\<newline>` line continuations. After C preprocessor
--- and joins them with `\<newline>` line continuations. After C preprocessor
--- phase 2 (line splicing), the macro body collapses to a single logical line. --- phase 2 (line splicing), the macro body collapses to a single logical line.
--- Each delay-marker identifier expands to empty (its definition --- Each delay-marker identifier expands to empty (its definition `#define GteDelay_ // ...` consumes the `//` line comment during preprocessing
--- `#define GteDelay_ // ...` consumes the `//` line comment during preprocessing --- of the definition itself, leaving an empty replacement list).
--- of the definition itself, leaving an empty replacement list). When a token --- When a token is purely a delay marker with only a trailing comment, the `,` the metaprogram normally adds before
--- is purely a delay marker with only a trailing comment, the `,` the metaprogram --- each token-after-the-first brackets empty content and produces the syntax error `,,` (`expected expression before ',' token`) at C compile.
--- normally adds before each token-after-the-first brackets empty content and --- The metaprogram therefore emits such tokens WITHOUT the leading `,` (see `token_skips_leading_comma`) —
--- produces the syntax error `,,` (`expected expression before ',' token`) at --- but the marker + trailing comment are still emitted verbatim so the annotation is preserved in `gen/macs.h`.
--- C compile. The metaprogram therefore emits such tokens WITHOUT the leading
--- `,` (see `token_skips_leading_comma`) — but the marker + trailing comment
--- are still emitted verbatim so the annotation is preserved in `gen/macs.h`.
--- @param tok string -- a single token from split_top_level_commas (already trimmed at the start, may contain trailing whitespace + block comment) --- @param tok string -- a single token from split_top_level_commas (already trimmed at the start, may contain trailing whitespace + block comment)
--- @return boolean --- @return boolean
local function is_pure_delay_marker_token(tok) local function is_pure_delay_marker_token(tok)
local markers = duffle.DELAY_MARKERS local markers = duffle.DELAY_MARKERS ---@type table<string, boolean> -- bag: delay-marker ident -> true
if type(markers) ~= "table" then return false end if type(markers) ~= "table" then return false end
-- Identify a leading delay-marker identifier (e.g. `GteDelay_`). -- Identify a leading delay-marker identifier (e.g. `GteDelay_`).
local ident_end = 1 local ident_end = 1 ---@type integer
while ident_end <= #tok do while ident_end <= #tok do
local ch = tok:sub(ident_end, ident_end) local ch = tok:sub(ident_end, ident_end) ---@type string
if ch:match("[%w_]") then if ch:match("[%w_]") then
ident_end = ident_end + 1 ident_end = ident_end + 1
else else
break break
end end
end end
local ident = tok:sub(1, ident_end - 1) local ident = tok:sub(1, ident_end - 1) ---@type string
if not markers[ident] then return false end if not markers[ident] then return false end
-- Walk the remainder: only whitespace and block comments are allowed. -- Walk the remainder: only whitespace and block comments are allowed.
local scan = ident_end local scan = ident_end ---@type integer
while scan <= #tok do while scan <= #tok do
local ch = tok:sub(scan, scan) local ch = tok:sub(scan, scan) ---@type string
if ch:match("%s") then if ch:match("%s") then
scan = scan + 1 scan = scan + 1
elseif ch == "/" and tok:sub(scan + 1, scan + 1) == "*" then elseif ch == "/" and tok:sub(scan + 1, scan + 1) == "*" then
local close = tok:find("*/", scan + 2, true) local close = tok:find("*/", scan + 2, true) ---@type integer|nil
if not close then return false end if not close then return false end
scan = close + 2 scan = close + 2
else else
@@ -625,17 +630,14 @@ end
--- followed by whitespace + optional block comment and NOTHING ELSE) expand --- followed by whitespace + optional block comment and NOTHING ELSE) expand
--- to empty at C preprocessor time. Emitting them WITHOUT the leading `,` --- to empty at C preprocessor time. Emitting them WITHOUT the leading `,`
--- separator that the metaprogram normally adds before each token after the --- separator that the metaprogram normally adds before each token after the
--- first keeps exactly one `,` between the surrounding real expressions in --- first keeps exactly one `,` between the surrounding real expressions in the spliced macro body:
--- the spliced macro body:
---
--- * before this rule: `<tok1> ,\t<gdelay> ,\t<tok3>` → after expansion --- * before this rule: `<tok1> ,\t<gdelay> ,\t<tok3>` → after expansion
--- `<tok1> , /* comment */ , <tok3>` → `,,` syntax error. --- `<tok1> , /* comment */ , <tok3>` → `,,` syntax error.
--- * after this rule: `<tok1> \t<gdelay> ,\t<tok3>` → after expansion --- * after this rule: `<tok1> \t<gdelay> ,\t<tok3>` → after expansion
--- `<tok1> /* comment */ , <tok3>` → `<tok1>, <tok3>` — valid. --- `<tok1> /* comment */ , <tok3>` → `<tok1>, <tok3>` — valid.
--- ---
--- Tokens like `GteDelay_ nop2` keep the leading `,` (the marker is followed --- Tokens like `GteDelay_ nop2` keep the leading `,`
--- by a real instruction, so the marker + instruction together need the --- (the marker is followed by a real instruction, so the marker + instruction together need the separator on the LEFT to land between two real expressions).
--- separator on the LEFT to land between two real expressions).
--- @param tok string --- @param tok string
--- @return boolean -- true if the token needs NO leading `,` separator. --- @return boolean -- true if the token needs NO leading `,` separator.
local function token_skips_leading_comma(tok) local function token_skips_leading_comma(tok)
@@ -645,16 +647,24 @@ end
--- Emit the `#define mac_X(sig) \<newline>\t<tok1> \<newline>,\t<tok2> ...` block. --- Emit the `#define mac_X(sig) \<newline>\t<tok1> \<newline>,\t<tok2> ...` block.
--- Converts `//` line comments to `/* */` block comments in each token so they don't break the C macro `\` line continuations. --- Converts `//` line comments to `/* */` block comments in each token so they don't break the C macro `\` line continuations.
--- ---
--- Pure delay-marker tokens (`GteDelay_` / `LdSlot_` / `BdSlot_` / `DmaSlot_` with only a trailing block comment, no real instruction) are emitted WITHOUT a leading `,` separator; the annotation IS preserved in the generated header (so the comment + marker remain visible to anyone reading `gen/macs.h`), but the C preprocessor expands the marker to empty, so leaving the `,` separator out is what stops the `,,` syntax error. See `token_skips_leading_comma` for the contract. --- Pure delay-marker tokens (`GteDelay_` / `LdSlot_` / `BdSlot_` / `DmaSlot_` with only a trailing block comment, no real instruction) are emitted WITHOUT a leading `,` separator;
--- the annotation IS preserved in the generated header
--- (so the comment + marker remain visible to anyone reading `gen/macs.h`), but the C preprocessor expands the marker to empty, so leaving the `,`
--- separator out is what stops the `,,` syntax error. See `token_skips_leading_comma` for the contract.
--- @param lines string[]
--- @param c Component
--- @param sig string
--- @param tokens string[]
--- @return nil
local function emit_macro_body(lines, c, sig, tokens) local function emit_macro_body(lines, c, sig, tokens)
for tok_idx = 1, #tokens do for tok_idx = 1, #tokens do ---@type integer
tokens[tok_idx] = convert_line_comments_to_block(tokens[tok_idx]) tokens[tok_idx] = convert_line_comments_to_block(tokens[tok_idx])
end end
if #tokens == 0 then return end if #tokens == 0 then return end
lines[#lines + 1] = "#define mac_" .. c.name .. "(" .. sig .. ") \\" lines[#lines + 1] = "#define mac_" .. c.name .. "(" .. sig .. ") \\"
lines[#lines + 1] = "\t" .. tokens[1] .. " \\" lines[#lines + 1] = "\t" .. tokens[1] .. " \\"
for tok_idx = 2, #tokens do for tok_idx = 2, #tokens do ---@type integer
local sep = token_skips_leading_comma(tokens[tok_idx]) and "\t" or ",\t" local sep = token_skips_leading_comma(tokens[tok_idx]) and "\t" or ",\t" ---@type string
lines[#lines + 1] = sep .. tokens[tok_idx] .. " \\" lines[#lines + 1] = sep .. tokens[tok_idx] .. " \\"
end end
strip_trailing_continuation(lines) strip_trailing_continuation(lines)
@@ -666,11 +676,10 @@ end
--- The marker is a single line, the comment comes next, and the `#define` line follows. The `debug_skip` stamp is scanner-owned --- The marker is a single line, the comment comes next, and the `#define` line follows. The `debug_skip` stamp is scanner-owned
--- (`a.debug_skip == true` on the declaration record); the components pass projects it directly. --- (`a.debug_skip == true` on the declaration record); the components pass projects it directly.
--- @param c Component --- @param c Component
--- @param components Component[] --- @param counts table<string, integer> -- bag: bare component name -> word count
--- @param wc table<string, integer>
--- @return string[] -- list of lines for this component --- @return string[] -- list of lines for this component
local function build_component_lines(c, counts) local function build_component_lines(c, counts)
local lines = {} local lines = {} ---@type string[]
-- Marker comment: emitted once for every skipped component. -- Marker comment: emitted once for every skipped component.
-- The marker is scanner-owned (declared by `atom_dbg_skip` immediately before the declaration in the source); -- The marker is scanner-owned (declared by `atom_dbg_skip` immediately before the declaration in the source);
@@ -680,16 +689,16 @@ local function build_component_lines(c, counts)
end end
if c.comment and c.comment ~= "" then if c.comment and c.comment ~= "" then
for _, line in ipairs(split_comment_lines(c.comment)) do for _, line in ipairs(split_comment_lines(c.comment)) do ---@type integer, string
lines[#lines + 1] = line lines[#lines + 1] = line
end end
end end
local tokens = duffle.split_top_level_commas(c.body) local tokens = duffle.split_top_level_commas(c.body) ---@type string[]
for i = 1, #tokens do tokens[i] = duffle.trim(tokens[i]) end for i = 1, #tokens do tokens[i] = duffle.trim(tokens[i]) end ---@type integer
local sig = signature_from_args(c.args) local sig = signature_from_args(c.args) ---@type string
-- Direct lookup against the per-source precomputed `counts` table (built once by count_all_components). -- Direct lookup against the per-source precomputed `counts` table (built once by count_all_components).
local n = counts[c.name] local n = counts[c.name] ---@type integer
if n > 0 then if n > 0 then
emit_macro_body(lines, c, sig, tokens) emit_macro_body(lines, c, sig, tokens)
@@ -712,11 +721,11 @@ end
--- @param sources SourceFile[] -- Sources contributing to this directory (for the header comment) --- @param sources SourceFile[] -- Sources contributing to this directory (for the header comment)
--- @return string[] --- @return string[]
local function header_boilerplate(dir, sources) local function header_boilerplate(dir, sources)
local source_lines = { "// Directory: " .. duffle.to_absolute_path(dir) .. "/" } local source_lines = { "// Directory: " .. duffle.to_absolute_path(dir) .. "/" } ---@type string[]
for _, src in ipairs(sources) do for _, src in ipairs(sources) do ---@type integer, SourceFile
source_lines[#source_lines + 1] = "// source: " .. duffle.to_absolute_path(src.path) source_lines[#source_lines + 1] = "// source: " .. duffle.to_absolute_path(src.path)
end end
local source_blob = table.concat(source_lines, "\n") local source_blob = table.concat(source_lines, "\n") ---@type string
return { return {
-- #pragma once wrapped in #ifdef INTELLISENSE_DIRECTIVES, matching the convention in lottes_tape.h. -- #pragma once wrapped in #ifdef INTELLISENSE_DIRECTIVES, matching the convention in lottes_tape.h.
-- The build does manual unity includes (the user controls include order), so the pragma is only active for IDE/tooling. -- The build does manual unity includes (the user controls include order), so the pragma is only active for IDE/tooling.
@@ -744,8 +753,8 @@ end
--- @return string -- Output directory --- @return string -- Output directory
--- @return string -- Full output path --- @return string -- Full output path
local function compute_macs_h_path(dir) local function compute_macs_h_path(dir)
local out_dir = dir .. "/" .. GEN_SUBDIR local out_dir = dir .. "/" .. GEN_SUBDIR ---@type string
local out_path = out_dir .. "/" .. MACS_FILENAME local out_path = out_dir .. "/" .. MACS_FILENAME ---@type string
return out_dir, out_path return out_dir, out_path
end end
@@ -755,20 +764,20 @@ end
--- @param dir string -- Absolute source directory --- @param dir string -- Absolute source directory
--- @param sources SourceFile[] -- Sources contributing to this directory (for the header comment) --- @param sources SourceFile[] -- Sources contributing to this directory (for the header comment)
--- @param components Component[] -- Aggregated components from all sources in this directory --- @param components Component[] -- Aggregated components from all sources in this directory
--- @param counts table<string, integer> -- Precomputed word counts (from count_all_components) --- @param counts table<string, integer> -- bag: bare component name -> word count
--- @return string|nil -- Path to the written file (nil if no components) --- @return string|nil -- Path to the written file (nil if no components)
local function emit_component_macros_h(ctx, dir, sources, components, counts) local function emit_component_macros_h(ctx, dir, sources, components, counts)
if #components == 0 then return nil end if #components == 0 then return nil end
local out_dir, out_path = compute_macs_h_path(dir) local out_dir, out_path = compute_macs_h_path(dir) ---@type string, string
local lines = header_boilerplate(dir, sources) local lines = header_boilerplate(dir, sources) ---@type string[]
for _, c in ipairs(components) do for _, c in ipairs(components) do ---@type integer, Component
for _, l in ipairs(build_component_lines(c, counts)) do for _, l in ipairs(build_component_lines(c, counts)) do ---@type integer, string
lines[#lines + 1] = l lines[#lines + 1] = l
end end
end end
local content = table.concat(lines, "\n") .. "\n" local content = table.concat(lines, "\n") .. "\n" ---@type string
duffle.ensure_dir(out_dir) duffle.ensure_dir(out_dir)
duffle.write_file_lf(out_path, content) duffle.write_file_lf(out_path, content)
print(string.format(" -> %s", out_path)) print(string.format(" -> %s", out_path))
@@ -781,59 +790,55 @@ end
--- (internal) Extend `corpus.word_counts` with this source's component macros so offsets sees them without re-reading the file. --- (internal) Extend `corpus.word_counts` with this source's component macros so offsets sees them without re-reading the file.
--- First declaration wins: a later caller's count is dropped (the existing entry from the first source is preserved). --- First declaration wins: a later caller's count is dropped (the existing entry from the first source is preserved).
--- @param corpus table -- the corpus --- @param corpus Corpus
--- @param components Component[] --- @param components Component[]
--- @param counts table<string, integer> -- precomputed word counts (from count_all_components) --- @param counts table<string, integer> -- bag: bare component name -> word count
--- @return nil
local function update_canonical_word_counts(corpus, components, counts) local function update_canonical_word_counts(corpus, components, counts)
local wc = corpus.word_counts local wc = corpus.word_counts ---@type WordCounts
for _, c in ipairs(components) do for _, c in ipairs(components) do ---@type integer, Component
local key = "mac_" .. c.name local key = "mac_" .. c.name ---@type string
if wc[key] == nil then if wc[key] == nil then
wc[key] = counts[c.name] wc[key] = counts[c.name]
end end
end end
end end
--- @class ComponentDef --- (internal) Populate `corpus.components` with this source's one component row per bare name.
--- @field name string -- Bare name (without ac_/mac_ prefix)
--- @field line integer -- Definition source line (line of `MipsAtomComp_(ac_X)` / `MipsAtomComp_Proc_(ac_X, ...)`)
--- @field path string -- Absolute source path of the definition
--- @field kind string -- "comp_bare" | "comp_proc" (atom_proc is NOT a component)
--- @field debug_skip boolean -- Mirror of the scanner-owned `a.debug_skip`; consumers read this directly
--- (internal) Populate `corpus.components` with this source's components-by-name map.
--- First declaration wins; later declarations of the same bare name are dropped and recorded as a collision via `corpus.collisions` (kind = "component"). --- First declaration wins; later declarations of the same bare name are dropped and recorded as a collision via `corpus.collisions` (kind = "component").
--- The pass does NOT write to `ctx.shared.components`. --- The pass does NOT write to `ctx.shared.components`.
--- No parallel skip map is built here; consumers that need the per-component skip state read `corpus.components[name].debug_skip` directly. --- No parallel skip map is built here; consumers that need the per-component skip state read `corpus.components[name].debug_skip` directly.
--- The `cycle_cost` + `gp0_contrib` fields are populated from `metadata[c.name]` (computed by `compute_components_metadata` against the original `MipsAtomComp_` body). --- The `cycle_cost` + `gp0_contrib` fields are populated from `metadata[c.name]` (computed by `compute_components_metadata` against the original `MipsAtomComp_` body).
--- @param corpus table -- the corpus --- @param corpus Corpus
--- @param src SourceFile --- @param src SourceFile
--- @param components Component[] --- @param components Component[]
--- @param metadata table<string, {cycle_cost=integer, gp0_contrib=integer}> --- @param metadata ComponentMetaMap
local function update_canonical_components(corpus, src, components, metadata) --- @param scan SourceScan
local rel_path = src.path:gsub("\\", "/") --- @return nil
for _, c in ipairs(components) do local function update_canonical_components(corpus, src, components, metadata, scan)
local rel_path = src.path:gsub("\\", "/") ---@type string
local line_of = scan and scan.line_of ---@type (fun(pos: integer): integer)|nil
for _, c in ipairs(components) do ---@type integer, Component
-- Keyed by bare name (e.g. `yield`, `load_tri_indices`). -- Keyed by bare name (e.g. `yield`, `load_tri_indices`).
-- The atoms_source_map pass looks up components by bare name from the corpus; -- The atoms_source_map pass looks up components by bare name from the corpus;
-- `mac_` prefix lives at the call-site identifier and is stripped before lookup. -- `mac_` prefix lives at the call-site identifier and is stripped before lookup.
local m = metadata and metadata[c.name] or nil local m = metadata and metadata[c.name] or nil ---@type ComponentMeta|nil
if corpus.components[c.name] == nil then if corpus.components[c.name] == nil then
corpus.components[c.name] = { c.path = rel_path
name = c.name, c.source = src.path
line = c.line, c.line_of = line_of
path = rel_path, c.kind = c.kind or "comp_bare"
kind = c.kind or "comp_bare", c.debug_skip = c.debug_skip == true
debug_skip = c.debug_skip == true, c.cycle_cost = m and m.cycle_cost or nil
cycle_cost = m and m.cycle_cost or nil, c.gp0_contrib = m and m.gp0_contrib or nil
gp0_contrib = m and m.gp0_contrib or nil, corpus.components[c.name] = c
}
else else
-- A second declaration of the same bare name: record a typed collision so static-analysis + the report can surface it. -- A second declaration of the same bare name: record a typed collision so static-analysis + the report can surface it.
-- Identical-shape declarations (same path + line) reuse the first-wins entry without a collision record. -- Identical-shape declarations (same path + line) reuse the first-wins entry without a collision record.
local existing = corpus.components[c.name] local existing = corpus.components[c.name] ---@type Component
if existing.path ~= rel_path or existing.line ~= c.line then if existing.path ~= rel_path or existing.line ~= c.line then
local kind = c.kind or "comp_bare" local kind = c.kind or "comp_bare" ---@type string
local first_kind = existing.kind or "comp_bare" local first_kind = existing.kind or "comp_bare" ---@type string
corpus.collisions[#corpus.collisions + 1] = { corpus.collisions[#corpus.collisions + 1] = {
kind = "component", kind = "component",
name = c.name, name = c.name,
@@ -847,39 +852,15 @@ local function update_canonical_components(corpus, src, components, metadata)
end end
end end
--- (internal) Populate `corpus.component_body_index` with this source's body index entries.
--- First declaration wins; later declarations are dropped (no separate collision record: the components collision is already surfaced by `update_canonical_components`).
--- The pass writes to `corpus.component_body_index` only (the corpus owns this projection).
--- @param corpus table -- the corpus
--- @param src SourceFile
--- @param components Component[]
--- @param scan table -- the SourceScan payload (for line_of)
local function update_canonical_component_body_index(corpus, src, components, scan)
local line_of = scan and scan.line_of
for _, c in ipairs(components) do
if corpus.component_body_index[c.name] == nil then
corpus.component_body_index[c.name] = {
body_tokens = c.body_tokens,
body_off = c.body_off,
line_of = line_of,
source = src.path,
declaration = c.line,
kind = c.kind,
arg_names = c.arg_names,
}
end
end
end
--- @param ctx PassCtx --- @param ctx PassCtx
--- @return PassResult --- @return PassResult
function M.run(ctx) function M.run(ctx)
local outputs = {} local outputs = {} ---@type MacsOutput[]
local errors = {} local errors = {} ---@type Finding[]
local warnings = {} local warnings = {} ---@type Finding[]
-- Corpus ownership gate. -- Corpus ownership gate.
local corpus = ctx.shared and ctx.shared.corpus local corpus = ctx.shared and ctx.shared.corpus ---@type Corpus|nil
if type(corpus) ~= "table" then if type(corpus) ~= "table" then
error("components.run requires ctx.shared.corpus.", 0) error("components.run requires ctx.shared.corpus.", 0)
end end
@@ -894,41 +875,39 @@ function M.run(ctx)
-- Projection ownership: -- Projection ownership:
-- * `corpus.word_counts["mac_"..name]` — current component count -- * `corpus.word_counts["mac_"..name]` — current component count
-- * `corpus.components[name]` — bare-name component definition -- * `corpus.components[name]` — one row: body, line_of, source, cost
-- * `corpus.component_body_index[name]` — body / line_of / source index
-- The pass writes to the corpus only; consumers read from the corpus directly. -- The pass writes to the corpus only; consumers read from the corpus directly.
-- Per-directory aggregation: every source in the same directory contributes to one `gen/macs.h`. -- Per-directory aggregation: every source in the same directory contributes to one `gen/macs.h`.
-- The directory itself is the namespace. `corpus.sources_by_dir` preserves source-order within each bucket (matches `corpus.source_order`). -- The directory itself is the namespace. `corpus.sources_by_dir` preserves source-order within each bucket (matches `corpus.source_order`).
local sources_by_dir = corpus.sources_by_dir or duffle.group_sources_by_dir(corpus.source_order) local sources_by_dir = corpus.sources_by_dir or duffle.group_sources_by_dir(corpus.source_order) ---@type table<string, SourceFile[]>
for dir, sources in pairs(sources_by_dir) do for dir, sources in pairs(sources_by_dir) do ---@type string, SourceFile[]
-- Aggregate components from every source in this directory. -- Aggregate components from every source in this directory.
-- `project_components` returns nil for sources with no `MipsAtomComp_` declarations; we skip those. -- `project_components` returns nil for sources with no `MipsAtomComp_` declarations; we skip those.
local aggregated_components = {} local aggregated_components = {} ---@type Component[]
local metadata_per_source = {} local metadata_per_source = {} ---@type table<SourceFile, ComponentMetaMap>
for _, src in ipairs(sources) do for _, src in ipairs(sources) do ---@type integer, SourceFile
local per_source = project_components(src.text, src.scan) or {} local per_source = project_components(src.text, src.scan) or {} ---@type Component[]
for _, c in ipairs(per_source) do for _, c in ipairs(per_source) do ---@type integer, Component
aggregated_components[#aggregated_components + 1] = c aggregated_components[#aggregated_components + 1] = c
end end
if #per_source > 0 then 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
end end
if #aggregated_components > 0 then if #aggregated_components > 0 then
-- Compute word counts across the aggregated set. `corpus.word_counts` carries the -- Compute word counts across the aggregated set. `corpus.word_counts` carries the
-- same-source + prior-directory entries so the recursive lookup sees both. -- same-source + prior-directory entries so the recursive lookup sees both.
local counts = count_all_components(aggregated_components, corpus.word_counts) local counts = count_all_components(aggregated_components, corpus.word_counts) ---@type table<string, integer> -- bag: bare name -> word count
local macs_path = emit_component_macros_h(ctx, dir, sources, aggregated_components, counts) local macs_path = emit_component_macros_h(ctx, dir, sources, aggregated_components, counts) ---@type string|nil
if macs_path then if macs_path then
outputs[#outputs + 1] = { macs_h = macs_path } outputs[#outputs + 1] = { macs_h = macs_path }
-- Populate the projections AFTER disk emission (byte-identical `.macs.h` contract). -- Populate the projections AFTER disk emission (byte-identical `.macs.h` contract).
update_canonical_word_counts(corpus, aggregated_components, counts) update_canonical_word_counts(corpus, aggregated_components, counts)
for _, src in ipairs(sources) do for _, src in ipairs(sources) do ---@type integer, SourceFile
local per_source = project_components(src.text, src.scan) or {} local per_source = project_components(src.text, src.scan) or {} ---@type Component[]
if #per_source > 0 then if #per_source > 0 then
update_canonical_components(corpus, src, per_source, metadata_per_source[src]) update_canonical_components(corpus, src, per_source, metadata_per_source[src], src.scan)
update_canonical_component_body_index(corpus, src, per_source, src.scan)
end end
end end
end end
File diff suppressed because it is too large Load Diff
+164 -55
View File
@@ -1,13 +1,13 @@
--- passes/emission_model.lua: Per-atom emission projection. --- passes/emission_model.lua: Per-atom emission projection.
--- ---
--- The `emission-model` pass owns `atom.paths`, the canonical per-atom mutable surface for atoms and raw atoms with bodies in `ctx.shared.corpus.source_order`. --- The `emission-model` pass owns `atom.paths`, the canonical per-atom mutable surface for atoms and raw atoms with bodies in `ctx.shared.corpus.source_order`.
--- For each atom, the pass invokes `duffle.project_emission(body_text, component_index, word_counts, components)`. --- For each atom, the pass invokes `duffle.project_emission(body_text, components, word_counts, components)`.
--- It stores the ordered `items` stream plus the dense `word_events` / `markers` / `invocations` views on `atom.paths`. --- It stores the ordered `items` stream plus the dense `word_events` / `markers` / `invocations` views on `atom.paths`.
--- ---
--- Public boundary: --- Public boundary:
--- * `M.run(ctx)` is the only entry point. --- * `M.run(ctx)` is the only entry point.
--- * The pass returns `{outputs = {}, errors = ..., warnings = ...}`. --- * 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: --- Source-order discipline:
--- * `corpus.source_order` sets the source-record order. --- * `corpus.source_order` sets the source-record order.
@@ -26,13 +26,94 @@
--- ---
--- `passes.scan_source` strips its private `_code_macros` / `_code_macro_bodies` tables before this pass runs. --- `passes.scan_source` strips its private `_code_macros` / `_code_macro_bodies` tables before this pass runs.
local M = {} --- @class BodyToken
--- @field tok string
--- @field rel integer
--- @class EmissionItem
--- @field kind string
--- @field encoder string|nil
--- @field args string[]|nil
--- @field i integer|nil
--- @field word_count integer|nil
--- @field line integer|nil
--- @field call_text string|nil
--- @field root_call_text string|nil
--- @field invocation_ids integer[]|nil
--- @field outermost_invocation_id integer|nil
--- @field gpr_keys string[]|nil
--- @field ident string|nil
--- @field isa_kind string|nil
--- @field nop_words integer|nil
--- @field is_yield boolean|nil
--- @field is_load boolean|nil
--- @field is_branch boolean|nil
--- @field is_unconditional_jump boolean|nil
--- @field is_terminal_jump boolean|nil
--- @field gp0_shape string|nil
--- @field name string|nil
--- @field target string|nil
--- @field word_index integer|nil
--- @field consuming_encoder string|nil
--- @field consuming_arg_pos integer|nil
--- @field invocation_id integer|nil
--- @class WordEvent
--- @field i integer
--- @field encoder string
--- @field args string[]
--- @field def_path string
--- @field def_line integer
--- @field call_text string|nil
--- @field root_call_text string|nil
--- @field invocation_ids integer[]
--- @field outermost_invocation_id integer
--- @field word_count integer
--- @field gpr_keys string[]|nil
--- @field ident string
--- @field kind string
--- @field nop_words integer
--- @field is_yield boolean
--- @field is_load boolean
--- @field is_branch boolean
--- @field is_unconditional_jump boolean
--- @field is_terminal_jump boolean
--- @field gp0_shape string|nil
--- @field body_line integer|nil
--- @field call_line integer|nil
--- @field call_path string|nil
--- @class EmissionMarker
--- @field kind string
--- @field name string
--- @field line integer
--- @field word_index integer
--- @field target string|nil
--- @field consuming_encoder string|nil
--- @field consuming_arg_pos integer|nil
-- Finding: see ps1_meta.lua
--- @class AtomPaths
--- @field tokens BodyToken[]
--- @field line_in_body table<integer, integer> -- bag: body byte offset -> 1-based line
--- @field items EmissionItem[]
--- @field word_events WordEvent[]
--- @field markers EmissionMarker[]
--- @field invocations InvocationRecord[]
--- @field errors Finding[]
--- @field warnings Finding[]
--- @class EmissionModelPass
--- @field run fun(ctx: PassCtx): PassResult
local M = {} ---@type EmissionModelPass
-- ───────────────────────────────────────────────────────────────────────── -- ─────────────────────────────────────────────────────────────────────────
-- 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. -- 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 _bootstrap_dir = debug.getinfo(1, "S").source:match("^@?(.*[/\\])") or "./" ---@type string
local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua") local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua") ---@type DuffleExport
-- ───────────────────────────────────────────────────────────────────────── -- ─────────────────────────────────────────────────────────────────────────
-- Helpers -- Helpers
@@ -45,12 +126,17 @@ local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua")
-- * ROOT invocations (`inv.parent_id == 0`) receive body-relative `call_line` values directly from `M.LineIndex(body_text)` in the walker. -- * ROOT invocations (`inv.parent_id == 0`) receive body-relative `call_line` values directly from `M.LineIndex(body_text)` in the walker.
-- The source `line_of` closure supplies physical lines at the close site, so this function converts each root value exactly once. -- The source `line_of` closure supplies physical lines at the close site, so this function converts each root value exactly once.
-- * INNER invocations (`inv.parent_id ~= 0`) receive physical `call_line` values directly from the COMPONENT's `line_of` in the walker. -- * INNER invocations (`inv.parent_id ~= 0`) receive physical `call_line` values directly from the COMPONENT's `line_of` in the walker.
-- Recursive descent forwards that closure through `corpus.component_body_index[name].line_of`; those values arrive physical and remain unchanged. -- Recursive descent forwards that closure through `corpus.components[name].line_of`; those values arrive physical and remain unchanged.
-- --
-- After this function, every `inv.call_line` is physical. DWARF and provenance output read it directly. -- After this function, every `inv.call_line` is physical. DWARF and provenance output read it directly.
-- The word-event loop forwards the already-physical `outer_inv.call_line` into `we.call_line` for words inside an invocation. -- The word-event loop forwards the already-physical `outer_inv.call_line` into `we.call_line` for words inside an invocation.
--- @param projection EmissionProjection
--- @param atom_record AtomEntry
--- @param src SourceFile
--- @param corpus Corpus
--- @return nil
local function stamp_root_provenance(projection, atom_record, src, corpus) local function stamp_root_provenance(projection, atom_record, src, corpus)
local root_line_of = src.scan and src.scan.line_of local root_line_of = src.scan and src.scan.line_of ---@type (fun(pos: integer): integer)|nil
assert(type(root_line_of) == "function" assert(type(root_line_of) == "function"
, "emission_model: src.scan.line_of is required (canonical LineIndex closure over the source text) to stamp physical provenance") , "emission_model: src.scan.line_of is required (canonical LineIndex closure over the source text) to stamp physical provenance")
assert(type(atom_record.body_off) == "number" assert(type(atom_record.body_off) == "number"
@@ -58,26 +144,29 @@ local function stamp_root_provenance(projection, atom_record, src, corpus)
-- `root_body_line` is the physical source line of the ATOM HEADER byte containing the opening `{`; that byte is one byte BEFORE `atom_record.body_off`. -- `root_body_line` is the physical source line of the ATOM HEADER byte containing the opening `{`; that byte is one byte BEFORE `atom_record.body_off`.
-- The walker assigns line 2 to the body's first content line because line 1 is the trailing `\n` after `{`. Body-text line k therefore maps to `root_body_line + (k - 1)`. -- The walker assigns line 2 to the body's first content line because line 1 is the trailing `\n` after `{`. Body-text line k therefore maps to `root_body_line + (k - 1)`.
-- `body_off - 1` points at the opening `{`, whose line index identifies the header line. `body_off` points after `{` and would shift every word row forward by one line. -- `body_off - 1` points at the opening `{`, whose line index identifies the header line. `body_off` points after `{` and would shift every word row forward by one line.
local root_body_line = root_line_of(atom_record.body_off - 1) or atom_record.line or 0 local root_body_line = root_line_of(atom_record.body_off - 1) or atom_record.line or 0 ---@type integer
local component_index = corpus.component_body_index or {} local components = corpus.components or {} ---@type table<string, Component>
local word_items = {} local word_items = {} ---@type EmissionItem[]
for _, item in ipairs(projection.items) do for _, item in ipairs(projection.items) do ---@type integer, EmissionItem
if item.kind == "word" then word_items[#word_items + 1] = item end if item.kind == "word" then word_items[#word_items + 1] = item end
end end
-- Resolve one word's physical body line, where the byte containing that word appears in source. -- Resolve one word's physical body line, where the byte containing that word appears in source.
-- * Component expansions carry `invocation_ids`; the component's full-file `line_of` leaves `item.line` physical. -- * Component expansions carry `invocation_ids`; the component's full-file `line_of` leaves `item.line` physical.
-- * Raw tokens in the root atom body carry an empty `invocation_ids` list and a body-relative `item.line`; convert them here. -- * Raw tokens in the root atom body carry an empty `invocation_ids` list and a body-relative `item.line`; convert them here.
--- @param event WordEvent
--- @param item EmissionItem
--- @return integer
local function body_line_for(event, item) local function body_line_for(event, item)
local ids = event.invocation_ids or {} local ids = event.invocation_ids or {} ---@type integer[]
-- The innermost open invocation identifies which line index the walker used. -- The innermost open invocation identifies which line index the walker used.
-- A component `line_of` makes `item.line` physical; the atom's `body_text` line index makes it body-relative. -- A component `line_of` makes `item.line` physical; the atom's `body_text` line index makes it body-relative.
if ids and #ids > 0 then if ids and #ids > 0 then
local inner_id = ids[#ids] local inner_id = ids[#ids] ---@type integer
local inner_inv = inner_id and projection.invocations[inner_id] local inner_inv = inner_id and projection.invocations[inner_id] ---@type InvocationRecord|nil
if inner_inv then if inner_inv then
local component = component_index[inner_inv.component_name] local component = components[inner_inv.component_name] ---@type Component|nil
if component and component.line_of then if component and component.line_of then
-- Walker used `comp.line_of`, which is the source's physical LineIndex. item.line is already physical. -- Walker used `comp.line_of`, which is the source's physical LineIndex. item.line is already physical.
return item.line or 0 return item.line or 0
@@ -92,8 +181,8 @@ local function stamp_root_provenance(projection, atom_record, src, corpus)
-- Stamp the root source path onto invocation records whose `call_path` the walker left empty. -- Stamp the root source path onto invocation records whose `call_path` the walker left empty.
-- The walker passes `body_entry.source` to `emit_invoke_begin`; `M.project_emission` creates the root `body_entry` with source `""`, leaving its `call_path` empty. -- The walker passes `body_entry.source` to `emit_invoke_begin`; `M.project_emission` creates the root `body_entry` with source `""`, leaving its `call_path` empty.
-- This stamp gives every invocation a physical `call_path` matching `passes/atoms_source_map.lua`'s in-memory provenance projection. -- This stamp gives every invocation a physical `call_path` matching `passes/atoms_source_map.lua`'s in-memory provenance projection.
local root_path = src.path or "" local root_path = src.path or "" ---@type string
for _, inv in ipairs(projection.invocations) do for _, inv in ipairs(projection.invocations) do ---@type integer, InvocationRecord
if inv.call_path == nil or inv.call_path == "" then if inv.call_path == nil or inv.call_path == "" then
inv.call_path = root_path inv.call_path = root_path
end end
@@ -102,7 +191,7 @@ local function stamp_root_provenance(projection, atom_record, src, corpus)
-- Normalize `inv.call_line` to a physical source line. -- Normalize `inv.call_line` to a physical source line.
-- * ROOT invocations (`parent_id == 0`) carry body-relative `call_line` values from `M.LineIndex(body_text)`; convert them once with `root_body_line`. -- * ROOT invocations (`parent_id == 0`) carry body-relative `call_line` values from `M.LineIndex(body_text)`; convert them once with `root_body_line`.
-- * INNER invocations (`parent_id ~= 0`) carry physical `call_line` values from the component's `line_of`; retain them unchanged. -- * INNER invocations (`parent_id ~= 0`) carry physical `call_line` values from the component's `line_of`; retain them unchanged.
for _, inv in ipairs(projection.invocations) do for _, inv in ipairs(projection.invocations) do ---@type integer, InvocationRecord
if inv.parent_id == 0 then if inv.parent_id == 0 then
inv.call_line = (root_body_line or 0) + (inv.call_line or 1) - 1 inv.call_line = (root_body_line or 0) + (inv.call_line or 1) - 1
end end
@@ -111,14 +200,14 @@ local function stamp_root_provenance(projection, atom_record, src, corpus)
-- Build `body_lines` for each invocation. -- Build `body_lines` for each invocation.
-- `atoms_source_map` and `dwarf_injection` read `inv.body_lines[k]` directly from the invocation record created here. -- `atoms_source_map` and `dwarf_injection` read `inv.body_lines[k]` directly from the invocation record created here.
-- Component words already carry physical `item.line` values from the walker's COMPONENT line index, so `body_line_for` returns them unchanged. -- Component words already carry physical `item.line` values from the walker's COMPONENT line index, so `body_line_for` returns them unchanged.
for _, inv in ipairs(projection.invocations) do for _, inv in ipairs(projection.invocations) do ---@type integer, InvocationRecord
local sw = inv.start_word local sw = inv.start_word ---@type integer
local ew = inv.end_word local ew = inv.end_word ---@type integer
local bls = {} local bls = {} ---@type integer[]
for i = sw, ew do for i = sw, ew do ---@type integer
local it = projection.items and projection.items[i] local it = projection.items and projection.items[i] ---@type EmissionItem|nil
if it and it.kind == "word" then if it and it.kind == "word" then
local fake_event = { invocation_ids = { inv.id } } local fake_event = { invocation_ids = { inv.id } } ---@type WordEvent
bls[#bls + 1] = body_line_for(fake_event, it) or 0 bls[#bls + 1] = body_line_for(fake_event, it) or 0
end end
end end
@@ -128,15 +217,15 @@ local function stamp_root_provenance(projection, atom_record, src, corpus)
-- Resolve each `word_event`'s physical `body_line` and `call_line`. -- Resolve each `word_event`'s physical `body_line` and `call_line`.
-- For words inside an invocation, `we.call_line` identifies the OUTER atom source line containing the `mac_X(...)` token that triggered expansion. -- For words inside an invocation, `we.call_line` identifies the OUTER atom source line containing the `mac_X(...)` token that triggered expansion.
-- The root-invocation conversion above makes every `inv.call_line` physical; forward it directly and use each raw word's `body_line` as the fallback. -- The root-invocation conversion above makes every `inv.call_line` physical; forward it directly and use each raw word's `body_line` as the fallback.
for index, we in ipairs(projection.word_events) do for index, we in ipairs(projection.word_events) do ---@type integer, WordEvent
local item = word_items[index] or {} local item = word_items[index] or {} ---@type EmissionItem
local body_line = body_line_for(we, item) local body_line = body_line_for(we, item) ---@type integer
item.line = body_line item.line = body_line
we.body_line = body_line we.body_line = body_line
local call_line = body_line local call_line = body_line ---@type integer
local outer_id = we.outermost_invocation_id or 0 local outer_id = we.outermost_invocation_id or 0 ---@type integer
local outer_inv = projection.invocations[outer_id] local outer_inv = projection.invocations[outer_id] ---@type InvocationRecord|nil
if outer_inv then if outer_inv then
-- `outer_inv.call_line` is physical after the conversion loop above, so use it directly. -- `outer_inv.call_line` is physical after the conversion loop above, so use it directly.
call_line = outer_inv.call_line call_line = outer_inv.call_line
@@ -151,16 +240,20 @@ end
-- Project one atom record into `atom.paths`. -- Project one atom record into `atom.paths`.
-- Mutates the atom record in-place and returns the projection (for pass-level error/warning accumulation). -- Mutates the atom record in-place and returns the projection (for pass-level error/warning accumulation).
--- @param atom_record AtomEntry
--- @param src SourceFile
--- @param corpus Corpus
--- @return EmissionProjection
local function project_atom(atom_record, src, corpus) local function project_atom(atom_record, src, corpus)
local body = atom_record.body or "" local body = atom_record.body or "" ---@type string
local wc = corpus.word_counts or {} local wc = corpus.word_counts or {} ---@type WordCounts
local cbi = corpus.component_body_index or {} local comps = corpus.components or {} ---@type table<string, Component>
local schema = nil local schema = nil ---@type RegUseSchema|nil
if atom_record.reg_use_schema_name then if atom_record.reg_use_schema_name then
schema = corpus.reg_use_schemas and corpus.reg_use_schemas[atom_record.reg_use_schema_name] schema = corpus.reg_use_schemas and corpus.reg_use_schemas[atom_record.reg_use_schema_name]
end end
-- That construction site stamps `invocation.debug_skip` while appending each record to `proj.invocations`. -- 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, comps, wc, comps, { ---@type EmissionProjection
reg_use_schema = schema, reg_use_schema = schema,
reg_use_param = atom_record.reg_use_param_name, reg_use_param = atom_record.reg_use_param_name,
atom_name = atom_record.name, atom_name = atom_record.name,
@@ -168,16 +261,25 @@ local function project_atom(atom_record, src, corpus)
}) })
if atom_record.reg_use_schema_name and not schema then if atom_record.reg_use_schema_name and not schema then
proj.errors[#proj.errors + 1] = { proj.errors[#proj.errors + 1] = {
kind = "reguse_missing_schema", kind = "error",
check = "reguse_missing_schema",
msg = string.format("RegUse schema %q is missing", atom_record.reg_use_schema_name), msg = string.format("RegUse schema %q is missing", atom_record.reg_use_schema_name),
schema_name = atom_record.reg_use_schema_name,
} }
end end
for _, err in ipairs(corpus.reg_use_errors or {}) do for _, err in ipairs(corpus.reg_use_errors or {}) do ---@type integer, RegUseError
if err.schema_name == atom_record.reg_use_schema_name then if err.schema_name == atom_record.reg_use_schema_name then
proj.errors[#proj.errors + 1] = err proj.errors[#proj.errors + 1] = {
kind = "error",
check = err.kind,
line = err.line or err.source_line or 0,
msg = err.msg or "",
source = err.source or err.source_file,
schema_name = err.schema_name,
}
end end
end end
local paths = { local paths = { ---@type AtomPaths
tokens = atom_record.body_tokens or {}, tokens = atom_record.body_tokens or {},
line_in_body = duffle.build_body_line_index(body), line_in_body = duffle.build_body_line_index(body),
items = proj.items, items = proj.items,
@@ -199,35 +301,42 @@ end
--- @param ctx PassCtx -- { shared = { corpus = ... }, out_root, ... } --- @param ctx PassCtx -- { shared = { corpus = ... }, out_root, ... }
--- @return PassResult --- @return PassResult
function M.run(ctx) function M.run(ctx)
local outputs = {} local outputs = {} ---@type PassOutputEntry[]
local errors = {} local errors = {} ---@type Finding[]
local warnings = {} local warnings = {} ---@type Finding[]
local corpus = ctx and ctx.shared and ctx.shared.corpus local corpus = ctx and ctx.shared and ctx.shared.corpus ---@type Corpus|nil
if type(corpus) ~= "table" then error("emission_model: ctx.shared.corpus is required (canonical projection)", 0) end 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 if type(corpus.source_order) ~= "table" then error("emission_model: ctx.shared.corpus.source_order is required", 0) end
-- Project once, collect errors + warnings for one atom. -- Project once, collect errors + warnings for one atom.
-- Kind must be one of: atom | atom_proc | raw_atom | comp_bare | comp_proc. -- Kind must be one of: atom | atom_proc | raw_atom | comp_bare | comp_proc.
--- @param atom AtomEntry
--- @param src SourceFile
--- @return nil
local function process_atom(atom, src) local function process_atom(atom, src)
if not (atom and atom.body) then return end if not (atom and atom.body) then return end
local kind = atom.kind local kind = atom.kind ---@type string
if kind ~= "atom" and kind ~= "atom_proc" and kind ~= "raw_atom" and kind ~= "comp_bare" and kind ~= "comp_proc" then if kind ~= "atom" and kind ~= "atom_proc" and kind ~= "raw_atom" and kind ~= "comp_bare" and kind ~= "comp_proc" then
return return
end end
local proj = project_atom(atom, src, corpus) local proj = project_atom(atom, src, corpus) ---@type EmissionProjection
for _, e in ipairs(proj.errors) do for _, e in ipairs(proj.errors) do ---@type integer, Finding
-- Preserve `kind` (cycle / count_mismatch / unbalanced) so readers dispatch on the diagnostic class and leave the message string as display text. -- Finding.kind is severity. Finding.check holds the diagnostic code
-- (cycle / count_mismatch / unbalanced / reguse_*).
errors[#errors + 1] = { errors[#errors + 1] = {
kind = e.kind, kind = "error",
check = e.check,
line = e.line, line = e.line,
msg = e.msg, msg = e.msg,
source = e.source or src.path, source = e.source or src.path,
schema_name = e.schema_name,
} }
end end
for _, w in ipairs(proj.warnings) do for _, w in ipairs(proj.warnings) do ---@type integer, Finding
warnings[#warnings + 1] = { warnings[#warnings + 1] = {
kind = w.kind, kind = "warning",
check = w.check,
line = w.line, line = w.line,
msg = w.msg, msg = w.msg,
} }
@@ -237,12 +346,12 @@ function M.run(ctx)
-- Walk `corpus.source_order`; within each source, visit atoms followed by raw_atoms. -- Walk `corpus.source_order`; within each source, visit atoms followed by raw_atoms.
-- Recognized kinds (atom | atom_proc | raw_atom | comp_bare | comp_proc) each receive the atom.paths projection via duffle.project_emission. -- Recognized kinds (atom | atom_proc | raw_atom | comp_bare | comp_proc) each receive the atom.paths projection via duffle.project_emission.
-- Components are macros inlined into atom bodies; focused tests and isolated component analyses consume atom.paths directly. -- Components are macros inlined into atom bodies; focused tests and isolated component analyses consume atom.paths directly.
for _, src in ipairs(corpus.source_order) do for _, src in ipairs(corpus.source_order) do ---@type integer, SourceFile
local scan = src.scan or {} local scan = src.scan or {} ---@type SourceScan
for _, atom in ipairs(scan.atoms or {}) do for _, atom in ipairs(scan.atoms or {}) do ---@type integer, AtomEntry
process_atom(atom, src) process_atom(atom, src)
end end
for _, atom in ipairs(scan.raw_atoms or {}) do for _, atom in ipairs(scan.raw_atoms or {}) do ---@type integer, AtomEntry
process_atom(atom, src) process_atom(atom, src)
end end
end end
+112 -87
View File
@@ -9,13 +9,6 @@
--- Per-directory aggregation: every source in the same directory contributes to the same `gen/offsets.h`. --- Per-directory aggregation: every source in the same directory contributes to the same `gen/offsets.h`.
--- The directory itself is the namespace; the filename does not repeat the module name. --- The directory itself is the namespace; the filename does not repeat the module name.
--- ---
--- (Task 12.16 note: atom-namespaced enum names — e.g., `atom_offset__normalize_v3s4__srav_path__aligned_done` —
--- were considered to prevent cross-atom label collisions, but the C-side `atom_offset(F, T)` macro in
--- `code/duffle/dsl.atom.h` doesn't know the current atom_name at expansion time, so any namespacing
--- on the metaprogram side breaks the C build. Reverted. The C-side would need a per-atom
--- `CURRENT_ATOM` #define (set by `MipsAtom_`/`MipsAtom_Proc_` macros) plus an updated `atom_offset`
--- macro that uses it. That's a coordinated refactor — deferred to a future track.)
---
--- The offset is `target_word - branch_word - 1` (the standard MIPS branch-immediate encoding: branch_offset = relative_pc_in_words - 1). --- The offset is `target_word - branch_word - 1` (the standard MIPS branch-immediate encoding: branch_offset = relative_pc_in_words - 1).
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
@@ -27,41 +20,25 @@
-- Uses `debug.getinfo` to find this file's own directory, so it works both standalone and when require'd from the orchestrator. -- Uses `debug.getinfo` to find this file's own directory, so it works both standalone and when require'd from the orchestrator.
-- Bootstrap: load `duffle_paths.lua` via `debug.getinfo(1, "S").source` (works both standalone + when require'd). -- Bootstrap: load `duffle_paths.lua` via `debug.getinfo(1, "S").source` (works both standalone + when require'd).
-- duffle_paths.lua sets package.path then returns `require("duffle")` at the bottom, so the dofile value IS the duffle module. -- duffle_paths.lua sets package.path then returns `require("duffle")` at the bottom, so the dofile value IS the duffle module.
local _bootstrap_dir = debug.getinfo(1, "S").source:match("^@?(.*[/\\])") or "./" local _bootstrap_dir = debug.getinfo(1, "S").source:match("^@?(.*[/\\])") or "./" ---@type string
local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua") local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua") ---@type DuffleExport
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
-- Constants -- Constants
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
-- Offset macro/enum naming prefixes (the emitted header uses these). -- Offset macro/enum naming prefixes (the emitted header uses these).
local OFFSET_MACRO_PREFIX = "_atom_offset_" local OFFSET_MACRO_PREFIX = "_atom_offset_" ---@type string
local OFFSET_ENUM_PREFIX = "atom_offset_" local OFFSET_ENUM_PREFIX = "atom_offset_" ---@type string
-- Column width for the `#define _atom_offset_F_T = N` alignment. -- Column width for the `#define _atom_offset_F_T = N` alignment.
local OFFSET_MACRO_COL = 44 local OFFSET_MACRO_COL = 44 ---@type integer
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
-- Type declarations -- Type declarations
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
--- @class SourceFile -- SourceFile, PassCtx, PassResult: see ps1_meta.lua
--- @field path string -- Absolute path to the source file
--- @field text string -- Full source text
--- @field dir string -- Directory containing the source
--- @field basename string -- Filename without extension
--- @field scan table -- Pre-scanned SourceScan payload (from duffle.scan_source)
--- @class PassCtx
--- @field shared table -- Cross-pass shared state
--- @field shared.corpus table -- Corpus projection
--- @field shared.word_counts table
--- @field out_root string -- Output root (e.g. "build/gen")
--- @class PassResult
--- @field outputs table[] -- {kind=, path=} entries describing emit files
--- @field errors table[] -- {line=, msg=} entries; build-stops
--- @field warnings table[] -- {line=, msg=} entries; build-succeeds
--- @class BranchOffset --- @class BranchOffset
--- @field tag string -- Marker tag (e.g. "F" in `atom_offset(F, T)`) --- @field tag string -- Marker tag (e.g. "F" in `atom_offset(F, T)`)
@@ -76,6 +53,32 @@ local OFFSET_MACRO_COL = 44
--- @field total_words integer -- Total word count of the atom body --- @field total_words integer -- Total word count of the atom body
--- @field offsets BranchOffset[] -- Per-branch offset list --- @field offsets BranchOffset[] -- Per-branch offset list
--- @class OffsetBranch
--- @field tag string
--- @field target string
--- @field branch_word integer
--- @field consuming_encoder string|nil
--- @field consuming_arg_pos integer|nil
--- @field line integer|nil
--- @class MarkerProjectState
--- @field labels table<string, integer> -- bag: label name -> word index
--- @field branches OffsetBranch[]
--- @class OffsetConst
--- @field macro_name string
--- @field enum_name string
--- @field value integer
--- @class OffsetOutput
--- @field offsets_h string
--- @class OffsetsPass
--- @field run fun(ctx: PassCtx): PassResult
--- @class AtomEntry
--- @field paths AtomPaths|nil
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
-- Canonical marker projection -- Canonical marker projection
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
@@ -83,10 +86,16 @@ local OFFSET_MACRO_COL = 44
-- MARKER_PROJECTORS is the marker-kind data table. -- MARKER_PROJECTORS is the marker-kind data table.
-- The emission-model pass already records marker word positions + consuming-instruction context; -- The emission-model pass already records marker word positions + consuming-instruction context;
-- this pass only projects those records into the label/branch lookup shape needed by offset computation. -- this pass only projects those records into the label/branch lookup shape needed by offset computation.
local MARKER_PROJECTORS = { local MARKER_PROJECTORS = { ---@type table<string, fun(state: MarkerProjectState, marker: EmissionMarker): nil>
--- @param state MarkerProjectState
--- @param marker EmissionMarker
--- @return nil
label = function(state, marker) label = function(state, marker)
state.labels[marker.name] = marker.word_index state.labels[marker.name] = marker.word_index
end, end,
--- @param state MarkerProjectState
--- @param marker EmissionMarker
--- @return nil
offset = function(state, marker) offset = function(state, marker)
state.branches[#state.branches + 1] = { state.branches[#state.branches + 1] = {
tag = marker.name, tag = marker.name,
@@ -100,12 +109,13 @@ local MARKER_PROJECTORS = {
--- Project canonical marker records into the two lookup tables used by the offset renderer. --- Project canonical marker records into the two lookup tables used by the offset renderer.
--- No source text, body text, or body token is inspected. --- No source text, body text, or body token is inspected.
--- @param markers table[] -- atom.paths.markers --- @param markers EmissionMarker[]
--- @return table<string, integer>, table[] --- @return table<string, integer>
--- @return OffsetBranch[]
local function project_markers(markers) local function project_markers(markers)
local state = { labels = {}, branches = {} } local state = { labels = {}, branches = {} } ---@type MarkerProjectState
for _, marker in ipairs(markers or {}) do for _, marker in ipairs(markers or {}) do ---@type integer, EmissionMarker
local project = MARKER_PROJECTORS[marker.kind] local project = MARKER_PROJECTORS[marker.kind] ---@type (fun(state: MarkerProjectState, marker: EmissionMarker): nil)|nil
if project then project(state, marker) end if project then project(state, marker) end
end end
return state.labels, state.branches return state.labels, state.branches
@@ -124,38 +134,47 @@ end
--- For cross-module `j`/`jal` (atom body in one module, target in another), the linker emits a `R_MIPS_26` relocation against the lower 26 bits; the upper 4 bits come from the PC of the delay slot following the `j`. --- For cross-module `j`/`jal` (atom body in one module, target in another), the linker emits a `R_MIPS_26` relocation against the lower 26 bits; the upper 4 bits come from the PC of the delay slot following the `j`.
--- The metaprogram doesn't know either at compile time, so the emitted value is the relative word offset that the duffle `enc_i` macro places in the immediate field; the toolchain handles the rest. --- The metaprogram doesn't know either at compile time, so the emitted value is the relative word offset that the duffle `enc_i` macro places in the immediate field; the toolchain handles the rest.
--- `jump_reg` / `call_reg` / `jump_link` -> ERROR. Register-form jumps have no offset field; `atom_offset` is invalid. --- `jump_reg` / `call_reg` / `jump_link` -> ERROR. Register-form jumps have no offset field; `atom_offset` is invalid.
--- --- missing `consuming_encoder` -> ERROR. A lone top-level `atom_offset` is not a branch.
--- Top-level `atom_offset(F, T)` markers (where the marker is the entire token — `consuming_encoder` == nil) default to `branch_*` behavior (relative offset).
--- This preserves backward compatibility for any top-level marker that may exist outside a control-transfer instruction.
--- @param labels table<string, integer> --- @param labels table<string, integer>
--- @param branches table[] --- @param branches OffsetBranch[]
--- @param errors Finding[]
--- @return BranchOffset[] --- @return BranchOffset[]
local function compute_offsets(labels, branches) local function compute_offsets(labels, branches, errors)
local results = {} local results = {} ---@type BranchOffset[]
for _, br in ipairs(branches) do for _, br in ipairs(branches) do ---@type integer, OffsetBranch
local target = labels[br.target] local target = labels[br.target] ---@type integer|nil
if not target then if not target then
error("Branch target '" .. br.target .. "' has no atom_label (at word " .. br.branch_word .. ")") errors[#errors + 1] = {
end line = br.line or 0,
local consuming = br.consuming_encoder msg = "Branch target '" .. br.target .. "' has no atom_label (at word " .. br.branch_word .. ")",
local offset }
if consuming == "jump_reg" or consuming == "call_reg" or consuming == "jump_link" then else
-- Register-form jumps have no offset field. `atom_offset` cannot be used here. local consuming = br.consuming_encoder ---@type string|nil
error("atom_offset cannot be used with " .. consuming if consuming == nil or consuming == "" then
.. " (register-form jumps have no offset field); at word " .. br.branch_word) errors[#errors + 1] = {
end line = br.line or 0,
-- All other consuming instructions (including `branch_*`, `jump`, `call_addr`, and nil for top-level markers) use the same relative offset value. msg = "atom_offset requires a consuming encoder (branch_*, jump, call_addr); top-level atom_offset is invalid; at word " .. br.branch_word,
}
elseif 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
-- Consuming instructions with an offset field (`branch_*`, `jump`, `call_addr`) 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. -- 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] = { results[#results + 1] = {
target = br.target, target = br.target,
tag = br.tag, tag = br.tag,
branch_word = br.branch_word, branch_word = br.branch_word,
offset = offset, offset = target - br.branch_word - 1,
consuming_encoder = br.consuming_encoder, consuming_encoder = br.consuming_encoder,
consuming_arg_pos = br.consuming_arg_pos, consuming_arg_pos = br.consuming_arg_pos,
} }
end end
end
end
return results return results
end end
@@ -169,7 +188,7 @@ end
--- (internal) Build a constant-table entry `{macro_name, enum_name, value}` from a BranchOffset. --- (internal) Build a constant-table entry `{macro_name, enum_name, value}` from a BranchOffset.
--- @param bo BranchOffset --- @param bo BranchOffset
--- @return table --- @return OffsetConst
local function make_offset_const(bo) local function make_offset_const(bo)
return { return {
macro_name = OFFSET_MACRO_PREFIX .. bo.tag .. "_" .. bo.target, macro_name = OFFSET_MACRO_PREFIX .. bo.tag .. "_" .. bo.target,
@@ -181,20 +200,21 @@ end
--- (internal) Emit one atom's offset constants + enum into the lines buffer. --- (internal) Emit one atom's offset constants + enum into the lines buffer.
--- @param add fun(s: string) --- @param add fun(s: string)
--- @param atom AtomData --- @param atom AtomData
--- @return nil
local function emit_atom_offsets(add, atom) local function emit_atom_offsets(add, atom)
if #atom.offsets == 0 then return end if #atom.offsets == 0 then return end
add("// --- atom: " .. atom.name .. " (" .. atom.total_words .. " words) ---") add("// --- atom: " .. atom.name .. " (" .. atom.total_words .. " words) ---")
add("") add("")
local consts = {} local consts = {} ---@type OffsetConst[]
for _, r in ipairs(atom.offsets) do for _, r in ipairs(atom.offsets) do ---@type integer, BranchOffset
consts[#consts + 1] = make_offset_const(r) consts[#consts + 1] = make_offset_const(r)
end end
for _, c in ipairs(consts) do for _, c in ipairs(consts) do ---@type integer, OffsetConst
add("#define " .. pad_right(c.macro_name, OFFSET_MACRO_COL) .. " " .. c.value) add("#define " .. pad_right(c.macro_name, OFFSET_MACRO_COL) .. " " .. c.value)
end end
add("") add("")
add("enum {") add("enum {")
for _, c in ipairs(consts) do for _, c in ipairs(consts) do ---@type integer, OffsetConst
add(" " .. c.enum_name .. " = " .. c.macro_name .. ",") add(" " .. c.enum_name .. " = " .. c.macro_name .. ",")
end end
add("};") add("};")
@@ -202,19 +222,21 @@ local function emit_atom_offsets(add, atom)
end end
--- Generate the per-directory .offsets.h header. --- Generate the per-directory .offsets.h header.
--- @param dir string -- the absolute source directory --- @param dir string
--- @param sources table[] -- sources contributing to this directory (for the header comment) --- @param sources SourceFile[]
--- @param atoms_data AtomData[] --- @param atoms_data AtomData[]
--- @return string --- @return string
local function generate_header(dir, sources, atoms_data) local function generate_header(dir, sources, atoms_data)
local dir_basename = duffle.basename_no_ext(dir) local dir_basename = duffle.basename_no_ext(dir) ---@type string
local lines = {} local lines = {} ---@type string[]
--- @param s string
--- @return nil
local function add(s) lines[#lines + 1] = s end local function add(s) lines[#lines + 1] = s end
add("// Auto-generated by ps1_meta.lua (passes/offsets.lua) — DO NOT EDIT") add("// Auto-generated by ps1_meta.lua (passes/offsets.lua) — DO NOT EDIT")
add("// Directory: " .. dir:gsub("/", "\\") .. "\\") add("// Directory: " .. dir:gsub("/", "\\") .. "\\")
for _, src in ipairs(sources) do for _, src in ipairs(sources) do ---@type integer, SourceFile
add("// source: " .. src.path:gsub("/", "\\")) add("// source: " .. src.path:gsub("/", "\\"))
end end
add("#pragma once") add("#pragma once")
@@ -222,7 +244,7 @@ local function generate_header(dir, sources, atoms_data)
add("#pragma region " .. dir_basename) add("#pragma region " .. dir_basename)
add("") add("")
add("") add("")
for _, atom in ipairs(atoms_data) do for _, atom in ipairs(atoms_data) do ---@type integer, AtomData
emit_atom_offsets(add, atom) emit_atom_offsets(add, atom)
end end
add("#pragma endregion " .. dir_basename) add("#pragma endregion " .. dir_basename)
@@ -230,36 +252,39 @@ local function generate_header(dir, sources, atoms_data)
return table.concat(lines, "\n") .. "\n" return table.concat(lines, "\n") .. "\n"
end end
local M = {} local M = {} ---@type OffsetsPass
--- (internal) Aggregate atoms from every source in one directory, render the per-directory `offsets.h`. --- (internal) Aggregate atoms from every source in one directory, render the per-directory `offsets.h`.
--- Returns the offsets_h path if a header was written, or nil. --- Returns the offsets_h path if a header was written, or nil.
--- @param ctx PassCtx --- @param ctx PassCtx
--- @param dir string -- the absolute source directory --- @param dir string
--- @param sources SourceFile[] -- sources in this directory --- @param sources SourceFile[]
--- @return string|nil -- the offsets_h path --- @param errors Finding[]
local function process_directory(ctx, dir, sources) --- @return string|nil
local atoms_data = {} local function process_directory(ctx, dir, sources, errors)
local atoms_data = {} ---@type AtomData[]
--- @param atom AtomEntry
--- @return nil
local function append_atom(atom) local function append_atom(atom)
local paths = atom and atom.paths local paths = atom and atom.paths ---@type AtomPaths|nil
if not paths then return end if not paths then return end
local labels, branches = project_markers(paths.markers) local labels, branches = project_markers(paths.markers) ---@type table<string, integer>, OffsetBranch[]
atoms_data[#atoms_data + 1] = { atoms_data[#atoms_data + 1] = {
name = atom.raw_name or atom.name, name = atom.raw_name or atom.name,
total_words = #(paths.word_events or {}), total_words = #(paths.word_events or {}),
offsets = compute_offsets(labels, branches), offsets = compute_offsets(labels, branches, errors),
} }
end end
for _, src in ipairs(sources) do for _, src in ipairs(sources) do ---@type integer, SourceFile
local scan = src.scan or {} local scan = src.scan or {} ---@type SourceScan
for _, atom in ipairs(scan.atoms or {}) do append_atom(atom) end for _, atom in ipairs(scan.atoms or {}) do append_atom(atom) end ---@type integer, AtomEntry
for _, atom in ipairs(scan.raw_atoms or {}) do append_atom(atom) end for _, atom in ipairs(scan.raw_atoms or {}) do append_atom(atom) end ---@type integer, AtomEntry
end end
if #atoms_data == 0 then return nil end if #atoms_data == 0 then return nil end
local out_path = dir .. "/gen/offsets.h" local out_path = dir .. "/gen/offsets.h" ---@type string
duffle.ensure_dir(duffle.dirname(out_path)) duffle.ensure_dir(duffle.dirname(out_path))
duffle.write_file(out_path, generate_header(dir, sources, atoms_data)) duffle.write_file(out_path, generate_header(dir, sources, atoms_data))
return out_path return out_path
@@ -271,11 +296,11 @@ end
--- @param ctx PassCtx --- @param ctx PassCtx
--- @return PassResult --- @return PassResult
function M.run(ctx) function M.run(ctx)
local outputs = {} local outputs = {} ---@type OffsetOutput[]
local errors = {} local errors = {} ---@type Finding[]
local warnings = {} local warnings = {} ---@type Finding[]
local corpus = ctx.shared and ctx.shared.corpus local corpus = ctx.shared and ctx.shared.corpus ---@type Corpus|nil
if type(corpus) ~= "table" then if type(corpus) ~= "table" then
error("offsets.run requires ctx.shared.corpus", 0) error("offsets.run requires ctx.shared.corpus", 0)
end end
@@ -284,9 +309,9 @@ function M.run(ctx)
end end
-- Per-directory aggregation: every source in the same directory contributes to one `gen/offsets.h`. -- 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) local sources_by_dir = corpus.sources_by_dir or duffle.group_sources_by_dir(corpus.source_order) ---@type table<string, SourceFile[]>
for dir, sources in pairs(sources_by_dir) do for dir, sources in pairs(sources_by_dir) do ---@type string, SourceFile[]
local out_path = process_directory(ctx, dir, sources) local out_path = process_directory(ctx, dir, sources, errors) ---@type string|nil
if out_path then if out_path then
outputs[#outputs + 1] = { offsets_h = out_path } outputs[#outputs + 1] = { offsets_h = out_path }
end end
+457 -257
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+16 -32
View File
@@ -4,13 +4,13 @@
--- 1. **Public utility** `M.count_token_words(token, wc)`: Used by `passes/offsets.lua`, `passes/annotation.lua`, and other passes. --- 1. **Public utility** `M.count_token_words(token, wc)`: Used by `passes/offsets.lua`, `passes/annotation.lua`, and other passes.
--- 2. **Pass entry** `M.run(ctx)`: Loads the authored `word_count.metadata.h` into `ctx.shared.corpus.word_counts` for downstream passes. --- 2. **Pass entry** `M.run(ctx)`: Loads the authored `word_count.metadata.h` into `ctx.shared.corpus.word_counts` for downstream passes.
--- The generated `.macs.h` files are OUTPUT artifacts and are NOT inputs to this pass; --- The generated `.macs.h` files are OUTPUT artifacts and are NOT inputs to this pass;
--- Current component counts are owned by `passes/components.lua` (which populates `corpus.word_counts` and `corpus.component_body_index` --- Current component counts are owned by `passes/components.lua` (which populates `corpus.word_counts` and `corpus.components`
--- AFTER computing each current count from the just-built body + `corpus.word_counts`). --- AFTER computing each current count from the just-built body + `corpus.word_counts`).
--- ---
--- **Canonical contract**: --- **Canonical contract**:
--- * `ctx.shared.corpus.word_counts` is the count table. --- * `ctx.shared.corpus.word_counts` is the count table.
--- * `corpus.word_counts` is the sole count table. Consumers read `corpus.word_counts` directly. --- * `corpus.word_counts` is the sole count table. Consumers read `corpus.word_counts` directly.
--- * `ctx.shared.components` and `ctx.shared.component_body_index` are NOT created by this pass (projections only). --- * `ctx.shared.components` is NOT created by this pass (projections only).
--- * No `.macs.h` recursive discovery (no `scan_dir`, no scan cache, no `_invalidate_scan_cache`). --- * No `.macs.h` recursive discovery (no `scan_dir`, no scan cache, no `_invalidate_scan_cache`).
--- ---
--- **Conventions**: tabs (1/level), EmmyLua annotations, no regex, --- **Conventions**: tabs (1/level), EmmyLua annotations, no regex,
@@ -23,44 +23,28 @@
-- Bootstrap: load `scripts/duffle_paths.lua` (sets package.path + package.cpath). -- Bootstrap: load `scripts/duffle_paths.lua` (sets package.path + package.cpath).
-- Uses `debug.getinfo` to find this file's own directory, so it works both standalone and when require'd from the orchestrator. -- Uses `debug.getinfo` to find this file's own directory, so it works both standalone and when require'd from the orchestrator.
-- duffle_paths.lua sets package.path then returns `require("duffle")` at the bottom, so the dofile value IS the duffle module. -- duffle_paths.lua sets package.path then returns `require("duffle")` at the bottom, so the dofile value IS the duffle module.
local _bootstrap_dir = debug.getinfo(1, "S").source:match("^@?(.*[/\\])") or "./" local _bootstrap_dir = debug.getinfo(1, "S").source:match("^@?(.*[/\\])") or "./" ---@type string
local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua") local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua") ---@type DuffleExport
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
-- Type declarations -- Type declarations
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
--- @class WordCounts --- @class WordCounts
--- @field [string] integer -- macro name -> word count --- @field [string] integer -- bag: macro name -> word count
--- @class SourceFile --- @class WordCountEval
--- @field path string -- absolute path to the source file --- @field count_token_words fun(token: string, wc: WordCounts): integer
--- @field text string -- the full source text --- @field run fun(ctx: PassCtx): PassResult
--- @field dir string -- the directory containing the source
--- @field basename string -- filename without extension
--- @class PassCtx -- SourceFile, PassCtx, PassResult: see ps1_meta.lua
--- @field sources SourceFile[] -- all source files in the build -- DuffleExport: see duffle.lua (facade returned by duffle_paths.lua)
--- @field metadata_path string -- path to word_count.metadata.h
--- @field shared table -- cross-pass shared state
--- @field shared.corpus table -- canonical corpus (required)
--- @field shared.corpus.word_counts WordCounts -- canonical count table (populated by this pass)
--- @field out_root string -- output root (e.g. "build/gen")
--- @field project_root string -- project root (e.g. "code/")
--- @field upstream table<string, table> -- per-pass upstream outputs
--- @field flags table -- CLI flags
--- @field verbose boolean -- if true, log diagnostic info
--- @class PassResult
--- @field outputs table[] -- {kind=, path=} entries describing emit files
--- @field errors table[] -- {line=, msg=} entries; build-stops
--- @field warnings table[] -- {line=, msg=} entries; build-succeeds
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
-- Module exports -- Module exports
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
local M = {} local M = {} ---@type WordCountEval
-- ┌────────────────────────────────────────────────────────────────────┐ -- ┌────────────────────────────────────────────────────────────────────┐
-- │ Shared utility: count_token_words │ -- │ Shared utility: count_token_words │
@@ -74,12 +58,12 @@ local M = {}
--- @param wc WordCounts -- the shared word-count table --- @param wc WordCounts -- the shared word-count table
--- @return integer --- @return integer
function M.count_token_words(token, wc) function M.count_token_words(token, wc)
local s = duffle.trim(token) local s = duffle.trim(token) ---@type string
if s == "" then return 0 end if s == "" then return 0 end
local name, after = duffle.read_ident(s, 1) local name, after = duffle.read_ident(s, 1) ---@type string|nil, integer
if not name then return 1 end if not name then return 1 end
if wc[name] then return wc[name] end if wc[name] then return wc[name] end
local paren_pos = duffle.skip_ws_and_cmt(s, after) local paren_pos = duffle.skip_ws_and_cmt(s, after) ---@type integer
if s:sub(paren_pos, paren_pos) == "(" then if s:sub(paren_pos, paren_pos) == "(" then
io.stderr:write(" warning: unknown macro '" .. name .. "', assuming 1 word\n") io.stderr:write(" warning: unknown macro '" .. name .. "', assuming 1 word\n")
end end
@@ -105,7 +89,7 @@ end
--- @return PassResult --- @return PassResult
function M.run(ctx) function M.run(ctx)
-- 1. Canonical-corpus ownership gate. -- 1. Canonical-corpus ownership gate.
local corpus = ctx.shared and ctx.shared.corpus local corpus = ctx.shared and ctx.shared.corpus ---@type Corpus|nil
if type(corpus) ~= "table" then if type(corpus) ~= "table" then
error("word_count_eval.run requires ctx.shared.corpus (canonical corpus). The fixture must install the corpus before running this pass.", 0) error("word_count_eval.run requires ctx.shared.corpus (canonical corpus). The fixture must install the corpus before running this pass.", 0)
end end
@@ -117,7 +101,7 @@ function M.run(ctx)
-- 3. Load authored metadata. Generated .macs.h files are NOT scanned -- 3. Load authored metadata. Generated .macs.h files are NOT scanned
-- (the pass computes their counts from the just-built bodies after disk emission; see passes/components.lua). -- (the pass computes their counts from the just-built bodies after disk emission; see passes/components.lua).
local wc = duffle.load_word_counts(ctx.metadata_path) local wc = duffle.load_word_counts(ctx.metadata_path) ---@type WordCounts
-- 4. Assign the count table. ONE assignment, no copy. The assignment creates no secondary alias. -- 4. Assign the count table. ONE assignment, no copy. The assignment creates no secondary alias.
corpus.word_counts = wc corpus.word_counts = wc
+241 -158
View File
@@ -19,8 +19,8 @@
-- fall back to `debug.getinfo(1, "S").source` when this file is being dofile()'d or require()'d (in which case `arg[0]` is the *caller's* path). -- fall back to `debug.getinfo(1, "S").source` when this file is being dofile()'d or require()'d (in which case `arg[0]` is the *caller's* path).
-- That single statement: (a) sets `package.path` + `package.cpath`, (b) at the bottom returns `require("duffle")`. -- That single statement: (a) sets `package.path` + `package.cpath`, (b) at the bottom returns `require("duffle")`.
-- So the dofile's return value is the duffle module. -- So the dofile's return value is the duffle module.
local _is_entry_script = arg and arg[0] and arg[0]:match("ps1_meta%.lua$") ~= nil local _is_entry_script = arg and arg[0] and arg[0]:match("ps1_meta%.lua$") ~= nil ---@type boolean
local _bootstrap_src local _bootstrap_src ---@type string
if _is_entry_script then if _is_entry_script then
_bootstrap_src = arg[0] _bootstrap_src = arg[0]
else else
@@ -28,26 +28,26 @@ else
-- strip the leading "@" so the directory match works in both cases. -- strip the leading "@" so the directory match works in both cases.
_bootstrap_src = debug.getinfo(1, "S").source:sub(2) _bootstrap_src = debug.getinfo(1, "S").source:sub(2)
end end
local duffle = dofile((_bootstrap_src:match("(.*[/\\])") or "./") .. "duffle_paths.lua") local duffle = dofile((_bootstrap_src:match("(.*[/\\])") or "./") .. "duffle_paths.lua") ---@type DuffleExport
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
-- Constants -- Constants
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
-- Exit codes (per the --help text and the post-build summary convention). -- Exit codes (per the --help text and the post-build summary convention).
local EXIT_OK = 0 local EXIT_OK = 0 ---@type integer
local EXIT_VALIDATION_ERRORS = 1 local EXIT_VALIDATION_ERRORS = 1 ---@type integer
local EXIT_INTERNAL_ERROR = 2 local EXIT_INTERNAL_ERROR = 2 ---@type integer
-- Default --out-root value if not provided. -- Default --out-root value if not provided.
local DEFAULT_OUT_ROOT = "build/gen" local DEFAULT_OUT_ROOT = "build/gen" ---@type string
-- Sentinel for "all passes" in `PASS_FLAG_TO_NAME`. Distinguishes `--all` from the per-pass flags (which map to individual pass names). -- Sentinel for "all passes" in `PASS_FLAG_TO_NAME`. Distinguishes `--all` from the per-pass flags (which map to individual pass names).
local ALL_PASSES_SENTINEL = "__all__" local ALL_PASSES_SENTINEL = "__all__" ---@type string
-- Sentinel key for the pass-flag dispatcher in `FLAG_HANDLERS`. -- Sentinel key for the pass-flag dispatcher in `FLAG_HANDLERS`.
-- The actual pass names are looked up via `PASS_FLAG_TO_NAME`, not direct dispatch, so this key never matches a real flag. -- The actual pass names are looked up via `PASS_FLAG_TO_NAME`, not direct dispatch, so this key never matches a real flag.
local PASS_FLAG_DISPATCH_KEY = "__pass__" local PASS_FLAG_DISPATCH_KEY = "__pass__" ---@type string
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
-- Type declarations -- Type declarations
@@ -60,29 +60,98 @@ local PASS_FLAG_DISPATCH_KEY = "__pass__"
--- @field deps string[] -- Names of upstream passes --- @field deps string[] -- Names of upstream passes
--- @field groups string[]? -- OPTIONAL build-phase groups this pass is a root of (e.g. { "pre-link" }, { "post-link" }); absent ⇒ dependency-only --- @field groups string[]? -- OPTIONAL build-phase groups this pass is a root of (e.g. { "pre-link" }, { "post-link" }); absent ⇒ dependency-only
--- @class SourceFile --- @class Corpus
--- @field path string -- Absolute path to the source file --- @field unity_root string|nil
--- @field text string -- Full source text --- @field project_root string
--- @field dir string -- Directory containing the source --- @field code_root string
--- @field basename string -- Filename without extension --- @field source_order SourceFile[]
--- @field sources_by_path table<Path, SourceFile>
--- @field sources_by_dir table<string, SourceFile[]>
--- @field atoms_by_name table<AtomName, AtomEntry>
--- @field binds_by_name table<string, BindsEntry>
--- @field atom_infos AtomInfoEntry[]
--- @field register_alias_registry table<string, AliasEntry>
--- @field type_name_registry table<string, TypeNameEntry>
--- @field atom_views table<AtomName, AtomViewEntry>
--- @field atom_ctxs table<AtomName, AtomCtxEntry>
--- @field atom_phases table<string, AtomPhaseGroup>
--- @field word_counts WordCounts
--- @field components table<string, Component>
--- @field atom_bundles table<string, AtomBundle>|nil
--- @field tape_emits TapeEmit[]|nil
--- @field collisions CorpusCollision[]
--- @field resolver SourceResolver
--- @field component_atom_infos AtomInfoEntry[]|nil
--- @field atom_auto_regs table<AtomName, table<string, string>>|nil
--- @field phase_auto_regs table<string, table<string, string>>|nil
--- @field reg_use_schemas table<string, RegUseSchema>|nil
--- @field reg_use_errors RegUseError[]|nil
--- @field static_analysis_results table<string, AtomAnalysis>|nil
--- @field tape_chains table<string, TapeChain>|nil
--- @class PassShared
--- @field corpus Corpus
--- @class PassFlags
--- @field gdb_runtime boolean|nil
--- @field dwarf_injection boolean|nil
--- @field elf_path string|nil
--- @class PassCtx --- @class PassCtx
--- @field metadata_path string -- Path to word_count.metadata.h --- @field metadata_path string -- Path to word_count.metadata.h
--- @field shared table -- Cross-pass shared state --- @field shared PassShared -- Cross-pass shared state
--- @field shared.corpus table -- Authored-source/project projection
--- @field out_root string -- Output root (e.g. "build/gen") --- @field out_root string -- Output root (e.g. "build/gen")
--- @field project_root string -- PS1 repository root --- @field project_root string -- PS1 repository root
--- @field flags table -- CLI flags + per-pass stash --- @field flags PassFlags -- CLI flags + per-pass stash
--- @field verbose boolean -- If true, log diagnostic info --- @field verbose boolean -- If true, log diagnostic info
--- CheckName: see static_analysis.lua. AtomName: see duffle.lua.
--- @class Finding --- @class Finding
--- @field line integer -- Source line (or 0 for pass-level) --- @field line integer
--- @field msg string -- Finding message --- @field msg string
--- @field kind string|nil -- error | warning | info
--- @field atom AtomName|nil
--- @field check CheckName|nil
--- @field source string|nil -- optional; emit/reguse path
--- @field schema_name string|nil -- optional; emit/reguse
--- @class PassScratch
--- @field corpus Corpus|nil
--- @field info_by_atom table<string, AtomInfoEntry>|nil
--- @field binds_index table<string, BindsEntry>|nil
--- @field atom_index table<string, AtomEntry>|nil
--- @field annot_counts table<string, integer>|nil -- bag
--- @field types table<string, RegTypeDefault>|nil
--- @field atom_views table<string, AtomViewEntry>|nil
--- @field seen_defaults table<string, integer>|nil -- bag
--- @field seen_field table<string, integer>|nil -- bag
--- @field _scan SourceScan|nil
--- @field word_counts WordCounts|nil
--- @field register_alias_registry table<string, AliasEntry>|nil
--- @field type_name_registry table<string, TypeNameEntry>|nil
--- @field type_occurrences RegTypeOccurrence[]|nil
--- @field atom_infos_list AtomInfoEntry[]|nil
--- @field binds_list BindsEntry[]|nil
--- @field unknown_seen table<string, integer>|nil -- bag
--- @field atoms AtomEntry[]|nil
--- @field components_by_name table<string, Component>|nil
--- @field atoms_by_name table<string, AtomEntry>|nil
--- @field tape_chains table<string, string[]>|nil
--- @field source_order SourceFile[]|nil
--- @field component_atom_infos AtomInfoEntry[]|nil
--- @field atom_infos_all AtomInfoEntry[]|nil
--- @field gte_cr_alias_groups GteCrAliasGroup[]|nil
--- @field line_for_word_event (fun(ev: WordEvent): integer)|nil
--- @class PassOutputEntry
--- @field kind string
--- @field path string
--- @class PassResult --- @class PassResult
--- @field outputs PassOutputEntry[] -- Emitted file paths --- @field outputs PassOutputEntry[]
--- @field errors Finding[] -- Build-stops (per-pass kind policy) --- @field errors Finding[] -- Build-stops (per-pass kind policy)
--- @field warnings Finding[] -- Informational --- @field warnings Finding[] -- Informational
--- @field info Finding[]|nil -- static_analysis only
--- @class ParsedArgs --- @class ParsedArgs
--- @field requested_set string[] -- Pass names to run (explicit --all expanded) --- @field requested_set string[] -- Pass names to run (explicit --all expanded)
@@ -92,6 +161,18 @@ local PASS_FLAG_DISPATCH_KEY = "__pass__"
--- @field out_root string -- --out-root value (default "build/gen") --- @field out_root string -- --out-root value (default "build/gen")
--- @field project_root string -- PS1 repository root (derived from metadata by default) --- @field project_root string -- PS1 repository root (derived from metadata by default)
--- @field verbose boolean -- If true, log diagnostic info --- @field verbose boolean -- If true, log diagnostic info
--- @field flags PassFlags|nil -- Per-pass stash; copied onto PassCtx.flags
--- @alias FlagHandler fun(args: ParsedArgs, argv: string[]|nil, arg_idx: integer|nil): integer|nil
--- @class PassModule
--- @field run fun(ctx: PassCtx): PassResult
--- @class Ps1MetaMod
--- @field PASSES table<string, PassDescriptor>
--- @field PASS_KIND_STOP_ON_ERROR table<string, boolean>
--- @field parse_args fun(argv: string[]): ParsedArgs
--- @field build_ctx fun(args: ParsedArgs): PassCtx
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
-- PASSES Table -- PASSES Table
@@ -104,7 +185,7 @@ local PASS_FLAG_DISPATCH_KEY = "__pass__"
-- A row without a `groups` entry is dependency-only: it runs only when a transitive dep requests it, -- A row without a `groups` entry is dependency-only: it runs only when a transitive dep requests it,
-- but it remains directly requestable through its explicit CLI flag (e.g. --atoms-source-map, --scan-source). -- but it remains directly requestable through its explicit CLI flag (e.g. --atoms-source-map, --scan-source).
local PASSES = { local PASSES = { ---@type table<string, PassDescriptor>
["scan-source"] = { ["scan-source"] = {
module = "passes.scan_source", module = "passes.scan_source",
kind = "shared", deps = {}, kind = "shared", deps = {},
@@ -142,8 +223,7 @@ local PASSES = {
}, },
["static-analysis"] = { ["static-analysis"] = {
module = "passes.static_analysis", module = "passes.static_analysis",
-- "diagnostic" — every `error`/`warning` finding is written to the report file; -- "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).
-- Report severity is independent from process exit policy. -- Report severity is independent from process exit policy.
kind = "diagnostic", kind = "diagnostic",
deps = {"scan-source", "word-counts", "components", "emission-model"}, deps = {"scan-source", "word-counts", "components", "emission-model"},
@@ -175,10 +255,10 @@ local PASSES = {
--- @param group_name string -- Build-phase group ("pre-link" | "post-link") --- @param group_name string -- Build-phase group ("pre-link" | "post-link")
--- @return string[] -- Sorted root pass names belonging to that group --- @return string[] -- Sorted root pass names belonging to that group
local function roots_for_group(group_name) local function roots_for_group(group_name)
local names = {} local names = {} ---@type string[]
for name, pass in pairs(PASSES) do for name, pass in pairs(PASSES) do ---@type string, PassDescriptor
if pass.groups then if pass.groups then
for _, g in ipairs(pass.groups) do for _, g in ipairs(pass.groups) do ---@type integer, string
if g == group_name then if g == group_name then
names[#names + 1] = name names[#names + 1] = name
break break
@@ -195,27 +275,25 @@ end
--- cannot silently fall through to pre-link (or any other default) and dispatch nothing. --- cannot silently fall through to pre-link (or any other default) and dispatch nothing.
--- @param args ParsedArgs --- @param args ParsedArgs
--- @param group_name string --- @param group_name string
--- @return nil
local function request_roots_for_group(args, group_name) local function request_roots_for_group(args, group_name)
local roots = roots_for_group(group_name) local roots = roots_for_group(group_name) ---@type string[]
if #roots == 0 then if #roots == 0 then
error(string.format("ps1_meta: build-phase group %q has zero roots in PASSES; check PASSES rows for a `groups = { %q }` field" error(string.format("ps1_meta: build-phase group %q has zero roots in PASSES; check PASSES rows for a `groups = { %q }` field"
, group_name, group_name)) , group_name, group_name))
end end
for _, name in ipairs(roots) do for _, name in ipairs(roots) do ---@type integer, string
args.requested_set[#args.requested_set + 1] = name args.requested_set[#args.requested_set + 1] = name
end end
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. -- Report severity is independent from process exit policy.
-- A "diagnostic" pass still writes every `error`/`warning` finding into its report file, -- Adding a new pass kind requires listing it here explicitly; an unknown kind must not silently fall back to "true".
-- but `report_validation_errors` returns early for non-stopping kinds, so nothing is printed to stderr and the orchestrator does not exit non-zero. local PASS_KIND_STOP_ON_ERROR = { ---@type table<string, boolean> -- bag: pass kind -> stop-on-error
-- 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, ["shared"] = false,
["header-output"] = true, ["header-output"] = false,
["validation"] = true, ["validation"] = false,
["diagnostic"] = false, ["diagnostic"] = false,
["report"] = false, ["report"] = false,
} }
@@ -224,7 +302,7 @@ local PASS_KIND_STOP_ON_ERROR = {
-- Per-pass flags (e.g. --word-counts); phase flags (--pre-link, --post-link, --all) are within FLAG_HANDLERS because they own side effects or invoke group-derivation logic. -- Per-pass flags (e.g. --word-counts); phase flags (--pre-link, --post-link, --all) are within FLAG_HANDLERS because they own side effects or invoke group-derivation logic.
-- dwarf-injection is *also* a per-pass opt-in flag, but its selection + opt-in state are both owned by the explicit FLAG_HANDLERS entry below -- dwarf-injection is *also* a per-pass opt-in flag, but its selection + opt-in state are both owned by the explicit FLAG_HANDLERS entry below
-- (it sets args.flags.dwarf_injection and appends "dwarf-injection" to requested_set), so it is intentionally absent from this table. -- (it sets args.flags.dwarf_injection and appends "dwarf-injection" to requested_set), so it is intentionally absent from this table.
local PASS_FLAG_TO_NAME = { local PASS_FLAG_TO_NAME = { ---@type table<string, string> -- bag: CLI flag -> pass name or ALL_PASSES_SENTINEL
["--word-counts"] = "word-counts", ["--word-counts"] = "word-counts",
["--components"] = "components", ["--components"] = "components",
["--validate"] = "annotation", ["--validate"] = "annotation",
@@ -239,24 +317,26 @@ local PASS_FLAG_TO_NAME = {
--- Append every pass name to args.requested_set. --- Append every pass name to args.requested_set.
--- Names are derived from PASSES (no parallel name list); used by --all and by any caller that wants the full closure. --- Names are derived from PASSES (no parallel name list); used by --all and by any caller that wants the full closure.
--- @param args ParsedArgs --- @param args ParsedArgs
--- @return nil
local function request_all_passes(args) local function request_all_passes(args)
local names = {} local names = {} ---@type string[]
for name in pairs(PASSES) do names[#names + 1] = name end for name in pairs(PASSES) do names[#names + 1] = name end ---@type string
table.sort(names) table.sort(names)
for _, n in ipairs(names) do for _, n in ipairs(names) do ---@type integer, string
args.requested_set[#args.requested_set + 1] = n args.requested_set[#args.requested_set + 1] = n
end end
end end
-- Per-flag handlers. Each handler takes (args, argv, arg_idx) and returns the new arg_idx (so multi-arg flags like --source FILE advance it). -- Per-flag handlers. Each handler takes (args, argv, arg_idx) and returns the new arg_idx (so multi-arg flags like --source FILE advance it).
-- Returning nil + os.exit() handles termination flags (--help). -- Returning nil + os.exit() handles termination flags (--help).
local FLAG_HANDLERS = {} local FLAG_HANDLERS = {} ---@type table<string, FlagHandler>
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
-- CLI parsing -- CLI parsing
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
--- Print the CLI usage to stdout and exit 0. --- Print the CLI usage to stdout and exit 0.
--- @return nil
local function print_help() local function print_help()
io.write([[ io.write([[
ps1_meta.lua - Tape-atom metaprogram orchestrator ps1_meta.lua - Tape-atom metaprogram orchestrator
@@ -295,8 +375,7 @@ COMMON_FLAGS:
--help Show this help and exit --help Show this help and exit
EXIT CODES: EXIT CODES:
0 All requested passes succeeded 0 Ran. Findings print on stderr and in the report; they do not fail the process.
1 Validation errors found
2 Metaprogram internal error 2 Metaprogram internal error
EXAMPLES: EXAMPLES:
@@ -306,7 +385,7 @@ EXAMPLES:
]]) ]])
end end
local FLAG_VALUE_NAMES = { local FLAG_VALUE_NAMES = { ---@type table<string, string> -- bag: flag -> value metavar
["--source"] = "FILE", ["--source"] = "FILE",
["--unity-root"] = "FILE", ["--unity-root"] = "FILE",
["--metadata"] = "PATH", ["--metadata"] = "PATH",
@@ -315,9 +394,14 @@ local FLAG_VALUE_NAMES = {
["--elf"] = "PATH", ["--elf"] = "PATH",
} }
--- @param argv string[]
--- @param arg_idx integer
--- @param flag string
--- @return string
--- @return integer
local function require_flag_value(argv, arg_idx, flag) local function require_flag_value(argv, arg_idx, flag)
local value = argv[arg_idx + 1] local value = argv[arg_idx + 1] ---@type string|nil
local next_known = type(value) == "string" local next_known = type(value) == "string" ---@type boolean
and (FLAG_HANDLERS[value] ~= nil or PASS_FLAG_TO_NAME[value] ~= nil) and (FLAG_HANDLERS[value] ~= nil or PASS_FLAG_TO_NAME[value] ~= nil)
if value == nil or next_known then if value == nil or next_known then
io.stderr:write("ps1_meta: " .. flag .. " requires " .. FLAG_VALUE_NAMES[flag] .. "\n") io.stderr:write("ps1_meta: " .. flag .. " requires " .. FLAG_VALUE_NAMES[flag] .. "\n")
@@ -331,49 +415,81 @@ end
-- Populated AFTER print_help so the --help handler can reference it as an upvalue (Lua resolves locals at closure-call time, -- Populated AFTER print_help so the --help handler can reference it as an upvalue (Lua resolves locals at closure-call time,
-- but if the closure is defined before the local, it falls back to _G). -- but if the closure is defined before the local, it falls back to _G).
--- @param args ParsedArgs
--- @return nil
FLAG_HANDLERS["--help"] = function(args) print_help(); os.exit(0) end FLAG_HANDLERS["--help"] = function(args) print_help(); os.exit(0) end
--- @param args ParsedArgs
--- @return nil
FLAG_HANDLERS["--verbose"] = function(args) args.verbose = true end FLAG_HANDLERS["--verbose"] = function(args) args.verbose = true end
--- @param args ParsedArgs
--- @param argv string[]
--- @param arg_idx integer
--- @return integer
FLAG_HANDLERS["--source"] = function(args, argv, arg_idx) FLAG_HANDLERS["--source"] = function(args, argv, arg_idx)
local value, value_idx = require_flag_value(argv, arg_idx, "--source") local value, value_idx = require_flag_value(argv, arg_idx, "--source") ---@type string, integer
args.sources[#args.sources + 1] = value args.sources[#args.sources + 1] = value
return value_idx return value_idx
end end
--- @param args ParsedArgs
--- @param argv string[]
--- @param arg_idx integer
--- @return integer
FLAG_HANDLERS["--unity-root"] = function(args, argv, arg_idx) FLAG_HANDLERS["--unity-root"] = function(args, argv, arg_idx)
local value, value_idx = require_flag_value(argv, arg_idx, "--unity-root") local value, value_idx = require_flag_value(argv, arg_idx, "--unity-root") ---@type string, integer
args.unity_root = value args.unity_root = value
return value_idx return value_idx
end end
--- @param args ParsedArgs
--- @param argv string[]
--- @param arg_idx integer
--- @return integer
FLAG_HANDLERS["--metadata"] = function(args, argv, arg_idx) FLAG_HANDLERS["--metadata"] = function(args, argv, arg_idx)
local value, value_idx = require_flag_value(argv, arg_idx, "--metadata") local value, value_idx = require_flag_value(argv, arg_idx, "--metadata") ---@type string, integer
args.metadata = value args.metadata = value
return value_idx return value_idx
end end
--- @param args ParsedArgs
--- @param argv string[]
--- @param arg_idx integer
--- @return integer
FLAG_HANDLERS["--out-root"] = function(args, argv, arg_idx) FLAG_HANDLERS["--out-root"] = function(args, argv, arg_idx)
local value, value_idx = require_flag_value(argv, arg_idx, "--out-root") local value, value_idx = require_flag_value(argv, arg_idx, "--out-root") ---@type string, integer
args.out_root = value args.out_root = value
return value_idx return value_idx
end end
--- @param args ParsedArgs
--- @param argv string[]
--- @param arg_idx integer
--- @return integer
FLAG_HANDLERS["--project-root"] = function(args, argv, arg_idx) FLAG_HANDLERS["--project-root"] = function(args, argv, arg_idx)
local value, value_idx = require_flag_value(argv, arg_idx, "--project-root") local value, value_idx = require_flag_value(argv, arg_idx, "--project-root") ---@type string, integer
args.project_root = value args.project_root = value
return value_idx return value_idx
end end
-- Per-pass stash flags. Read by `passes/atoms_source_map.lua` to opt into the post-link gdb-runtime emission. -- Per-pass stash flags. Read by `passes/atoms_source_map.lua` to opt into the post-link gdb-runtime emission.
-- Same shape as the existing per-flag handlers. mutates `args.flags` (which propagates into `ctx.flags`). -- Same shape as the existing per-flag handlers. mutates `args.flags` (which propagates into `ctx.flags`).
--- @param args ParsedArgs
--- @return nil
FLAG_HANDLERS["--gdb-runtime"] = function(args) FLAG_HANDLERS["--gdb-runtime"] = function(args)
args.flags = args.flags or {} args.flags = args.flags or {}
args.flags.gdb_runtime = true args.flags.gdb_runtime = true
end end
--- @param args ParsedArgs
--- @param argv string[]
--- @param arg_idx integer
--- @return integer
FLAG_HANDLERS["--elf"] = function(args, argv, arg_idx) FLAG_HANDLERS["--elf"] = function(args, argv, arg_idx)
local value, value_idx = require_flag_value(argv, arg_idx, "--elf") local value, value_idx = require_flag_value(argv, arg_idx, "--elf") ---@type string, integer
args.flags = args.flags or {} args.flags = args.flags or {}
args.flags.elf_path = value args.flags.elf_path = value
return value_idx return value_idx
end end
-- Enable DWARF injection (default OFF). Opts in to the post-link pass and sets the flag in one shot. -- Enable DWARF injection (default OFF). Opts in to the post-link pass and sets the flag in one shot.
-- The explicit handler below owns both selection and opt-in state, so --dwarf-injection is intentionally absent from PASS_FLAG_TO_NAME. -- The explicit handler below owns both selection and opt-in state, so --dwarf-injection is intentionally absent from PASS_FLAG_TO_NAME.
--- @param args ParsedArgs
--- @return nil
FLAG_HANDLERS["--dwarf-injection"] = function(args) FLAG_HANDLERS["--dwarf-injection"] = function(args)
args.flags = args.flags or {} args.flags = args.flags or {}
args.flags.dwarf_injection = true args.flags.dwarf_injection = true
@@ -381,12 +497,16 @@ FLAG_HANDLERS["--dwarf-injection"] = function(args)
end end
-- Build-phase flags: --pre-link and --post-link request the roots of their declared groups (see roots_for_group). -- Build-phase flags: --pre-link and --post-link request the roots of their declared groups (see roots_for_group).
-- topo_sort closes transitive deps from those roots; dispatch_passes runs every pass in the resolved closure without phase-filtering. -- topo_sort closes transitive deps from those roots; dispatch_passes runs every pass in the resolved closure without phase-filtering.
--- @param args ParsedArgs
--- @return nil
FLAG_HANDLERS["--pre-link"] = function(args) FLAG_HANDLERS["--pre-link"] = function(args)
request_roots_for_group(args, "pre-link") request_roots_for_group(args, "pre-link")
end end
-- Batch post-link phase: gdb-runtime + dwarf-injection in one luajit cold start. -- Batch post-link phase: gdb-runtime + dwarf-injection in one luajit cold start.
-- Sets the same opt-in flags as --gdb-runtime + --dwarf-injection and selects the post-link build-phase group. -- Sets the same opt-in flags as --gdb-runtime + --dwarf-injection and selects the post-link build-phase group.
-- elf is required; parse_args enforces it after all flags are parsed. -- elf is required; parse_args enforces it after all flags are parsed.
--- @param args ParsedArgs
--- @return nil
FLAG_HANDLERS["--post-link"] = function(args) FLAG_HANDLERS["--post-link"] = function(args)
args.flags = args.flags or {} args.flags = args.flags or {}
args.flags.gdb_runtime = true args.flags.gdb_runtime = true
@@ -397,8 +517,11 @@ end
-- `--dwarf-injection` also emits atom-local debug data. -- `--dwarf-injection` also emits atom-local debug data.
-- Pass-flag handler. Reads the closed-set table, expands --all, appends to requested_set. -- Pass-flag handler. Reads the closed-set table, expands --all, appends to requested_set.
--- @param args ParsedArgs
--- @param a string
--- @return nil
FLAG_HANDLERS[PASS_FLAG_DISPATCH_KEY] = function(args, a) FLAG_HANDLERS[PASS_FLAG_DISPATCH_KEY] = function(args, a)
local name = PASS_FLAG_TO_NAME[a] local name = PASS_FLAG_TO_NAME[a] ---@type string|nil
if name == ALL_PASSES_SENTINEL then if name == ALL_PASSES_SENTINEL then
request_all_passes(args) request_all_passes(args)
return return
@@ -410,7 +533,7 @@ end
--- @param argv string[] --- @param argv string[]
--- @return ParsedArgs --- @return ParsedArgs
local function parse_args(argv) local function parse_args(argv)
local args = { local args = { ---@type ParsedArgs
requested_set = {}, requested_set = {},
sources = {}, sources = {},
unity_root = nil, unity_root = nil,
@@ -420,10 +543,10 @@ local function parse_args(argv)
verbose = false, verbose = false,
} }
local pos = 1 local pos = 1 ---@type integer
while pos <= #argv do while pos <= #argv do
local a = argv[pos] local a = argv[pos] ---@type string
local handler = FLAG_HANDLERS[a] local handler = FLAG_HANDLERS[a] ---@type FlagHandler|nil
if handler then if handler then
pos = handler(args, argv, pos) or pos pos = handler(args, argv, pos) or pos
elseif PASS_FLAG_TO_NAME[a] then elseif PASS_FLAG_TO_NAME[a] then
@@ -448,14 +571,14 @@ local function parse_args(argv)
-- `<repo>/code/duffle/word_count.metadata.h` is the canonical metadata location. -- `<repo>/code/duffle/word_count.metadata.h` is the canonical metadata location.
-- `project_root` names `<repo>`; the resolver derives `<project_root>/code` separately. -- `project_root` names `<repo>`; the resolver derives `<project_root>/code` separately.
if not args.project_root then if not args.project_root then
local metadata_dir = duffle.dirname(duffle.normalize_path(args.metadata)) local metadata_dir = duffle.dirname(duffle.normalize_path(args.metadata)) ---@type string
local code_root = duffle.dirname(metadata_dir) local code_root = duffle.dirname(metadata_dir) ---@type string
args.project_root = duffle.dirname(code_root) args.project_root = duffle.dirname(code_root)
else else
args.project_root = duffle.normalize_path(args.project_root) args.project_root = duffle.normalize_path(args.project_root)
end end
local has_unity = type(args.unity_root) == "string" and args.unity_root ~= "" local has_unity = type(args.unity_root) == "string" and args.unity_root ~= "" ---@type boolean
if has_unity and #args.sources > 0 then if has_unity and #args.sources > 0 then
io.stderr:write("ps1_meta: --unity-root FILE and --source FILE are mutually exclusive\n") io.stderr:write("ps1_meta: --unity-root FILE and --source FILE are mutually exclusive\n")
os.exit(EXIT_INTERNAL_ERROR) os.exit(EXIT_INTERNAL_ERROR)
@@ -468,10 +591,10 @@ local function parse_args(argv)
-- Post-link opt-ins (--gdb-runtime, --dwarf-injection) write output that depends on the linked ELF. -- Post-link opt-ins (--gdb-runtime, --dwarf-injection) write output that depends on the linked ELF.
-- Without --elf the metaprogram can't satisfy those requests, so refuse loud and early. -- Without --elf the metaprogram can't satisfy those requests, so refuse loud and early.
-- This covers the explicit --post-link batch, --dwarf-injection by itself, and --gdb-runtime by itself. -- This covers the explicit --post-link batch, --dwarf-injection by itself, and --gdb-runtime by itself.
local flags = args.flags or {} local flags = args.flags or {} ---@type PassFlags
local elf_path = flags.elf_path local elf_path = flags.elf_path ---@type string|nil
local has_elf = type(elf_path) == "string" and #elf_path > 0 local has_elf = type(elf_path) == "string" and #elf_path > 0 ---@type boolean
local post_links = flags.gdb_runtime or flags.dwarf_injection local post_links = flags.gdb_runtime or flags.dwarf_injection ---@type boolean
if post_links and not has_elf then if post_links and not has_elf then
io.stderr:write("ps1_meta: --elf PATH is required for post-link output\n") io.stderr:write("ps1_meta: --elf PATH is required for post-link output\n")
os.exit(EXIT_INTERNAL_ERROR) os.exit(EXIT_INTERNAL_ERROR)
@@ -490,9 +613,9 @@ end
--- @param args ParsedArgs --- @param args ParsedArgs
--- @return PassCtx --- @return PassCtx
local function build_ctx(args) local function build_ctx(args)
local normalized_project_root = duffle.normalize_path(args.project_root) local normalized_project_root = duffle.normalize_path(args.project_root) ---@type string
local project_root = normalized_project_root local project_root = normalized_project_root ---@type string
local project_root_is_absolute = normalized_project_root:match("^%a:/") local project_root_is_absolute = normalized_project_root:match("^%a:/") ---@type boolean
or normalized_project_root:sub(1, 2) == "//" or normalized_project_root:sub(1, 2) == "//"
or normalized_project_root:sub(1, 1) == "/" or normalized_project_root:sub(1, 1) == "/"
if not project_root_is_absolute then if not project_root_is_absolute then
@@ -503,9 +626,9 @@ local function build_ctx(args)
-- Do not route POSIX/UNC/drive-absolute paths through to_absolute_path. -- Do not route POSIX/UNC/drive-absolute paths through to_absolute_path.
duffle.canonical_path_key(project_root) duffle.canonical_path_key(project_root)
end end
local resolution local resolution ---@type Corpus
if args.unity_root then if args.unity_root then
local ok_resolve, resolved = pcall(duffle.resolve_source_corpus, { local ok_resolve, resolved = pcall(duffle.resolve_source_corpus, { ---@type boolean, Corpus|string
unity_root = args.unity_root, unity_root = args.unity_root,
project_root = project_root, project_root = project_root,
}) })
@@ -515,59 +638,18 @@ local function build_ctx(args)
end end
resolution = resolved resolution = resolved
else else
local source_order = {} local ok_exact, exact = pcall(duffle.resolve_exact_sources, { ---@type boolean, Corpus|string
local sources_by_path = {} sources = args.sources,
local resolver = { project_root = project_root,
resolved = {}, })
skipped = {}, if not ok_exact then
shadowed = {}, io.stderr:write("ps1_meta: cannot resolve --source: " .. tostring(exact) .. "\n")
}
for _, input_path in ipairs(args.sources) do
local path = duffle.normalize_path(input_path)
local key_ok, key_or_error = pcall(duffle.canonical_path_key, path)
if not key_ok then
error("ps1_meta: invalid --source " .. input_path .. ": " .. tostring(key_or_error), 0)
end
local file = io.open(path, "r")
if not file then
io.stderr:write("ps1_meta: cannot open --source " .. input_path .. "\n")
os.exit(EXIT_INTERNAL_ERROR) os.exit(EXIT_INTERNAL_ERROR)
end end
local text = file:read("*a") resolution = exact
file:close()
local source = {
path = path,
text = text,
dir = duffle.dirname(path),
basename = duffle.basename_no_ext(path),
}
source_order[#source_order + 1] = source
local key = key_or_error
if not sources_by_path[key] then sources_by_path[key] = source end
resolver.resolved[#resolver.resolved + 1] = {
include_path = path,
include_text = nil,
root_source = nil,
root_line = nil,
candidate_a = path,
candidate_b = nil,
selected_path = path,
disposition = "exact",
}
end
resolution = {
unity_root = nil,
project_root = project_root,
code_root = duffle.normalize_path(project_root .. "/code"),
source_order = source_order,
sources_by_path = sources_by_path,
sources_by_dir = duffle.group_sources_by_dir(source_order),
resolver = resolver,
}
end end
local corpus = { local corpus = { ---@type Corpus
unity_root = resolution.unity_root, unity_root = resolution.unity_root,
project_root = resolution.project_root, project_root = resolution.project_root,
code_root = resolution.code_root, code_root = resolution.code_root,
@@ -584,11 +666,12 @@ local function build_ctx(args)
atom_phases = {}, atom_phases = {},
word_counts = {}, word_counts = {},
components = {}, components = {},
component_body_index = {}, atom_bundles = {},
tape_emits = {},
collisions = {}, collisions = {},
resolver = resolution.resolver, resolver = resolution.resolver,
} }
local ctx = { local ctx = { ---@type PassCtx
metadata_path = args.metadata, metadata_path = args.metadata,
shared = { corpus = corpus }, shared = { corpus = corpus },
out_root = args.out_root, out_root = args.out_root,
@@ -617,15 +700,15 @@ end
--- Keeping these blocks local makes the topological sort self-contained. --- Keeping these blocks local makes the topological sort self-contained.
local function topo_sort(passes, requested_set) local function topo_sort(passes, requested_set)
-- Dependency closure: include every pass transitively required by `requested_set`. -- Dependency closure: include every pass transitively required by `requested_set`.
local needed = {} local needed = {} ---@type table<string, boolean> -- bag: pass name -> needed
for _, name in ipairs(requested_set) do needed[name] = true end for _, name in ipairs(requested_set) do needed[name] = true end ---@type integer, string
local changed = true local changed = true ---@type boolean
while changed do while changed do
changed = false changed = false
for name, _ in pairs(needed) do for name, _ in pairs(needed) do ---@type string, boolean
local pass = passes[name] local pass = passes[name] ---@type PassDescriptor
if not pass then error("unknown pass '" .. name .. "' requested") end if not pass then error("unknown pass '" .. name .. "' requested") end
for _, dep in ipairs(pass.deps) do for _, dep in ipairs(pass.deps) do ---@type integer, string
if not needed[dep] then if not needed[dep] then
needed[dep] = true needed[dep] = true
changed = true changed = true
@@ -635,10 +718,10 @@ local function topo_sort(passes, requested_set)
end end
-- In-degree calculation: count each needed pass's needed dependencies. -- In-degree calculation: count each needed pass's needed dependencies.
local in_degree = {} local in_degree = {} ---@type table<string, integer> -- bag: pass name -> in-degree
for name, _ in pairs(needed) do in_degree[name] = 0 end for name, _ in pairs(needed) do in_degree[name] = 0 end ---@type string, boolean
for name, _ in pairs(needed) do for name, _ in pairs(needed) do ---@type string, boolean
for _, dep in ipairs(passes[name].deps) do for _, dep in ipairs(passes[name].deps) do ---@type integer, string
if needed[dep] then if needed[dep] then
in_degree[name] = in_degree[name] + 1 in_degree[name] = in_degree[name] + 1
end end
@@ -646,21 +729,21 @@ local function topo_sort(passes, requested_set)
end end
-- Ready-queue seeding: add zero-in-degree passes in deterministic order. -- Ready-queue seeding: add zero-in-degree passes in deterministic order.
local ready = {} local ready = {} ---@type string[]
for name, deg in pairs(in_degree) do for name, deg in pairs(in_degree) do ---@type string, integer
if deg == 0 then ready[#ready + 1] = name end if deg == 0 then ready[#ready + 1] = name end
end end
table.sort(ready) table.sort(ready)
-- Ready-queue drain: decrement dependents when each pass is emitted. -- Ready-queue drain: decrement dependents when each pass is emitted.
-- Newly-zero-degree passes are inserted back into the ready queue (kept sorted). -- Newly-zero-degree passes are inserted back into the ready queue (kept sorted).
local order = {} local order = {} ---@type string[]
while #ready > 0 do while #ready > 0 do
local just_finished = table.remove(ready, 1) local just_finished = table.remove(ready, 1) ---@type string
order[#order + 1] = just_finished order[#order + 1] = just_finished
for name, _ in pairs(needed) do for name, _ in pairs(needed) do ---@type string, boolean
if name ~= just_finished then if name ~= just_finished then
for _, dep in ipairs(passes[name].deps) do for _, dep in ipairs(passes[name].deps) do ---@type integer, string
if dep == just_finished then if dep == just_finished then
in_degree[name] = in_degree[name] - 1 in_degree[name] = in_degree[name] - 1
if in_degree[name] == 0 then if in_degree[name] == 0 then
@@ -676,10 +759,10 @@ local function topo_sort(passes, requested_set)
-- Cycle detection: if `order` doesn't include all needed passes, some are stuck with in_degree > 0 -- Cycle detection: if `order` doesn't include all needed passes, some are stuck with in_degree > 0
-- (the cycle closed on itself before Kahn could process them). -- (the cycle closed on itself before Kahn could process them).
-- Without this check, a fully-closed cycle (e.g. A -> B -> A) would silently return an empty order list, leaving the orchestrator to dispatch nothing. -- Without this check, a fully-closed cycle (e.g. A -> B -> A) would silently return an empty order list, leaving the orchestrator to dispatch nothing.
local needed_count = 0 local needed_count = 0 ---@type integer
for _ in pairs(needed) do needed_count = needed_count + 1 end -- count hash entries; Lua's #t doesn't work for _ in pairs(needed) do needed_count = needed_count + 1 end ---@type string -- count hash entries; Lua's #t doesn't work
if #order ~= needed_count then if #order ~= needed_count then
for name, deg in pairs(in_degree) do for name, deg in pairs(in_degree) do ---@type string, integer
if deg > 0 then if deg > 0 then
error("dependency cycle detected involving pass '" .. name .. "'") error("dependency cycle detected involving pass '" .. name .. "'")
end end
@@ -693,19 +776,19 @@ end
-- Main Orchestrator -- Main Orchestrator
-- ════════════════════════════════════════════════════════════════════════════ -- ════════════════════════════════════════════════════════════════════════════
--- (internal) If the pass's kind is in PASS_KIND_STOP_ON_ERROR and it reported errors, write each error to stderr. --- (internal) Write every pass error to stderr.
--- Returns true if any validation errors were reported. --- Returns true only when the pass kind still stops the build.
--- @param pass_name string --- @param pass_name string
--- @param pass PassDescriptor --- @param pass PassDescriptor
--- @param result PassResult --- @param result PassResult
--- @return boolean --- @return boolean
local function report_validation_errors(pass_name, pass, result) local function report_validation_errors(pass_name, pass, result)
local has_errors = result.errors and #result.errors > 0 local has_errors = result.errors and #result.errors > 0 ---@type boolean
if not (has_errors and PASS_KIND_STOP_ON_ERROR[pass.kind]) then return false end if not has_errors then return false end
for _, e in ipairs(result.errors) do for _, e in ipairs(result.errors) do ---@type integer, Finding
io.stderr:write(string.format("[%s] line %d: %s\n", pass_name, e.line or 0, e.msg or "")) io.stderr:write(string.format("[%s] line %d: %s\n", pass_name, e.line or 0, e.msg or ""))
end end
return true return PASS_KIND_STOP_ON_ERROR[pass.kind] == true
end end
--- (internal) Run each pass in `order` in topological sequence. --- (internal) Run each pass in `order` in topological sequence.
@@ -713,11 +796,11 @@ end
--- @param order string[] --- @param order string[]
--- @return boolean -- true if any validation errors were reported --- @return boolean -- true if any validation errors were reported
local function dispatch_passes(ctx, order) local function dispatch_passes(ctx, order)
local had_errors = false local had_errors = false ---@type boolean
for _, pass_name in ipairs(order) do for _, pass_name in ipairs(order) do ---@type integer, string
local pass = PASSES[pass_name] local pass = PASSES[pass_name] ---@type PassDescriptor
local mod = require(pass.module) local mod = require(pass.module) ---@type PassModule
local result = mod.run(ctx) local result = mod.run(ctx) ---@type PassResult
if report_validation_errors(pass_name, pass, result) then if report_validation_errors(pass_name, pass, result) then
had_errors = true had_errors = true
end end
@@ -727,16 +810,16 @@ end
--- Main entry point. Runs the requested passes in dep-topological order. --- Main entry point. Runs the requested passes in dep-topological order.
--- @param argv string[] --- @param argv string[]
--- @return nil
local function main(argv) local function main(argv)
local ok, err = pcall(function() local ok, err = pcall(function() ---@type boolean, string|nil
local args = parse_args(argv) local args = parse_args(argv) ---@type ParsedArgs
local ctx = build_ctx(args) local ctx = build_ctx(args) ---@type PassCtx
local requested = args.requested_set local requested = args.requested_set ---@type string[]
local closed = topo_sort(PASSES, requested) local closed = topo_sort(PASSES, requested) ---@type string[]
local had_errors = dispatch_passes(ctx, closed) dispatch_passes(ctx, closed)
if had_errors then os.exit(EXIT_VALIDATION_ERRORS) end
end) end)
if not ok then if not ok then
@@ -750,7 +833,7 @@ end
-- Module export for in-process consumers (tests that dofile this script). -- Module export for in-process consumers (tests that dofile this script).
-- The conditional `main(...)` call below only fires when this file is invoked as the entry script (arg[0] ends in "ps1_meta.lua"); -- The conditional `main(...)` call below only fires when this file is invoked as the entry script (arg[0] ends in "ps1_meta.lua");
-- in dofile() mode (test's arg[0] does not match), main() is skipped and the chunk returns `_M` to the caller. -- in dofile() mode (test's arg[0] does not match), main() is skipped and the chunk returns `_M` to the caller.
local _M = { local _M = { ---@type Ps1MetaMod
PASSES = PASSES, PASSES = PASSES,
PASS_KIND_STOP_ON_ERROR = PASS_KIND_STOP_ON_ERROR, PASS_KIND_STOP_ON_ERROR = PASS_KIND_STOP_ON_ERROR,
parse_args = parse_args, parse_args = parse_args,