From 4a161450b211d8a6a39280a39ee6d8daefc91954 Mon Sep 17 00:00:00 2001 From: Magnus Andersson Date: Thu, 5 Nov 2026 14:52:18 +0100 Subject: [PATCH 1/4] Add addFunctionBreakpointInContext Signed-off-by: Magnus Andersson --- README.md | 14 +++++- package.json | 20 +++++++++ src/breakpointManager.ts | 16 ++++++- src/extension.ts | 69 ++++++++++++++++++++++++++++++ src/tcfDebugSession.ts | 8 +++- src/test/breakpointManager.test.ts | 38 ++++++++++++++++ test/symbolContexts.test.ts | 12 +++++- 7 files changed, 171 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 53ea97c..f822ae2 100644 --- a/README.md +++ b/README.md @@ -163,7 +163,7 @@ reason "breakpoint"; step again to continue. Breakpoints are kept in sync with the agent's breakpoint table: adding, editing and removing breakpoints in VS Code sends `Breakpoints.add`, `change` and `remove`. Breakpoints apply to all contexts the agent debugs, except -instruction breakpoints, see below. +explicitly scoped function breakpoints and instruction breakpoints, see below. | VS Code | TCF breakpoint properties | |---------------------------------|----------------------------------------------------| @@ -190,6 +190,18 @@ Function breakpoints stop at the first instruction of the function. The reference TCF agent also supports a non-standard `SkipPrologue` property which is not used, to stay within the TCF specification. +Use **Simics Target Debug: Add Function Breakpoint in Context...** from the +Command Palette, the Breakpoints toolbar, or a stack frame's context menu to +choose a core before entering the symbol. The selected call-stack context is +offered first; **All contexts** keeps the breakpoint global. Scoped entries +appear as `start_kernel @ armv8.cluster[0].core[0]` in the Breakpoints panel. +The ` @ ` suffix is reserved for the exact TCF context ID and is saved with +the breakpoint; selecting another frame does not change its scope. The same +syntax can be entered or edited using the built-in function-breakpoint `+`. +Scoped function breakpoints require the agent's `ContextIds` support and are +never silently planted globally when it is unavailable. Their scope applies +to that context (and, for a process context, its threads), not other cores. + An address only has a meaning in one address space, so an instruction breakpoint is limited to the process of the context shown in the Disassembly view (its `ProcessID`). `ContextIds` lists the process and all its threads: diff --git a/package.json b/package.json index e879cc1..fefd515 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,12 @@ ], "contributes": { "commands": [ + { + "command": "simics-target-debug.addFunctionBreakpointInContext", + "title": "Add Function Breakpoint in Context...", + "category": "Simics Target Debug", + "icon": "$(debug-breakpoint-function)" + }, { "command": "simics-target-debug.addPathMapRule", "title": "Add Path Map Rule...", @@ -236,6 +242,10 @@ ], "menus": { "commandPalette": [ + { + "command": "simics-target-debug.addFunctionBreakpointInContext", + "when": "debugType == simics-target-debug" + }, { "command": "simics-target-debug.saveSymbolFiles", "when": "debugType == simics-target-debug" @@ -383,6 +393,11 @@ } ], "view/title": [ + { + "command": "simics-target-debug.addFunctionBreakpointInContext", + "when": "view == workbench.debug.viewlet.state && debugType == simics-target-debug", + "group": "navigation@1" + }, { "command": "simics-target-debug.openConsole", "when": "view == simics-target-debug.consoles && debugType == simics-target-debug && !simics-target-debug.hasConsoles", @@ -488,6 +503,11 @@ } ], "debug/callstack/context": [ + { + "command": "simics-target-debug.addFunctionBreakpointInContext", + "when": "debugType == 'simics-target-debug' && callStackItemType == 'stackFrame'", + "group": "3_modification" + }, { "command": "simics-target-debug.copyFrameAddress", "when": "debugType == 'simics-target-debug' && callStackItemType == 'stackFrame'", diff --git a/src/breakpointManager.ts b/src/breakpointManager.ts index 29fb0d3..2930a38 100644 --- a/src/breakpointManager.ts +++ b/src/breakpointManager.ts @@ -218,10 +218,24 @@ export class BreakpointManager { const caps = await this.capabilities(); const specs = requested.map((r): BreakpointSpec => { const spec: BreakpointSpec = { kind: "function", key: r.name }; + const separator = r.name.lastIndexOf(" @ "); + const contextId = separator >= 0 ? r.name.slice(separator + 3).trim() : undefined; + const location = separator >= 0 ? r.name.slice(0, separator).trim() : r.name; if (caps.Location === false) { return { ...spec, error: "The TCF agent does not support breakpoints on expressions" }; } - return this.withCommonProps(spec, { Location: r.name }, r, caps); + if (separator >= 0 && (!contextId || !location)) { + return { ...spec, error: "Use 'symbol @ context ID' for a context-scoped function breakpoint" }; + } + if (contextId && caps.ContextIds === false) { + return { ...spec, error: "Context-scoped function breakpoints require breakpoint ContextIds support" }; + } + return this.withCommonProps( + spec, + { Location: location, ...(contextId && { ContextIds: [contextId] }) }, + r, + caps, + ); }); this.functionBreakpoints = await this.update(this.functionBreakpoints, specs); return this.report(this.functionBreakpoints); diff --git a/src/extension.ts b/src/extension.ts index fd01c4e..3027344 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -86,6 +86,10 @@ export function activate(context: vscode.ExtensionContext): void { copyFrameAddress(sessions, context), ), vscode.commands.registerCommand("simics-target-debug.addSymbolFile", () => addSymbolFile(symbolFiles)), + vscode.commands.registerCommand( + "simics-target-debug.addFunctionBreakpointInContext", + (_source: unknown, frameContext: unknown) => addFunctionBreakpointInContext(sessions, frameContext), + ), vscode.commands.registerCommand("simics-target-debug.editSymbolFile", (item?: SymbolFileViewItem) => editSymbolFile(item), ), @@ -150,6 +154,71 @@ function sessionLog(channel: vscode.LogOutputChannel, session: string): TcfLog { }; } +async function addFunctionBreakpointInContext( + sessions: Map, + frameContext: unknown, +): Promise { + const frame = parseCallStackContext(frameContext); + const active = vscode.debug.activeStackItem; + const session = frame ? sessions.get(frame.sessionId) : (active?.session ?? vscode.debug.activeDebugSession); + if (session?.type !== DEBUG_TYPE) { + void vscode.window.showErrorMessage("Select an active Simics Target Debug session."); + return; + } + const threadId = frame?.threadId ?? (active?.session.id === session.id ? active.threadId : undefined); + try { + const contexts = (await session.customRequest("getFunctionBreakpointContexts")) as { + id: string; + name: string; + threadId?: number; + }[]; + const selected = contexts.find((context) => threadId !== undefined && context.threadId === threadId); + const choices: SymbolContextChoice[] = [ + ...(selected + ? [ + { + label: selected.name, + description: "Selected call-stack context", + detail: selected.id, + id: selected.id, + }, + ] + : []), + { label: "All contexts", description: "No context filter", id: "" }, + ...contexts + .filter((context) => context !== selected) + .map((context) => ({ label: context.name, description: context.id, id: context.id })), + ]; + const context = await vscode.window.showQuickPick(choices, { + title: "Function Breakpoint Context", + placeHolder: "Choose a core or all contexts", + matchOnDescription: true, + matchOnDetail: true, + }); + if (!context) { + return; + } + const symbol = await vscode.window.showInputBox({ + title: "Add Function Breakpoint", + prompt: context.id ? `Symbol in ${context.id}` : "Symbol in all contexts", + validateInput: (value) => (!value.trim() ? "Enter a symbol or expression" : undefined), + }); + if (!symbol?.trim()) { + return; + } + const name = context.id ? `${symbol.trim()} @ ${context.id}` : symbol.trim(); + if ( + !vscode.debug.breakpoints.some((bp) => bp instanceof vscode.FunctionBreakpoint && bp.functionName === name) + ) { + vscode.debug.addBreakpoints([new vscode.FunctionBreakpoint(name)]); + } + } catch (err) { + void vscode.window.showErrorMessage( + `Could not add function breakpoint: ${err instanceof Error ? err.message : String(err)}`, + ); + } +} + async function addSymbolFile(symbolFiles: SymbolFilesProvider): Promise { const session = vscode.debug.activeDebugSession; if (session?.type !== DEBUG_TYPE) { diff --git a/src/tcfDebugSession.ts b/src/tcfDebugSession.ts index 0148c71..f1b86fc 100644 --- a/src/tcfDebugSession.ts +++ b/src/tcfDebugSession.ts @@ -476,7 +476,7 @@ export class TcfDebugSession extends DebugSession { if (await this.memoryRequest(command, response, args)) { return; } - if (command === "getSymbolContexts") { + if (command === "getSymbolContexts" || command === "getFunctionBreakpointContexts") { await this.handle(response, async () => { let ids: string[]; if (this.channel?.hasService("ContextQuery")) { @@ -488,7 +488,11 @@ export class TcfDebugSession extends DebugSession { } else { ids = this.contextOrder.filter((id) => this.contexts.get(id)?.HasState === true); } - response.body = ids.map((id) => ({ id, name: this.contextDisplayName(id) })); + response.body = ids.map((id) => ({ + id, + name: this.contextDisplayName(id), + ...(command === "getFunctionBreakpointContexts" && { threadId: this.threadIdByCtx.get(id) }), + })); }); return; } diff --git a/src/test/breakpointManager.test.ts b/src/test/breakpointManager.test.ts index 8ef31c2..7e3b060 100644 --- a/src/test/breakpointManager.test.ts +++ b/src/test/breakpointManager.test.ts @@ -72,6 +72,44 @@ function fakeHost(threads: string[]): BreakpointHost & { events: unknown[] } { }; } +test("function breakpoint names preserve explicit context scopes", async () => { + const agent = fakeAgent(); + const manager = new BreakpointManager(agent.service, fakeHost(["thread1"])); + await manager.setFunctionBreakpoints([ + { name: "start_kernel @ armv8.cluster[0].core[0]", condition: "ready", hitCondition: "3" }, + { name: "start_kernel" }, + { name: "start_kernel @ armv8.cluster[1].core[0]" }, + ]); + const properties = agent.commands.map(([, props]) => props as BreakpointProperties); + assert.equal(properties[0].Location, "start_kernel"); + assert.deepEqual(properties[0].ContextIds, ["armv8.cluster[0].core[0]"]); + assert.equal(properties[0].Condition, "ready"); + assert.equal(properties[0].IgnoreCount, 2); + assert.equal(properties[1].Location, "start_kernel"); + assert.equal(properties[1].ContextIds, undefined); + assert.deepEqual(properties[2].ContextIds, ["armv8.cluster[1].core[0]"]); + await manager.setFunctionBreakpoints([{ name: "start_kernel @ armv8.cluster[0].core[0]", condition: "updated" }]); + const changed = agent.commands.find(([command]) => command === "change")?.[1] as BreakpointProperties; + assert.equal(changed.ID, properties[0].ID); + assert.deepEqual(changed.ContextIds, properties[0].ContextIds); +}); + +test("scoped function breakpoints never fall back to global planting", async () => { + const agent = fakeAgent(); + agent.caps.ContextIds = false; + const manager = new BreakpointManager(agent.service, fakeHost(["thread1"])); + const [unsupported, missingContext, missingSymbol] = await manager.setFunctionBreakpoints([ + { name: "start_kernel @ cpu0" }, + { name: "start_kernel @ " }, + { name: " @ cpu0" }, + ]); + assert.equal(unsupported.verified, false); + assert.match(unsupported.message ?? "", /ContextIds support/); + assert.match(missingContext.message ?? "", /symbol @ context ID/); + assert.match(missingSymbol.message ?? "", /symbol @ context ID/); + assert.equal(agent.commands.length, 0); +}); + test("path map edits remap planted source breakpoints without changing IDs", async () => { const agent = fakeAgent(); const host = fakeHost(["thread1"]); diff --git a/test/symbolContexts.test.ts b/test/symbolContexts.test.ts index 690980b..1258b81 100644 --- a/test/symbolContexts.test.ts +++ b/test/symbolContexts.test.ts @@ -11,6 +11,7 @@ test("symbol context suggestions query the agent for stateful cores", async () = channel: TcfChannel; contexts: Map; contextOrder: string[]; + threadIdByCtx: Map; }; const commands: unknown[][] = []; state.channel = { @@ -23,9 +24,10 @@ test("symbol context suggestions query the agent for stateful cores", async () = state.contexts.set("board", { ID: "board", Name: "board", IsContainer: true }); state.contexts.set("core-0", { ID: "core-0", ParentID: "board", Name: "cpu0", HasState: true }); state.contextOrder.push("board", "core-0"); + state.threadIdByCtx.set("core-0", 7); let sequence = 0; - function listContexts(): Promise { + function listContexts(command = "getSymbolContexts"): Promise { return new Promise((resolve) => { const next = ++sequence; const listener = session.onDidSendMessage((message) => { @@ -38,15 +40,21 @@ test("symbol context suggestions query the agent for stateful cores", async () = session.handleMessage({ seq: next, type: "request", - command: "getSymbolContexts", + command, } as DebugProtocol.Request); }); } assert.deepEqual((await listContexts()).body, [{ id: "core-0", name: "board.cpu0" }]); assert.deepEqual(commands, [["ContextQuery", "query", "HasState=true"]]); + assert.deepEqual((await listContexts("getFunctionBreakpointContexts")).body, [ + { id: "core-0", name: "board.cpu0", threadId: 7 }, + ]); state.channel = { hasService: () => false } as unknown as TcfChannel; assert.deepEqual((await listContexts()).body, [{ id: "core-0", name: "board.cpu0" }]); + assert.deepEqual((await listContexts("getFunctionBreakpointContexts")).body, [ + { id: "core-0", name: "board.cpu0", threadId: 7 }, + ]); state.contexts.delete("core-0"); assert.deepEqual((await listContexts()).body, []); }); From dd537e4e4d3435016b503b71becccf3c326e36f4 Mon Sep 17 00:00:00 2001 From: Magnus Andersson Date: Thu, 5 Nov 2026 16:59:05 +0100 Subject: [PATCH 2/4] Add setting breakpoints from symbol files that have a context Signed-off-by: Magnus Andersson --- README.md | 7 ++++++ package.json | 43 +++++++++++++++++++++++++------------ src/extension.ts | 42 ++++++++++++++++++++++++++---------- src/symbolFiles.ts | 23 ++++++++++++++++++++ src/tcfDebugSession.ts | 24 +++++++++++++++------ test/symbolContexts.test.ts | 35 +++++++++++++++++++++++++++++- 6 files changed, 142 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index f822ae2..8124322 100644 --- a/README.md +++ b/README.md @@ -202,6 +202,13 @@ Scoped function breakpoints require the agent's `ContextIds` support and are never silently planted globally when it is unavailable. Their scope applies to that context (and, for a process context, its threads), not other cores. +In **Symbol Files**, right-click a configured symbol file with a context and +select **Add Function Breakpoint...** to enter a symbol directly. The breakpoint +uses the file's context rather than the selected call-stack core. Context names +are resolved to exact TCF IDs; unavailable or ambiguous contexts produce an error +instead of creating a global breakpoint. Files without a context do not offer +this action. + An address only has a meaning in one address space, so an instruction breakpoint is limited to the process of the context shown in the Disassembly view (its `ProcessID`). `ContextIds` lists the process and all its threads: diff --git a/package.json b/package.json index fefd515..c783975 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,12 @@ ], "contributes": { "commands": [ + { + "command": "simics-target-debug.addSymbolFileBreakpoint", + "title": "Add Function Breakpoint...", + "category": "Simics Target Debug", + "icon": "$(debug-breakpoint-function)" + }, { "command": "simics-target-debug.addFunctionBreakpointInContext", "title": "Add Function Breakpoint in Context...", @@ -218,19 +224,19 @@ "id": "simics-target-debug.pathMap", "name": "Path Maps", "icon": "$(replace-all)", - "when": "debugType == simics-target-debug" + "when": "debugType == 'simics-target-debug'" }, { "id": "simics-target-debug.symbolFiles", "name": "Symbol Files", "icon": "$(file-code)", - "when": "debugType == simics-target-debug" + "when": "debugType == 'simics-target-debug'" }, { "id": "simics-target-debug.consoles", "name": "Consoles", "icon": "$(terminal)", - "when": "debugType == simics-target-debug || simics-target-debug.hasConsoles" + "when": "debugType == 'simics-target-debug' || simics-target-debug.hasConsoles" } ] }, @@ -242,25 +248,29 @@ ], "menus": { "commandPalette": [ + { + "command": "simics-target-debug.addSymbolFileBreakpoint", + "when": "false" + }, { "command": "simics-target-debug.addFunctionBreakpointInContext", - "when": "debugType == simics-target-debug" + "when": "debugType == 'simics-target-debug'" }, { "command": "simics-target-debug.saveSymbolFiles", - "when": "debugType == simics-target-debug" + "when": "debugType == 'simics-target-debug'" }, { "command": "simics-target-debug.addPathMapRule", - "when": "debugType == simics-target-debug" + "when": "debugType == 'simics-target-debug'" }, { "command": "simics-target-debug.savePathMap", - "when": "debugType == simics-target-debug" + "when": "debugType == 'simics-target-debug'" }, { "command": "simics-target-debug.refreshPathMap", - "when": "debugType == simics-target-debug" + "when": "debugType == 'simics-target-debug'" }, { "command": "simics-target-debug.editPathMapRule", @@ -280,11 +290,11 @@ }, { "command": "simics-target-debug.locateSourceFile", - "when": "debugType == simics-target-debug" + "when": "debugType == 'simics-target-debug'" }, { "command": "simics-target-debug.openLaunchJson", - "when": "activeCustomEditorId == simics-target-debug.launchSettings" + "when": "activeCustomEditorId == 'simics-target-debug.launchSettings'" }, { "command": "simics-target-debug.copyFrameAddress", @@ -395,7 +405,7 @@ "view/title": [ { "command": "simics-target-debug.addFunctionBreakpointInContext", - "when": "view == workbench.debug.viewlet.state && debugType == simics-target-debug", + "when": "view == 'workbench.debug.viewlet.state' && debugType == 'simics-target-debug'", "group": "navigation@1" }, { @@ -435,6 +445,11 @@ } ], "view/item/context": [ + { + "command": "simics-target-debug.addSymbolFileBreakpoint", + "when": "view == simics-target-debug.symbolFiles && viewItem =~ /\\bsymbolFile\\b/ && viewItem =~ /\\bscoped\\b/", + "group": "breakpoints@1" + }, { "command": "simics-target-debug.connectConsole", "when": "view == simics-target-debug.consoles && viewItem =~ /\\bconnect\\b/", @@ -482,17 +497,17 @@ }, { "command": "simics-target-debug.editSymbolFile", - "when": "view == simics-target-debug.symbolFiles && viewItem == symbolFile", + "when": "view == simics-target-debug.symbolFiles && viewItem =~ /\\bsymbolFile\\b/", "group": "inline@1" }, { "command": "simics-target-debug.reloadSymbolFile", - "when": "view == simics-target-debug.symbolFiles && viewItem == symbolFile", + "when": "view == simics-target-debug.symbolFiles && viewItem =~ /\\bsymbolFile\\b/", "group": "inline@2" }, { "command": "simics-target-debug.removeSymbolFile", - "when": "view == simics-target-debug.symbolFiles && viewItem == symbolFile", + "when": "view == simics-target-debug.symbolFiles && viewItem =~ /\\bsymbolFile\\b/", "group": "inline@3" } ], diff --git a/src/extension.ts b/src/extension.ts index 3027344..75b5cb4 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -10,7 +10,12 @@ import { setNamedConfigurationValue } from "./launchSettings/launchJson"; import { MissingSource, SourceLocator } from "./locateSource"; import { pathMapProblems } from "./pathMapper"; import { registerPathMapView } from "./pathMapView"; -import { ForeignSymbolFile, SymbolFileConfig, describeForeignSymbolFile } from "./symbolFiles"; +import { + ForeignSymbolFile, + SymbolFileConfig, + describeForeignSymbolFile, + symbolFileBreakpointContext, +} from "./symbolFiles"; import { suggestSymbolPaths } from "./symbolPath"; import { registerConsolesView } from "./consolesView"; import { CONSOLE_AVAILABLE_EVENT, simicsCliOptions } from "./simicsConsole"; @@ -90,6 +95,10 @@ export function activate(context: vscode.ExtensionContext): void { "simics-target-debug.addFunctionBreakpointInContext", (_source: unknown, frameContext: unknown) => addFunctionBreakpointInContext(sessions, frameContext), ), + vscode.commands.registerCommand( + "simics-target-debug.addSymbolFileBreakpoint", + (item?: SymbolFileViewItem) => item && addFunctionBreakpointInContext(sessions, undefined, item), + ), vscode.commands.registerCommand("simics-target-debug.editSymbolFile", (item?: SymbolFileViewItem) => editSymbolFile(item), ), @@ -157,17 +166,25 @@ function sessionLog(channel: vscode.LogOutputChannel, session: string): TcfLog { async function addFunctionBreakpointInContext( sessions: Map, frameContext: unknown, + symbolFile?: SymbolFileViewItem, ): Promise { const frame = parseCallStackContext(frameContext); const active = vscode.debug.activeStackItem; - const session = frame ? sessions.get(frame.sessionId) : (active?.session ?? vscode.debug.activeDebugSession); + const session = + symbolFile?.session ?? + (frame ? sessions.get(frame.sessionId) : (active?.session ?? vscode.debug.activeDebugSession)); if (session?.type !== DEBUG_TYPE) { - void vscode.window.showErrorMessage("Select an active Simics Target Debug session."); + void vscode.window.showErrorMessage( + "Start or select an active Simics Target Debug session before adding a function breakpoint.", + ); return; } const threadId = frame?.threadId ?? (active?.session.id === session.id ? active.threadId : undefined); try { - const contexts = (await session.customRequest("getFunctionBreakpointContexts")) as { + const contexts = (await session.customRequest( + "getFunctionBreakpointContexts", + symbolFile ? { includeContainers: true } : undefined, + )) as { id: string; name: string; threadId?: number; @@ -189,12 +206,14 @@ async function addFunctionBreakpointInContext( .filter((context) => context !== selected) .map((context) => ({ label: context.name, description: context.id, id: context.id })), ]; - const context = await vscode.window.showQuickPick(choices, { - title: "Function Breakpoint Context", - placeHolder: "Choose a core or all contexts", - matchOnDescription: true, - matchOnDetail: true, - }); + const context = symbolFile + ? { id: symbolFileBreakpointContext(symbolFile.file, contexts) } + : await vscode.window.showQuickPick(choices, { + title: "Function Breakpoint Context", + placeHolder: "Choose a core or all contexts", + matchOnDescription: true, + matchOnDetail: true, + }); if (!context) { return; } @@ -669,7 +688,7 @@ async function changeSymbolFiles(command: string, item?: SymbolFileViewItem): Pr } class SymbolFileViewItem extends vscode.TreeItem { - override readonly contextValue = "symbolFile"; + override readonly contextValue: string; constructor( readonly file: SymbolFileConfig, @@ -677,6 +696,7 @@ class SymbolFileViewItem extends vscode.TreeItem { readonly session: vscode.DebugSession, ) { super(path.basename(file.path)); + this.contextValue = file.context ? "symbolFile scoped" : "symbolFile"; const scope = [file.context && `context ${file.context}`, file.contextQuery && `query ${file.contextQuery}`] .filter(Boolean) diff --git a/src/symbolFiles.ts b/src/symbolFiles.ts index b45f63c..efa93d6 100644 --- a/src/symbolFiles.ts +++ b/src/symbolFiles.ts @@ -35,6 +35,29 @@ export interface SymbolFile { absolute?: true; } +export function symbolFileBreakpointContext( + file: Pick, + contexts: readonly { id: string; name: string }[], +): string { + const scope = file.context; + if (!scope) { + throw new Error("The symbol file has no context"); + } + const exact = contexts.find((context) => context.id === scope); + if (exact) { + return exact.id; + } + const matches = contexts.filter((context) => context.name === scope); + if (matches.length !== 1) { + throw new Error( + matches.length === 0 + ? `Symbol file context '${scope}' is not available in this session` + : `Symbol file context '${scope}' is ambiguous; use its exact TCF context ID`, + ); + } + return matches[0].id; +} + /** * Validates the "symbolFiles" attribute of a debug configuration. Returns the * valid entries and a message for each invalid one. diff --git a/src/tcfDebugSession.ts b/src/tcfDebugSession.ts index f1b86fc..b152e81 100644 --- a/src/tcfDebugSession.ts +++ b/src/tcfDebugSession.ts @@ -479,7 +479,15 @@ export class TcfDebugSession extends DebugSession { if (command === "getSymbolContexts" || command === "getFunctionBreakpointContexts") { await this.handle(response, async () => { let ids: string[]; - if (this.channel?.hasService("ContextQuery")) { + const includeContainers = + command === "getFunctionBreakpointContexts" && + typeof args === "object" && + args !== null && + "includeContainers" in args && + args.includeContainers === true; + if (includeContainers) { + ids = this.contextOrder; + } else if (this.channel?.hasService("ContextQuery")) { const [result] = await this.channel.command("ContextQuery", "query", "HasState=true"); if (!Array.isArray(result) || !result.every((id) => typeof id === "string")) { throw new Error("Invalid ContextQuery.query response"); @@ -488,11 +496,15 @@ export class TcfDebugSession extends DebugSession { } else { ids = this.contextOrder.filter((id) => this.contexts.get(id)?.HasState === true); } - response.body = ids.map((id) => ({ - id, - name: this.contextDisplayName(id), - ...(command === "getFunctionBreakpointContexts" && { threadId: this.threadIdByCtx.get(id) }), - })); + response.body = ids.map((id) => { + const threadId = + command === "getFunctionBreakpointContexts" ? this.threadIdByCtx.get(id) : undefined; + return { + id, + name: this.contextDisplayName(id), + ...(threadId !== undefined && { threadId }), + }; + }); }); return; } diff --git a/test/symbolContexts.test.ts b/test/symbolContexts.test.ts index 1258b81..4fe5115 100644 --- a/test/symbolContexts.test.ts +++ b/test/symbolContexts.test.ts @@ -4,6 +4,27 @@ import { DebugProtocol } from "@vscode/debugprotocol"; import { TcfChannel } from "../src/tcf/channel"; import { RunControlContext } from "../src/tcf/services/runControl"; import { TcfDebugSession } from "../src/tcfDebugSession"; +import { symbolFileBreakpointContext } from "../src/symbolFiles"; + +test("symbol file breakpoints resolve their own context, not the selected thread", () => { + const contexts = [ + { id: "core-0", name: "board.cpu0" }, + { id: "core-1", name: "board.cpu1" }, + ]; + assert.equal(symbolFileBreakpointContext({ context: "core-1" }, contexts), "core-1"); + assert.equal(symbolFileBreakpointContext({ context: "board.cpu0" }, contexts), "core-0"); + assert.equal( + symbolFileBreakpointContext({ context: "core-0" }, [...contexts, { id: "other", name: "core-0" }]), + "core-0", + ); + assert.throws(() => symbolFileBreakpointContext({}, contexts), /no context/); + assert.throws(() => symbolFileBreakpointContext({ context: "missing" }, contexts), /not available/); + assert.throws( + () => + symbolFileBreakpointContext({ context: "board.cpu0" }, [...contexts, { id: "other", name: "board.cpu0" }]), + /ambiguous/, + ); +}); test("symbol context suggestions query the agent for stateful cores", async () => { const session = new TcfDebugSession(); @@ -27,7 +48,7 @@ test("symbol context suggestions query the agent for stateful cores", async () = state.threadIdByCtx.set("core-0", 7); let sequence = 0; - function listContexts(command = "getSymbolContexts"): Promise { + function listContexts(command = "getSymbolContexts", args?: unknown): Promise { return new Promise((resolve) => { const next = ++sequence; const listener = session.onDidSendMessage((message) => { @@ -41,6 +62,7 @@ test("symbol context suggestions query the agent for stateful cores", async () = seq: next, type: "request", command, + arguments: args, } as DebugProtocol.Request); }); } @@ -50,11 +72,22 @@ test("symbol context suggestions query the agent for stateful cores", async () = assert.deepEqual((await listContexts("getFunctionBreakpointContexts")).body, [ { id: "core-0", name: "board.cpu0", threadId: 7 }, ]); + const symbolFileContexts = (await listContexts("getFunctionBreakpointContexts", { includeContainers: true })).body; + assert.deepEqual(symbolFileContexts, [ + { id: "board", name: "board" }, + { id: "core-0", name: "board.cpu0", threadId: 7 }, + ]); + assert.equal(Object.hasOwn(symbolFileContexts[0], "threadId"), false); + assert.equal(symbolFileBreakpointContext({ context: "board" }, symbolFileContexts), "board"); state.channel = { hasService: () => false } as unknown as TcfChannel; assert.deepEqual((await listContexts()).body, [{ id: "core-0", name: "board.cpu0" }]); assert.deepEqual((await listContexts("getFunctionBreakpointContexts")).body, [ { id: "core-0", name: "board.cpu0", threadId: 7 }, ]); + state.threadIdByCtx.delete("core-0"); + assert.deepEqual((await listContexts("getFunctionBreakpointContexts")).body, [ + { id: "core-0", name: "board.cpu0" }, + ]); state.contexts.delete("core-0"); assert.deepEqual((await listContexts()).body, []); }); From 805ea55a030d7efd552bb0c8c0ecb77314249407 Mon Sep 17 00:00:00 2001 From: Magnus Andersson Date: Thu, 5 Nov 2026 19:12:42 +0100 Subject: [PATCH 3/4] Extracted the context separator Signed-off-by: Magnus Andersson --- package.json | 44 +++++++++++++++--------------- src/breakpointManager.ts | 7 ++--- src/extension.ts | 3 +- src/test/breakpointManager.test.ts | 11 ++++++++ 4 files changed, 38 insertions(+), 27 deletions(-) diff --git a/package.json b/package.json index c783975..7696ef7 100644 --- a/package.json +++ b/package.json @@ -356,12 +356,12 @@ "editor/title": [ { "command": "simics-target-debug.openLaunchSettings", - "when": "resourcePath =~ /[\\\\/]\\.vscode[\\\\/]launch\\.json$/ && activeCustomEditorId != simics-target-debug.launchSettings", + "when": "resourcePath =~ /[\\\\/]\\.vscode[\\\\/]launch\\.json$/ && activeCustomEditorId != 'simics-target-debug.launchSettings'", "group": "navigation" }, { "command": "simics-target-debug.openLaunchJson", - "when": "activeCustomEditorId == simics-target-debug.launchSettings", + "when": "activeCustomEditorId == 'simics-target-debug.launchSettings'", "group": "navigation" }, { @@ -410,104 +410,104 @@ }, { "command": "simics-target-debug.openConsole", - "when": "view == simics-target-debug.consoles && debugType == simics-target-debug && !simics-target-debug.hasConsoles", + "when": "view == 'simics-target-debug.consoles' && debugType == 'simics-target-debug' && !simics-target-debug.hasConsoles", "group": "navigation@1" }, { "command": "simics-target-debug.saveSymbolFiles", - "when": "view == simics-target-debug.symbolFiles", + "when": "view == 'simics-target-debug.symbolFiles'", "group": "navigation@2" }, { "command": "simics-target-debug.addPathMapRule", - "when": "view == simics-target-debug.pathMap", + "when": "view == 'simics-target-debug.pathMap'", "group": "navigation@1" }, { "command": "simics-target-debug.savePathMap", - "when": "view == simics-target-debug.pathMap", + "when": "view == 'simics-target-debug.pathMap'", "group": "navigation@2" }, { "command": "simics-target-debug.refreshPathMap", - "when": "view == simics-target-debug.pathMap", + "when": "view == 'simics-target-debug.pathMap'", "group": "navigation@3" }, { "command": "simics-target-debug.addSymbolFile", - "when": "view == simics-target-debug.symbolFiles", + "when": "view == 'simics-target-debug.symbolFiles'", "group": "navigation@1" }, { "command": "simics-target-debug.reloadSymbolFiles", - "when": "view == simics-target-debug.symbolFiles", + "when": "view == 'simics-target-debug.symbolFiles'", "group": "navigation@3" } ], "view/item/context": [ { "command": "simics-target-debug.addSymbolFileBreakpoint", - "when": "view == simics-target-debug.symbolFiles && viewItem =~ /\\bsymbolFile\\b/ && viewItem =~ /\\bscoped\\b/", + "when": "view == 'simics-target-debug.symbolFiles' && viewItem =~ /\\bsymbolFile\\b/ && viewItem =~ /\\bscoped\\b/", "group": "breakpoints@1" }, { "command": "simics-target-debug.connectConsole", - "when": "view == simics-target-debug.consoles && viewItem =~ /\\bconnect\\b/", + "when": "view == 'simics-target-debug.consoles' && viewItem =~ /\\bconnect\\b/", "group": "inline@0" }, { "command": "simics-target-debug.reconnectConsole", - "when": "view == simics-target-debug.consoles && viewItem =~ /\\breconnect\\b/", + "when": "view == 'simics-target-debug.consoles' && viewItem =~ /\\breconnect\\b/", "group": "inline@0" }, { "command": "simics-target-debug.consoleKeysToVsCode", - "when": "view == simics-target-debug.consoles && viewItem =~ /\\bkeysToSimics\\b/", + "when": "view == 'simics-target-debug.consoles' && viewItem =~ /\\bkeysToSimics\\b/", "group": "inline@0" }, { "command": "simics-target-debug.consoleKeysToSimics", - "when": "view == simics-target-debug.consoles && viewItem =~ /\\bkeysToVsCode\\b/", + "when": "view == 'simics-target-debug.consoles' && viewItem =~ /\\bkeysToVsCode\\b/", "group": "inline@0" }, { "command": "simics-target-debug.clearConsoleHistory", - "when": "view == simics-target-debug.consoles && viewItem =~ /^simicsCli/", + "when": "view == 'simics-target-debug.consoles' && viewItem =~ /^simicsCli/", "group": "inline@1" }, { "command": "simics-target-debug.editPathMapRule", - "when": "view == simics-target-debug.pathMap && viewItem == pathMapRule", + "when": "view == 'simics-target-debug.pathMap' && viewItem == 'pathMapRule'", "group": "inline@1" }, { "command": "simics-target-debug.removePathMapRule", - "when": "view == simics-target-debug.pathMap && viewItem == pathMapRule", + "when": "view == 'simics-target-debug.pathMap' && viewItem == 'pathMapRule'", "group": "inline@2" }, { "command": "simics-target-debug.movePathMapRuleUp", - "when": "view == simics-target-debug.pathMap && viewItem == pathMapRule", + "when": "view == 'simics-target-debug.pathMap' && viewItem == 'pathMapRule'", "group": "order@1" }, { "command": "simics-target-debug.movePathMapRuleDown", - "when": "view == simics-target-debug.pathMap && viewItem == pathMapRule", + "when": "view == 'simics-target-debug.pathMap' && viewItem == 'pathMapRule'", "group": "order@2" }, { "command": "simics-target-debug.editSymbolFile", - "when": "view == simics-target-debug.symbolFiles && viewItem =~ /\\bsymbolFile\\b/", + "when": "view == 'simics-target-debug.symbolFiles' && viewItem =~ /\\bsymbolFile\\b/", "group": "inline@1" }, { "command": "simics-target-debug.reloadSymbolFile", - "when": "view == simics-target-debug.symbolFiles && viewItem =~ /\\bsymbolFile\\b/", + "when": "view == 'simics-target-debug.symbolFiles' && viewItem =~ /\\bsymbolFile\\b/", "group": "inline@2" }, { "command": "simics-target-debug.removeSymbolFile", - "when": "view == simics-target-debug.symbolFiles && viewItem =~ /\\bsymbolFile\\b/", + "when": "view == 'simics-target-debug.symbolFiles' && viewItem =~ /\\bsymbolFile\\b/", "group": "inline@3" } ], diff --git a/src/breakpointManager.ts b/src/breakpointManager.ts index 2930a38..4cca440 100644 --- a/src/breakpointManager.ts +++ b/src/breakpointManager.ts @@ -11,6 +11,7 @@ import { } from "./tcf/services/breakpoints"; import { toBigInt } from "./tcf/json"; import { instructionReference, parseInstructionReference } from "./disassembly"; +import { parseFunctionBreakpointName } from "./functionBreakpoint"; import type { TargetPath } from "./pathMapper"; type BreakpointKind = "source" | "function" | "data" | "instruction"; @@ -218,13 +219,11 @@ export class BreakpointManager { const caps = await this.capabilities(); const specs = requested.map((r): BreakpointSpec => { const spec: BreakpointSpec = { kind: "function", key: r.name }; - const separator = r.name.lastIndexOf(" @ "); - const contextId = separator >= 0 ? r.name.slice(separator + 3).trim() : undefined; - const location = separator >= 0 ? r.name.slice(0, separator).trim() : r.name; + const { location, contextId, hasContextDelimiter } = parseFunctionBreakpointName(r.name); if (caps.Location === false) { return { ...spec, error: "The TCF agent does not support breakpoints on expressions" }; } - if (separator >= 0 && (!contextId || !location)) { + if (hasContextDelimiter && (!contextId || !location)) { return { ...spec, error: "Use 'symbol @ context ID' for a context-scoped function breakpoint" }; } if (contextId && caps.ContextIds === false) { diff --git a/src/extension.ts b/src/extension.ts index 75b5cb4..ab5d0ce 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -7,6 +7,7 @@ import { parseInstructionReference } from "./disassembly"; import { registerFunctionHeaderFileSystem } from "./functionHeaderFileSystem"; import { applyAndSave, registerLaunchSettings } from "./launchSettings/editorProvider"; import { setNamedConfigurationValue } from "./launchSettings/launchJson"; +import { formatFunctionBreakpointName } from "./functionBreakpoint"; import { MissingSource, SourceLocator } from "./locateSource"; import { pathMapProblems } from "./pathMapper"; import { registerPathMapView } from "./pathMapView"; @@ -225,7 +226,7 @@ async function addFunctionBreakpointInContext( if (!symbol?.trim()) { return; } - const name = context.id ? `${symbol.trim()} @ ${context.id}` : symbol.trim(); + const name = formatFunctionBreakpointName(symbol.trim(), context.id); if ( !vscode.debug.breakpoints.some((bp) => bp instanceof vscode.FunctionBreakpoint && bp.functionName === name) ) { diff --git a/src/test/breakpointManager.test.ts b/src/test/breakpointManager.test.ts index 7e3b060..1bf7b78 100644 --- a/src/test/breakpointManager.test.ts +++ b/src/test/breakpointManager.test.ts @@ -2,6 +2,7 @@ import { test } from "node:test"; import assert from "node:assert/strict"; import { BreakpointHost, BreakpointManager, parseHitCondition } from "../breakpointManager"; import { instructionReference } from "../disassembly"; +import { formatFunctionBreakpointName, parseFunctionBreakpointName } from "../functionBreakpoint"; import { PathMapper } from "../pathMapper"; import { BreakpointCapabilities, @@ -73,6 +74,16 @@ function fakeHost(threads: string[]): BreakpointHost & { events: unknown[] } { } test("function breakpoint names preserve explicit context scopes", async () => { + assert.deepEqual(parseFunctionBreakpointName(formatFunctionBreakpointName("start_kernel", "cpu0")), { + location: "start_kernel", + contextId: "cpu0", + hasContextDelimiter: true, + }); + assert.deepEqual(parseFunctionBreakpointName(formatFunctionBreakpointName("start_kernel")), { + location: "start_kernel", + contextId: undefined, + hasContextDelimiter: false, + }); const agent = fakeAgent(); const manager = new BreakpointManager(agent.service, fakeHost(["thread1"])); await manager.setFunctionBreakpoints([ From 7032d6a41d6dc4db92a9d5ab6b271a1d5602a43e Mon Sep 17 00:00:00 2001 From: Magnus Andersson Date: Thu, 5 Nov 2026 19:17:04 +0100 Subject: [PATCH 4/4] Add context separator src file Signed-off-by: Magnus Andersson --- src/functionBreakpoint.ts | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 src/functionBreakpoint.ts diff --git a/src/functionBreakpoint.ts b/src/functionBreakpoint.ts new file mode 100644 index 0000000..7f42cf9 --- /dev/null +++ b/src/functionBreakpoint.ts @@ -0,0 +1,23 @@ +export const FUNCTION_BREAKPOINT_CONTEXT_SEPARATOR = " @ "; + +export interface ParsedFunctionBreakpointName { + location: string; + contextId?: string; + hasContextDelimiter: boolean; +} + +export function formatFunctionBreakpointName(location: string, contextId?: string): string { + return contextId ? `${location}${FUNCTION_BREAKPOINT_CONTEXT_SEPARATOR}${contextId}` : location; +} + +export function parseFunctionBreakpointName(name: string): ParsedFunctionBreakpointName { + const separator = name.lastIndexOf(FUNCTION_BREAKPOINT_CONTEXT_SEPARATOR); + const hasContextDelimiter = separator >= 0; + return { + location: hasContextDelimiter ? name.slice(0, separator).trim() : name, + contextId: hasContextDelimiter + ? name.slice(separator + FUNCTION_BREAKPOINT_CONTEXT_SEPARATOR.length).trim() + : undefined, + hasContextDelimiter, + }; +}