From 764ded4557e2e5d4c5d9215676528836365021a3 Mon Sep 17 00:00:00 2001 From: Ed_ Date: Fri, 14 Aug 2026 18:57:37 -0400 Subject: [PATCH] initial plugin setup for syntax highlighting in vscode... --- .vscode/tape-atom-syntax/classifier.js | 195 +++++++++++++++ .vscode/tape-atom-syntax/extension.js | 111 +++++++++ .vscode/tape-atom-syntax/lexer.js | 186 +++++++++++++++ .vscode/tape-atom-syntax/package.json | 81 +++++++ .vscode/tape-atom-syntax/source-index.js | 222 ++++++++++++++++++ .../syntaxes/tape_atom.tmLanguage.json | 85 +++++++ .../tape-atom-syntax-0.2.0.vsix | Bin 0 -> 10468 bytes .../tape-atom-syntax/test/classifier.test.js | 83 +++++++ .../tape-atom-syntax/test/contract.test.js | 93 ++++++++ .vscode/tape-atom-syntax/test/lexer.test.js | 77 ++++++ .../test/source-index.test.js | 77 ++++++ 11 files changed, 1210 insertions(+) create mode 100644 .vscode/tape-atom-syntax/classifier.js create mode 100644 .vscode/tape-atom-syntax/extension.js create mode 100644 .vscode/tape-atom-syntax/lexer.js create mode 100644 .vscode/tape-atom-syntax/package.json create mode 100644 .vscode/tape-atom-syntax/source-index.js create mode 100644 .vscode/tape-atom-syntax/syntaxes/tape_atom.tmLanguage.json create mode 100644 .vscode/tape-atom-syntax/tape-atom-syntax-0.2.0.vsix create mode 100644 .vscode/tape-atom-syntax/test/classifier.test.js create mode 100644 .vscode/tape-atom-syntax/test/contract.test.js create mode 100644 .vscode/tape-atom-syntax/test/lexer.test.js create mode 100644 .vscode/tape-atom-syntax/test/source-index.test.js diff --git a/.vscode/tape-atom-syntax/classifier.js b/.vscode/tape-atom-syntax/classifier.js new file mode 100644 index 0000000..a3ccdd5 --- /dev/null +++ b/.vscode/tape-atom-syntax/classifier.js @@ -0,0 +1,195 @@ +"use strict"; + +const { nearestCall } = require("./lexer"); +const { mergeIndexes, scanSource } = require("./source-index"); + +const TOKEN_TYPES = [ + "tapeAtomKeyword", + "tapeAtomName", + "tapeComponentKeyword", + "tapeComponentName", + "tapeAnnotation", + "tapeBindType", + "tapePhase", + "tapeLabel", + "tapeCpuInstruction", + "tapeControlFlow", + "tapeGteInstruction", + "tapeGpuInstruction", + "tapeComponentInstruction", + "tapeDelaySlot", + "tapeGprRegister", + "tapeCop2Register", + "tapeDuffleType", + "tapeAttribute", +]; + +const TOKEN_MODIFIERS = ["declaration", "tapeRead", "tapeWrite", "tapeAuto"]; +const TOKEN_TYPE_INDEX = new Map(TOKEN_TYPES.map((name, index) => [name, index])); +const TOKEN_MODIFIER_INDEX = new Map(TOKEN_MODIFIERS.map((name, index) => [name, index])); + +const ATOM_KEYWORDS = new Set(["MipsAtom_", "MipsAtom_Proc_"]); +const COMPONENT_KEYWORDS = new Set(["MipsAtomComp_", "MipsAtomComp_Proc_"]); +const ANNOTATIONS = new Set([ + "atom_info", "atom_bind", "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 DELAY_SLOT_KEYWORDS = new Set(["LdSlot_", "BdSlot_"]); + +const CONTROL_FLOW_PREFIXES = /^(?:branch_|jump_|call_)/; + +const ROLE_TO_TYPE = { + atomName: "tapeAtomName", + componentName: "tapeComponentName", + bindType: "tapeBindType", + duffleType: "tapeDuffleType", + gprRegister: "tapeGprRegister", + cop2Register: "tapeCop2Register", +}; + +function registerType(name, index) { + const kind = index.registers.get(name); + if (kind === "gpr" || /^R_[A-Za-z0-9_]+$/.test(name)) return "tapeGprRegister"; + if (kind === "cop2" || /^(?:C2_|gte_cr_)[A-Za-z0-9_]+$/.test(name)) return "tapeCop2Register"; + return null; +} + +function instructionType(name, index) { + const domain = index.macros.get(name); + if (domain === "cpu") return "tapeCpuInstruction"; + if (domain === "gte") return "tapeGteInstruction"; + if (domain === "gpu") return "tapeGpuInstruction"; + if (domain === "component") return "tapeComponentInstruction"; + if (/^gte_(?!cr_)/.test(name)) return "tapeGteInstruction"; + if (/^gp[01]_/.test(name)) return "tapeGpuInstruction"; + if (/^mac_/.test(name)) return "tapeComponentInstruction"; + return null; +} + +function modifierMask(modifiers) { + let mask = 0; + for (const modifier of modifiers) { + const index = TOKEN_MODIFIER_INDEX.get(modifier); + if (index !== undefined) mask |= (1 << index); + } + return mask; +} + +function isRegUseAccess(tokens, tokenIndex) { + const prev = tokens[tokenIndex - 1]; + if (!prev || prev.text !== ".") return false; + const prevPrev = tokens[tokenIndex - 2]; + if (!prevPrev || prevPrev.kind !== "identifier") return false; + if (prevPrev.text === "r") return true; + const prev3 = tokens[tokenIndex - 3]; + const prev4 = tokens[tokenIndex - 4]; + if (prev3 && prev3.text === "." && prev4 && prev4.kind === "identifier" && prev4.text === "r") return true; + return false; +} + +function classifyDocument(source, filePath, workspaceIndex, shouldCancel = () => false) { + const scanned = scanSource(source, filePath); + const index = mergeIndexes(workspaceIndex, scanned.index); + const spans = []; + + for (let tokenIndex = 0; tokenIndex < scanned.tokens.length; tokenIndex += 1) { + if (shouldCancel()) break; + const token = scanned.tokens[tokenIndex]; + if (token.kind !== "identifier") continue; + + let type = null; + let modifiers = []; + const declaration = scanned.declarations.get(token.start); + const context = nearestCall(scanned.contexts, tokenIndex); + + if (declaration) { + type = ROLE_TO_TYPE[declaration.role] || null; + modifiers = declaration.modifiers.slice(); + } else if (ATOM_KEYWORDS.has(token.text)) { + type = "tapeAtomKeyword"; + } else if (COMPONENT_KEYWORDS.has(token.text)) { + type = "tapeComponentKeyword"; + } else if (ANNOTATIONS.has(token.text)) { + type = "tapeAnnotation"; + } else if (context && context.callee === "atom_bind" && context.argIndex === 0) { + type = "tapeBindType"; + } else if (context && context.callee === "atom_phase" && context.argIndex === 0) { + type = "tapePhase"; + modifiers = ["declaration"]; + } else if (context && context.callee === "atom_ctx" && context.argIndex === 0) { + type = "tapeAtomName"; + } else if (context && context.callee === "atom_label" && context.argIndex === 0) { + type = "tapeLabel"; + modifiers = ["declaration"]; + } else if (context && context.callee === "atom_offset" && context.argIndex <= 1) { + type = "tapeLabel"; + } else if (context && context.callee === "atom_reads") { + type = registerType(token.text, index); + if (type) modifiers = ["tapeRead"]; + } else if (context && context.callee === "atom_writes") { + type = registerType(token.text, index); + if (type) modifiers = ["tapeWrite"]; + } else if (context && context.callee === "atom_auto_reg") { + if (context.argIndex === 0) type = "tapeAtomName"; + if (context.argIndex === 1) { + type = "tapeGprRegister"; + modifiers = ["declaration", "tapeAuto"]; + } + } else if (context && context.callee === "phase_auto_reg") { + if (context.argIndex === 0) type = "tapePhase"; + if (context.argIndex === 1) { + type = "tapeGprRegister"; + modifiers = ["declaration", "tapeAuto"]; + } + } + + if (!type && index.bindTypes.has(token.text)) type = "tapeBindType"; + if (!type && index.types.has(token.text)) type = "tapeDuffleType"; + if (!type && index.attributes.has(token.text)) type = "tapeAttribute"; + if (!type) type = registerType(token.text, index); + if (!type && DELAY_SLOT_KEYWORDS.has(token.text)) type = "tapeDelaySlot"; + if (!type) { + const domain = index.macros.get(token.text); + if (domain && CONTROL_FLOW_PREFIXES.test(token.text)) { + type = "tapeControlFlow"; + } + } + if (!type && isRegUseAccess(scanned.tokens, tokenIndex)) type = "tapeGprRegister"; + if (!type) type = instructionType(token.text, index); + if (!type && index.atoms.has(token.text)) type = "tapeAtomName"; + if (!type && index.components.has(token.text)) type = "tapeComponentName"; + if (!type && index.phases.has(token.text)) type = "tapePhase"; + if (!type && index.labels.has(token.text)) type = "tapeLabel"; + if (!type) continue; + + spans.push({ + text: token.text, + type, + typeIndex: TOKEN_TYPE_INDEX.get(type), + modifiers, + modifierMask: modifierMask(modifiers), + start: token.start, + length: token.end - token.start, + line: token.line, + character: token.character, + }); + } + + spans.sort((left, right) => left.start - right.start || left.length - right.length); + const nonOverlapping = []; + for (const span of spans) { + const previous = nonOverlapping[nonOverlapping.length - 1]; + if (!previous || previous.start + previous.length <= span.start) nonOverlapping.push(span); + } + + return { spans: nonOverlapping, errors: scanned.errors }; +} + +module.exports = { + TOKEN_MODIFIERS, + TOKEN_TYPES, + classifyDocument, + modifierMask, +}; diff --git a/.vscode/tape-atom-syntax/extension.js b/.vscode/tape-atom-syntax/extension.js new file mode 100644 index 0000000..e8a35f6 --- /dev/null +++ b/.vscode/tape-atom-syntax/extension.js @@ -0,0 +1,111 @@ +"use strict"; + +const vscode = require("vscode"); +const { TOKEN_MODIFIERS, TOKEN_TYPES, classifyDocument } = require("./classifier"); +const { createIndex, mergeIndexes, scanSource } = require("./source-index"); + +const SOURCE_GLOB = "**/*.{c,h,cc,cpp,cxx,hh,hpp,hxx}"; +const EXCLUDE_GLOB = "**/{gen,build,.slop_cache,toolchain,node_modules}/**"; +const EXCLUDED_SEGMENTS = new Set(["gen", "build", ".slop_cache", "toolchain", "node_modules"]); + +function isExcluded(uri) { + const segments = uri.fsPath.replaceAll("\\", "/").split("/"); + return segments.some((segment) => EXCLUDED_SEGMENTS.has(segment)); +} + +function formatError(filePath, error) { + return `${filePath}:${error.offset}: ${error.kind}`; +} + +async function activate(context) { + const output = vscode.window.createOutputChannel("Tape Atom DSL"); + const emitter = new vscode.EventEmitter(); + const legend = new vscode.SemanticTokensLegend(TOKEN_TYPES, TOKEN_MODIFIERS); + let workspaceIndex = createIndex(); + let rebuildGeneration = 0; + let debounceHandle = null; + + async function rebuildIndex() { + const generation = ++rebuildGeneration; + const files = await vscode.workspace.findFiles(SOURCE_GLOB, EXCLUDE_GLOB); + let nextIndex = createIndex(); + + for (const uri of files) { + if (generation !== rebuildGeneration) return; + if (isExcluded(uri)) continue; + try { + const bytes = await vscode.workspace.fs.readFile(uri); + const source = Buffer.from(bytes).toString("utf8"); + const result = scanSource(source, uri.fsPath); + nextIndex = mergeIndexes(nextIndex, result.index); + for (const error of result.errors) output.appendLine(formatError(uri.fsPath, error)); + } catch (error) { + output.appendLine(`${uri.fsPath}: ${error.stack || error.message || error}`); + } + } + + if (generation !== rebuildGeneration) return; + workspaceIndex = nextIndex; + emitter.fire(); + } + + function scheduleRebuild(uri) { + if (uri && isExcluded(uri)) return; + if (debounceHandle !== null) clearTimeout(debounceHandle); + debounceHandle = setTimeout(() => { + debounceHandle = null; + rebuildIndex().catch((error) => output.appendLine(error.stack || String(error))); + }, 100); + } + + const provider = { + onDidChangeSemanticTokens: emitter.event, + provideDocumentSemanticTokens(document, cancellationToken) { + try { + const result = classifyDocument( + document.getText(), + document.uri.fsPath, + workspaceIndex, + () => cancellationToken.isCancellationRequested + ); + const builder = new vscode.SemanticTokensBuilder(legend); + for (const span of result.spans) { + if (cancellationToken.isCancellationRequested) break; + builder.push(span.line, span.character, span.length, span.typeIndex, span.modifierMask); + } + for (const error of result.errors) { + output.appendLine(formatError(document.uri.fsPath || document.uri.toString(), error)); + } + return builder.build(); + } catch (error) { + output.appendLine(`${document.uri}: ${error.stack || error.message || error}`); + return new vscode.SemanticTokensBuilder(legend).build(); + } + }, + }; + + const selector = [ + { language: "c", scheme: "file" }, + { language: "c", scheme: "untitled" }, + { language: "cpp", scheme: "file" }, + { language: "cpp", scheme: "untitled" }, + ]; + const watcher = vscode.workspace.createFileSystemWatcher(SOURCE_GLOB); + + context.subscriptions.push( + output, + emitter, + watcher, + watcher.onDidCreate(scheduleRebuild), + watcher.onDidChange(scheduleRebuild), + watcher.onDidDelete(scheduleRebuild), + vscode.languages.registerDocumentSemanticTokensProvider(selector, provider, legend), + { dispose() { if (debounceHandle !== null) clearTimeout(debounceHandle); } } + ); + + await rebuildIndex(); +} + +function deactivate() {} + +module.exports = { activate, deactivate }; diff --git a/.vscode/tape-atom-syntax/lexer.js b/.vscode/tape-atom-syntax/lexer.js new file mode 100644 index 0000000..bfe6908 --- /dev/null +++ b/.vscode/tape-atom-syntax/lexer.js @@ -0,0 +1,186 @@ +"use strict"; + +function isIdentifierStart(code) { + return code === 95 || + (code >= 65 && code <= 90) || + (code >= 97 && code <= 122); +} + +function isIdentifierContinue(code) { + return isIdentifierStart(code) || (code >= 48 && code <= 57); +} + +function lex(source) { + if (typeof source !== "string") throw new TypeError("source must be a string"); + + const tokens = []; + const errors = []; + let offset = 0; + let line = 0; + let character = 0; + + function advance() { + if (source[offset] === "\r" && source[offset + 1] === "\n") { + offset += 2; + line += 1; + character = 0; + return; + } + if (source[offset] === "\n") { + offset += 1; + line += 1; + character = 0; + return; + } + offset += 1; + character += 1; + } + + function pushToken(kind, start, startLine, startCharacter) { + tokens.push({ + kind, + text: source.slice(start, offset), + start, + end: offset, + line: startLine, + character: startCharacter, + }); + } + + while (offset < source.length) { + const ch = source[offset]; + + if (/\s/.test(ch)) { + advance(); + continue; + } + + if (ch === "/" && source[offset + 1] === "/") { + while (offset < source.length && source[offset] !== "\r" && source[offset] !== "\n") advance(); + continue; + } + + if (ch === "/" && source[offset + 1] === "*") { + const start = offset; + advance(); + advance(); + let closed = false; + while (offset < source.length) { + if (source[offset] === "*" && source[offset + 1] === "/") { + advance(); + advance(); + closed = true; + break; + } + advance(); + } + if (!closed) errors.push({ kind: "unterminated-block-comment", offset: start }); + continue; + } + + if (ch === "\"" || ch === "'") { + const quote = ch; + const start = offset; + advance(); + let closed = false; + while (offset < source.length) { + if (source[offset] === "\\") { + advance(); + if (offset < source.length) advance(); + continue; + } + if (source[offset] === quote) { + advance(); + closed = true; + break; + } + if (source[offset] === "\n" || source[offset] === "\r") break; + advance(); + } + if (!closed) errors.push({ kind: "unterminated-literal", offset: start }); + continue; + } + + const code = source.charCodeAt(offset); + if (isIdentifierStart(code)) { + const start = offset; + const startLine = line; + const startCharacter = character; + advance(); + while (offset < source.length && isIdentifierContinue(source.charCodeAt(offset))) advance(); + pushToken("identifier", start, startLine, startCharacter); + continue; + } + + const start = offset; + const startLine = line; + const startCharacter = character; + advance(); + pushToken("punctuation", start, startLine, startCharacter); + } + + return { tokens, errors }; +} + +function buildCallContexts(tokens) { + const contexts = Array.from({ length: tokens.length }, () => []); + const calls = []; + const errors = []; + const stack = []; + + for (let tokenIndex = 0; tokenIndex < tokens.length; tokenIndex += 1) { + const token = tokens[tokenIndex]; + + if (token.text === ")") { + if (stack.length === 0) { + errors.push({ kind: "unmatched-close-paren", offset: token.start }); + } else { + const frame = stack.pop(); + if (frame.callee !== null) calls.push({ ...frame, closeTokenIndex: tokenIndex }); + } + } + + contexts[tokenIndex] = stack + .filter((frame) => frame.callee !== null) + .map((frame) => ({ + callee: frame.callee, + calleeTokenIndex: frame.calleeTokenIndex, + openTokenIndex: frame.openTokenIndex, + argIndex: frame.argIndex, + })); + + if (token.text === "(") { + const previous = tokens[tokenIndex - 1]; + const hasCallee = previous && previous.kind === "identifier"; + stack.push({ + callee: hasCallee ? previous.text : null, + calleeTokenIndex: hasCallee ? tokenIndex - 1 : -1, + openTokenIndex: tokenIndex, + argIndex: 0, + }); + continue; + } + + if (token.text === "," && stack.length > 0) { + const frame = stack[stack.length - 1]; + if (frame.callee !== null) frame.argIndex += 1; + } + } + + for (const frame of stack) { + errors.push({ kind: "unmatched-open-paren", offset: tokens[frame.openTokenIndex].start }); + } + + return { contexts, calls, errors }; +} + +function nearestCall(contexts, tokenIndex, callee) { + const entries = contexts[tokenIndex] || []; + for (let contextIndex = entries.length - 1; contextIndex >= 0; contextIndex -= 1) { + const entry = entries[contextIndex]; + if (callee === undefined || entry.callee === callee) return entry; + } + return null; +} + +module.exports = { buildCallContexts, lex, nearestCall }; diff --git a/.vscode/tape-atom-syntax/package.json b/.vscode/tape-atom-syntax/package.json new file mode 100644 index 0000000..c58e883 --- /dev/null +++ b/.vscode/tape-atom-syntax/package.json @@ -0,0 +1,81 @@ +{ + "name": "tape-atom-syntax", + "displayName": "Tape Atom DSL", + "description": "Source-derived semantic highlighting for the Tape/Atom MIPS macro DSL", + "publisher": "local", + "version": "0.2.0", + "engines": { "vscode": "^1.80.0" }, + "categories": ["Programming Languages"], + "activationEvents": ["onLanguage:c", "onLanguage:cpp"], + "main": "./extension.js", + "files": [ + "classifier.js", + "extension.js", + "lexer.js", + "source-index.js", + "syntaxes/tape_atom.tmLanguage.json" + ], + "scripts": { + "test": "node --test test/*.test.js", + "package": "npx --yes @vscode/vsce@3.6.1 package --allow-missing-repository --skip-license --out tape-atom-syntax-0.2.0.vsix" + }, + "contributes": { + "semanticTokenTypes": [ + { "id": "tapeAtomKeyword", "description": "Tape atom declaration keyword" }, + { "id": "tapeAtomName", "description": "Tape atom name" }, + { "id": "tapeComponentKeyword", "description": "Tape atom component declaration keyword" }, + { "id": "tapeComponentName", "description": "Tape atom component name" }, + { "id": "tapeAnnotation", "description": "Tape atom annotation" }, + { "id": "tapeBindType", "description": "Tape bind structure type" }, + { "id": "tapePhase", "description": "Tape atom phase" }, + { "id": "tapeLabel", "description": "Tape atom branch label" }, + { "id": "tapeCpuInstruction", "description": "MIPS CPU instruction emitter" }, + { "id": "tapeControlFlow", "description": "MIPS branch or jump instruction" }, + { "id": "tapeGteInstruction", "description": "GTE instruction emitter" }, + { "id": "tapeGpuInstruction", "description": "GPU command emitter" }, + { "id": "tapeComponentInstruction", "description": "Tape atom component invocation" }, + { "id": "tapeDelaySlot", "description": "Load or branch delay slot annotation" }, + { "id": "tapeGprRegister", "description": "MIPS GPR alias" }, + { "id": "tapeCop2Register", "description": "COP2 data or control register alias" }, + { "id": "tapeDuffleType", "description": "Duffle type or type constructor" }, + { "id": "tapeAttribute", "description": "Duffle linkage or storage attribute" } + ], + "semanticTokenModifiers": [ + { "id": "tapeRead", "description": "Register declared in atom_reads" }, + { "id": "tapeWrite", "description": "Register declared in atom_writes" }, + { "id": "tapeAuto", "description": "Auto-allocated register" } + ], + "semanticTokenScopes": [ + { + "language": "c", + "scopes": { + "tapeAtomKeyword": ["keyword.control.duffle.atom"], + "tapeAtomName": ["entity.name.function.duffle.atom"], + "tapeComponentKeyword": ["keyword.control.duffle.component"], + "tapeComponentName": ["entity.name.function.duffle.component"], + "tapeAnnotation": ["support.function.duffle.annotation"], + "tapeBindType": ["entity.name.type.duffle.bind"], + "tapePhase": ["entity.name.tag.duffle.phase"], + "tapeLabel": ["entity.name.label.duffle.atom"], + "tapeCpuInstruction": ["support.function.duffle.cpu"], + "tapeControlFlow": ["keyword.control.duffle.branch"], + "tapeGteInstruction": ["support.function.duffle.gte"], + "tapeGpuInstruction": ["support.function.duffle.gpu"], + "tapeComponentInstruction": ["support.function.duffle.component"], + "tapeDelaySlot": ["keyword.operator.duffle.delayslot"], + "tapeGprRegister": ["variable.other.constant.duffle.gpr"], + "tapeCop2Register": ["variable.other.constant.duffle.cop2"], + "tapeDuffleType": ["storage.type.duffle.type"], + "tapeAttribute": ["storage.modifier.duffle.attr"] + } + } + ], + "grammars": [ + { + "scopeName": "tape_atom.injection", + "path": "./syntaxes/tape_atom.tmLanguage.json", + "injectTo": ["source.c", "source.cpp"] + } + ] + } +} diff --git a/.vscode/tape-atom-syntax/source-index.js b/.vscode/tape-atom-syntax/source-index.js new file mode 100644 index 0000000..bd0f0b6 --- /dev/null +++ b/.vscode/tape-atom-syntax/source-index.js @@ -0,0 +1,222 @@ +"use strict"; + +const path = require("node:path"); +const { buildCallContexts, lex, nearestCall } = require("./lexer"); + +const BASE_TYPES = [ + "B1", "B2", "B4", "B8", "F4", "F8", "S1", "S2", "S4", "S8", + "U1", "U2", "U4", "U8", "MipsAtom", "MipsCode", "Reg", +]; + +const BASE_ATTRIBUTES = [ + "FI_", "I_", "NI_", "Relative_", "Struct_", "Enum_", "Union_", + "TypeR_", "TypeV_", "align_", "internal", "local_persist", "global", +]; + +function createIndex() { + return { + atoms: new Set(), + components: new Set(), + componentAliases: new Set(), + macros: new Map(), + registers: new Map(), + bindTypes: new Set(), + types: new Set(BASE_TYPES), + phases: new Set(), + labels: new Set(), + attributes: new Set(BASE_ATTRIBUTES), + }; +} + +function cloneIndex(source) { + const result = createIndex(); + for (const key of ["atoms", "components", "componentAliases", "bindTypes", "types", "phases", "labels", "attributes"]) { + for (const value of source[key]) result[key].add(value); + } + for (const [name, domain] of source.macros) result.macros.set(name, domain); + for (const [name, domain] of source.registers) result.registers.set(name, domain); + return result; +} + +function mergeIndexes(...sources) { + const result = createIndex(); + for (const source of sources) { + if (!source) continue; + for (const key of ["atoms", "components", "componentAliases", "bindTypes", "types", "phases", "labels", "attributes"]) { + for (const value of source[key]) result[key].add(value); + } + for (const [name, domain] of source.macros) result.macros.set(name, domain); + for (const [name, domain] of source.registers) result.registers.set(name, domain); + } + return result; +} + +function domainFromPath(filePath) { + const base = path.basename(filePath.replaceAll("\\", "/")).toLowerCase(); + if (base === "mips.h" || base === "mips.atom.c") return "cpu"; + if (base === "gte.h" || base === "gte.atom.c") return "gte"; + if (base === "gp.h" || base === "gp.atom.c") return "gpu"; + return "component"; +} + +function registerKind(name) { + if (/^R_[A-Za-z0-9_]+$/.test(name)) return "gpr"; + if (/^(?:C2_|gte_cr_)[A-Za-z0-9_]+$/.test(name)) return "cop2"; + return null; +} + +function componentAlias(name) { + return name.startsWith("ac_") ? `mac_${name.slice(3)}` : null; +} + +function findFunctionNameBefore(tokens, calleeTokenIndex) { + let closeIndex = calleeTokenIndex - 1; + while (closeIndex >= 0 && tokens[closeIndex].kind === "identifier" && tokens[closeIndex].text === "atom_dbg_skip") { + closeIndex -= 1; + } + if (!tokens[closeIndex] || tokens[closeIndex].text !== ")") return null; + + let depth = 1; + for (let tokenIndex = closeIndex - 1; tokenIndex >= 0; tokenIndex -= 1) { + if (tokens[tokenIndex].text === ")") depth += 1; + if (tokens[tokenIndex].text === "(") depth -= 1; + if (depth !== 0) continue; + const name = tokens[tokenIndex - 1]; + return name && name.kind === "identifier" ? name : null; + } + return null; +} + +function scanSource(source, filePath) { + const lexical = lex(source); + const balanced = buildCallContexts(lexical.tokens); + const tokens = lexical.tokens; + const contexts = balanced.contexts; + const index = createIndex(); + const declarations = new Map(); + const domain = domainFromPath(filePath); + + function mark(token, role, modifiers = ["declaration"]) { + declarations.set(token.start, { role, modifiers }); + } + + function addComponent(token) { + index.components.add(token.text); + mark(token, "componentName"); + const alias = componentAlias(token.text); + if (alias) { + index.componentAliases.add(alias); + index.macros.set(alias, domain); + } + } + + for (let tokenIndex = 0; tokenIndex < tokens.length; tokenIndex += 1) { + const token = tokens[tokenIndex]; + if (token.kind !== "identifier") continue; + + const kind = registerKind(token.text); + if (kind) { + index.registers.set(token.text, kind); + if (tokens[tokenIndex + 1] && tokens[tokenIndex + 1].text === "=") { + mark(token, kind === "gpr" ? "gprRegister" : "cop2Register"); + } + } + + const context = nearestCall(contexts, tokenIndex); + if (context && context.argIndex === 0) { + if (context.callee === "MipsAtom_") { + index.atoms.add(token.text); + mark(token, "atomName"); + } + if (context.callee === "MipsAtomComp_") addComponent(token); + if (context.callee === "atom_bind") index.bindTypes.add(token.text); + if (context.callee === "atom_phase" || context.callee === "phase_auto_reg") index.phases.add(token.text); + if (context.callee === "atom_label" || context.callee === "atom_offset") index.labels.add(token.text); + } + + const isWrappedType = context && ( + ((context.callee === "Struct_" || context.callee === "Union_") && context.argIndex === 0) || + (context.callee === "Enum_" && context.argIndex === 1) + ); + if (isWrappedType) { + index.types.add(token.text); + mark(token, token.text.startsWith("Binds_") ? "bindType" : "duffleType"); + if (token.text.startsWith("Binds_")) index.bindTypes.add(token.text); + } + + if (context && context.callee === "atom_offset" && context.argIndex === 1) index.labels.add(token.text); + + if (context && context.callee === "atom_auto_reg") { + if (context.argIndex === 0) index.atoms.add(token.text); + if (context.argIndex === 1) { + index.registers.set(token.text, "gpr"); + mark(token, "gprRegister", ["declaration", "tapeAuto"]); + } + } + + if (context && context.callee === "phase_auto_reg" && context.argIndex === 1) { + index.registers.set(token.text, "gpr"); + mark(token, "gprRegister", ["declaration", "tapeAuto"]); + } + + if (token.text === "define" && tokens[tokenIndex - 1] && tokens[tokenIndex - 1].text === "#") { + const name = tokens[tokenIndex + 1]; + if (name && name.kind === "identifier" && name.line === token.line) index.macros.set(name.text, domain); + } + + if (token.text === "typedef") { + let endIndex = tokenIndex + 1; + let hasBrace = false; + let lastIdentifier = null; + while (endIndex < tokens.length && tokens[endIndex].text !== ";") { + if (tokens[endIndex].text === "{") hasBrace = true; + if (tokens[endIndex].kind === "identifier") lastIdentifier = tokens[endIndex]; + endIndex += 1; + } + if (!hasBrace && lastIdentifier) { + index.types.add(lastIdentifier.text); + mark(lastIdentifier, "duffleType"); + } + } + + if (token.text === "MipsAtom_Proc_") { + const functionName = findFunctionNameBefore(tokens, tokenIndex); + if (functionName) { + const atomName = functionName.text.endsWith("_proc") + ? functionName.text.slice(0, -5) + : functionName.text; + index.atoms.add(atomName); + index.atoms.add(functionName.text); + mark(functionName, "atomName"); + } + } + + if (token.text === "MipsAtomComp_Proc_") { + const functionName = findFunctionNameBefore(tokens, tokenIndex); + if (functionName) addComponent(functionName); + } + } + + for (const call of balanced.calls) { + if (domain === "component") continue; + const name = tokens[call.calleeTokenIndex]; + const after = tokens[call.closeTokenIndex + 1]; + if (!name || !after || after.text !== "{") continue; + if (/^(?:gp0_|gp1_|gte_|mac_)/.test(name.text)) index.macros.set(name.text, domain); + } + + return { + index: cloneIndex(index), + declarations, + tokens, + contexts, + errors: [...lexical.errors, ...balanced.errors], + }; +} + +module.exports = { + createIndex, + domainFromPath, + mergeIndexes, + scanSource, +}; diff --git a/.vscode/tape-atom-syntax/syntaxes/tape_atom.tmLanguage.json b/.vscode/tape-atom-syntax/syntaxes/tape_atom.tmLanguage.json new file mode 100644 index 0000000..7edf983 --- /dev/null +++ b/.vscode/tape-atom-syntax/syntaxes/tape_atom.tmLanguage.json @@ -0,0 +1,85 @@ +{ + "scopeName": "tape_atom.injection", + "injectionSelector": "L:source.c -comment -string, L:source.cpp -comment -string", + "patterns": [ + { "include": "#atom-declarations" }, + { "include": "#component-declarations" }, + { "include": "#annotation-arguments" }, + { "include": "#annotations" }, + { "include": "#instructions" }, + { "include": "#registers" }, + { "include": "#types" }, + { "include": "#attributes" } + ], + "repository": { + "atom-declarations": { + "patterns": [ + { + "match": "\\b(MipsAtom_)\\s*\\(\\s*([A-Za-z_][A-Za-z0-9_]*)", + "captures": { + "1": { "name": "keyword.control.duffle.atom" }, + "2": { "name": "entity.name.function.duffle.atom" } + } + }, + { "match": "\\bMipsAtom_Proc_\\b", "name": "keyword.control.duffle.atom" }, + { "match": "\\b[A-Za-z_][A-Za-z0-9_]*_proc\\b", "name": "entity.name.function.duffle.atom" } + ] + }, + "component-declarations": { + "patterns": [ + { + "match": "\\b(MipsAtomComp_)\\s*\\(\\s*(ac_[A-Za-z0-9_]*)", + "captures": { + "1": { "name": "keyword.control.duffle.component" }, + "2": { "name": "entity.name.function.duffle.component" } + } + }, + { "match": "\\bMipsAtomComp_Proc_\\b", "name": "keyword.control.duffle.component" }, + { "match": "\\bac_[A-Za-z0-9_]+\\b", "name": "entity.name.function.duffle.component" } + ] + }, + "annotation-arguments": { + "patterns": [ + { "match": "(?<=\\batom_bind\\()\\s*Binds_[A-Za-z0-9_]+", "name": "entity.name.type.duffle.bind" }, + { "match": "(?<=\\batom_phase\\()\\s*[A-Za-z_][A-Za-z0-9_]*", "name": "entity.name.tag.duffle.phase" }, + { "match": "(?<=\\batom_label\\()\\s*[A-Za-z_][A-Za-z0-9_]*", "name": "entity.name.label.duffle.atom" }, + { + "match": "\\b(atom_offset)\\s*\\(\\s*([A-Za-z_][A-Za-z0-9_]*)\\s*,\\s*([A-Za-z_][A-Za-z0-9_]*)", + "captures": { + "1": { "name": "support.function.duffle.annotation" }, + "2": { "name": "entity.name.label.duffle.atom" }, + "3": { "name": "entity.name.label.duffle.atom" } + } + } + ] + }, + "annotations": { + "match": "\\b(atom_info|atom_bind|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)\\b", + "name": "support.function.duffle.annotation" + }, + "instructions": { + "patterns": [ + { "match": "\\b(branch_|jump_|jump_rel|call_)[A-Za-z0-9_]*\\b", "name": "keyword.control.duffle.branch" }, + { "match": "\\b(LdSlot_|BdSlot_)\\b", "name": "keyword.operator.duffle.delayslot" }, + { "match": "\\b(load_|store_|add_|sub_|shift_|set_lt_|and_i|or_i|xor_i|nop[0-9]*)[A-Za-z0-9_]*\\b", "name": "support.function.duffle.cpu" }, + { "match": "\\bgte_(?!cr_)[A-Za-z0-9_]+\\b", "name": "support.function.duffle.gte" }, + { "match": "\\bgp[01]_[A-Za-z0-9_]+\\b", "name": "support.function.duffle.gpu" }, + { "match": "\\bmac_[A-Za-z0-9_]+\\b", "name": "support.function.duffle.component" } + ] + }, + "registers": { + "patterns": [ + { "match": "\\bR_[A-Za-z0-9_]+\\b", "name": "variable.other.constant.duffle.gpr" }, + { "match": "\\b(?:C2_|gte_cr_)[A-Za-z0-9_]+\\b", "name": "variable.other.constant.duffle.cop2" } + ] + }, + "types": { + "match": "\\b(?:[USFB][1248]|MipsAtom|MipsCode|Reg|Binds_[A-Za-z0-9_]+|Struct_|Enum_|Union_|TypeR_|TypeV_)\\b", + "name": "storage.type.duffle.type" + }, + "attributes": { + "match": "\\b(?:FI_|I_|NI_|Relative_|align_|internal|local_persist|global)\\b", + "name": "storage.modifier.duffle.attr" + } + } +} diff --git a/.vscode/tape-atom-syntax/tape-atom-syntax-0.2.0.vsix b/.vscode/tape-atom-syntax/tape-atom-syntax-0.2.0.vsix new file mode 100644 index 0000000000000000000000000000000000000000..b15f1cc5289a3fe961f5efbda2881669077e55e9 GIT binary patch literal 10468 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 literal 0 HcmV?d00001 diff --git a/.vscode/tape-atom-syntax/test/classifier.test.js b/.vscode/tape-atom-syntax/test/classifier.test.js new file mode 100644 index 0000000..c19275d --- /dev/null +++ b/.vscode/tape-atom-syntax/test/classifier.test.js @@ -0,0 +1,83 @@ +"use strict"; + +const assert = require("node:assert/strict"); +const test = require("node:test"); + +const { classifyDocument } = require("../classifier"); +const { createIndex } = require("../source-index"); + +function byText(result, text) { + return result.spans.filter((span) => span.text === text); +} + +test("classifyDocument distinguishes declaration, annotation, phase, bind, and label roles", () => { + const source = [ + "typedef Struct_(Binds_CubeTri) { U4 PrimCursor; };", + "MipsAtom_(cube_g4_face) atom_info(atom_bind(Binds_CubeTri), atom_phase(cube_g4),", + "\tatom_reads(R_PrimCursor), atom_writes(R_FaceCursor)) {", + "\tbranch_le_zero(R_T0, atom_offset(cull, exit)),", + "\tatom_label(exit)", + "};", + ].join("\n"); + + const result = classifyDocument(source, "C:/x/code/hello_camera/hello_camera.atom.c", createIndex()); + + assert.equal(byText(result, "MipsAtom_")[0].type, "tapeAtomKeyword"); + assert.deepEqual(byText(result, "cube_g4_face")[0].modifiers, ["declaration"]); + assert.equal(byText(result, "atom_bind")[0].type, "tapeAnnotation"); + assert.equal(byText(result, "Binds_CubeTri").at(-1).type, "tapeBindType"); + assert.equal(byText(result, "cube_g4")[0].type, "tapePhase"); + assert.deepEqual(byText(result, "cube_g4")[0].modifiers, ["declaration"]); + assert.equal(byText(result, "cull")[0].type, "tapeLabel"); + assert.equal(byText(result, "exit").every((span) => span.type === "tapeLabel"), true); +}); + +test("classifyDocument applies read and write modifiers to GPRs", () => { + const source = "atom_info(atom_reads(R_PrimCursor), atom_writes(R_FaceCursor))"; + const result = classifyDocument(source, "C:/x/code/test.atom.c", createIndex()); + + assert.deepEqual(byText(result, "R_PrimCursor")[0].modifiers, ["tapeRead"]); + assert.deepEqual(byText(result, "R_FaceCursor")[0].modifiers, ["tapeWrite"]); +}); + +test("classifyDocument separates CPU, GTE, GPU, and component domains", () => { + const workspace = createIndex(); + workspace.macros.set("load_word", "cpu"); + workspace.macros.set("gte_cmdw_rtpt", "gte"); + workspace.macros.set("gp1_word_DisplayOn", "gpu"); + workspace.macros.set("mac_yield", "component"); + workspace.componentAliases.add("mac_yield"); + + const source = "load_word(R_T0, R_T1, 0), gte_cmdw_rtpt, gp1_word_DisplayOn(), mac_yield(), C2_MAC0, gte_cr_OFX_Code"; + const result = classifyDocument(source, "C:/x/code/test.c", workspace); + + assert.equal(byText(result, "load_word")[0].type, "tapeCpuInstruction"); + assert.equal(byText(result, "gte_cmdw_rtpt")[0].type, "tapeGteInstruction"); + assert.equal(byText(result, "gp1_word_DisplayOn")[0].type, "tapeGpuInstruction"); + assert.equal(byText(result, "mac_yield")[0].type, "tapeComponentInstruction"); + assert.equal(byText(result, "C2_MAC0")[0].type, "tapeCop2Register"); + assert.equal(byText(result, "gte_cr_OFX_Code")[0].type, "tapeCop2Register"); +}); + +test("document-local declarations override an empty workspace index", () => { + const source = [ + "MipsAtomComp_(ac_new_component) { nop };", + "mac_new_component(),", + ].join("\n"); + const result = classifyDocument(source, "C:/x/code/duffle/math.atom.c", createIndex()); + + assert.equal(byText(result, "ac_new_component")[0].type, "tapeComponentName"); + assert.equal(byText(result, "mac_new_component")[0].type, "tapeComponentInstruction"); +}); + +test("classifier returns ordered non-overlapping spans and partial malformed output", () => { + const source = "atom_reads(R_A /* broken"; + const result = classifyDocument(source, "C:/x/code/test.atom.c", createIndex()); + + assert.equal(result.errors.some((error) => error.kind === "unterminated-block-comment"), true); + for (let spanIndex = 1; spanIndex < result.spans.length; spanIndex += 1) { + const previous = result.spans[spanIndex - 1]; + const current = result.spans[spanIndex]; + assert.equal(previous.start + previous.length <= current.start, true); + } +}); diff --git a/.vscode/tape-atom-syntax/test/contract.test.js b/.vscode/tape-atom-syntax/test/contract.test.js new file mode 100644 index 0000000..05b0d31 --- /dev/null +++ b/.vscode/tape-atom-syntax/test/contract.test.js @@ -0,0 +1,93 @@ +"use strict"; + +const assert = require("node:assert/strict"); +const fs = require("node:fs"); +const path = require("node:path"); +const test = require("node:test"); + +const { TOKEN_MODIFIERS, TOKEN_TYPES } = require("../classifier"); + +const ROOT = path.resolve(__dirname, ".."); + +function readJson(filePath) { + return JSON.parse(fs.readFileSync(filePath, "utf8")); +} + +function collectScopeNames(value, output = new Set()) { + if (Array.isArray(value)) { + for (const entry of value) collectScopeNames(entry, output); + return output; + } + if (!value || typeof value !== "object") return output; + if (typeof value.name === "string") output.add(value.name); + for (const child of Object.values(value)) collectScopeNames(child, output); + return output; +} + +test("package semantic legend matches classifier exports", () => { + const packageJson = readJson(path.join(ROOT, "package.json")); + const contributedTypes = packageJson.contributes.semanticTokenTypes.map((entry) => entry.id); + const contributedModifiers = packageJson.contributes.semanticTokenModifiers.map((entry) => entry.id); + + assert.equal(packageJson.version, "0.2.0"); + assert.deepEqual(contributedTypes, TOKEN_TYPES); + assert.deepEqual(contributedModifiers, TOKEN_MODIFIERS.filter((name) => name !== "declaration")); +}); + +test("package includes runtime files only and acknowledges local-only metadata", () => { + const packageJson = readJson(path.join(ROOT, "package.json")); + + assert.deepEqual(packageJson.files, [ + "classifier.js", + "extension.js", + "lexer.js", + "source-index.js", + "syntaxes/tape_atom.tmLanguage.json", + ]); + assert.equal(packageJson.scripts.package.includes("--allow-missing-repository"), true); + assert.equal(packageJson.scripts.package.includes("--skip-license"), true); +}); + +test("every semantic token has a TextMate fallback scope and grammar scope", () => { + const packageJson = readJson(path.join(ROOT, "package.json")); + const grammar = readJson(path.join(ROOT, "syntaxes", "tape_atom.tmLanguage.json")); + const mappings = packageJson.contributes.semanticTokenScopes[0].scopes; + const grammarScopes = collectScopeNames(grammar); + + for (const tokenType of TOKEN_TYPES) { + assert.equal(Array.isArray(mappings[tokenType]), true, `missing scope mapping: ${tokenType}`); + assert.equal(mappings[tokenType].some((scope) => grammarScopes.has(scope)), true, `grammar does not emit: ${tokenType}`); + } +}); + +test("TextMate offset labels stay scoped to atom_offset calls", () => { + const grammar = readJson(path.join(ROOT, "syntaxes", "tape_atom.tmLanguage.json")); + const serialized = JSON.stringify(grammar); + const offsetRule = grammar.repository["annotation-arguments"].patterns + .find((rule) => rule.match.includes("atom_offset")); + + assert.equal(serialized.includes("(?<=,)"), false); + assert.equal(offsetRule.captures[1].name, "support.function.duffle.annotation"); + assert.equal(offsetRule.captures[2].name, "entity.name.label.duffle.atom"); + assert.equal(offsetRule.captures[3].name, "entity.name.label.duffle.atom"); +}); + +test("workspace enables semantic highlighting and colors every custom token", () => { + const settings = readJson(path.resolve(ROOT, "..", "settings.json")); + const semantic = settings["editor.semanticTokenColorCustomizations"]; + + assert.equal(settings["editor.semanticHighlighting.enabled"], true); + assert.equal(semantic.enabled, true); + for (const tokenType of TOKEN_TYPES) { + assert.equal(Object.hasOwn(semantic.rules, tokenType), true, `missing color: ${tokenType}`); + } + for (const rule of [ + "tapeGprRegister.tapeRead", + "tapeGprRegister.tapeWrite", + "tapeCop2Register.tapeRead", + "tapeCop2Register.tapeWrite", + "*.tapeAuto", + ]) { + assert.equal(Object.hasOwn(semantic.rules, rule), true, `missing modifier color: ${rule}`); + } +}); diff --git a/.vscode/tape-atom-syntax/test/lexer.test.js b/.vscode/tape-atom-syntax/test/lexer.test.js new file mode 100644 index 0000000..8d06b80 --- /dev/null +++ b/.vscode/tape-atom-syntax/test/lexer.test.js @@ -0,0 +1,77 @@ +"use strict"; + +const assert = require("node:assert/strict"); +const test = require("node:test"); + +const { buildCallContexts, lex, nearestCall } = require("../lexer"); + +test("lex skips comments, strings, and character literals", () => { + const source = [ + "MipsAtom_(visible)", + "// MipsAtom_(line_comment)", + "const char *s = \"atom_reads(R_Hidden)\";", + "char c = '\\''; /* gte_cmdw_hidden */", + "atom_reads(R_Visible)", + ].join("\n"); + + const result = lex(source); + const identifiers = result.tokens + .filter((token) => token.kind === "identifier") + .map((token) => token.text); + + assert.deepEqual(result.errors, []); + assert.equal(identifiers.includes("visible"), true); + assert.equal(identifiers.includes("R_Visible"), true); + assert.equal(identifiers.includes("line_comment"), false); + assert.equal(identifiers.includes("R_Hidden"), false); + assert.equal(identifiers.includes("gte_cmdw_hidden"), false); +}); + +test("lex reports unterminated block comments without returning comment tokens", () => { + const result = lex("R_Visible /* atom_reads(R_Hidden)"); + + assert.equal(result.tokens.some((token) => token.text === "R_Visible"), true); + assert.equal(result.tokens.some((token) => token.text === "R_Hidden"), false); + assert.deepEqual(result.errors.map((error) => error.kind), ["unterminated-block-comment"]); +}); + +test("line comments stop at CRLF boundaries", () => { + const result = lex("// atom_reads(R_Hidden)\r\natom_reads(R_Visible)\r\n"); + const identifiers = result.tokens + .filter((token) => token.kind === "identifier") + .map((token) => token.text); + + assert.equal(identifiers.includes("R_Hidden"), false); + assert.equal(identifiers.includes("R_Visible"), true); +}); + +test("balanced contexts retain multiline nesting and argument indexes", () => { + const source = [ + "atom_info(", + "\tatom_phase(cube_g4),", + "\tatom_reads(R_A, nested(R_B, R_C)),", + "\tatom_writes(R_D)", + ")", + ].join("\n"); + const lexical = lex(source); + const balanced = buildCallContexts(lexical.tokens); + + const byText = new Map(); + lexical.tokens.forEach((token, index) => { + if (token.kind === "identifier") byText.set(token.text, index); + }); + + assert.equal(nearestCall(balanced.contexts, byText.get("cube_g4")).callee, "atom_phase"); + assert.equal(nearestCall(balanced.contexts, byText.get("R_A")).callee, "atom_reads"); + assert.equal(nearestCall(balanced.contexts, byText.get("R_A")).argIndex, 0); + assert.equal(nearestCall(balanced.contexts, byText.get("R_C")).callee, "nested"); + assert.equal(nearestCall(balanced.contexts, byText.get("R_D")).callee, "atom_writes"); + assert.deepEqual(balanced.errors, []); +}); + +test("balanced contexts report unmatched parentheses", () => { + const lexical = lex("atom_reads(R_A"); + const balanced = buildCallContexts(lexical.tokens); + + assert.deepEqual(balanced.errors.map((error) => error.kind), ["unmatched-open-paren"]); +}); diff --git a/.vscode/tape-atom-syntax/test/source-index.test.js b/.vscode/tape-atom-syntax/test/source-index.test.js new file mode 100644 index 0000000..c662737 --- /dev/null +++ b/.vscode/tape-atom-syntax/test/source-index.test.js @@ -0,0 +1,77 @@ +"use strict"; + +const assert = require("node:assert/strict"); +const test = require("node:test"); + +const { + createIndex, + domainFromPath, + mergeIndexes, + scanSource, +} = require("../source-index"); + +test("scanSource discovers current atom and component forms", () => { + const source = [ + "MipsAtom_(cube_g4_face) atom_info(atom_phase(cube_g4), atom_reads(R_PrimCursor)) { mac_yield() };", + "MipsAtomComp_(ac_load_pair) { load_word(R_T0, R_T1, 0) };", + "internal MipsAtom* normalize_proc(AtomArena_R aa) MipsAtom_Proc_(aa, { mac_yield() })", + "FI_ void ac_store_pair(MipsAtomBuilder_R ab) atom_dbg_skip MipsAtomComp_Proc_(ab, { store_word(R_T0, R_T1, 0) })", + ].join("\n"); + + const result = scanSource(source, "C:/projects/Pikuma/ps1/code/duffle/mips.atom.c"); + + assert.equal(result.index.atoms.has("cube_g4_face"), true); + assert.equal(result.index.atoms.has("normalize"), true); + assert.equal(result.index.components.has("ac_load_pair"), true); + assert.equal(result.index.components.has("ac_store_pair"), true); + assert.equal(result.index.componentAliases.has("mac_load_pair"), true); + assert.equal(result.index.componentAliases.has("mac_store_pair"), true); + assert.equal(result.index.macros.get("mac_store_pair"), "cpu"); + assert.equal(result.index.phases.has("cube_g4"), true); + assert.equal(result.index.registers.get("R_PrimCursor"), "gpr"); + assert.deepEqual(result.errors, []); +}); + +test("scanSource discovers binds, labels, registers, typedefs, and macro domains", () => { + const source = [ + "typedef Struct_(Binds_CubeTri) { U4 PrimCursor; };", + "typedef Enum_(U4, PadStatus) { PadStatus_Ok };", + "typedef U4 const MipsCode;", + "enum { R_PrimCursor = R_T7 atom_reg, C2_Custom = 12, gte_cr_Custom = 13 };", + "#define load_word(rt, base, off) enc_i(rt, base, off)", + "atom_bind(Binds_CubeTri)", + "atom_label(exit)", + "atom_offset(entry, exit)", + ].join("\n"); + + const result = scanSource(source, "C:/projects/Pikuma/ps1/code/duffle/mips.h"); + + assert.equal(result.index.bindTypes.has("Binds_CubeTri"), true); + assert.equal(result.index.types.has("PadStatus"), true); + assert.equal(result.index.types.has("MipsCode"), true); + assert.equal(result.index.registers.get("R_PrimCursor"), "gpr"); + assert.equal(result.index.registers.get("C2_Custom"), "cop2"); + assert.equal(result.index.registers.get("gte_cr_Custom"), "cop2"); + assert.equal(result.index.macros.get("load_word"), "cpu"); + assert.equal(result.index.labels.has("entry"), true); + assert.equal(result.index.labels.has("exit"), true); +}); + +test("domainFromPath uses the declaration file rather than parent directory names", () => { + assert.equal(domainFromPath("C:/x/code/hello_gte/hello_gte.atom.c"), "component"); + assert.equal(domainFromPath("C:/x/code/duffle/mips.h"), "cpu"); + assert.equal(domainFromPath("C:/x/code/duffle/gte.atom.c"), "gte"); + assert.equal(domainFromPath("C:/x/code/duffle/gp.h"), "gpu"); +}); + +test("mergeIndexes preserves domain-specific aliases", () => { + const left = createIndex(); + left.macros.set("load_word", "cpu"); + const right = createIndex(); + right.componentAliases.add("mac_gte_store"); + right.macros.set("mac_gte_store", "gte"); + + const merged = mergeIndexes(left, right); + assert.equal(merged.macros.get("load_word"), "cpu"); + assert.equal(merged.macros.get("mac_gte_store"), "gte"); +});