Skip to content

Commit

Permalink
feat: add support for nargo DAP (#51)
Browse files Browse the repository at this point in the history
Co-authored-by: Martin Verzilli <[email protected]>
  • Loading branch information
ggiraldez and mverzilli authored Mar 13, 2024
1 parent a03b2ef commit 369fa91
Show file tree
Hide file tree
Showing 4 changed files with 293 additions and 2 deletions.
74 changes: 73 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@
"Programming Languages"
],
"activationEvents": [
"onLanguage:noir"
"onLanguage:noir",
"onStartupFinished",
"onDebug"
],
"main": "./out/extension",
"contributes": {
Expand Down Expand Up @@ -86,6 +88,76 @@
"mac": "shift+alt+cmd+n p"
}
],
"breakpoints": [
{
"language": "noir"
}
],
"debuggers": [
{
"type": "noir",
"languages": [
"noir"
],
"label": "Noir Debugging",
"configurationAttributes": {
"launch": {
"required": [
"projectFolder"
],
"properties": {
"projectFolder": {
"type": "string",
"description": "Absolute path to the Nargo project directory.",
"default": "${workspaceFolder}"
},
"package": {
"type": "string",
"description": "Optional name of the binary package to debug",
"default": null
},
"proverName": {
"type": "string",
"description": "Name of the prover input to use (default Prover)",
"default": "Prover"
},
"generateAcir": {
"type": "boolean",
"description": "If true, generate ACIR opcodes instead of Brillig which will be closer to release binaries but less convenient for debugging",
"default": false
},
"skipInstrumentation": {
"type": "boolean",
"description": "Skips variables debugging instrumentation of code, making debugging less convenient but the resulting binary smaller and closer to production",
"default": false
}
}
}
},
"initialConfigurations": [
{
"type": "noir",
"request": "launch",
"name": "Noir binary package",
"projectFolder": "${workspaceFolder}",
"proverName": "Prover"
}
],
"configurationSnippets": [
{
"label": "Noir Debug: Prove package",
"description": "A new configuration for debugging a Noir binary package",
"body": {
"type": "noir",
"request": "launch",
"name": "Noir binary package",
"projectFolder": "${workspaceFolder}",
"proverName": "Prover"
}
}
]
}
],
"configuration": {
"type": "object",
"title": "Noir Language Server configuration",
Expand Down
171 changes: 171 additions & 0 deletions src/debugger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
import {
debug,
window,
workspace,
DebugAdapterDescriptorFactory,
DebugSession,
DebugAdapterExecutable,
DebugAdapterDescriptor,
ExtensionContext,
OutputChannel,
DebugConfigurationProvider,
CancellationToken,
DebugConfiguration,
ProviderResult,
} from 'vscode';

import { spawn } from 'child_process';
import findNargo from './find-nargo';
import findNearestPackageFrom from './find-nearest-package';

let outputChannel: OutputChannel;

export function activateDebugger(context: ExtensionContext) {
outputChannel = window.createOutputChannel('NoirDebugger');

context.subscriptions.push(
debug.registerDebugAdapterDescriptorFactory('noir', new NoirDebugAdapterDescriptorFactory()),
debug.registerDebugConfigurationProvider('noir', new NoirDebugConfigurationProvider()),
debug.onDidTerminateDebugSession(() => {
outputChannel.appendLine(`Debug session ended.`);
}),
);
}

export class NoirDebugAdapterDescriptorFactory implements DebugAdapterDescriptorFactory {
async createDebugAdapterDescriptor(
_session: DebugSession,
_executable: DebugAdapterExecutable,
): ProviderResult<DebugAdapterDescriptor> {
const config = workspace.getConfiguration('noir');

const configuredNargoPath = config.get<string | undefined>('nargoPath');
const nargoPath = configuredNargoPath || findNargo();

return new DebugAdapterExecutable(nargoPath, ['dap']);
}
}

class NoirDebugConfigurationProvider implements DebugConfigurationProvider {
async resolveDebugConfiguration(
_folder: WorkspaceFolder | undefined,
config: DebugConfiguration,
_token?: CancellationToken,
): ProviderResult<DebugConfiguration> {
if (
(!config.projectFolder || config.projectFolder === ``) &&
window.activeTextEditor?.document.languageId != 'noir'
)
return window.showInformationMessage(`Select a Noir file to debug`);

const currentFilePath = window.activeTextEditor.document.uri.fsPath;
const projectFolder =
config.projectFolder && config.projectFolder !== ``
? config.projectFolder
: findNearestPackageFrom(currentFilePath, outputChannel);

const resolvedConfig = {
type: config.type || 'noir',
name: config.name || 'Noir binary package',
request: 'launch',
program: currentFilePath,
projectFolder,
package: config.package || ``,
proverName: config.proverName || `Prover`,
generateAcir: config.generateAcir || false,
skipInstrumentation: config.skipInstrumentation || false,
};

return resolvedConfig;
}

async resolveDebugConfigurationWithSubstitutedVariables(
_folder: WorkspaceFolder | undefined,
config: DebugConfiguration,
_token?: CancellationToken,
): ProviderResult<DebugConfiguration> {
const workspaceConfig = workspace.getConfiguration('noir');
const nargoPath = workspaceConfig.get<string | undefined>('nargoPath') || findNargo();

outputChannel.clear();

outputChannel.appendLine(`Using nargo at ${nargoPath}`);
outputChannel.appendLine(`Compiling Noir project...`);
outputChannel.appendLine(``);

// Run Nargo's DAP in "pre-flight mode", which test runs
// the DAP initialization code without actually starting the DAP server.
// This lets us gracefully handle errors that happen *before*
// the DAP loop is established, which otherwise are considered
// "out of band".
// This was necessary due to the VS Code project being reluctant to let extension authors capture
// stderr output generated by a DAP server wrapped in DebugAdapterExecutable.
//
// More details here: https://github.com/microsoft/vscode/issues/108138
const preflightArgs = [
'dap',
'--preflight-check',
'--preflight-project-folder',
config.projectFolder,
'--preflight-prover-name',
config.proverName,
];

if (config.package !== ``) {
preflightArgs.push(`--preflight-package`);
preflightArgs.push(config.package);
}

if (config.generateAcir) {
preflightArgs.push(`--preflight-generate-acir`);
}

if (config.skipInstrumentation) {
preflightArgs.push(`--preflight-skip-instrumentation`);
}

const preflightCheck = spawn(nargoPath, preflightArgs);

// Create a promise to block until the preflight check child process
// ends.
let ready: (r: boolean) => void;
const preflightCheckMonitor = new Promise((resolve) => (ready = resolve));

preflightCheck.stderr.on('data', (ev_buffer) => preflightCheckPrinter(ev_buffer, outputChannel));
preflightCheck.stdout.on('data', (ev_buffer) => preflightCheckPrinter(ev_buffer, outputChannel));
preflightCheck.on('data', (ev_buffer) => preflightCheckPrinter(ev_buffer, outputChannel));
preflightCheck.on('exit', async (code) => {
if (code !== 0) {
outputChannel.appendLine(`Exited with code ${code}`);
}
ready(code == 0);
});

if (!(await preflightCheckMonitor)) {
outputChannel.show();
throw new Error(`Error launching debugger. Please inspect the Output pane for more details.`);
} else {
outputChannel.appendLine(`Starting debugger session...`);
}

return config;
}
}

/**
* Takes stderr or stdout output from the Nargo's DAP
* preflight check and formats it in an Output pane friendly way,
* by removing all special characters.
*
* Note: VS Code's output panes only support plain text.
*
*/
function preflightCheckPrinter(buffer: Buffer, output: OutputChannel) {
const formattedOutput = buffer
.toString()
// eslint-disable-next-line no-control-regex
.replace(/\x1b(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g, '')
.replace(/[^ -~\n\t]/g, '');

output.appendLine(formattedOutput);
}
6 changes: 5 additions & 1 deletion src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
*/

import {
window,
workspace,
commands,
ExtensionContext,
Expand All @@ -33,11 +34,12 @@ import {
TaskPanelKind,
TaskGroup,
ProcessExecution,
window,
ProgressLocation,
} from 'vscode';
import os from 'os';

import { activateDebugger } from './debugger';

import { languageId } from './constants';
import Client from './client';
import findNargo, { findNargoBinaries } from './find-nargo';
Expand Down Expand Up @@ -362,6 +364,8 @@ export async function activate(context: ExtensionContext): Promise<void> {
const disposable = await didOpenTextDocument(doc);
context.subscriptions.push(disposable);
}

activateDebugger(context);
}

export async function deactivate(): Promise<void> {
Expand Down
44 changes: 44 additions & 0 deletions src/find-nearest-package.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import * as vscode from 'vscode';
import * as path from 'path';
import * as fs from 'fs';

/**
* Given a program file path, walk up the file system until
* finding the nearest a Nargo.toml in a directory that contains
* the program.
*
* To reduce the odds of accidentally choosing the wrong Nargo package,
* end the walk at the root of the current VS Code open files.
*/
export default function findNearestPackageFolder(program: string, outputChannel: vscode.OutputChannel): string {
const workspaceFolders = vscode.workspace.workspaceFolders;
if (!workspaceFolders) {
throw new Error(`No workspace is currently open in VS Code.`);
}

const workspaceRoots = workspaceFolders.map((wf) => wf.uri.fsPath);

let currentFolder = path.dirname(program);

try {
while (currentFolder !== path.dirname(currentFolder)) {
const maybeNargoProject = path.join(currentFolder, 'Nargo.toml');

if (fs.existsSync(maybeNargoProject)) {
return currentFolder;
}

if (workspaceRoots.includes(currentFolder)) {
break;
}

currentFolder = path.dirname(currentFolder);
}
} catch (error) {
outputChannel.appendLine(`Error looking for Nargo.toml: {error.message}`);
outputChannel.show();
throw new Error(`Could not find a Nargo package associated to this file.`);
}

throw new Error(`Could not find a Nargo package associated to this file.`);
}

0 comments on commit 369fa91

Please sign in to comment.