Skip to content

DAP & VSC debugger#58

Open
Manyfestation wants to merge 10 commits intokaspanet:masterfrom
Manyfestation:dap-and-vsc-debugger
Open

DAP & VSC debugger#58
Manyfestation wants to merge 10 commits intokaspanet:masterfrom
Manyfestation:dap-and-vsc-debugger

Conversation

@Manyfestation
Copy link
Collaborator

@Manyfestation Manyfestation commented Mar 10, 2026

DAP crate & a Visual Studio Code debugger for SilverScript.

VS Code uses uses the Debug Adapter Protocol to talk to debuggers for breakpoints, stepping, locals, stack traces, and launch requests.

This PR adds a new debugger/dap crate which builds on top of the debugger introduced in #43 and exposes it through DAP.
The adapter can be launched by VS Code, or through CLI with --run-config-json. That CLI path exists so the same launch shape can be used from tests, scripts or AI agents.

Pressing F5, or using the inline CodeLens above a contract, opens a panel for choosing the entrypoint, filling constructor and function args, and then either running or debugging the contract.

The panel integrates with launch.json so scenarios can be loaded and saved instead of re-entering args each time. CodeLens surfaces saved-scenario state directly above each entrypoint with a N scenarios saved summary, and clicking that count opens the same side panel and jumps straight into picking a saved scenario for that entrypoint when one exists. Advanced fields like tx stay in launch.json, while the panel focuses on the common run/debug path.

This PR also adds symbolic identities for contracts that take key-related inputs. Launch args can use placeholders like keypair1.pubkey, keypair1.secret, and keypair1.pkh, and the adapter resolves them at launch time into a matching keypair. This keeps the panel simple while still supporting pubkeys, PKHs, and auto-signed sig arguments.

@Manyfestation Manyfestation marked this pull request as ready for review March 11, 2026 14:18
Copy link
Collaborator

@IzioDev IzioDev left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

First comments on the presentation / devxp and release cycles.

Note: LSP has the exact same needs (embed a binary and release extensions per target).
We'll need to move to a CI environment in the future for the release cycle as this isn't manageable manually.

@@ -9,6 +22,7 @@
- Build the extension once with `npm run compile` (or keep it rebuilding with `npm run watch`).
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should mention: npm run bundle:adapter somewhere in the flow, otherwise compilation fails for this section.

"watch:tsc": "tsc --noEmit --watch --project tsconfig.json",
"package": "npm run check-types && npm run lint && node esbuild.js --production",
"prepare-release": "npm run bundle:adapter && npm run package",
"package:vsix": "npm exec vsce package -- --pre-release",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd recommend if we enforce manual usage of this command and its parameters, explanation as a comment in the README.md

"watch:esbuild": "node esbuild.js --watch",
"watch:tsc": "tsc --noEmit --watch --project tsconfig.json",
"package": "npm run check-types && npm run lint && node esbuild.js --production",
"prepare-release": "npm run bundle:adapter && npm run package",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a need to have this behavior as a sub script instead of copy/pasting its content to vscode:prepublish?


```bash
npm run package:vsix
```
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When embedding binaries into the extension, we must publish the extension with a target platform (win, darwin, linux, etc).

Usage: vsce package --target [target]

win32-x64, win32-arm64, linux-x64, linux-arm64, linux-armhf, darwin-x64, darwin-arm64, alpine-x64, alpine-arm64, web

I suggest we don't provide automatic command for that and only some common cmd list usages. Anyway, only the publisher cares about this command, maybe we should even move it to the end of the document?


Then in the Extension Development Host, press `Ctrl+R` to reload and apply parser/query updates.

### Contract Debugging
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right about providing usage documentation here, I even think this should come first in the README as this is what's being displayed on the store (and from the extension page within the IDE).

- `SILVERSCRIPT_VSCODE_TARGET`, for example `darwin-arm64` or `linux-x64`
- `SILVERSCRIPT_CARGO_TARGET`, for example `aarch64-apple-darwin`

Then run `npm run package:vsix`.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is redundant with the first file line

Copy link
Collaborator

@IzioDev IzioDev left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Part 1 of extension domain

edit: I made a mistake, I didn't intend to approve the changes*

