├── .gitignore ├── .vscode-test.mjs ├── .vscode ├── extensions.json ├── launch.json ├── settings.json └── tasks.json ├── .vscodeignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── esbuild.js ├── eslint.config.mjs ├── images ├── extension-icon.png └── mbtmap.png ├── package-lock.json ├── package.json ├── src ├── extension.ts └── test │ ├── extension.test.js │ └── extension.test.ts ├── tsconfig.json ├── vsc-extension-quickstart.md └── wasm-sourcemap.py /.gitignore: -------------------------------------------------------------------------------- 1 | out 2 | dist 3 | node_modules 4 | .vscode-test/ 5 | *.vsix 6 | mbt_test/ 7 | mbt_test2/ -------------------------------------------------------------------------------- /.vscode-test.mjs: -------------------------------------------------------------------------------- 1 | import { defineConfig } from '@vscode/test-cli'; 2 | 3 | export default defineConfig({ 4 | files: 'out/test/**/*.test.js', 5 | }); 6 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See http://go.microsoft.com/fwlink/?LinkId=827846 3 | // for the documentation about the extensions.json format 4 | "recommendations": [ 5 | "dbaeumer.vscode-eslint", 6 | "ms-vscode.extension-test-runner", 7 | "connor4312.esbuild-problem-matchers" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that compiles the extension and then opens it inside a new window 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | { 6 | "version": "0.2.0", 7 | "configurations": [ 8 | { 9 | "name": "Run Extension", 10 | "type": "extensionHost", 11 | "request": "launch", 12 | "args": [ 13 | "${workspaceFolder}/mbt_test2", 14 | "--extensionDevelopmentPath=${workspaceFolder}" 15 | ], 16 | "outFiles": [ 17 | "${workspaceFolder}/out/**/*.js" 18 | ], 19 | "preLaunchTask": "${defaultBuildTask}" 20 | } 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "files.exclude": { 4 | "out": false // set this to true to hide the "out" folder with the compiled JS files 5 | }, 6 | "search.exclude": { 7 | "out": true // set this to false to include "out" folder in search results 8 | }, 9 | // Turn off tsc task auto detection since we have the necessary tasks as npm scripts 10 | "typescript.tsc.autoDetect": "off" 11 | } 12 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | // See https://go.microsoft.com/fwlink/?LinkId=733558 2 | // for the documentation about the tasks.json format 3 | { 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "label": "watch", 8 | "dependsOn": ["npm: watch:tsc", "npm: watch:esbuild"], 9 | "presentation": { 10 | "reveal": "never" 11 | }, 12 | "group": { 13 | "kind": "build", 14 | "isDefault": true 15 | } 16 | }, 17 | { 18 | "type": "npm", 19 | "script": "watch:esbuild", 20 | "group": "build", 21 | "problemMatcher": "$esbuild-watch", 22 | "isBackground": true, 23 | "label": "npm: watch:esbuild", 24 | "presentation": { 25 | "group": "watch", 26 | "reveal": "never" 27 | } 28 | }, 29 | { 30 | "type": "npm", 31 | "script": "watch:tsc", 32 | "group": "build", 33 | "problemMatcher": "$tsc-watch", 34 | "isBackground": true, 35 | "label": "npm: watch:tsc", 36 | "presentation": { 37 | "group": "watch", 38 | "reveal": "never" 39 | } 40 | } 41 | ] 42 | } 43 | 44 | -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/** 2 | .vscode-test/** 3 | src/** 4 | .gitignore 5 | .yarnrc 6 | vsc-extension-quickstart.md 7 | **/tsconfig.json 8 | **/eslint.config.mjs 9 | **/*.map 10 | **/*.ts 11 | **/.vscode-test.* 12 | **/*.ts 13 | !dist/** 14 | node_modules/** 15 | # Ignore everything in node_modules except mappings.wasm and vscode package 16 | node_modules/** 17 | !node_modules/vscode/** 18 | 19 | esbuild.js 20 | 21 | mbt_test/ 22 | mbt_test2/ -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to the "mbtmap" extension will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/), and this project adheres to [Semantic Versioning](https://semver.org/). 6 | 7 | ## [0.0.1] - 2024-10-19 8 | 9 | ### Added 10 | 11 | - Initial release of MoonBit WASM Debugger (Unofficial). 12 | - Transforms WASM stack traces into readable error messages with accurate file names and line numbers. 13 | - Integrates with VSCode's Problems panel for easy navigation to error locations. 14 | - Added command `Clean WASM Errors` to clear error messages manually. 15 | 16 | ## [0.0.2] - 2024-10-19 17 | 18 | ### Fixed 19 | 20 | - Solutions to Ensure `mappings.wasm` is Included. 21 | 22 | ## [0.0.3] - 2024-10-20 23 | 24 | ### Added 25 | 26 | - README.md add pictures. -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2024 zmr-233 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MoonBit WASM Debugger (Unofficial) 2 | 3 | ## What’s MoonBit WASM Debugger 4 | 5 | **MoonBit WASM Debugger** is an unofficial VSCode extension that enhances your debugging experience with MoonBit by providing precise line-number error displays for WASM errors. No more untraceable unwraps! It works alongside the official MoonBit plugin to make debugging a breeze. 6 | 7 | ## Features 8 | 9 | - Transforms cryptic WASM stack traces into readable error messages with accurate file names and line numbers. 10 | - Seamlessly integrates with VSCode's Problems panel for quick navigation to error locations. 11 | 12 | Before: 13 | ![image](https://github.com/user-attachments/assets/37eb4a6c-a991-4564-92d8-2761d1675b4b) 14 | 15 | Now: 16 | ![image](https://github.com/user-attachments/assets/5ce63b9b-d861-4cb9-aee2-05c0e22b7353) 17 | ![image](https://github.com/user-attachments/assets/fc32b347-8538-4f2f-9a8f-24e03e481b9c) 18 | 19 | 20 | ## Usage 21 | 22 | To clear the error messages, you can press `Ctrl+Shift+P`, type `Clean WASM Errors`, and select the command. Or just wait until the next time you run your code—the errors will update automatically. 23 | 24 | ## Feedback 25 | 26 | Got questions or suggestions? Feel free to open an issue on our [GitHub repository](https://github.com/zmr-233/MoonBit-WASM-Debugger/issues). We'd love to hear from you! 27 | 28 | ## License 29 | 30 | This project is licensed under the MIT License. 31 | 32 | ## Contact Us 33 | 34 | For any inquiries, please reach out at [zmr_233@outlook.com](mailto:zmr_233@outlook.com). 35 | -------------------------------------------------------------------------------- /esbuild.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const esbuild = require('esbuild'); 4 | 5 | const production = process.argv.includes('--production'); 6 | const watch = process.argv.includes('--watch'); 7 | 8 | async function main() { 9 | const ctx = await esbuild.context({ 10 | entryPoints: ['src/extension.ts'], 11 | bundle: true, 12 | format: 'cjs', 13 | minify: production, 14 | sourcemap: !production, 15 | sourcesContent: false, 16 | platform: 'node', 17 | outfile: 'dist/extension.js', 18 | external: ['vscode'], 19 | logLevel: 'silent', 20 | loader: { 21 | '.wasm': 'file', // Ensure WASM files are treated as assets 22 | }, 23 | assetNames: '[name]', // Keep the original name of WASM files 24 | plugins: [ 25 | esbuildProblemMatcherPlugin 26 | ], 27 | }); 28 | 29 | // Copy mappings.wasm after build 30 | if (!watch) { 31 | await ctx.rebuild(); 32 | copyMappingsWasm(); 33 | await ctx.dispose(); 34 | } else { 35 | await ctx.watch(); 36 | } 37 | } 38 | 39 | function copyMappingsWasm() { 40 | const wasmSourcePath = path.resolve(__dirname, 'node_modules/source-map/lib/mappings.wasm'); 41 | const wasmDestPath = path.resolve(__dirname, 'dist/mappings.wasm'); 42 | 43 | fs.copyFileSync(wasmSourcePath, wasmDestPath); 44 | console.log('Copied mappings.wasm to dist folder'); 45 | } 46 | 47 | /** 48 | * @type {import('esbuild').Plugin} 49 | */ 50 | const esbuildProblemMatcherPlugin = { 51 | name: 'esbuild-problem-matcher', 52 | setup(build) { 53 | build.onStart(() => { 54 | console.log('[watch] build started'); 55 | }); 56 | build.onEnd(result => { 57 | result.errors.forEach(({ text, location }) => { 58 | console.error(`✘ [ERROR] ${text}`); 59 | console.error(` ${location.file}:${location.line}:${location.column}:`); 60 | }); 61 | console.log('[watch] build finished'); 62 | }); 63 | } 64 | }; 65 | 66 | main().catch(e => { 67 | console.error(e); 68 | process.exit(1); 69 | }); 70 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import typescriptEslint from "@typescript-eslint/eslint-plugin"; 2 | import tsParser from "@typescript-eslint/parser"; 3 | 4 | export default [{ 5 | files: ["**/*.ts"], 6 | }, { 7 | plugins: { 8 | "@typescript-eslint": typescriptEslint, 9 | }, 10 | 11 | languageOptions: { 12 | parser: tsParser, 13 | ecmaVersion: 2022, 14 | sourceType: "module", 15 | }, 16 | 17 | rules: { 18 | "@typescript-eslint/naming-convention": ["warn", { 19 | selector: "import", 20 | format: ["camelCase", "PascalCase"], 21 | }], 22 | 23 | curly: "warn", 24 | eqeqeq: "warn", 25 | "no-throw-literal": "warn", 26 | semi: "warn", 27 | }, 28 | }]; -------------------------------------------------------------------------------- /images/extension-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zmr-233/MoonBit-WASM-Debugger/ee6eb9c5f767f2ea20f96bff962d12aab5887a74/images/extension-icon.png -------------------------------------------------------------------------------- /images/mbtmap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zmr-233/MoonBit-WASM-Debugger/ee6eb9c5f767f2ea20f96bff962d12aab5887a74/images/mbtmap.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mbtmap", 3 | "displayName": "MoonBit WASM Debugger (Unofficial)", 4 | "description": "Provides precise line-number error display for MoonBit's WASM errors; UNTRACEABLE UNWRAPS NO MORE! (Works with the official plugin)", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/zmr-233/MoonBit-WASM-Debugger.git" 8 | }, 9 | "version": "0.0.3", 10 | "engines": { 11 | "vscode": "^1.74.0" 12 | }, 13 | "categories": [ 14 | "Other" 15 | ], 16 | "publisher": "zmr-devtools", 17 | "license": "MIT", 18 | "main": "./dist/extension.js", 19 | "icon": "images/extension-icon.png", 20 | "activationEvents": [ 21 | "onStartupFinished", 22 | "workspaceContains:moon.mod.json", 23 | "onTaskType:shell" 24 | ], 25 | "contributes": { 26 | "commands": [ 27 | { 28 | "command": "mbtmap.clearErrors", 29 | "title": "Clean WASM Errors" 30 | } 31 | ] 32 | }, 33 | "scripts": { 34 | "compile": "npm run check-types && node esbuild.js", 35 | "check-types": "tsc --noEmit", 36 | "watch": "npm-run-all -p watch:*", 37 | "watch:esbuild": "node esbuild.js --watch", 38 | "watch:tsc": "tsc --noEmit --watch --project tsconfig.json", 39 | "vscode:prepublish": "npm run package", 40 | "package": "npm run check-types && node esbuild.js --production", 41 | "lint": "eslint src", 42 | "test": "vscode-test" 43 | }, 44 | "devDependencies": { 45 | "@types/mocha": "^10.0.8", 46 | "@types/node": "20.x", 47 | "@types/vscode": "^1.74.0", 48 | "@typescript-eslint/eslint-plugin": "^8.7.0", 49 | "@typescript-eslint/parser": "^8.7.0", 50 | "@vscode/test-cli": "^0.0.10", 51 | "@vscode/test-electron": "^2.4.1", 52 | "esbuild": "^0.24.0", 53 | "eslint": "^9.11.1", 54 | "typescript": "^5.6.2" 55 | }, 56 | "dependencies": { 57 | "source-map": "^0.7.4" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/extension.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import * as fs from 'fs'; 3 | import * as path from 'path'; 4 | import * as sourcemap from 'source-map'; 5 | 6 | let Top: any = {}; 7 | let BK: any = {}; 8 | let SP: any = {}; 9 | let ERR: any = {}; 10 | let diagCol: vscode.DiagnosticCollection; 11 | 12 | export function activate(context: vscode.ExtensionContext) { 13 | console.log('Extension activated'); 14 | 15 | diagCol = vscode.languages.createDiagnosticCollection('mbtmap'); 16 | context.subscriptions.push(diagCol); 17 | 18 | const disposable = vscode.tasks.onDidEndTaskProcess(async (event) => { 19 | const task = event.execution.task; 20 | console.log('Task ended:', task.name); 21 | 22 | if (task.name === 'moon test' || task.name === 'moon run') { 23 | try { 24 | console.log('Processing task:', task.name); 25 | await vscode.commands.executeCommand('workbench.action.terminal.copyLastCommandOutput'); 26 | Top.content = await vscode.env.clipboard.readText(); 27 | console.log('Clipboard content:', Top.content); 28 | 29 | if (!Top.content.includes('wasm-function')) { 30 | console.log('Content does not contain "wasm-function", skipping'); 31 | return; 32 | } 33 | 34 | if (!vscode.workspace.workspaceFolders) { 35 | vscode.window.showErrorMessage('Workspace folder not found. Please submit an issue at github.com/zmr-233/MoonBit-WASM-Debugger.'); 36 | return; 37 | } 38 | 39 | const workspaceFolder = vscode.workspace.workspaceFolders[0].uri.fsPath; 40 | console.log('Workspace path:', workspaceFolder); 41 | 42 | const moonModPath = path.join(workspaceFolder, 'moon.mod.json'); 43 | console.log('moon.mod.json path:', moonModPath); 44 | 45 | if (!fs.existsSync(moonModPath)) { 46 | vscode.window.showErrorMessage('moon.mod.json not found. Please submit an issue at github.com/zmr-233/MoonBit-WASM-Debugger.'); 47 | return; 48 | } 49 | 50 | const moonModContent = fs.readFileSync(moonModPath, 'utf8'); 51 | const moonMod = JSON.parse(moonModContent); 52 | Top.name = moonMod.name; 53 | Top.source = path.join(workspaceFolder, moonMod.source); 54 | console.log('Project name:', Top.name, 'Source path:', Top.source); 55 | 56 | Top.command = task.definition.id.split(',')[1]; 57 | console.log('Task command:', Top.command); 58 | 59 | let success = false; 60 | if (task.name === 'moon run') { 61 | console.log('Handling moon run task'); 62 | success = await handleMoonRunTask(context, task, workspaceFolder); 63 | } else if (task.name === 'moon test') { 64 | console.log('Handling moon test task'); 65 | success = await handleMoonTestTask(context, task, workspaceFolder); 66 | } 67 | if (!success) { return; } 68 | 69 | if (!BK.map_file) { 70 | vscode.window.showErrorMessage('Source map file not found. Please submit an issue at github.com/zmr-233/MoonBit-WASM-Debugger.'); 71 | return; 72 | } 73 | 74 | console.log('Parsing source-map'); 75 | ERR.file_list = await runSourcemap(Top.content, BK.map_file); 76 | console.log('Parsed file list:', ERR.file_list); 77 | 78 | console.log('Adding paths to error files'); 79 | ERR.prefix_file_list = handleFileList(ERR.file_list, Top.source); 80 | console.log('Prefixed error file list:', ERR.prefix_file_list); 81 | 82 | console.log('Generating VSCode problem output'); 83 | errorOutput(ERR.prefix_file_list); 84 | console.log('VSCode problem output completed'); 85 | 86 | } catch (error: any) { 87 | // console.error('Failed to get task output:', error); 88 | vscode.window.showErrorMessage(`Failed to get task output: ${error.message}. Please submit an issue at github.com/zmr-233/MoonBit-WASM-Debugger.`); 89 | } 90 | } 91 | }); 92 | 93 | context.subscriptions.push(disposable); 94 | 95 | const clearErrorsCommand = vscode.commands.registerCommand('mbtmap.clearErrors', () => { 96 | diagCol.clear(); 97 | console.log('Cleared error messages'); 98 | }); 99 | context.subscriptions.push(clearErrorsCommand); 100 | } 101 | 102 | async function handleMoonRunTask(context: vscode.ExtensionContext, task: vscode.Task, workspaceFolder: string) { 103 | console.log('Processing moon run task'); 104 | const args = Top.command.split(' '); 105 | BK.backend_type = args[args.indexOf('--target') + 1]; 106 | console.log('Backend type:', BK.backend_type); 107 | 108 | // Find main file path (assumed to be the last non-option argument) 109 | const nonOptionArgs = args.filter((arg: string) => !arg.startsWith('-') && !arg.startsWith('--')); 110 | const mainFileArgs = nonOptionArgs.filter((arg: string) => arg !== 'moon' && arg !== 'run'); 111 | const mainFileArg = mainFileArgs[mainFileArgs.length - 1]; 112 | console.log('Main file argument:', mainFileArg); 113 | 114 | // Compute the full path of the main file 115 | const mainFilePath = path.isAbsolute(mainFileArg) ? mainFileArg : path.join(workspaceFolder, mainFileArg); 116 | console.log('Main file full path:', mainFilePath); 117 | 118 | // Compute Top.folder by subtracting Top.source 119 | if (mainFilePath.startsWith(Top.source)) { 120 | Top.folder = path.relative(Top.source, mainFilePath); 121 | } else { 122 | Top.folder = path.relative(path.join(workspaceFolder, Top.source), mainFilePath); 123 | } 124 | console.log('Source folder (Top.folder):', Top.folder); 125 | console.log('args:', args); 126 | 127 | // Check for -g or --debug option 128 | if (!args.includes('-g') && !args.includes('--debug')) { 129 | console.log('Adding --debug option and re-executing task'); 130 | const newCommand = `moon clean && ${Top.command} --debug`; 131 | console.log('New command:', newCommand); 132 | 133 | const newTask = new vscode.Task( 134 | task.definition, 135 | task.scope ?? vscode.TaskScope.Workspace, 136 | task.name, 137 | task.source, 138 | new vscode.ShellExecution(newCommand) 139 | ); 140 | vscode.tasks.executeTask(newTask); 141 | return false; 142 | } 143 | 144 | BK.target_folder = path.join(workspaceFolder, 'target', BK.backend_type, 'debug', 'build', Top.folder); 145 | console.log('Target folder:', BK.target_folder); 146 | 147 | const wasmFiles = fs.readdirSync(BK.target_folder).filter((file) => file.endsWith('.wasm')); 148 | if (wasmFiles.length !== 1) { 149 | vscode.window.showErrorMessage('Unique wasm file not found. Please submit an issue at github.com/zmr-233/MoonBit-WASM-Debugger.'); 150 | return false; 151 | } 152 | 153 | Top.main_name = path.parse(wasmFiles[0]).name; 154 | console.log('Main wasm file name:', Top.main_name); 155 | 156 | BK.map_file = path.join(BK.target_folder, Top.main_name) + '.wasm.map'; 157 | console.log('Source map file path:', BK.map_file); 158 | 159 | return true; 160 | } 161 | 162 | async function handleMoonTestTask(context: vscode.ExtensionContext, task: vscode.Task, workspaceFolder: string) { 163 | console.log('Processing moon test task'); 164 | const args = Top.command.split(' '); 165 | BK.backend_type = args[args.indexOf('--target') + 1]; 166 | console.log('Backend type:', BK.backend_type); 167 | 168 | const pPath = args[args.indexOf('-p') + 1]; 169 | const pFullPath = path.isAbsolute(pPath) ? pPath : path.join(workspaceFolder, pPath); 170 | console.log('Parameter -p full path:', pFullPath); 171 | 172 | // Compute Top.folder by subtracting Top.name 173 | if (pFullPath.startsWith(Top.name)) { 174 | Top.folder = path.relative(Top.name, pFullPath); 175 | } else { 176 | Top.folder = path.relative(path.join(workspaceFolder, Top.name), pFullPath); 177 | } 178 | console.log('Source folder (Top.folder):', Top.folder); 179 | 180 | // Check for -g or --debug option 181 | if (!args.includes('-g') && !args.includes('--debug')) { 182 | console.log('Adding --debug option and re-executing task'); 183 | const newCommand = `moon clean && ${Top.command} --debug`; 184 | console.log('New command:', newCommand); 185 | 186 | const newTask = new vscode.Task( 187 | task.definition, 188 | task.scope ?? vscode.TaskScope.Workspace, 189 | task.name, 190 | task.source, 191 | new vscode.ShellExecution(newCommand) 192 | ); 193 | vscode.tasks.executeTask(newTask); 194 | return false; 195 | } 196 | 197 | if (args.includes('-f')) { 198 | const fileArg = args[args.indexOf('-f') + 1]; 199 | SP.black_or_internal = fileArg.endsWith('_test.mbt') ? 'blackbox_test' : 'internal_test'; 200 | console.log('Test type:', SP.black_or_internal); 201 | } 202 | 203 | BK.target_folder = path.join(workspaceFolder, 'target', BK.backend_type, 'debug', 'test', Top.folder); 204 | console.log('Target folder:', BK.target_folder); 205 | 206 | Top.main_name = `${Top.folder}.${SP.black_or_internal}`; 207 | console.log('Main file name:', Top.main_name); 208 | 209 | BK.map_file = path.join(BK.target_folder, Top.main_name) + '.wasm.map'; 210 | console.log('Source map file path:', BK.map_file); 211 | 212 | return true; 213 | } 214 | 215 | async function runSourcemap(content: string, mapFile: string): Promise> { 216 | console.log('Reading source map file:', mapFile); 217 | 218 | const mapData = fs.readFileSync(mapFile, 'utf8'); 219 | console.log('mapData = ', mapData) 220 | const smc = await new sourcemap.SourceMapConsumer(mapData); 221 | 222 | // Preprocess content to remove line breaks within tokens 223 | content = content.replace(/([^\s])\n([^\s])/g, '$1$2'); 224 | 225 | // Updated regex to handle the pattern, accounting for possible extra whitespace 226 | const regex = /at\s+(.+?)\s+\(wasm:\/\/.*?:wasm-function\[\d+\]:(0x[0-9a-fA-F]+)\)/g; 227 | let match; 228 | const fileList = []; 229 | const cwd = './'; 230 | 231 | while ((match = regex.exec(content)) !== null) { 232 | const message = match[1].trim(); 233 | const addr = match[2]; 234 | console.log('Matched address:', addr); 235 | 236 | const pos = smc.originalPositionFor({ line: 1, column: parseInt(addr, 16) }); 237 | console.log('Source position:', pos); 238 | 239 | if (pos.source) { 240 | let sourcePath = pos.source; 241 | sourcePath = path.isAbsolute(sourcePath) ? path.relative(cwd, sourcePath) : sourcePath; 242 | console.log('Source path:', sourcePath); 243 | 244 | fileList.push({ 245 | file: sourcePath, 246 | line: pos.line, 247 | column: pos.column, 248 | name: pos.name || '', 249 | message: message, 250 | }); 251 | } else { 252 | fileList.push({ 253 | file: '', 254 | line: 0, 255 | column: 0, 256 | name: '', 257 | message: 'Unresolvable address', 258 | }); 259 | } 260 | } 261 | 262 | smc.destroy(); 263 | return fileList; 264 | } 265 | 266 | function handleFileList(fileList: Array, source: string): Array { 267 | console.log('Processing file list:', fileList); 268 | return fileList.map((error) => { 269 | if (error.file && error.file !== '') { 270 | console.log('Processed file path:', error.file); 271 | } 272 | return error; 273 | }); 274 | } 275 | 276 | interface ErrorItem { 277 | file: string; 278 | line: number; 279 | column: number; 280 | name: string; 281 | message: string; 282 | } 283 | 284 | function errorOutput(prefixFileList: ErrorItem[]) { 285 | console.log('Generating VSCode problem output prefixFileList:', prefixFileList); 286 | 287 | diagCol.clear(); 288 | const diagnosticsMap = new Map(); 289 | 290 | // Define files to exclude 291 | const excludedFiles = new Set([ 292 | '', 293 | '__generated_driver_for_blackbox_test.mbt', 294 | '__generated_driver_for_internal_test.mbt' 295 | ]); 296 | 297 | let errorCounter = 1; 298 | 299 | prefixFileList.forEach((error) => { 300 | try { 301 | const fileName = path.basename(error.file); 302 | 303 | // Skip excluded files 304 | if (excludedFiles.has(error.file) || excludedFiles.has(fileName)) { 305 | console.log(`Skipping excluded file: ${error.file}`); 306 | return; 307 | } 308 | 309 | const filePath = path.resolve(error.file); 310 | const fileUri = vscode.Uri.file(filePath); 311 | 312 | const range = new vscode.Range( 313 | new vscode.Position((error.line || 1) - 1, (error.column || 1) - 1), 314 | new vscode.Position((error.line || 1) - 1, Number.MAX_SAFE_INTEGER) 315 | ); 316 | 317 | const diagnosticMessage = `WASM ErrStack #${errorCounter}: ${error.message}`; 318 | const diagnostic = new vscode.Diagnostic(range, diagnosticMessage, vscode.DiagnosticSeverity.Error); 319 | 320 | const uriString = fileUri.toString(); 321 | if (!diagnosticsMap.has(uriString)) { 322 | diagnosticsMap.set(uriString, []); 323 | } 324 | diagnosticsMap.get(uriString)!.push(diagnostic); 325 | errorCounter++; 326 | } catch (err) { 327 | // console.warn('Skipping unresolvable file:', error.file, err); 328 | } 329 | }); 330 | 331 | diagnosticsMap.forEach((diagnostics, uriString) => { 332 | const fileUri = vscode.Uri.parse(uriString); 333 | diagCol.set(fileUri, diagnostics); 334 | }); 335 | } -------------------------------------------------------------------------------- /src/test/extension.test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | var assert = require("assert"); 4 | // You can import and use all API from the 'vscode' module 5 | // as well as import your extension to test it 6 | var vscode = require("vscode"); 7 | // import * as myExtension from '../../extension'; 8 | suite('Extension Test Suite', function () { 9 | vscode.window.showInformationMessage('Start all tests.'); 10 | test('Sample test', function () { 11 | assert.strictEqual(-1, [1, 2, 3].indexOf(5)); 12 | assert.strictEqual(-1, [1, 2, 3].indexOf(0)); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /src/test/extension.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | 3 | // You can import and use all API from the 'vscode' module 4 | // as well as import your extension to test it 5 | import * as vscode from 'vscode'; 6 | // import * as myExtension from '../../extension'; 7 | 8 | suite('Extension Test Suite', () => { 9 | vscode.window.showInformationMessage('Start all tests.'); 10 | 11 | test('Sample test', () => { 12 | assert.strictEqual(-1, [1, 2, 3].indexOf(5)); 13 | assert.strictEqual(-1, [1, 2, 3].indexOf(0)); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "commonjs", 5 | "lib": ["ES2020"], 6 | "strict": true, 7 | "esModuleInterop": true, 8 | "skipLibCheck": true, 9 | "noEmit": true 10 | }, 11 | "include": ["src"] 12 | } 13 | -------------------------------------------------------------------------------- /vsc-extension-quickstart.md: -------------------------------------------------------------------------------- 1 | # Welcome to your VS Code Extension 2 | 3 | ## What's in the folder 4 | 5 | * This folder contains all of the files necessary for your extension. 6 | * `package.json` - this is the manifest file in which you declare your extension and command. 7 | * The sample plugin registers a command and defines its title and command name. With this information VS Code can show the command in the command palette. It doesn’t yet need to load the plugin. 8 | * `src/extension.ts` - this is the main file where you will provide the implementation of your command. 9 | * The file exports one function, `activate`, which is called the very first time your extension is activated (in this case by executing the command). Inside the `activate` function we call `registerCommand`. 10 | * We pass the function containing the implementation of the command as the second parameter to `registerCommand`. 11 | 12 | ## Get up and running straight away 13 | 14 | * Press `F5` to open a new window with your extension loaded. 15 | * Run your command from the command palette by pressing (`Ctrl+Shift+P` or `Cmd+Shift+P` on Mac) and typing `Hello World`. 16 | * Set breakpoints in your code inside `src/extension.ts` to debug your extension. 17 | * Find output from your extension in the debug console. 18 | 19 | ## Make changes 20 | 21 | * You can relaunch the extension from the debug toolbar after changing code in `src/extension.ts`. 22 | * You can also reload (`Ctrl+R` or `Cmd+R` on Mac) the VS Code window with your extension to load your changes. 23 | 24 | ## Explore the API 25 | 26 | * You can open the full set of our API when you open the file `node_modules/@types/vscode/index.d.ts`. 27 | 28 | ## Run tests 29 | 30 | * Install the [Extension Test Runner](https://marketplace.visualstudio.com/items?itemName=ms-vscode.extension-test-runner) 31 | * Run the "watch" task via the **Tasks: Run Task** command. Make sure this is running, or tests might not be discovered. 32 | * Open the Testing view from the activity bar and click the Run Test" button, or use the hotkey `Ctrl/Cmd + ; A` 33 | * See the output of the test result in the Test Results view. 34 | * Make changes to `src/test/extension.test.ts` or create new test files inside the `test` folder. 35 | * The provided test runner will only consider files matching the name pattern `**.test.ts`. 36 | * You can create folders inside the `test` folder to structure your tests any way you want. 37 | 38 | ## Go further 39 | 40 | * [Follow UX guidelines](https://code.visualstudio.com/api/ux-guidelines/overview) to create extensions that seamlessly integrate with VS Code's native interface and patterns. 41 | * Reduce the extension size and improve the startup time by [bundling your extension](https://code.visualstudio.com/api/working-with-extensions/bundling-extension). 42 | * [Publish your extension](https://code.visualstudio.com/api/working-with-extensions/publishing-extension) on the VS Code extension marketplace. 43 | * Automate builds by setting up [Continuous Integration](https://code.visualstudio.com/api/working-with-extensions/continuous-integration). 44 | * Integrate to the [report issue](https://code.visualstudio.com/api/get-started/wrapping-up#issue-reporting) flow to get issue and feature requests reported by users. 45 | -------------------------------------------------------------------------------- /wasm-sourcemap.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Utility tools that extracts DWARF information encoded in a wasm output 3 | produced by the LLVM tools, and encodes it as a wasm source map. Additionally, 4 | it can collect original sources, change files prefixes, and strip debug 5 | sections from a wasm file. 6 | """ 7 | import argparse 8 | from collections import OrderedDict 9 | import json 10 | import logging 11 | import os 12 | import re 13 | from subprocess import Popen, PIPE 14 | import sys 15 | def parse_args(): 16 | parser = argparse.ArgumentParser(prog='wasm-sourcemap.py', description=__doc__) 17 | parser.add_argument('wasm', help='wasm file') 18 | parser.add_argument('-o', '--output', help='output source map') 19 | parser.add_argument('-p', '--prefix', nargs='*', help='replace source filename prefix', default=[]) 20 | parser.add_argument('-s', '--sources', action='store_true', help='read and embed source files') 21 | parser.add_argument('-w', nargs='?', help='set output wasm file') 22 | parser.add_argument('-x', '--strip', action='store_true', help='removes debug and linking sections') 23 | parser.add_argument('-u', '--source-map-url', nargs='?', help='specifies sourceMappingURL section contest') 24 | parser.add_argument('--dwarfdump', help="path to llvm-dwarfdump executable") 25 | parser.add_argument('--dwarfdump-output', nargs='?', help=argparse.SUPPRESS) 26 | return parser.parse_args() 27 | def encode_vlq(n): 28 | VLQ_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" 29 | x = (n << 1) if n >= 0 else ((-n << 1) + 1) 30 | result = "" 31 | while x > 31: 32 | result = result + VLQ_CHARS[32 + (x & 31)] 33 | x = x >> 5 34 | return result + VLQ_CHARS[x] 35 | def read_var_uint(wasm, pos): 36 | n = 0 37 | shift = 0 38 | b = ord(wasm[pos:pos + 1]) 39 | pos = pos + 1 40 | while b >= 128: 41 | n = n | ((b - 128) << shift) 42 | b = ord(wasm[pos:pos + 1]) 43 | pos = pos + 1 44 | shift += 7 45 | return n + (b << shift), pos 46 | def strip_debug_sections(wasm): 47 | logging.debug('Strip debug sections') 48 | pos = 8 49 | stripped = wasm[:pos] 50 | while pos < len(wasm): 51 | section_start = pos 52 | section_id, pos_ = read_var_uint(wasm, pos) 53 | section_size, section_body = read_var_uint(wasm, pos_) 54 | pos = section_body + section_size 55 | if section_id == 0: 56 | name_len, name_pos = read_var_uint(wasm, section_body) 57 | name_end = name_pos + name_len 58 | name = wasm[name_pos:name_end] 59 | if name == "linking" or name == "sourceMappingURL" or name.startswith("reloc..debug_") or name.startswith(".debug_"): 60 | continue # skip debug related sections 61 | stripped = stripped + wasm[section_start:pos] 62 | return stripped 63 | def encode_uint_var(n): 64 | result = bytearray() 65 | while n > 127: 66 | result.append(128 | (n & 127)) 67 | n = n >> 7 68 | result.append(n) 69 | return bytes(result) 70 | def append_source_mapping(wasm, url): 71 | logging.debug('Append sourceMappingURL section') 72 | section_name = "sourceMappingURL" 73 | section_content = encode_uint_var(len(section_name)) + section_name + encode_uint_var(len(url)) + url 74 | return wasm + encode_uint_var(0) + encode_uint_var(len(section_content)) + section_content 75 | def get_code_section_offset(wasm): 76 | logging.debug('Read sections index') 77 | pos = 8 78 | while pos < len(wasm): 79 | section_id, pos_ = read_var_uint(wasm, pos) 80 | section_size, pos = read_var_uint(wasm, pos_) 81 | if section_id == 10: 82 | return pos 83 | pos = pos + section_size 84 | def read_dwarf_entries(wasm, options): 85 | if options.dwarfdump_output: 86 | output = open(options.dwarfdump_output, 'r').read() 87 | output = output.decode('utf-8') 88 | elif options.dwarfdump: 89 | logging.debug('Reading DWARF information from %s' % wasm) 90 | if not os.path.exists(options.dwarfdump): 91 | logging.error('llvm-dwarfdump not found: ' + options.dwarfdump) 92 | sys.exit(1) 93 | process = Popen([options.dwarfdump, "-debug-line", wasm], stdout=PIPE) 94 | output, err = process.communicate() 95 | output = output.decode('utf-8') 96 | exit_code = process.wait() 97 | if exit_code != 0: 98 | logging.error('Error during llvm-dwarfdump execution (%s)' % exit_code) 99 | sys.exit(1) 100 | else: 101 | logging.error('Please specify either --dwarfdump or --dwarfdump-output') 102 | sys.exit(1) 103 | entries = [] 104 | debug_line_chunks = re.split(r"(debug_line\[0x[0-9a-f]*\])", output) 105 | for i in range(1, len(debug_line_chunks), 2): 106 | line_chunk = debug_line_chunks[i + 1] 107 | # include_directories[ 1] = "/Users/yury/Work/junk/sqlite-playground/src" 108 | # file_names[ 1]: 109 | # name: "playground.c" 110 | # dir_index: 1 111 | # mod_time: 0x00000000 112 | # length: 0x00000000 113 | # 114 | # Address Line Column File ISA Discriminator Flags 115 | # ------------------ ------ ------ ------ --- ------------- ------------- 116 | # 0x0000000000000006 22 0 1 0 0 is_stmt 117 | # 0x0000000000000007 23 10 1 0 0 is_stmt prologue_end 118 | # 0x000000000000000f 23 3 1 0 0 119 | # 0x0000000000000010 23 3 1 0 0 end_sequence 120 | # 0x0000000000000011 28 0 1 0 0 is_stmt 121 | include_directories = {'0': ""} 122 | for dir in re.finditer(r"include_directories\[\s*(\d+)\] = \"([^\"]*)", line_chunk): 123 | include_directories[dir.group(1)] = dir.group(2) 124 | files = {} 125 | for file in re.finditer(r"file_names\[\s*(\d+)\]:\s+name: \"([^\"]*)\"\s+dir_index: (\d+)", line_chunk): 126 | dir = include_directories[file.group(3)] 127 | file_path = (dir + '/' if dir != '' else '') + file.group(2) 128 | files[file.group(1)] = file_path 129 | for line in re.finditer(r"\n0x([0-9a-f]+)\s+(\d+)\s+(\d+)\s+(\d+)", line_chunk): 130 | entry = {'address': int(line.group(1), 16), 'line': int(line.group(2)), 'column': int(line.group(3)), 'file': files[line.group(4)]} 131 | entries.append(entry) 132 | return entries 133 | def build_sourcemap(entries, code_section_offset, prefixes, collect_sources): 134 | sources = [] 135 | sources_content = [] if collect_sources else None 136 | mappings = [] 137 | sources_map = {} 138 | last_address = 0 139 | last_source_id = 0 140 | last_line = 1 141 | last_column = 1 142 | for entry in entries: 143 | line = entry['line'] 144 | column = entry['column'] 145 | if line == 0 or column == 0: 146 | continue 147 | address = entry['address'] + code_section_offset 148 | file_name = entry['file'] 149 | if file_name not in sources_map: 150 | source_id = len(sources) 151 | sources_map[file_name] = source_id 152 | source_name = file_name 153 | for p in prefixes: 154 | if file_name.startswith(p['prefix']): 155 | if p['replacement'] is None: 156 | source_name = file_name[len(p['prefix'])::] 157 | else: 158 | source_name = p['replacement'] + file_name[len(p['prefix'])::] 159 | break 160 | sources.append(source_name) 161 | if collect_sources: 162 | try: 163 | with open(file_name, 'r') as infile: 164 | source_content = infile.read() 165 | sources_content.append(source_content) 166 | except: 167 | print('Failed to read source: %s' % file_name) 168 | sources_content.append(None) 169 | else: 170 | source_id = sources_map[file_name] 171 | address_delta = address - last_address 172 | source_id_delta = source_id - last_source_id 173 | line_delta = line - last_line 174 | column_delta = column - last_column 175 | mappings.append(encode_vlq(address_delta) + encode_vlq(source_id_delta) + encode_vlq(line_delta) + encode_vlq(column_delta)) 176 | last_address = address 177 | last_source_id = source_id 178 | last_line = line 179 | last_column = column 180 | return OrderedDict([('version', 3), 181 | ('names', []), 182 | ('sources', sources), 183 | ('sourcesContent', sources_content), 184 | ('mappings', ','.join(mappings))]) 185 | def main(): 186 | options = parse_args() 187 | wasm_input = options.wasm 188 | with open(wasm_input, 'rb') as infile: 189 | wasm = infile.read() 190 | entries = read_dwarf_entries(wasm_input, options) 191 | code_section_offset = get_code_section_offset(wasm) 192 | prefixes = [] 193 | for p in options.prefix: 194 | if '=' in p: 195 | prefix, replacement = p.split('=') 196 | prefixes.append({'prefix': prefix, 'replacement': replacement}) 197 | else: 198 | prefixes.append({'prefix': p, 'replacement': None}) 199 | logging.debug('Saving to %s' % options.output) 200 | map = build_sourcemap(entries, code_section_offset, prefixes, options.sources) 201 | with open(options.output, 'w') as outfile: 202 | json.dump(map, outfile, separators=(',', ':')) 203 | if options.strip: 204 | wasm = strip_debug_sections(wasm) 205 | if options.source_map_url: 206 | wasm = append_source_mapping(wasm, options.source_map_url) 207 | if options.w: 208 | logging.debug('Saving wasm to %s' % options.w) 209 | with open(options.w, 'wb') as outfile: 210 | outfile.write(wasm) 211 | logging.debug('Done') 212 | return 0 213 | if __name__ == '__main__': 214 | logging.basicConfig(level=logging.DEBUG if os.environ.get('EMCC_DEBUG') else logging.INFO) 215 | sys.exit(main()) --------------------------------------------------------------------------------