Comment on lines +41 to +47
function escapeHtml(value) {
return String(value ?? "")
.replace(/&/g, "&")
.replace(/"/g, """)
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;");
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ideal way is to no sanitize anything and display content as DOMNode.textContent
i see this is used to be put into raw html, in this case, i'd use DOMPurify from "dompurify"

@@ -0,0 +1,47 @@
<!DOCTYPE html>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this webapp be using a lightweight framework to simplify readability / expressiveness?

astro/react/vue/svelte?

Comment on lines +94 to +97
- `keypair<N>.pubkey`
- `keypair<N>.secret`
- `keypair<N>.pkh`

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd suggest declaring consistent naming: pk, sk, pkHash or publicKey, secretKey, publicKeyHash

secret alone can have different interpretations, especially if coupled with a wallet. But it doesn't matter that much if we provide abstractions on top (like the panel that edit these values for you)

Comment on lines +372 to +373
logDebugEnabled =
context.extensionMode !== vscode.ExtensionMode.Production;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could probably come from an optional extension settings

Comment on lines +386 to +389
const semanticEnabled = vscode.workspace
.getConfiguration("silverscript")
.get<boolean>("enableSemanticTokens", true);
logInfo(`enableSemanticTokens=${semanticEnabled}`);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I removed it in LSP as we have no use for it, but you can keep it and I'll remove later

Comment on lines +125 to +126
Array.isArray(value) ||
(value !== null && typeof value === "object")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it true that launch args are expected to be array | object, instead of one or the other explicitly?

Comment on lines +149 to +163
if (Array.isArray(input)) {
for (const [index, param] of params.entries()) {
if (index < input.length) {
defaults[param.name] = stringifyLaunchArg(input[index]);
}
}
return defaults;
}
if (input && typeof input === "object") {
for (const param of params) {
if (Object.prototype.hasOwnProperty.call(input, param.name)) {
defaults[param.name] = stringifyLaunchArg(input[param.name]);
}
}
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same question here for array | object

model: ContractModel,
scriptPath: string,
): string {
return model.name && model.name !== "Unknown"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In what cases the contract model name in unknown?

Shouldn't parseContractModel reject the parsing if name is not set?

function resolvedLaunchName(
state: PanelHostState,
): string {
return typeof state.baseConfig.name === "string" &&
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

name is expected to be a string already

Comment on lines +255 to +265
if (
restoringPrimaryEditor ||
!panel ||
!editor ||
editor.document.languageId !== "silverscript" ||
editor.viewColumn === vscode.ViewColumn.One ||
panel.viewColumn === undefined ||
editor.viewColumn !== panel.viewColumn
) {
return;
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not able to understand what this is for, I think it's worth adding a small comment

@IzioDev IzioDev self-requested a review March 18, 2026 17:40
@IzioDev
Copy link
Collaborator

IzioDev commented Mar 18, 2026

Investigated the issue on windows where upon executing the debugger it would run in a new document: \\?\D:\Dev\kaspa\silverscript\silverscript-lang\tests\examples\openhashlock.sil due to the NS path prefix.

If we truly need canonicalize (basically resolve symlinks), I suggest we add a platform specific handling to remove prefix (on win). If we don't need, we can just return absolute path instead of canonicalize.

Comment on lines +20 to +25
function normalizePath(fsPath: string): string {
const normalized = path.normalize(fsPath);
return process.platform === "win32"
? normalized.toLowerCase()
: normalized;
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function only is used in one place, in launchConfigMatchesScript, I think we can afford .toLowerCase() on the calling site regardless of the platform:

record.resolvedScriptPath.toLowerCase() === scriptUri.fsPath.toLowerCase()

And, now we realize we can also remove launchConfigMatchesScript (by the way the export key is useless because not used externally) and move the logic directly into: listMatchingSilverScriptLaunchConfigs

}

records.push({
id: `${folder.uri.toString()}::${index}`,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

toString(true) (skip encoding) seems more appropriate for an id

export type RawLaunchConfiguration = vscode.DebugConfiguration & Record<string, unknown>;

export type SilverScriptLaunchConfigRecord = {
id: string;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can add a comment to inform the format: fspath::config_index

import * as path from "path";
import * as vscode from "vscode";

export type RawLaunchConfiguration = vscode.DebugConfiguration & Record<string, unknown>;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideally we also have a LaunchConfiguration type, with an associated type-guard function (isLaunchConfiguration), so that calling site can rely on pre validated configuration properties, this will remove some complexity found in different places.
Then we can make RawLaunchConfiguration a local type (not exported), since only the surfaced type will be relevant to external readers.

Small catch: internal (to this file) writer still want the ability to operate on "raw" or "unvalidated" configs, example: I have a non valid configuration file, but I still want to be able to create a new configuration (from the webview panel) - in this context, I don't need validation, I read the number of existing configuration, push the fresh one, and write the whole

Comment on lines +196 to +200
if (record.index >= configs.length) {
throw new Error(`Launch config '${record.config.name ?? record.id}' no longer exists.`);
}

configs[record.index] = nextConfig;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using index as the identification key is a mistake, it's not reliable (can change between a read/write)

(same in deleteSilverScriptLaunchConfig)

Comment on lines +207 to +209
typeof nextConfig.scriptPath === "string"
? nextConfig.scriptPath
: record.scriptPathValue,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When not using RawLaunchConfiguration but a pre-validated LaunchConfiguration, all of these call are simplified

Comment on lines +46 to +48
if (expanded.includes("${")) {
return undefined;
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems weird, I investigated a bit deeper, it seems expandPathVariables should not be accepting undefined values at the first place, this makes replacements deterministic (if there are placeholders, then they are filled for sure).

expandPathVariables is called by resolveLaunchScriptPath while also wrongly takes args as optionals (they should always be present; or there should be a validation happening prior this function)

Comment on lines +59 to +61
if (!expanded) {
return undefined;
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With the above comment, this shouldn't happens (unless there is a possible unfilled placeholder I'm not familiar with: and in this case, it should throw an error anyway)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants