├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .vscode ├── launch.json ├── settings.json └── tasks.json ├── .vscodeignore ├── CHANGELOG.md ├── LICENSE-MEDIA.txt ├── LICENSE.txt ├── README.md ├── build.mjs ├── eslint.config.js ├── example ├── .gitignore ├── .vscode │ ├── c_cpp_properties.json │ ├── launch.json │ └── settings.json ├── Makefile ├── README.md ├── design.sv └── design_sim.cc ├── media └── chip-debug.svg ├── package.json ├── src ├── cxxrtl │ ├── client.ts │ ├── link.ts │ └── proto.ts ├── debug │ ├── observer.ts │ ├── options.ts │ ├── session.ts │ └── watch.ts ├── debugger.ts ├── extension.ts ├── model │ ├── sample.ts │ ├── scope.ts │ ├── source.ts │ ├── styling.ts │ ├── time.ts │ └── variable.ts └── ui │ ├── diagnostic.ts │ ├── hover.ts │ ├── input.ts │ ├── sidebar.ts │ └── status.ts └── tsconfig.json /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - main 5 | tags: 6 | - v* 7 | pull_request: 8 | merge_group: 9 | name: CI 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Check out source code 15 | uses: actions/checkout@v4 16 | - name: Set up Node 17 | uses: actions/setup-node@v4 18 | with: 19 | node-version: v20.x 20 | - name: Install dependencies 21 | run: npm install 22 | - name: Package extension 23 | run: npm run package 24 | - name: Upload package artifact 25 | uses: actions/upload-artifact@v4 26 | with: 27 | name: package 28 | path: '*.vsix' 29 | publish: 30 | needs: build 31 | runs-on: ubuntu-latest 32 | if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags/v') 33 | steps: 34 | - name: Set up Node 35 | uses: actions/setup-node@v4 36 | with: 37 | node-version: v20.x 38 | - name: Install VSCE 39 | run: npm install -g @vscode/vsce 40 | - name: Download package artifact 41 | uses: actions/download-artifact@v4 42 | with: 43 | name: package 44 | - name: Publish package 45 | run: vsce publish -i *.vsix 46 | env: 47 | VSCE_PAT: ${{ secrets.VSCE_PAT }} 48 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | out 2 | node_modules 3 | package-lock.json 4 | *.vsix 5 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Run Extension", 6 | "type": "extensionHost", 7 | "request": "launch", 8 | "args": [ 9 | "--extensionDevelopmentPath=${workspaceFolder}", 10 | "${workspaceFolder}/example" 11 | ], 12 | "outFiles": [ 13 | "${workspaceFolder}/out/**/*.js" 14 | ], 15 | "preLaunchTask": "npm: esbuild:watch" 16 | }, 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsc.autoDetect": "off", 3 | "eslint.useFlatConfig": true, 4 | "[typescript]": { 5 | "editor.codeActionsOnSave": { 6 | "source.removeUnusedImports": "always", 7 | "source.fixAll.eslint": "always" 8 | } 9 | }, 10 | } 11 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "type": "npm", 6 | "script": "esbuild:watch", 7 | "problemMatcher": "$ts-esbuild-watch", 8 | "isBackground": true, 9 | "presentation": { 10 | "reveal": "never" 11 | }, 12 | "group": { 13 | "kind": "build" 14 | } 15 | }, 16 | { 17 | "type": "npm", 18 | "script": "tsc:watch", 19 | "problemMatcher": "$tsc-watch", 20 | "isBackground": true, 21 | "presentation": { 22 | "reveal": "never" 23 | }, 24 | "group": { 25 | "kind": "build" 26 | } 27 | } 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | ** 2 | !out/** 3 | !media/** 4 | !LICENSE* 5 | !README* 6 | !CHANGELOG* 7 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to the "RTL Debugger" extension will be documented in this file. 4 | 5 | ## [Unreleased] 6 | 7 | - Initial release -------------------------------------------------------------------------------- /LICENSE-MEDIA.txt: -------------------------------------------------------------------------------- 1 | Attribution 4.0 International 2 | 3 | ======================================================================= 4 | 5 | Creative Commons Corporation ("Creative Commons") is not a law firm and 6 | does not provide legal services or legal advice. Distribution of 7 | Creative Commons public licenses does not create a lawyer-client or 8 | other relationship. Creative Commons makes its licenses and related 9 | information available on an "as-is" basis. Creative Commons gives no 10 | warranties regarding its licenses, any material licensed under their 11 | terms and conditions, or any related information. Creative Commons 12 | disclaims all liability for damages resulting from their use to the 13 | fullest extent possible. 14 | 15 | Using Creative Commons Public Licenses 16 | 17 | Creative Commons public licenses provide a standard set of terms and 18 | conditions that creators and other rights holders may use to share 19 | original works of authorship and other material subject to copyright 20 | and certain other rights specified in the public license below. The 21 | following considerations are for informational purposes only, are not 22 | exhaustive, and do not form part of our licenses. 23 | 24 | Considerations for licensors: Our public licenses are 25 | intended for use by those authorized to give the public 26 | permission to use material in ways otherwise restricted by 27 | copyright and certain other rights. Our licenses are 28 | irrevocable. Licensors should read and understand the terms 29 | and conditions of the license they choose before applying it. 30 | Licensors should also secure all rights necessary before 31 | applying our licenses so that the public can reuse the 32 | material as expected. Licensors should clearly mark any 33 | material not subject to the license. This includes other CC- 34 | licensed material, or material used under an exception or 35 | limitation to copyright. More considerations for licensors: 36 | wiki.creativecommons.org/Considerations_for_licensors 37 | 38 | Considerations for the public: By using one of our public 39 | licenses, a licensor grants the public permission to use the 40 | licensed material under specified terms and conditions. If 41 | the licensor's permission is not necessary for any reason--for 42 | example, because of any applicable exception or limitation to 43 | copyright--then that use is not regulated by the license. Our 44 | licenses grant only permissions under copyright and certain 45 | other rights that a licensor has authority to grant. Use of 46 | the licensed material may still be restricted for other 47 | reasons, including because others have copyright or other 48 | rights in the material. A licensor may make special requests, 49 | such as asking that all changes be marked or described. 50 | Although not required by our licenses, you are encouraged to 51 | respect those requests where reasonable. More_considerations 52 | for the public: 53 | wiki.creativecommons.org/Considerations_for_licensees 54 | 55 | ======================================================================= 56 | 57 | Creative Commons Attribution 4.0 International Public License 58 | 59 | By exercising the Licensed Rights (defined below), You accept and agree 60 | to be bound by the terms and conditions of this Creative Commons 61 | Attribution 4.0 International Public License ("Public License"). To the 62 | extent this Public License may be interpreted as a contract, You are 63 | granted the Licensed Rights in consideration of Your acceptance of 64 | these terms and conditions, and the Licensor grants You such rights in 65 | consideration of benefits the Licensor receives from making the 66 | Licensed Material available under these terms and conditions. 67 | 68 | 69 | Section 1 -- Definitions. 70 | 71 | a. Adapted Material means material subject to Copyright and Similar 72 | Rights that is derived from or based upon the Licensed Material 73 | and in which the Licensed Material is translated, altered, 74 | arranged, transformed, or otherwise modified in a manner requiring 75 | permission under the Copyright and Similar Rights held by the 76 | Licensor. For purposes of this Public License, where the Licensed 77 | Material is a musical work, performance, or sound recording, 78 | Adapted Material is always produced where the Licensed Material is 79 | synched in timed relation with a moving image. 80 | 81 | b. Adapter's License means the license You apply to Your Copyright 82 | and Similar Rights in Your contributions to Adapted Material in 83 | accordance with the terms and conditions of this Public License. 84 | 85 | c. Copyright and Similar Rights means copyright and/or similar rights 86 | closely related to copyright including, without limitation, 87 | performance, broadcast, sound recording, and Sui Generis Database 88 | Rights, without regard to how the rights are labeled or 89 | categorized. For purposes of this Public License, the rights 90 | specified in Section 2(b)(1)-(2) are not Copyright and Similar 91 | Rights. 92 | 93 | d. Effective Technological Measures means those measures that, in the 94 | absence of proper authority, may not be circumvented under laws 95 | fulfilling obligations under Article 11 of the WIPO Copyright 96 | Treaty adopted on December 20, 1996, and/or similar international 97 | agreements. 98 | 99 | e. Exceptions and Limitations means fair use, fair dealing, and/or 100 | any other exception or limitation to Copyright and Similar Rights 101 | that applies to Your use of the Licensed Material. 102 | 103 | f. Licensed Material means the artistic or literary work, database, 104 | or other material to which the Licensor applied this Public 105 | License. 106 | 107 | g. Licensed Rights means the rights granted to You subject to the 108 | terms and conditions of this Public License, which are limited to 109 | all Copyright and Similar Rights that apply to Your use of the 110 | Licensed Material and that the Licensor has authority to license. 111 | 112 | h. Licensor means the individual(s) or entity(ies) granting rights 113 | under this Public License. 114 | 115 | i. Share means to provide material to the public by any means or 116 | process that requires permission under the Licensed Rights, such 117 | as reproduction, public display, public performance, distribution, 118 | dissemination, communication, or importation, and to make material 119 | available to the public including in ways that members of the 120 | public may access the material from a place and at a time 121 | individually chosen by them. 122 | 123 | j. Sui Generis Database Rights means rights other than copyright 124 | resulting from Directive 96/9/EC of the European Parliament and of 125 | the Council of 11 March 1996 on the legal protection of databases, 126 | as amended and/or succeeded, as well as other essentially 127 | equivalent rights anywhere in the world. 128 | 129 | k. You means the individual or entity exercising the Licensed Rights 130 | under this Public License. Your has a corresponding meaning. 131 | 132 | 133 | Section 2 -- Scope. 134 | 135 | a. License grant. 136 | 137 | 1. Subject to the terms and conditions of this Public License, 138 | the Licensor hereby grants You a worldwide, royalty-free, 139 | non-sublicensable, non-exclusive, irrevocable license to 140 | exercise the Licensed Rights in the Licensed Material to: 141 | 142 | a. reproduce and Share the Licensed Material, in whole or 143 | in part; and 144 | 145 | b. produce, reproduce, and Share Adapted Material. 146 | 147 | 2. Exceptions and Limitations. For the avoidance of doubt, where 148 | Exceptions and Limitations apply to Your use, this Public 149 | License does not apply, and You do not need to comply with 150 | its terms and conditions. 151 | 152 | 3. Term. The term of this Public License is specified in Section 153 | 6(a). 154 | 155 | 4. Media and formats; technical modifications allowed. The 156 | Licensor authorizes You to exercise the Licensed Rights in 157 | all media and formats whether now known or hereafter created, 158 | and to make technical modifications necessary to do so. The 159 | Licensor waives and/or agrees not to assert any right or 160 | authority to forbid You from making technical modifications 161 | necessary to exercise the Licensed Rights, including 162 | technical modifications necessary to circumvent Effective 163 | Technological Measures. For purposes of this Public License, 164 | simply making modifications authorized by this Section 2(a) 165 | (4) never produces Adapted Material. 166 | 167 | 5. Downstream recipients. 168 | 169 | a. Offer from the Licensor -- Licensed Material. Every 170 | recipient of the Licensed Material automatically 171 | receives an offer from the Licensor to exercise the 172 | Licensed Rights under the terms and conditions of this 173 | Public License. 174 | 175 | b. No downstream restrictions. You may not offer or impose 176 | any additional or different terms or conditions on, or 177 | apply any Effective Technological Measures to, the 178 | Licensed Material if doing so restricts exercise of the 179 | Licensed Rights by any recipient of the Licensed 180 | Material. 181 | 182 | 6. No endorsement. Nothing in this Public License constitutes or 183 | may be construed as permission to assert or imply that You 184 | are, or that Your use of the Licensed Material is, connected 185 | with, or sponsored, endorsed, or granted official status by, 186 | the Licensor or others designated to receive attribution as 187 | provided in Section 3(a)(1)(A)(i). 188 | 189 | b. Other rights. 190 | 191 | 1. Moral rights, such as the right of integrity, are not 192 | licensed under this Public License, nor are publicity, 193 | privacy, and/or other similar personality rights; however, to 194 | the extent possible, the Licensor waives and/or agrees not to 195 | assert any such rights held by the Licensor to the limited 196 | extent necessary to allow You to exercise the Licensed 197 | Rights, but not otherwise. 198 | 199 | 2. Patent and trademark rights are not licensed under this 200 | Public License. 201 | 202 | 3. To the extent possible, the Licensor waives any right to 203 | collect royalties from You for the exercise of the Licensed 204 | Rights, whether directly or through a collecting society 205 | under any voluntary or waivable statutory or compulsory 206 | licensing scheme. In all other cases the Licensor expressly 207 | reserves any right to collect such royalties. 208 | 209 | 210 | Section 3 -- License Conditions. 211 | 212 | Your exercise of the Licensed Rights is expressly made subject to the 213 | following conditions. 214 | 215 | a. Attribution. 216 | 217 | 1. If You Share the Licensed Material (including in modified 218 | form), You must: 219 | 220 | a. retain the following if it is supplied by the Licensor 221 | with the Licensed Material: 222 | 223 | i. identification of the creator(s) of the Licensed 224 | Material and any others designated to receive 225 | attribution, in any reasonable manner requested by 226 | the Licensor (including by pseudonym if 227 | designated); 228 | 229 | ii. a copyright notice; 230 | 231 | iii. a notice that refers to this Public License; 232 | 233 | iv. a notice that refers to the disclaimer of 234 | warranties; 235 | 236 | v. a URI or hyperlink to the Licensed Material to the 237 | extent reasonably practicable; 238 | 239 | b. indicate if You modified the Licensed Material and 240 | retain an indication of any previous modifications; and 241 | 242 | c. indicate the Licensed Material is licensed under this 243 | Public License, and include the text of, or the URI or 244 | hyperlink to, this Public License. 245 | 246 | 2. You may satisfy the conditions in Section 3(a)(1) in any 247 | reasonable manner based on the medium, means, and context in 248 | which You Share the Licensed Material. For example, it may be 249 | reasonable to satisfy the conditions by providing a URI or 250 | hyperlink to a resource that includes the required 251 | information. 252 | 253 | 3. If requested by the Licensor, You must remove any of the 254 | information required by Section 3(a)(1)(A) to the extent 255 | reasonably practicable. 256 | 257 | 4. If You Share Adapted Material You produce, the Adapter's 258 | License You apply must not prevent recipients of the Adapted 259 | Material from complying with this Public License. 260 | 261 | 262 | Section 4 -- Sui Generis Database Rights. 263 | 264 | Where the Licensed Rights include Sui Generis Database Rights that 265 | apply to Your use of the Licensed Material: 266 | 267 | a. for the avoidance of doubt, Section 2(a)(1) grants You the right 268 | to extract, reuse, reproduce, and Share all or a substantial 269 | portion of the contents of the database; 270 | 271 | b. if You include all or a substantial portion of the database 272 | contents in a database in which You have Sui Generis Database 273 | Rights, then the database in which You have Sui Generis Database 274 | Rights (but not its individual contents) is Adapted Material; and 275 | 276 | c. You must comply with the conditions in Section 3(a) if You Share 277 | all or a substantial portion of the contents of the database. 278 | 279 | For the avoidance of doubt, this Section 4 supplements and does not 280 | replace Your obligations under this Public License where the Licensed 281 | Rights include other Copyright and Similar Rights. 282 | 283 | 284 | Section 5 -- Disclaimer of Warranties and Limitation of Liability. 285 | 286 | a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE 287 | EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS 288 | AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF 289 | ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS, 290 | IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION, 291 | WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR 292 | PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS, 293 | ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT 294 | KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT 295 | ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU. 296 | 297 | b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE 298 | TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION, 299 | NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT, 300 | INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES, 301 | COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR 302 | USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN 303 | ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR 304 | DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR 305 | IN PART, THIS LIMITATION MAY NOT APPLY TO YOU. 306 | 307 | c. The disclaimer of warranties and limitation of liability provided 308 | above shall be interpreted in a manner that, to the extent 309 | possible, most closely approximates an absolute disclaimer and 310 | waiver of all liability. 311 | 312 | 313 | Section 6 -- Term and Termination. 314 | 315 | a. This Public License applies for the term of the Copyright and 316 | Similar Rights licensed here. However, if You fail to comply with 317 | this Public License, then Your rights under this Public License 318 | terminate automatically. 319 | 320 | b. Where Your right to use the Licensed Material has terminated under 321 | Section 6(a), it reinstates: 322 | 323 | 1. automatically as of the date the violation is cured, provided 324 | it is cured within 30 days of Your discovery of the 325 | violation; or 326 | 327 | 2. upon express reinstatement by the Licensor. 328 | 329 | For the avoidance of doubt, this Section 6(b) does not affect any 330 | right the Licensor may have to seek remedies for Your violations 331 | of this Public License. 332 | 333 | c. For the avoidance of doubt, the Licensor may also offer the 334 | Licensed Material under separate terms or conditions or stop 335 | distributing the Licensed Material at any time; however, doing so 336 | will not terminate this Public License. 337 | 338 | d. Sections 1, 5, 6, 7, and 8 survive termination of this Public 339 | License. 340 | 341 | 342 | Section 7 -- Other Terms and Conditions. 343 | 344 | a. The Licensor shall not be bound by any additional or different 345 | terms or conditions communicated by You unless expressly agreed. 346 | 347 | b. Any arrangements, understandings, or agreements regarding the 348 | Licensed Material not stated herein are separate from and 349 | independent of the terms and conditions of this Public License. 350 | 351 | 352 | Section 8 -- Interpretation. 353 | 354 | a. For the avoidance of doubt, this Public License does not, and 355 | shall not be interpreted to, reduce, limit, restrict, or impose 356 | conditions on any use of the Licensed Material that could lawfully 357 | be made without permission under this Public License. 358 | 359 | b. To the extent possible, if any provision of this Public License is 360 | deemed unenforceable, it shall be automatically reformed to the 361 | minimum extent necessary to make it enforceable. If the provision 362 | cannot be reformed, it shall be severed from this Public License 363 | without affecting the enforceability of the remaining terms and 364 | conditions. 365 | 366 | c. No term or condition of this Public License will be waived and no 367 | failure to comply consented to unless expressly agreed to by the 368 | Licensor. 369 | 370 | d. Nothing in this Public License constitutes or may be interpreted 371 | as a limitation upon, or waiver of, any privileges and immunities 372 | that apply to the Licensor or You, including from the legal 373 | processes of any jurisdiction or authority. 374 | 375 | 376 | ======================================================================= 377 | 378 | Creative Commons is not a party to its public 379 | licenses. Notwithstanding, Creative Commons may elect to apply one of 380 | its public licenses to material it publishes and in those instances 381 | will be considered the “Licensor.” The text of the Creative Commons 382 | public licenses is dedicated to the public domain under the CC0 Public 383 | Domain Dedication. Except for the limited purpose of indicating that 384 | material is shared under a Creative Commons public license or as 385 | otherwise permitted by the Creative Commons policies published at 386 | creativecommons.org/policies, Creative Commons does not authorize the 387 | use of the trademark "Creative Commons" or any other trademark or logo 388 | of Creative Commons without its prior written consent including, 389 | without limitation, in connection with any unauthorized modifications 390 | to any of its public licenses or any other arrangements, 391 | understandings, or agreements concerning use of licensed material. For 392 | the avoidance of doubt, this paragraph does not form part of the 393 | public licenses. 394 | 395 | Creative Commons may be contacted at creativecommons.org. -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | ISC License 2 | 3 | Copyright (C) Catherine 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any 6 | purpose with or without fee is hereby granted, provided that the above 7 | copyright notice and this permission notice appear in all copies. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 10 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 11 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 12 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 13 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 14 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 15 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RTL Debugger for VSCode 2 | 3 | TODO 4 | 5 | ## Features 6 | 7 | TODO 8 | 9 | ## Requirements 10 | 11 | TODO 12 | 13 | ## Extension Settings 14 | 15 | TODO 16 | 17 | ## Known Issues 18 | 19 | TODO 20 | 21 | ## Release Notes 22 | 23 | None thus far. 24 | 25 | ## License 26 | 27 | The extension code is distributed under the terms of the [ISC](LICENSE.txt) license; same as Yosys and CXXRTL. 28 | 29 | The artwork is derived from [Codicons](https://github.com/microsoft/vscode-codicons) and is distributed under the terms of the [CC-BY-4.0](LICENSE-MEDIA.txt) license. -------------------------------------------------------------------------------- /build.mjs: -------------------------------------------------------------------------------- 1 | import * as esbuild from 'esbuild'; 2 | 3 | const mode = (process.argv[2] ?? 'build'); 4 | 5 | const commonOptions = { 6 | logLevel: 'info', 7 | bundle: true, 8 | target: 'es2021', 9 | sourcemap: 'linked', 10 | outdir: 'out/', 11 | }; 12 | 13 | const extensionContext = await esbuild.context({ 14 | external: ['vscode'], 15 | format: 'cjs', 16 | platform: 'node', 17 | entryPoints: { 18 | 'extension': './src/extension.ts', 19 | }, 20 | ...commonOptions 21 | }); 22 | 23 | if (mode === 'build') { 24 | await extensionContext.rebuild(); 25 | await extensionContext.dispose(); 26 | } else if (mode === 'watch') { 27 | await extensionContext.watch(); 28 | } else { 29 | console.error(`Usage: ${process.argv0} [build|watch]`); 30 | } 31 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | { 3 | 'name': 'root', 4 | 'files': [ 5 | 'src/**/*.ts' 6 | ], 7 | 'languageOptions': { 8 | 'ecmaVersion': 6, 9 | 'sourceType': 'module', 10 | 'parser': require('@typescript-eslint/parser'), 11 | }, 12 | 'plugins': { 13 | '@stylistic': require('@stylistic/eslint-plugin'), 14 | '@typescript': require('@typescript-eslint/eslint-plugin'), 15 | }, 16 | 'rules': { 17 | 'no-throw-literal': 'error', 18 | 'semi': 'error', 19 | 'no-extra-semi': 'error', 20 | 'eqeqeq': 'error', 21 | 'prefer-const': 'warn', 22 | 'curly': 'warn', 23 | '@typescript/naming-convention': [ 24 | 'warn', 25 | { 26 | 'selector': 'import', 27 | 'format': [ 'camelCase', 'PascalCase' ] 28 | } 29 | ], 30 | '@stylistic/indent': ['warn', 4], 31 | '@stylistic/quotes': ['warn', 'single'], 32 | '@stylistic/brace-style': ['warn', '1tbs'], 33 | '@stylistic/curly-newline': ['warn', {'minElements': 1, 'consistent': true}], 34 | '@stylistic/keyword-spacing': 'warn', 35 | '@stylistic/space-before-blocks': 'warn', 36 | }, 37 | } 38 | ]; 39 | -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | design.cc 2 | design.h 3 | design_sim 4 | spool.bin -------------------------------------------------------------------------------- /example/.vscode/c_cpp_properties.json: -------------------------------------------------------------------------------- 1 | { 2 | "configurations": [ 3 | { 4 | "name": "Linux", 5 | "includePath": [ 6 | "${workspaceFolder}/**", 7 | "${workspaceFolder}/../../yosys/backends/cxxrtl/runtime/" 8 | ], 9 | "defines": [], 10 | "intelliSenseMode": "linux-clang-x64" 11 | } 12 | ], 13 | "version": 4 14 | } 15 | -------------------------------------------------------------------------------- /example/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "(gdb) Attach", 6 | "type": "cppdbg", 7 | "request": "attach", 8 | "program": "${workspaceFolder}/design_sim", 9 | "MIMode": "gdb", 10 | "setupCommands": [ 11 | { 12 | "description": "Enable pretty-printing for gdb", 13 | "text": "-enable-pretty-printing", 14 | "ignoreFailures": true 15 | }, 16 | { 17 | "description": "Set Disassembly Flavor to Intel", 18 | "text": "-gdb-set disassembly-flavor intel", 19 | "ignoreFailures": true 20 | } 21 | ] 22 | } 23 | ] 24 | } -------------------------------------------------------------------------------- /example/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "rtlDebugger.command": [ 3 | "${workspaceFolder}/design_sim" 4 | ], 5 | "rtlDebugger.watchList": [ 6 | { 7 | "id": "top data" 8 | } 9 | ], 10 | "rtlDebugger.variableOptions": { 11 | "data": { 12 | "radix": 16 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /example/Makefile: -------------------------------------------------------------------------------- 1 | YOSYS ?= ../../yosys/yosys 2 | 3 | CXXFLAGS = -Wno-array-bounds -Wno-shift-count-overflow 4 | CXXFLAGS += -I$(dir $(YOSYS))/backends/cxxrtl/runtime/ 5 | CXXFLAGS += -g -O0 6 | CXXFLAGS += -DCXXRTL_SERVER_TRACE 7 | 8 | design_sim: design_sim.cc design.cc 9 | clang++ $(CXXFLAGS) -o $@ $(abspath $^) 10 | 11 | design.h: design.cc 12 | design.cc: design.sv 13 | $(YOSYS) -q $(abspath $^) -p hierarchy -p 'write_cxxrtl -header $@' 14 | 15 | clean: 16 | rm -f *.o design.cc design.h design_sim 17 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | You need a custom checkout of Yosys: 2 | 3 | git clone https://github.com/whitequark/yosys -b cxxrtl-agent -------------------------------------------------------------------------------- /example/design.sv: -------------------------------------------------------------------------------- 1 | module counter( 2 | input clk, 3 | output reg [31:0] cnt = 0 4 | ); 5 | 6 | parameter integer LIMIT = 0; 7 | 8 | always @(posedge clk) 9 | if (cnt == LIMIT) 10 | cnt <= 0; 11 | else 12 | cnt <= cnt + 1; 13 | 14 | endmodule 15 | 16 | (* top *) 17 | module top( 18 | input clk, 19 | output [7:0] data, 20 | output [31:0] timer 21 | ); 22 | 23 | reg [7:0] message [14]; 24 | initial begin 25 | message[0] = "h"; 26 | message[1] = "e"; 27 | message[2] = "l"; 28 | message[3] = "l"; 29 | message[4] = "o"; 30 | message[5] = " "; 31 | message[6] = "w"; 32 | message[7] = "o"; 33 | message[8] = "r"; 34 | message[9] = "l"; 35 | message[10] = "d"; 36 | message[11] = "!"; 37 | message[12] = "\n"; 38 | end 39 | 40 | wire [7:0] message_index; 41 | counter #( 42 | .LIMIT(13) 43 | ) counter_message( 44 | .clk(clk), 45 | .cnt(message_index) 46 | ); 47 | 48 | assign data = message[message_index]; 49 | 50 | counter #( 51 | .LIMIT(32'hffffffff) 52 | ) counter_timer( 53 | .clk(clk), 54 | .cnt(timer), 55 | ); 56 | 57 | always @(*) 58 | $display("message_index = %d, timer = %d", message_index, timer); 59 | 60 | always @(posedge clk) 61 | assert (timer < 5); 62 | 63 | always @(posedge clk) 64 | assert (timer < 7); // two asserts at once 65 | 66 | endmodule 67 | -------------------------------------------------------------------------------- /example/design_sim.cc: -------------------------------------------------------------------------------- 1 | #include "design.h" 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | using namespace cxxrtl::time_literals; 8 | 9 | int main(int argc, char **argv) { 10 | if (argc != 1) { 11 | fprintf(stderr, "Usage: %s\n", argv[0]); 12 | return 1; 13 | } 14 | 15 | cxxrtl_design::p_top top; 16 | cxxrtl::agent agent(cxxrtl::spool("spool.bin"), top, "top "); 17 | 18 | std::string uri = agent.start_debugging(cxxrtl::tcp_link()); 19 | fprintf(stderr, "Simulation started on %s\n", uri.c_str()); 20 | 21 | agent.step(); 22 | 23 | size_t cycles = 0; 24 | do { 25 | agent.advance(1_ns); 26 | top.p_clk.set(false); 27 | agent.step(); 28 | 29 | agent.advance(1_ns); 30 | top.p_clk.set(true); 31 | agent.step(); 32 | 33 | if (cycles == 3) 34 | agent.breakpoint(CXXRTL_LOCATION); 35 | } while (cycles++ < 1000); 36 | 37 | return 0; 38 | } 39 | -------------------------------------------------------------------------------- /media/chip-debug.svg: -------------------------------------------------------------------------------- 1 | 2 | 15 | 17 | 36 | 42 | 43 | 50 | 53 | 54 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "publisher": "amaranth-lang", 3 | "name": "rtl-debugger", 4 | "displayName": "RTL Debugger", 5 | "description": "Debugger for HDLs supported by the open-source toolchain: Amaranth, Verilog, VHDL, ...", 6 | "version": "0.0.0", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/amaranth-lang/rtl-debugger" 10 | }, 11 | "categories": [ 12 | "Debuggers" 13 | ], 14 | "engines": { 15 | "vscode": "^1.94.0" 16 | }, 17 | "main": "./out/extension.js", 18 | "contributes": { 19 | "configuration": { 20 | "id": "RTLDebugger", 21 | "type": "object", 22 | "title": "RTL Debugger", 23 | "$comment": "UPSTREAM: Unfortunately there is no way to control the formatting of the extension name within the setting title. See microsoft/vscode#103592", 24 | "properties": { 25 | "rtlDebugger.command": { 26 | "type": "array", 27 | "items": { 28 | "type": "string" 29 | }, 30 | "minItems": 1, 31 | "description": "Specifies the command to run when launching a debug session." 32 | }, 33 | "rtlDebugger.cwd": { 34 | "type": "string", 35 | "default": "${workspaceFolder}", 36 | "description": "Specifies the working directory for the launch command." 37 | }, 38 | "rtlDebugger.env": { 39 | "type": "object", 40 | "patternProperties": { 41 | "": "string" 42 | }, 43 | "default": {}, 44 | "description": "Specifies the environment for the launch command." 45 | }, 46 | "rtlDebugger.port": { 47 | "type": "integer", 48 | "minimum": 1, 49 | "maximum": 65535, 50 | "default": 6618, 51 | "description": "Specifies the port that the CXXRTL agent within the debug session is listening at." 52 | }, 53 | "rtlDebugger.displayStyle": { 54 | "enum": [ 55 | "Verilog", 56 | "VHDL", 57 | "Python" 58 | ], 59 | "default": "Verilog", 60 | "markdownDescription": "Specifies the display format for variables." 61 | }, 62 | "rtlDebugger.variableOptions": { 63 | "type": "object", 64 | "patternProperties": { 65 | "": { 66 | "type": "object", 67 | "properties": { 68 | "radix": "number" 69 | } 70 | } 71 | } 72 | }, 73 | "rtlDebugger.watchList": { 74 | "type": "array", 75 | "items": { 76 | "type": "object", 77 | "properties": { 78 | "id": {"type": "string"}, 79 | "row": {"type": "integer"}, 80 | "bit": {"type": "integer"} 81 | }, 82 | "required": ["id"], 83 | "additionalProperties": false 84 | }, 85 | "description": "Specifies the list of variables being watched separately." 86 | } 87 | } 88 | }, 89 | "commands": [ 90 | { 91 | "command": "rtlDebugger.startSession", 92 | "category": "RTL Debugger", 93 | "title": "Start Session", 94 | "icon": "$(debug-start)" 95 | }, 96 | { 97 | "command": "rtlDebugger.stopSession", 98 | "category": "RTL Debugger", 99 | "title": "Stop Session", 100 | "icon": "$(debug-stop)" 101 | }, 102 | { 103 | "command": "rtlDebugger.runSimulation", 104 | "category": "RTL Debugger", 105 | "title": "Run Simulation" 106 | }, 107 | { 108 | "command": "rtlDebugger.runSimulationUntil", 109 | "category": "RTL Debugger", 110 | "title": "Run Simulation Until..." 111 | }, 112 | { 113 | "command": "rtlDebugger.pauseSimulation", 114 | "category": "RTL Debugger", 115 | "title": "Pause Simulation", 116 | "icon": "$(debug-pause)" 117 | }, 118 | { 119 | "command": "rtlDebugger.continueForward", 120 | "category": "RTL Debugger", 121 | "title": "Continue Forward", 122 | "icon": "$(debug-continue)" 123 | }, 124 | { 125 | "command": "rtlDebugger.stepForward", 126 | "category": "RTL Debugger", 127 | "title": "Step Forward", 128 | "icon": "$(debug-step-over)" 129 | }, 130 | { 131 | "command": "rtlDebugger.stepBackward", 132 | "category": "RTL Debugger", 133 | "title": "Step Backward", 134 | "icon": "$(debug-step-back)" 135 | }, 136 | { 137 | "command": "rtlDebugger.goToTime", 138 | "category": "RTL Debugger", 139 | "title": "Go to Time..." 140 | }, 141 | { 142 | "command": "rtlDebugger.setRadix.2", 143 | "category": "RTL Debugger", 144 | "title": "Use Radix 2" 145 | }, 146 | { 147 | "command": "rtlDebugger.setRadix.8", 148 | "category": "RTL Debugger", 149 | "title": "Use Radix 8" 150 | }, 151 | { 152 | "command": "rtlDebugger.setRadix.10", 153 | "category": "RTL Debugger", 154 | "title": "Use Radix 10" 155 | }, 156 | { 157 | "command": "rtlDebugger.setRadix.16", 158 | "category": "RTL Debugger", 159 | "title": "Use Radix 16" 160 | }, 161 | { 162 | "command": "rtlDebugger.watchVariable", 163 | "category": "RTL Debugger", 164 | "title": "Watch Variable", 165 | "icon": "$(eye-watch)" 166 | }, 167 | { 168 | "command": "rtlDebugger.unWatchVariable", 169 | "category": "RTL Debugger", 170 | "title": "Stop Watching", 171 | "icon": "$(remove)" 172 | } 173 | ], 174 | "viewsContainers": { 175 | "activitybar": [ 176 | { 177 | "id": "rtlDebugger", 178 | "title": "RTL Debugger", 179 | "icon": "media/chip-debug.svg" 180 | } 181 | ] 182 | }, 183 | "views": { 184 | "rtlDebugger": [ 185 | { 186 | "id": "rtlDebugger.sidebar", 187 | "name": "Session" 188 | } 189 | ] 190 | }, 191 | "viewsWelcome": [ 192 | { 193 | "view": "rtlDebugger.sidebar", 194 | "contents": "The debug session is not running.\n[Start Session](command:rtlDebugger.startSession)", 195 | "when": "rtlDebugger.sessionStatus == absent" 196 | }, 197 | { 198 | "view": "rtlDebugger.sidebar", 199 | "contents": "The debug session is initializing...", 200 | "when": "rtlDebugger.sessionStatus == starting" 201 | } 202 | ], 203 | "menus": { 204 | "commandPalette": [ 205 | { 206 | "command": "rtlDebugger.startSession", 207 | "when": "rtlDebugger.sessionStatus == absent" 208 | }, 209 | { 210 | "command": "rtlDebugger.stopSession", 211 | "when": "rtlDebugger.sessionStatus == running" 212 | }, 213 | { 214 | "command": "rtlDebugger.runSimulation", 215 | "when": "rtlDebugger.sessionStatus == running && rtlDebugger.simulationStatus == paused" 216 | }, 217 | { 218 | "command": "rtlDebugger.runSimulationUntil", 219 | "when": "rtlDebugger.sessionStatus == running && rtlDebugger.simulationStatus == paused" 220 | }, 221 | { 222 | "command": "rtlDebugger.pauseSimulation", 223 | "when": "rtlDebugger.sessionStatus == running && rtlDebugger.simulationStatus == running" 224 | }, 225 | { 226 | "command": "rtlDebugger.continueForward", 227 | "when": "rtlDebugger.sessionStatus == running" 228 | }, 229 | { 230 | "command": "rtlDebugger.stepForward", 231 | "when": "rtlDebugger.sessionStatus == running" 232 | }, 233 | { 234 | "command": "rtlDebugger.stepBackward", 235 | "when": "rtlDebugger.sessionStatus == running" 236 | }, 237 | { 238 | "command": "rtlDebugger.goToTime", 239 | "when": "rtlDebugger.sessionStatus == running" 240 | }, 241 | { 242 | "command": "rtlDebugger.setRadix.2", 243 | "when": "rtlDebugger.sessionStatus == running" 244 | }, 245 | { 246 | "command": "rtlDebugger.setRadix.8", 247 | "when": "rtlDebugger.sessionStatus == running" 248 | }, 249 | { 250 | "command": "rtlDebugger.setRadix.10", 251 | "when": "rtlDebugger.sessionStatus == running" 252 | }, 253 | { 254 | "command": "rtlDebugger.setRadix.16", 255 | "when": "rtlDebugger.sessionStatus == running" 256 | }, 257 | { 258 | "command": "rtlDebugger.watchVariable", 259 | "when": "rtlDebugger.sessionStatus == running" 260 | }, 261 | { 262 | "command": "rtlDebugger.unWatchVariable", 263 | "when": "rtlDebugger.sessionStatus == running" 264 | } 265 | ], 266 | "view/title": [ 267 | { 268 | "command": "rtlDebugger.stepBackward", 269 | "when": "view == rtlDebugger.sidebar && rtlDebugger.sessionStatus == running", 270 | "group": "navigation@1" 271 | }, 272 | { 273 | "command": "rtlDebugger.stepForward", 274 | "when": "view == rtlDebugger.sidebar && rtlDebugger.sessionStatus == running", 275 | "group": "navigation@2" 276 | }, 277 | { 278 | "command": "rtlDebugger.continueForward", 279 | "when": "view == rtlDebugger.sidebar && rtlDebugger.sessionStatus == running && rtlDebugger.simulationStatus != running", 280 | "group": "navigation@3" 281 | }, 282 | { 283 | "command": "rtlDebugger.pauseSimulation", 284 | "when": "view == rtlDebugger.sidebar && rtlDebugger.sessionStatus == running && rtlDebugger.simulationStatus == running", 285 | "group": "navigation@4" 286 | }, 287 | { 288 | "command": "rtlDebugger.startSession", 289 | "when": "view == rtlDebugger.sidebar && rtlDebugger.sessionStatus == absent", 290 | "group": "navigation@10" 291 | }, 292 | { 293 | "command": "rtlDebugger.stopSession", 294 | "when": "view == rtlDebugger.sidebar && rtlDebugger.sessionStatus == running", 295 | "group": "navigation@11" 296 | } 297 | ], 298 | "view/item/context": [ 299 | { 300 | "submenu": "rtlDebugger.setRadix", 301 | "when": "view == rtlDebugger.sidebar && viewItem =~ /canSetRadix/" 302 | }, 303 | { 304 | "command": "rtlDebugger.watchVariable", 305 | "when": "view == rtlDebugger.sidebar && viewItem =~ /canWatch/", 306 | "group": "inline" 307 | }, 308 | { 309 | "command": "rtlDebugger.unWatchVariable", 310 | "when": "view == rtlDebugger.sidebar && viewItem =~ /inWatchList/", 311 | "group": "inline" 312 | } 313 | ], 314 | "rtlDebugger.setRadix": [ 315 | { 316 | "command": "rtlDebugger.setRadix.2", 317 | "group": "radix@2" 318 | }, 319 | { 320 | "command": "rtlDebugger.setRadix.8", 321 | "group": "radix@8" 322 | }, 323 | { 324 | "command": "rtlDebugger.setRadix.10", 325 | "group": "radix@10" 326 | }, 327 | { 328 | "command": "rtlDebugger.setRadix.16", 329 | "group": "radix@16" 330 | } 331 | ] 332 | }, 333 | "submenus": [ 334 | { 335 | "id": "rtlDebugger.setRadix", 336 | "label": "Radix" 337 | } 338 | ] 339 | }, 340 | "scripts": { 341 | "lint": "eslint --fix", 342 | "tsc": "tsc --project tsconfig.json", 343 | "tsc:watch": "npm run tsc -- --watch", 344 | "esbuild": "node build.mjs build", 345 | "esbuild:watch": "node build.mjs watch", 346 | "vscode:prepublish": "npm run lint && npm run tsc && npm run esbuild", 347 | "package": "vsce package" 348 | }, 349 | "devDependencies": { 350 | "@types/node": "20.x", 351 | "@types/vscode": "^1.94", 352 | "@typescript-eslint/eslint-plugin": "^8", 353 | "@typescript-eslint/parser": "^8", 354 | "@vscode/vsce": "^3.x", 355 | "esbuild": "^0.24", 356 | "eslint": "^9.12", 357 | "typescript": "5.5.x" 358 | }, 359 | "dependencies": { 360 | "@stylistic/eslint-plugin": "^2.9.0" 361 | } 362 | } 363 | -------------------------------------------------------------------------------- /src/cxxrtl/client.ts: -------------------------------------------------------------------------------- 1 | import * as link from './link'; 2 | import * as proto from './proto'; 3 | 4 | export class CommandError extends Error { 5 | constructor(public readonly errorPacket: proto.Error) { 6 | super(`command returned '${errorPacket.error}': ${errorPacket.message}`); 7 | } 8 | } 9 | 10 | export enum ConnectionState { 11 | Initializing = 'initializing', 12 | Connected = 'connected', 13 | Disconnected = 'disconnected', 14 | } 15 | 16 | // Note that we trust that server returns well-formed JSON. It would take far too much time to 17 | // verify its adherence to the schema here, for little gain. 18 | export class Connection { 19 | private readonly link: link.ILink; 20 | 21 | private _state = ConnectionState.Initializing; 22 | 23 | private _commands: string[] = []; 24 | private _events: string[] = []; 25 | private _itemValuesEncodings: string[] = []; 26 | 27 | private promises: { 28 | resolve: (response: link.Packet) => void; 29 | reject: (error: Error) => void; 30 | }[] = []; 31 | private timestamps: Date[] = []; 32 | 33 | private sendIndex: number = 0; 34 | private recvIndex: number = 0; 35 | 36 | constructor(link_: link.ILink) { 37 | this.link = link_; 38 | this.link.onRecv = this.onLinkRecv.bind(this); 39 | this.link.onDone = this.onLinkDone.bind(this); 40 | this.send(link.Packet.fromObject({ 41 | type: 'greeting', 42 | version: 0, 43 | })); 44 | } 45 | 46 | dispose(): void { 47 | this.link.dispose(); 48 | } 49 | 50 | private traceSend(linkPacket: link.Packet) { 51 | const packet = linkPacket.asObject(); 52 | if (packet.type === 'greeting') { 53 | console.debug('[CXXRTL] C>S', packet); 54 | } else if (packet.type === 'command') { 55 | this.timestamps.push(new Date()); 56 | console.debug(`[CXXRTL] C>S#${this.sendIndex++}`, packet); 57 | } 58 | } 59 | 60 | private traceRecv(linkPacket: link.Packet) { 61 | const packet = linkPacket.asObject(); 62 | if (packet.type === 'greeting') { 63 | console.debug('[CXXRTL] S>C', packet); 64 | } else if (packet.type === 'response') { 65 | const elapsed = new Date().getTime() - this.timestamps.shift()!.getTime(); 66 | console.debug(`[CXXRTL] S>C#${this.recvIndex++}`, packet, `(${elapsed}ms)`); 67 | } else if (packet.type === 'error') { 68 | this.timestamps.shift(); 69 | console.error(`[CXXRTL] S>C#${this.recvIndex++}`, packet); 70 | } else if (packet.type === 'event') { 71 | console.debug('[CXXRTL] S>C', packet); 72 | } 73 | } 74 | 75 | private async send(linkPacket: link.Packet): Promise { 76 | this.traceSend(linkPacket); 77 | if (this._state === ConnectionState.Disconnected) { 78 | throw new Error('unable to send packet after link is shutdown'); 79 | } else { 80 | this.link.send(linkPacket); 81 | } 82 | } 83 | 84 | private async onLinkRecv(linkPacket: link.Packet): Promise { 85 | this.traceRecv(linkPacket); 86 | const packet = linkPacket.asObject(); 87 | if (this._state === ConnectionState.Initializing && packet.type === 'greeting') { 88 | if (packet.version === 0) { 89 | this._commands = packet.commands; 90 | this._events = packet.events; 91 | this._itemValuesEncodings = packet.features.item_values_encoding; 92 | this._state = ConnectionState.Connected; 93 | await this.onConnected(packet); 94 | } else { 95 | this.rejectPromises(new Error(`unexpected CXXRTL protocol version ${packet.version}`)); 96 | } 97 | } else if (this._state === ConnectionState.Connected && 98 | (packet.type === 'response' || packet.type === 'error')) { 99 | const nextPromise = this.promises.shift(); 100 | if (nextPromise !== undefined) { 101 | if (packet.type === 'response') { 102 | nextPromise.resolve(link.Packet.fromObject(packet)); 103 | } else { 104 | nextPromise.reject(new CommandError(packet)); 105 | } 106 | } else { 107 | this.rejectPromises(new Error(`unexpected '${packet.type}' reply with no commands queued`)); 108 | } 109 | } else if (this._state === ConnectionState.Connected && packet.type === 'event') { 110 | await this.onEvent(link.Packet.fromObject(packet)); 111 | } else { 112 | this.rejectPromises(new Error(`unexpected ${packet.type} packet received for ${this._state} connection`)); 113 | } 114 | } 115 | 116 | private async onLinkDone(): Promise { 117 | this.rejectPromises(new Error('commands will not receive a reply after link is done')); 118 | this._state = ConnectionState.Disconnected; 119 | await this.onDisconnected(); 120 | } 121 | 122 | private rejectPromises(error: Error): void { 123 | for (const promise of this.promises.splice(0, this.promises.length)) { 124 | promise.reject(error); 125 | } 126 | } 127 | 128 | async exchange(command: link.Packet): Promise> { 129 | await this.send(command); 130 | return new Promise((resolve, reject) => { 131 | this.promises.push({ resolve, reject }); 132 | }); 133 | } 134 | 135 | async onConnected(greetingPacket: proto.ServerGreeting): Promise {} 136 | 137 | async onDisconnected(): Promise {} 138 | 139 | async onEvent(_event: link.Packet): Promise {} 140 | 141 | get state(): ConnectionState { 142 | return this._state; 143 | } 144 | 145 | get commands(): string[] { 146 | return this._commands.slice(); 147 | } 148 | 149 | get events(): string[] { 150 | return this._events.slice(); 151 | } 152 | 153 | get itemValuesEncodings(): string[] { 154 | return this._itemValuesEncodings.slice(); 155 | } 156 | 157 | private async command(command: proto.AnyCommand): Promise { 158 | const response = await this.exchange(link.Packet.fromObject(command)); 159 | return response.cast().asObject(); 160 | } 161 | 162 | async listScopes(command: proto.CommandListScopes): Promise { 163 | return this.command(command); 164 | } 165 | 166 | async listItems(command: proto.CommandListItems): Promise { 167 | return this.command(command); 168 | } 169 | 170 | async referenceItems(command: proto.CommandReferenceItems): Promise { 171 | return this.command(command); 172 | } 173 | 174 | async queryInterval(command: proto.CommandQueryInterval): Promise { 175 | return this.command(command); 176 | } 177 | 178 | async getSimulationStatus(command: proto.CommandGetSimulationStatus): Promise { 179 | return this.command(command); 180 | } 181 | 182 | async runSimulation(command: proto.CommandRunSimulation): Promise { 183 | return this.command(command); 184 | } 185 | 186 | async pauseSimulation(command: proto.CommandPauseSimulation): Promise { 187 | return this.command(command); 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /src/cxxrtl/link.ts: -------------------------------------------------------------------------------- 1 | import * as stream from 'node:stream'; 2 | 3 | import * as proto from './proto'; 4 | 5 | // Lazily serialize/deserialize packets in case they only need to be passed along. 6 | export class Packet { 7 | private constructor( 8 | private serialized: string | undefined, 9 | private deserialized: T | undefined, 10 | ) { } 11 | 12 | static fromString(serialized: string) { 13 | return new Packet(serialized, undefined); 14 | } 15 | 16 | static fromObject(deserialized: T) { 17 | return new Packet(undefined, deserialized); 18 | } 19 | 20 | asString(): string { 21 | if (this.serialized === undefined) { 22 | this.serialized = JSON.stringify(this.deserialized!); 23 | } 24 | return this.serialized; 25 | } 26 | 27 | asObject(): T { 28 | if (this.deserialized === undefined) { 29 | this.deserialized = JSON.parse(this.serialized!); 30 | } 31 | return this.deserialized; 32 | } 33 | 34 | cast(): Packet { 35 | return >((this)); 36 | } 37 | 38 | // Make sure we don't unintentionally negate the performance advantages of this wrapper. 39 | toJSON(): never { 40 | throw new Error('call Packet.asObject() instead of serializing with JSON.stringify()'); 41 | } 42 | } 43 | 44 | export interface ILink { 45 | dispose(): void; 46 | 47 | onRecv: (packet: Packet) => Promise; 48 | onDone: () => Promise; 49 | 50 | send(packet: Packet): Promise; 51 | } 52 | 53 | export class MockLink implements ILink { 54 | constructor( 55 | private conversation: [proto.ClientPacket, proto.ServerPacket | proto.ServerPacket[]][] 56 | ) {} 57 | 58 | dispose(): void { 59 | if (this.conversation.length !== 0) { 60 | throw new Error('disposed of before end of conversation'); 61 | } 62 | } 63 | 64 | async onRecv(_serverPacket: Packet): Promise {} 65 | 66 | async onDone(): Promise {} 67 | 68 | async send(clientPacket: Packet): Promise { 69 | if (this.conversation.length === 0) { 70 | throw new Error('premature end of conversation'); 71 | } 72 | 73 | const [[expectedClient, expectedServer], ...restOfConversation] = this.conversation; 74 | 75 | if (clientPacket.asString() === JSON.stringify(expectedClient)) { 76 | if (expectedServer instanceof Array) { 77 | for (const serverPacket of expectedServer) { 78 | await this.onRecv(Packet.fromObject(serverPacket)); 79 | } 80 | } else { 81 | await this.onRecv(Packet.fromObject(expectedServer)); 82 | } 83 | } else { 84 | console.error('unexpected client packet', clientPacket, '; expected:', expectedClient); 85 | throw new Error('unexpected client packet'); 86 | } 87 | 88 | if (restOfConversation.length === 0) { 89 | await this.onDone(); 90 | } 91 | 92 | this.conversation = restOfConversation; 93 | } 94 | } 95 | 96 | export class NodeStreamLink implements ILink { 97 | private recvBuffer: string[] = []; 98 | 99 | constructor(private readonly stream: stream.Duplex) { 100 | stream.on('data', this.onStreamData.bind(this)); 101 | stream.on('end', this.onStreamEnd.bind(this)); 102 | stream.setEncoding('utf-8'); 103 | } 104 | 105 | private async onStreamData(chunk: string): Promise { 106 | // First, split off complete packets and buffer the rest. This shouldn't ever throw errors; 107 | // if it did, the reader could get desynchronized from the protocol. 108 | const packetTexts: string[] = []; 109 | const [first, ...rest] = chunk.split('\0'); 110 | this.recvBuffer.push(first); 111 | if (rest.length > 0) { 112 | packetTexts.push(this.recvBuffer.join('')); 113 | rest.forEach((packetText, index) => { 114 | if (index < rest.length - 1) { 115 | packetTexts.push(packetText); 116 | } 117 | }); 118 | this.recvBuffer.splice(0, this.recvBuffer.length, rest.at(-1)!); 119 | } 120 | 121 | // Second, convert the packet text to JSON. This can throw errors e.g. if there is foreign 122 | // data injected between server replies, or the server is malfunctioning. In that case, 123 | // stop processing input. 124 | const packets: Packet[] = []; 125 | for (const packetText of packetTexts) { 126 | packets.push(Packet.fromString(packetText)); 127 | } 128 | 129 | // Finally, run the handler for each of the packets. If the handler blocks, don't wait for 130 | // its completion, but run the next handler anyway; this is because a handler can send 131 | // another client packet, causing `onStreamData` to be re-entered, anyway. 132 | for (const packet of packets) { 133 | const success = (async (packet) => { 134 | try { 135 | await this.onRecv(packet); 136 | return true; 137 | } catch (error) { 138 | console.error('uncaught error in onRecv', error); 139 | this.stream.pause(); 140 | return false; 141 | } 142 | })(packet); 143 | if (!success) { 144 | break; 145 | } 146 | } 147 | } 148 | 149 | private async onStreamEnd(): Promise { 150 | try { 151 | await this.onDone(); 152 | } catch (error) { 153 | console.error('uncaught error in onDone', error); 154 | } 155 | } 156 | 157 | dispose(): void { 158 | this.stream.destroy(); 159 | } 160 | 161 | async onRecv(_serverPacket: Packet): Promise {} 162 | 163 | async onDone(): Promise {} 164 | 165 | async send(clientPacket: Packet): Promise { 166 | this.stream.write(clientPacket.asString()); 167 | this.stream.write('\0'); 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /src/cxxrtl/proto.ts: -------------------------------------------------------------------------------- 1 | // Refer to https://gist.github.com/whitequark/59520e2de0947da8747061bc2ea91639 for a general 2 | // description of the CXXRTL protocol. 3 | 4 | // # Message types 5 | 6 | // ## Message type: Greeting 7 | 8 | export type ClientGreeting = { 9 | type: 'greeting'; 10 | version: 0; 11 | }; 12 | 13 | export type ServerGreeting = { 14 | type: 'greeting'; 15 | version: 0; 16 | commands: string[]; 17 | events: string[]; 18 | features: { 19 | item_values_encoding: string[]; 20 | }; 21 | }; 22 | 23 | // ## Message type: Command 24 | 25 | export type Command = { 26 | type: 'command'; 27 | command: string; 28 | [argument: string]: any; 29 | }; 30 | 31 | // ## Message type: Response 32 | 33 | export type Response = { 34 | type: 'command'; 35 | command: string; 36 | [argument: string]: any; 37 | }; 38 | 39 | export type Error = { 40 | type: 'error'; 41 | error: string; 42 | [argument: string]: any; 43 | message: string; 44 | }; 45 | 46 | export type Event = { 47 | type: 'event'; 48 | event: string; 49 | [argument: string]: any; 50 | }; 51 | 52 | // # Commands 53 | 54 | // ## Attributes 55 | 56 | export type AttributeUnsignedInt = { 57 | type: 'unsigned_int'; 58 | value: string; 59 | }; 60 | 61 | export type AttributeSignedInt = { 62 | type: 'signed_int'; 63 | value: string; 64 | }; 65 | 66 | export type AttributeString = { 67 | type: 'string'; 68 | value: string; 69 | }; 70 | 71 | export type AttributeDouble = { 72 | type: 'double'; 73 | value: number; 74 | }; 75 | 76 | export type Attribute = 77 | | AttributeUnsignedInt 78 | | AttributeSignedInt 79 | | AttributeString 80 | | AttributeDouble; 81 | 82 | export type AttributeMap = { 83 | [name: string]: AttributeMap; 84 | }; 85 | 86 | // ## Command: List Scopes 87 | 88 | export type ScopeDescriptionModule = { 89 | type: 'module'; 90 | definition: { 91 | src: null | string; 92 | name: null | string; 93 | attributes: AttributeMap; 94 | }; 95 | instantiation: { 96 | src: null | string; 97 | attributes: AttributeMap; 98 | } 99 | }; 100 | 101 | export type ScopeDescription = 102 | | ScopeDescriptionModule; 103 | 104 | export type ScopeDescriptionMap = { 105 | [identifier: string]: ScopeDescription 106 | }; 107 | 108 | export type CommandListScopes = { 109 | type: 'command'; 110 | command: 'list_scopes'; 111 | scope: null | string; 112 | }; 113 | 114 | export type ResponseListScopes = { 115 | type: 'response'; 116 | command: 'list_scopes'; 117 | scopes: ScopeDescriptionMap; 118 | }; 119 | 120 | // ## Command: List Items 121 | 122 | export type ItemDescriptionNode = { 123 | src: null | string; 124 | type: 'node'; 125 | lsb_at: number; 126 | width: number; 127 | input: boolean; 128 | output: boolean; 129 | settable: boolean; 130 | attributes: AttributeMap; 131 | }; 132 | 133 | export type ItemDescriptionMemory = { 134 | src: null | string; 135 | type: 'memory'; 136 | lsb_at: number; 137 | width: number; 138 | zero_at: number; 139 | depth: number; 140 | settable: boolean; 141 | attributes: AttributeMap; 142 | }; 143 | 144 | export type ItemDescription = 145 | | ItemDescriptionNode 146 | | ItemDescriptionMemory; 147 | 148 | export type ItemDescriptionMap = { 149 | [identifier: string]: ItemDescription 150 | }; 151 | 152 | export type CommandListItems = { 153 | type: 'command'; 154 | command: 'list_items'; 155 | scope: null | string; 156 | }; 157 | 158 | export type ResponseListItems = { 159 | type: 'response'; 160 | command: 'list_items'; 161 | items: ItemDescriptionMap; 162 | }; 163 | 164 | // ## Command: Reference Items 165 | 166 | export type ItemDesignation = 167 | | [string] 168 | | [string, number, number]; 169 | 170 | export type CommandReferenceItems = { 171 | type: 'command'; 172 | command: 'reference_items'; 173 | reference: string; 174 | items: null | ItemDesignation[]; 175 | }; 176 | 177 | export type ResponseReferenceItems = { 178 | type: 'response'; 179 | command: 'reference_items'; 180 | }; 181 | 182 | // ## Command: Query Interval 183 | 184 | export type TimePoint = string; 185 | 186 | export type DiagnosticType = 187 | | 'break' 188 | | 'print' 189 | | 'assert' 190 | | 'assume'; 191 | 192 | export type Diagnostic = { 193 | type: DiagnosticType; 194 | text: string; 195 | src: null | string; 196 | }; 197 | 198 | export type Sample = { 199 | time: TimePoint; 200 | item_values?: string; 201 | diagnostics?: Diagnostic[]; 202 | }; 203 | 204 | export type CommandQueryInterval = { 205 | type: 'command'; 206 | command: 'query_interval'; 207 | interval: [TimePoint, TimePoint]; 208 | collapse: boolean; 209 | items: string | null; 210 | item_values_encoding: string | null; 211 | diagnostics: boolean; 212 | }; 213 | 214 | export type ResponseQueryInterval = { 215 | type: 'response'; 216 | command: 'query_interval'; 217 | samples: Sample[]; 218 | }; 219 | 220 | // ## Command: Get Simulation Status 221 | 222 | export type CommandGetSimulationStatus = { 223 | type: 'command'; 224 | command: 'get_simulation_status'; 225 | }; 226 | 227 | export type ResponseGetSimulationStatus = { 228 | type: 'response'; 229 | command: 'get_simulation_status'; 230 | status: 'paused'; 231 | latest_time: TimePoint; 232 | next_sample_time: TimePoint; 233 | } | { 234 | type: 'response'; 235 | command: 'get_simulation_status'; 236 | status: 'running' | 'finished'; 237 | latest_time: TimePoint; 238 | }; 239 | 240 | // ## Command: Run Simulation 241 | 242 | export type CommandRunSimulation = { 243 | type: 'command'; 244 | command: 'run_simulation'; 245 | until_time: null | TimePoint; 246 | until_diagnostics: DiagnosticType[]; 247 | sample_item_values: boolean; 248 | }; 249 | 250 | export type ResponseRunSimulation = { 251 | type: 'response'; 252 | command: 'run_simulation'; 253 | }; 254 | 255 | // ## Command: Pause Simulation 256 | 257 | export type CommandPauseSimulation = { 258 | type: 'command'; 259 | command: 'pause_simulation'; 260 | }; 261 | 262 | export type ResponsePauseSimulation = { 263 | type: 'response'; 264 | command: 'pause_simulation'; 265 | time: TimePoint; 266 | }; 267 | 268 | // # Events 269 | 270 | // ## Event: Simulation Paused 271 | 272 | export type PauseCause = 273 | | 'until_time' 274 | | 'until_diagnostics' 275 | ; 276 | 277 | export type EventSimulationPaused = { 278 | type: 'event'; 279 | event: 'simulation_paused'; 280 | time: TimePoint; 281 | cause: PauseCause; 282 | }; 283 | 284 | // ## Event: Simulation Finished 285 | 286 | export type EventSimulationFinished = { 287 | type: 'event'; 288 | event: 'simulation_finished'; 289 | time: TimePoint; 290 | }; 291 | 292 | // # Afterword 293 | 294 | export type AnyCommand = 295 | | CommandListScopes 296 | | CommandListItems 297 | | CommandReferenceItems 298 | | CommandQueryInterval 299 | | CommandGetSimulationStatus 300 | | CommandRunSimulation 301 | | CommandPauseSimulation 302 | ; 303 | 304 | export type AnyResponse = 305 | | ResponseListScopes 306 | | ResponseListItems 307 | | ResponseReferenceItems 308 | | ResponseQueryInterval 309 | | ResponseGetSimulationStatus 310 | | ResponseRunSimulation 311 | | ResponsePauseSimulation 312 | ; 313 | 314 | export type AnyEvent = 315 | | EventSimulationPaused 316 | | EventSimulationFinished 317 | ; 318 | 319 | export type ClientPacket = 320 | | ClientGreeting 321 | | AnyCommand; 322 | 323 | export type ServerPacket = 324 | | ServerGreeting 325 | | AnyResponse 326 | | AnyEvent 327 | | Error; 328 | -------------------------------------------------------------------------------- /src/debug/observer.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | 3 | import { UnboundReference, Designation, Reference } from '../model/sample'; 4 | import { Session } from './session'; 5 | 6 | class Observable { 7 | private callbacks: ((value: T) => void)[] = []; 8 | private value: T | undefined; 9 | 10 | constructor( 11 | readonly designation: Designation 12 | ) {} 13 | 14 | register(callback: (value: T) => void): { dispose(): void } { 15 | this.callbacks.push(callback); 16 | return { dispose: () => this.callbacks.splice(this.callbacks.indexOf(callback), 1) }; 17 | } 18 | 19 | query(): T | undefined { 20 | return this.value; 21 | } 22 | 23 | update(newValue: T) { 24 | if (this.value !== newValue) { 25 | this.value = newValue; 26 | this.callbacks = this.callbacks.filter((callback) => { 27 | const retain = callback(newValue); 28 | return retain === undefined || retain; 29 | }); 30 | } 31 | } 32 | } 33 | 34 | export class Observer { 35 | private observables: Map> = new Map(); 36 | private reference: Reference | undefined; 37 | 38 | private samplePromise: Promise | undefined; 39 | private refreshNeeded: boolean = false; 40 | 41 | private subscription: vscode.Disposable; 42 | 43 | constructor( 44 | private session: Session, 45 | private referenceName: string, 46 | ) { 47 | this.subscription = this.session.onDidChangeTimeCursor((_time) => 48 | this.invalidate()); 49 | } 50 | 51 | dispose() { 52 | this.observables.clear(); 53 | this.subscription.dispose(); 54 | } 55 | 56 | query(designation: Designation): T | undefined { 57 | const observable = this.observables.get(designation.canonicalKey); 58 | return observable?.query(); 59 | } 60 | 61 | observe(designation: Designation, callback: (value: T) => any): { dispose(): void } { 62 | let observable = this.observables.get(designation.canonicalKey); 63 | if (observable === undefined) { 64 | observable = new Observable(designation); 65 | this.observables.set(designation.canonicalKey, observable); 66 | this.reference = undefined; // invalidate reference 67 | this.invalidate(); 68 | } 69 | return observable.register(callback); 70 | } 71 | 72 | invalidate(): void { 73 | this.refreshNeeded = true; 74 | if (this.samplePromise === undefined) { 75 | this.samplePromise = this.refresh(); 76 | } 77 | } 78 | 79 | private async refresh(): Promise { 80 | while (this.refreshNeeded) { 81 | this.refreshNeeded = false; 82 | if (this.reference === undefined) { 83 | const unboundReference = new UnboundReference(); 84 | for (const observable of this.observables.values()) { 85 | unboundReference.add(observable.designation); 86 | } 87 | this.reference = this.session.bindReference(this.referenceName, unboundReference); 88 | } 89 | const reference = this.reference; // could get invalidated during `await` below 90 | const sample = await this.session.queryAtCursor({ reference }); 91 | for (const [designation, handle] of reference.allHandles()) { 92 | const observable = this.observables.get(designation.canonicalKey)!; 93 | observable.update(sample.extract(handle)); 94 | } 95 | // ... but we could've got another invalidate() call while awaiting, so check again. 96 | } 97 | this.samplePromise = undefined; 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/debug/options.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | 3 | export interface IVariableOptions { 4 | radix?: 2 | 8 | 10 | 16 5 | } 6 | 7 | export interface IVariableOptionStore { 8 | get(id: string): IVariableOptions; 9 | set(id: string, options: IVariableOptions): void; 10 | update(id: string, options: IVariableOptions): void; 11 | } 12 | 13 | export const globalVariableOptions: IVariableOptionStore = { 14 | get(id: string): IVariableOptions { 15 | const optionStore: {[id: string]: IVariableOptions} = 16 | vscode.workspace.getConfiguration('rtlDebugger').get('variableOptions') || {}; 17 | return optionStore[id] || {}; 18 | }, 19 | 20 | set(id: string, options: IVariableOptions): void { 21 | const optionStore: {[id: string]: IVariableOptions} = 22 | vscode.workspace.getConfiguration('rtlDebugger').get('variableOptions') || {}; 23 | optionStore[id] = options; 24 | vscode.workspace.getConfiguration('rtlDebugger').update('variableOptions', optionStore); 25 | }, 26 | 27 | update(id: string, addedOptions: IVariableOptions): void { 28 | const options = Object.fromEntries(Object.entries(this.get(id))); 29 | Object.assign(options, addedOptions); 30 | this.set(id, options); 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /src/debug/session.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | 3 | import * as proto from '../cxxrtl/proto'; 4 | import * as link from '../cxxrtl/link'; 5 | import { Connection } from '../cxxrtl/client'; 6 | import { TimeInterval, TimePoint } from '../model/time'; 7 | import { Diagnostic, DiagnosticType, Reference, Sample, UnboundReference } from '../model/sample'; 8 | import { Variable } from '../model/variable'; 9 | import { Scope } from '../model/scope'; 10 | import { Location } from '../model/source'; 11 | 12 | function lazy(thunk: () => Thenable): Thenable { 13 | return { then: (onfulfilled, onrejected) => thunk().then(onfulfilled, onrejected) }; 14 | } 15 | 16 | function matchLocation(location: Location, filename: string, position: vscode.Position) { 17 | if (location.file !== filename) { 18 | return false; 19 | } 20 | if (location.startLine !== position.line) { 21 | return false; 22 | } 23 | if (location.startColumn !== undefined && location.startColumn !== position.character) { 24 | return false; 25 | } 26 | return true; 27 | } 28 | 29 | export interface ISimulationStatus { 30 | status: 'running' | 'paused' | 'finished'; 31 | latestTime: TimePoint; 32 | nextSampleTime?: TimePoint; 33 | } 34 | 35 | export enum SimulationPauseReason { 36 | TimeReached, 37 | DiagnosticsReached, 38 | } 39 | 40 | export class Session { 41 | private connection: Connection; 42 | 43 | private secondaryLinks: link.ILink[] = []; 44 | private greetingPacketPromise: Promise; 45 | 46 | constructor(link: link.ILink) { 47 | this.connection = new Connection(link); 48 | this.greetingPacketPromise = new Promise((resolve, _reject) => { 49 | this.connection.onConnected = async (greetingPacket) => resolve(greetingPacket); 50 | }); 51 | this.connection.onDisconnected = async () => { 52 | for (const secondaryLink of this.secondaryLinks) { 53 | secondaryLink.onDone(); 54 | } 55 | }; 56 | this.connection.onEvent = async (linkEvent) => { 57 | for (const secondaryLink of this.secondaryLinks) { 58 | secondaryLink.onRecv(linkEvent); 59 | } 60 | const event = linkEvent.asObject(); 61 | if (event.event === 'simulation_paused') { 62 | await this.handleSimulationPausedEvent(event.cause); 63 | } else if (event.event === 'simulation_finished') { 64 | await this.handleSimulationFinishedEvent(); 65 | } 66 | }; 67 | this.querySimulationStatus(); // populate nextSampleTime 68 | } 69 | 70 | dispose() { 71 | this.connection.dispose(); 72 | } 73 | 74 | createSecondaryLink(): link.ILink { 75 | const secondaryLink: link.ILink = { 76 | dispose: () => { 77 | this.secondaryLinks.splice(this.secondaryLinks.indexOf(secondaryLink)); 78 | }, 79 | 80 | send: async (linkCommandPacket) => { 81 | const packet = linkCommandPacket.asObject(); 82 | if (packet.type === 'greeting') { 83 | const serverGreetingPacket = await this.greetingPacketPromise; 84 | if (packet.version === serverGreetingPacket.version) { 85 | await secondaryLink.onRecv(link.Packet.fromObject(serverGreetingPacket)); 86 | } else { 87 | throw new Error( 88 | `Secondary link requested greeting version ${packet.version}, ` + 89 | `but server greeting version is ${serverGreetingPacket.version}` 90 | ); 91 | } 92 | } else { 93 | const linkResponsePacket = await this.connection.exchange( 94 | linkCommandPacket.cast()); 95 | await secondaryLink.onRecv(linkResponsePacket); 96 | } 97 | }, 98 | 99 | onRecv: async (serverPacket) => {}, 100 | onDone: async () => {}, 101 | }; 102 | return secondaryLink; 103 | } 104 | 105 | // ======================================== Inspecting the design 106 | 107 | private itemCache: Map = new Map(); 108 | private scopeCache: Map = new Map(); 109 | private rootScopeDesc: proto.ScopeDescription | undefined; 110 | 111 | private async listItemsInScope(scopeIdentifier: string): Promise { 112 | let itemDescriptionMap = this.itemCache.get(scopeIdentifier); 113 | if (itemDescriptionMap === undefined) { 114 | const response = await this.connection.listItems({ 115 | type: 'command', 116 | command: 'list_items', 117 | scope: scopeIdentifier, 118 | }); 119 | itemDescriptionMap = response.items; 120 | this.itemCache.set(scopeIdentifier, itemDescriptionMap); 121 | } 122 | return itemDescriptionMap; 123 | } 124 | 125 | private async listScopesInScope(scopeIdentifier: string): Promise { 126 | let scopeDescriptionMap = this.scopeCache.get(scopeIdentifier); 127 | if (scopeDescriptionMap === undefined) { 128 | const response = await this.connection.listScopes({ 129 | type: 'command', 130 | command: 'list_scopes', 131 | scope: scopeIdentifier, 132 | }); 133 | const filteredScopes = Object.keys(response.scopes).filter((scopeName) => { 134 | if (scopeIdentifier === '') { 135 | return scopeName.length > 0 && scopeName.indexOf(' ') === -1; 136 | } else { 137 | return (scopeName.startsWith(scopeIdentifier + ' ') && 138 | scopeName.indexOf(' ', scopeIdentifier.length + 1) === -1); 139 | } 140 | }); 141 | scopeDescriptionMap = Object.fromEntries(filteredScopes.map((scopeName) => 142 | [scopeName, response.scopes[scopeName]])); 143 | this.scopeCache.set(scopeIdentifier, scopeDescriptionMap); 144 | } 145 | return scopeDescriptionMap; 146 | } 147 | 148 | async getVariablesIn(scopeIdentifier: string): Promise { 149 | const items = await this.listItemsInScope(scopeIdentifier); 150 | return Object.entries(items).map(([itemName, itemDesc]) => 151 | Variable.fromCXXRTL(itemName, itemDesc)); 152 | } 153 | 154 | async getScopesIn(scopeIdentifier: string): Promise { 155 | const scopes = await this.listScopesInScope(scopeIdentifier); 156 | return Object.entries(scopes).map(([scopeName, scopeDesc]) => 157 | Scope.fromCXXRTL( 158 | scopeName, 159 | scopeDesc, 160 | lazy(() => this.getScopesIn(scopeName)), 161 | lazy(() => this.getVariablesIn(scopeName)), 162 | )); 163 | } 164 | 165 | async getRootScope(): Promise { 166 | const scopeName = ''; 167 | if (this.rootScopeDesc === undefined) { 168 | const response = await this.connection.listScopes({ 169 | type: 'command', 170 | command: 'list_scopes', 171 | scope: scopeName, 172 | }); 173 | this.rootScopeDesc = response.scopes[scopeName]; 174 | if (this.rootScopeDesc === undefined) { 175 | // This can happen if the root scope has never been defined anywhere, i.e. if it 176 | // is synthesized for the simulation, e.g. by passing `"top "` as the last argument 177 | // to the CXXRTL agent constructor. 178 | this.rootScopeDesc = { 179 | type: 'module', 180 | definition: { 181 | src: null, 182 | name: null, 183 | attributes: {} 184 | }, 185 | instantiation: { 186 | src: null, 187 | attributes: {} 188 | }, 189 | }; 190 | } 191 | } 192 | return Scope.fromCXXRTL( 193 | scopeName, 194 | this.rootScopeDesc, 195 | lazy(() => this.getScopesIn(scopeName)), 196 | lazy(() => this.getVariablesIn(scopeName)) 197 | ); 198 | } 199 | 200 | async getVariable(variableIdentifier: string): Promise { 201 | const identifierParts = variableIdentifier.split(' '); 202 | const scopeIdentifier = identifierParts.slice(0, identifierParts.length - 1).join(' '); 203 | const items = await this.listItemsInScope(scopeIdentifier); 204 | if (variableIdentifier in items) { 205 | return Variable.fromCXXRTL(variableIdentifier, items[variableIdentifier]); 206 | } else { 207 | return null; 208 | } 209 | } 210 | 211 | async getVariablesForLocation(filename: string, position: vscode.Position): Promise { 212 | const variables: Variable[] = []; 213 | const extractVariablesForLocationFromScope = async (scope: string) => { 214 | const items = await this.listItemsInScope(scope); 215 | for (const [itemName, itemDesc] of Object.entries(items)) { 216 | const itemLocation = Location.fromCXXRTL(itemDesc.src); 217 | if (itemLocation !== null && matchLocation(itemLocation, filename, position)) { 218 | variables.push(Variable.fromCXXRTL(itemName, itemDesc)); 219 | } 220 | } 221 | const subScopes = await this.listScopesInScope(scope); 222 | for (const subScopeName of Object.keys(subScopes)) { 223 | await extractVariablesForLocationFromScope(subScopeName); 224 | } 225 | return null; 226 | }; 227 | await extractVariablesForLocationFromScope(''); 228 | return variables; 229 | } 230 | 231 | // ======================================== Querying the database 232 | 233 | private referenceEpochs: Map = new Map(); 234 | 235 | private advanceReferenceEpoch(name: string): number { 236 | const epoch = (this.referenceEpochs.get(name) || 0) + 1; 237 | this.referenceEpochs.set(name, epoch); 238 | return epoch; 239 | } 240 | 241 | private checkReferenceEpoch(name: string, requestedEpoch: number) { 242 | const currentEpoch = this.referenceEpochs.get(name); 243 | if (currentEpoch === undefined) { 244 | throw new ReferenceError( 245 | `Querying dangling reference ${name}#${requestedEpoch}`); 246 | } else if (currentEpoch !== requestedEpoch) { 247 | throw new ReferenceError( 248 | `Querying out-of-date reference ${name}#${requestedEpoch}; ` + 249 | `the current binding is ${name}#${currentEpoch}`); 250 | } 251 | } 252 | 253 | bindReference(name: string, reference: UnboundReference): Reference { 254 | const epoch = this.advanceReferenceEpoch(name); 255 | // Note that we do not wait for the command to complete. Although it is possible for 256 | // the command to fail, this would only happen if one of the designations is invalid, 257 | // which should never happen absent bugs. We still report the error in that case. 258 | this.connection.referenceItems({ 259 | type: 'command', 260 | command: 'reference_items', 261 | reference: name, 262 | items: reference.cxxrtlItemDesignations() 263 | }).catch((error) => { 264 | console.error('[CXXRTL] Invalid designation while binding reference', `${name}#${epoch}`, error); 265 | }); 266 | return new Reference(name, epoch, reference); 267 | } 268 | 269 | async queryInterval( 270 | interval: TimeInterval, 271 | options: { 272 | reference?: Reference, 273 | diagnostics?: boolean 274 | collapse?: boolean, 275 | } = {} 276 | ): Promise { 277 | const reference = options.reference; 278 | if (reference !== undefined) { 279 | this.checkReferenceEpoch(reference.name, reference.epoch); 280 | } 281 | const response = await this.connection.queryInterval({ 282 | type: 'command', 283 | command: 'query_interval', 284 | interval: interval.toCXXRTL(), 285 | items: reference?.name ?? null, 286 | item_values_encoding: reference ? 'base64(u32)' : null, 287 | diagnostics: options.diagnostics ?? false, 288 | collapse: options.collapse ?? true, 289 | }); 290 | return response.samples.map((cxxrtlSample) => { 291 | let itemValuesArray = null; 292 | let diagnosticsArray = null; 293 | if (cxxrtlSample.item_values !== undefined) { 294 | const itemValuesBuffer = Buffer.from(cxxrtlSample.item_values, 'base64'); 295 | itemValuesArray = new Uint32Array( 296 | itemValuesBuffer.buffer, 297 | itemValuesBuffer.byteOffset, 298 | itemValuesBuffer.length / Uint32Array.BYTES_PER_ELEMENT 299 | ); 300 | } 301 | if (cxxrtlSample.diagnostics !== undefined) { 302 | diagnosticsArray = Array.from(cxxrtlSample.diagnostics, Diagnostic.fromCXXRTL); 303 | } 304 | return new Sample( 305 | TimePoint.fromCXXRTL(cxxrtlSample.time), 306 | reference?.unbound ?? null, 307 | itemValuesArray, 308 | diagnosticsArray, 309 | ); 310 | }); 311 | } 312 | 313 | async queryAtCursor(options: { 314 | reference?: Reference, 315 | diagnostics?: boolean 316 | }): Promise { 317 | const interval = new TimeInterval(this.timeCursor, this.timeCursor); 318 | const [sample] = await this.queryInterval(interval, options); 319 | return sample; 320 | } 321 | 322 | // ======================================== Manipulating the simulation 323 | 324 | private simulationStatusTimeout: NodeJS.Timeout | null = null; 325 | 326 | private _onDidChangeSimulationStatus: vscode.EventEmitter = new vscode.EventEmitter(); 327 | readonly onDidChangeSimulationStatus: vscode.Event = this._onDidChangeSimulationStatus.event; 328 | 329 | private _onDidRunSimulation: vscode.EventEmitter = new vscode.EventEmitter(); 330 | readonly onDidRunSimulation: vscode.Event = this._onDidRunSimulation.event; 331 | 332 | private _onDidPauseSimulation: vscode.EventEmitter = new vscode.EventEmitter(); 333 | readonly onDidPauseSimulation: vscode.Event = this._onDidPauseSimulation.event; 334 | 335 | private _onDidFinishSimulation: vscode.EventEmitter = new vscode.EventEmitter(); 336 | readonly onDidFinishSimulation: vscode.Event = this._onDidFinishSimulation.event; 337 | 338 | private _simulationStatus: ISimulationStatus = { 339 | status: 'paused', 340 | latestTime: TimePoint.ZERO, 341 | }; 342 | 343 | get simulationStatus() { 344 | return this._simulationStatus; 345 | } 346 | 347 | private async querySimulationStatus(): Promise { 348 | if (this.simulationStatusTimeout !== null) { 349 | clearTimeout(this.simulationStatusTimeout); 350 | this.simulationStatusTimeout = null; 351 | } 352 | const response = await this.connection.getSimulationStatus({ 353 | type: 'command', 354 | command: 'get_simulation_status' 355 | }); 356 | const currentSimulationStatus = { 357 | status: response.status, 358 | latestTime: TimePoint.fromCXXRTL(response.latest_time), 359 | nextSampleTime: (response.status === 'paused') 360 | ? TimePoint.fromCXXRTL(response.next_sample_time) 361 | : undefined, 362 | }; 363 | if ((this._simulationStatus.status !== currentSimulationStatus.status) || 364 | !this._simulationStatus.latestTime.equals(currentSimulationStatus.latestTime) || 365 | // This part of the condition only fires once, when the initial status is updated. 366 | (this._simulationStatus.nextSampleTime === undefined && 367 | currentSimulationStatus.nextSampleTime !== undefined)) { 368 | this._simulationStatus = currentSimulationStatus; 369 | this._onDidChangeSimulationStatus.fire(this._simulationStatus); 370 | } 371 | if (currentSimulationStatus.status === 'running') { 372 | this.simulationStatusTimeout = setTimeout(() => this.querySimulationStatus(), 100); 373 | } 374 | } 375 | 376 | async runSimulation(options: { 377 | untilTime?: TimePoint, 378 | untilDiagnostics?: DiagnosticType[] 379 | } = {}): Promise { 380 | await this.connection.runSimulation({ 381 | type: 'command', 382 | command: 'run_simulation', 383 | until_time: options.untilTime?.toCXXRTL() ?? null, 384 | until_diagnostics: options.untilDiagnostics?.map((type) => type) ?? [], 385 | sample_item_values: true 386 | }); 387 | await this.querySimulationStatus(); 388 | this._onDidRunSimulation.fire(); 389 | } 390 | 391 | async pauseSimulation(): Promise { 392 | await this.connection.pauseSimulation({ 393 | type: 'command', 394 | command: 'pause_simulation' 395 | }); 396 | } 397 | 398 | private async handleSimulationPausedEvent(cause: proto.PauseCause): Promise { 399 | if (cause === 'until_time') { 400 | await this.querySimulationStatus(); 401 | this._onDidPauseSimulation.fire(SimulationPauseReason.TimeReached); 402 | } else if (cause === 'until_diagnostics') { 403 | // The `until_diagnostics` cause is a little cursed. For `always @(posedge clk)` 404 | // assertions, the pause time will be two steps ahead, and for `always @(*)` ones, 405 | // it will usually be one step ahead. This is because of several fencepost issues with 406 | // both the storage and the retrieval of diagnostics, which are baked into the CXXRTL 407 | // execution and replay model. (Diagnostics recorded from C++ are fine.) 408 | // 409 | // To handle this, rather than relying on the event time, we scan the database for any 410 | // diagnostics since the last time the simulation state was updated. (A diagnostic that 411 | // caused the simulation to be paused must be somewhere between that and the latest 412 | // sample in the database at the time of pausing.) This avoids the need to simulate 413 | // the entire interval twice, as would happen if querying the interval between the "Run 414 | // Simulation Until Diagnostics" command and the time of pausing. 415 | const latestTimeBeforePause = this.simulationStatus.latestTime; 416 | await this.querySimulationStatus(); 417 | const latestTimeAfterPause = this.simulationStatus.latestTime; 418 | const diagnosticAt = await this.searchIntervalForDiagnostics( 419 | new TimeInterval(latestTimeBeforePause, latestTimeAfterPause)); 420 | if (diagnosticAt === null) { 421 | console.error('[CXXRTL] Paused on diagnostic but no such diagnostics found'); 422 | return; 423 | } 424 | this.timeCursor = diagnosticAt; 425 | this._onDidPauseSimulation.fire(SimulationPauseReason.DiagnosticsReached); 426 | } 427 | } 428 | 429 | private async handleSimulationFinishedEvent(): Promise { 430 | await this.querySimulationStatus(); 431 | this._onDidFinishSimulation.fire(); 432 | } 433 | 434 | get isSimulationRunning(): boolean { 435 | return this._simulationStatus.status === 'running'; 436 | } 437 | 438 | // ======================================== Manipulating the time cursor 439 | 440 | private _onDidChangeTimeCursor: vscode.EventEmitter = new vscode.EventEmitter(); 441 | readonly onDidChangeTimeCursor: vscode.Event = this._onDidChangeTimeCursor.event; 442 | 443 | private _timeCursor: TimePoint = TimePoint.ZERO; 444 | 445 | // We don't know how far forward the next time step will be; the server doesn't provide this 446 | // information. A typical simulation has a consistent time step, or at least a time step of 447 | // a consistent order of magnitude; we guess this time step using binary search and then look 448 | // ahead to find out the actual next cursor position. The advantage of this approach is that 449 | // the simulation can advance its timeline however it wants. 450 | private _forwardTimeStep: bigint = 1n; // in femtos 451 | 452 | get timeCursor() { 453 | return this._timeCursor; 454 | } 455 | 456 | set timeCursor(newTimeCursor: TimePoint) { 457 | if (newTimeCursor.lessThan(TimePoint.ZERO) || 458 | newTimeCursor.greaterThan(this.simulationStatus.latestTime)) { 459 | throw new RangeError('Time cursor out of range'); 460 | } 461 | this._timeCursor = newTimeCursor; 462 | this._onDidChangeTimeCursor.fire(this._timeCursor); 463 | } 464 | 465 | async stepForward(): Promise { 466 | if (this.timeCursor.equals(this.simulationStatus.latestTime)) { 467 | if (this.simulationStatus.status === 'paused') { 468 | const nextSampleTime = this.simulationStatus.nextSampleTime!; 469 | await this.runSimulation({ untilTime: nextSampleTime }); 470 | if (!nextSampleTime.greaterThan(this.simulationStatus.latestTime)) { 471 | this.timeCursor = nextSampleTime; 472 | } 473 | } 474 | } else { 475 | let ordersOfMagnitude = 0; 476 | while (true) { 477 | const followingTimePoint = this.timeCursor.offsetByFemtos(this._forwardTimeStep); 478 | const response = await this.connection.queryInterval({ 479 | type: 'command', 480 | command: 'query_interval', 481 | interval: new TimeInterval(this._timeCursor, followingTimePoint).toCXXRTL(), 482 | collapse: true, 483 | items: null, 484 | item_values_encoding: null, 485 | diagnostics: false 486 | }); 487 | if (response.samples.length > 1) { 488 | this.timeCursor = TimePoint.fromCXXRTL(response.samples.at(1)!.time); 489 | break; 490 | } else if (ordersOfMagnitude < 30 /* femto -> peta */) { 491 | ordersOfMagnitude += 1; 492 | this._forwardTimeStep = this._forwardTimeStep * 10n; 493 | } else { 494 | throw new RangeError('Could not find a sample to step forward to'); 495 | } 496 | } 497 | } 498 | return this.timeCursor; 499 | } 500 | 501 | async stepBackward(): Promise { 502 | if (!this.timeCursor.equals(TimePoint.ZERO)) { 503 | const precedingTimePoint = this.timeCursor.offsetByFemtos(-1n); 504 | const response = await this.connection.queryInterval({ 505 | type: 'command', 506 | command: 'query_interval', 507 | interval: new TimeInterval(precedingTimePoint, precedingTimePoint).toCXXRTL(), 508 | collapse: true, 509 | items: null, 510 | item_values_encoding: null, 511 | diagnostics: false 512 | }); 513 | this.timeCursor = TimePoint.fromCXXRTL(response.samples.at(0)!.time); 514 | } 515 | return this.timeCursor; 516 | } 517 | 518 | async continueForward(): Promise { 519 | if (this.timeCursor.lessThan(this.simulationStatus.latestTime)) { 520 | const diagnosticAt = await this.searchIntervalForDiagnostics( 521 | new TimeInterval(this.timeCursor, this.simulationStatus.latestTime)); 522 | if (diagnosticAt !== null) { 523 | this.timeCursor = diagnosticAt; 524 | return; 525 | } 526 | } 527 | // No diagnostics between time cursor and end of database; run the simulation. 528 | if (this.simulationStatus.status === 'paused') { 529 | // The pause handler will run `searchIntervalForDiagnostics`. 530 | await this.runSimulation({ 531 | untilDiagnostics: [ 532 | DiagnosticType.Assert, 533 | DiagnosticType.Assume, 534 | DiagnosticType.Break 535 | ] 536 | }); 537 | } else if (this.simulationStatus.status === 'finished') { 538 | this.timeCursor = this.simulationStatus.latestTime; 539 | } 540 | } 541 | 542 | private async searchIntervalForDiagnostics(interval: TimeInterval): Promise { 543 | const response = await this.connection.queryInterval({ 544 | type: 'command', 545 | command: 'query_interval', 546 | interval: interval.toCXXRTL(), 547 | collapse: true, 548 | items: null, 549 | item_values_encoding: null, 550 | diagnostics: true 551 | }); 552 | for (const sample of response.samples) { 553 | const sampleTime = TimePoint.fromCXXRTL(sample.time); 554 | if (!sampleTime.greaterThan(interval.begin)) { 555 | continue; 556 | } 557 | for (const diagnostic of sample.diagnostics!) { 558 | if (['break', 'assert', 'assume'].includes(diagnostic.type)) { 559 | return sampleTime; 560 | } 561 | } 562 | } 563 | return null; 564 | } 565 | } 566 | -------------------------------------------------------------------------------- /src/debug/watch.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | 3 | export interface IWatchItem { 4 | id: string; 5 | row?: number; 6 | bit?: number; 7 | } 8 | 9 | export interface IWatchList { 10 | get(): IWatchItem[]; 11 | set(items: IWatchItem[]): void; 12 | append(item: IWatchItem): void; 13 | remove(index: number): void; 14 | 15 | onDidChange(callback: (items: IWatchItem[]) => any): vscode.Disposable; 16 | } 17 | 18 | export const globalWatchList: IWatchList = { 19 | get(): IWatchItem[] { 20 | return vscode.workspace.getConfiguration('rtlDebugger').get('watchList') || []; 21 | }, 22 | 23 | set(items: IWatchItem[]): void { 24 | vscode.workspace.getConfiguration('rtlDebugger').update('watchList', items); 25 | }, 26 | 27 | append(item: IWatchItem): void { 28 | this.set(this.get().concat(item)); 29 | }, 30 | 31 | remove(index: number): void { 32 | const items = this.get(); 33 | items.splice(index, 1); 34 | this.set(items); 35 | }, 36 | 37 | onDidChange(callback: (items: IWatchItem[]) => any): vscode.Disposable { 38 | return vscode.workspace.onDidChangeConfiguration((event) => { 39 | if (event.affectsConfiguration('rtlDebugger.watchList')) { 40 | callback(globalWatchList.get()); 41 | } 42 | }); 43 | }, 44 | }; 45 | -------------------------------------------------------------------------------- /src/debugger.ts: -------------------------------------------------------------------------------- 1 | import * as net from 'net'; 2 | import * as vscode from 'vscode'; 3 | 4 | import { NodeStreamLink } from './cxxrtl/link'; 5 | import { StatusBarItem } from './ui/status'; 6 | import { Session } from './debug/session'; 7 | 8 | export enum CXXRTLSimulationStatus { 9 | Paused = 'paused', 10 | Running = 'running', 11 | Finished = 'finished', 12 | } 13 | 14 | export enum CXXRTLSessionStatus { 15 | Absent = 'absent', 16 | Starting = 'starting', 17 | Running = 'running', 18 | } 19 | 20 | export class CXXRTLDebugger { 21 | private statusBarItem: StatusBarItem; 22 | private terminal: vscode.Terminal | null = null; 23 | session: Session | null = null; 24 | 25 | // Session properties. 26 | 27 | private _onDidChangeSession: vscode.EventEmitter = new vscode.EventEmitter(); 28 | readonly onDidChangeSession: vscode.Event = this._onDidChangeSession.event; 29 | 30 | private _sessionStatus: CXXRTLSessionStatus = CXXRTLSessionStatus.Absent; 31 | get sessionStatus() { 32 | return this._sessionStatus; 33 | } 34 | private _onDidChangeSessionStatus: vscode.EventEmitter = new vscode.EventEmitter(); 35 | readonly onDidChangeSessionStatus: vscode.Event = this._onDidChangeSessionStatus.event; 36 | 37 | // Simulation properties. 38 | 39 | private _simulationStatus: CXXRTLSimulationStatus = CXXRTLSimulationStatus.Finished; 40 | get simulationStatus() { 41 | return this._simulationStatus; 42 | } 43 | private _onDidChangeSimulationStatus: vscode.EventEmitter = new vscode.EventEmitter(); 44 | readonly onDidChangeSimulationStatus: vscode.Event = this._onDidChangeSimulationStatus.event; 45 | 46 | constructor() { 47 | this.statusBarItem = new StatusBarItem(this); 48 | } 49 | 50 | dispose() { 51 | this.statusBarItem.dispose(); 52 | this._onDidChangeSimulationStatus.dispose(); 53 | } 54 | 55 | async startSession(): Promise { 56 | if (this.terminal !== null) { 57 | vscode.window.showErrorMessage('A debug session is already in the process of being started.'); 58 | return; 59 | } 60 | 61 | const configuration = vscode.workspace.getConfiguration('rtlDebugger'); 62 | if (configuration.command.length !== 0) { 63 | this.terminal = vscode.window.createTerminal({ 64 | name: 'Simulation Process', 65 | shellPath: configuration.command[0], 66 | shellArgs: configuration.command.slice(1), 67 | cwd: configuration.cwd, 68 | env: configuration.env, 69 | isTransient: true, 70 | iconPath: new vscode.ThemeIcon('debug-console') 71 | }); 72 | this.setSessionStatus(CXXRTLSessionStatus.Starting); 73 | 74 | const processId = await this.terminal.processId; 75 | console.log('[RTL Debugger] Launched process %d', processId); 76 | 77 | setTimeout(() => { 78 | const socket = net.createConnection({ port: configuration.port, host: '::1' }, () => { 79 | vscode.window.showInformationMessage('Connected to the CXXRTL server.'); 80 | 81 | (async () => { 82 | this.session = new Session(new NodeStreamLink(socket)); 83 | this.session.onDidChangeSimulationStatus((status) => { 84 | this.setSimulationStatus(status.status as CXXRTLSimulationStatus); 85 | }); 86 | this.session.onDidFinishSimulation(() => { 87 | vscode.window.showInformationMessage('Simulation has finished.'); 88 | }); 89 | this.setSessionStatus(CXXRTLSessionStatus.Running); 90 | this._onDidChangeSession.fire(this.session); 91 | this.setSimulationStatus( 92 | this.session.simulationStatus.status as CXXRTLSimulationStatus 93 | ); 94 | console.log('[RTL Debugger] Initialized'); 95 | })().catch(() => { 96 | this.stopSession(); 97 | }); 98 | }); 99 | socket.on('error', (err: any) => { 100 | if (err.code === 'ECONNREFUSED') { 101 | vscode.window.showErrorMessage('The connection to the CXXRTL server was refused.'); 102 | } else { 103 | vscode.window.showErrorMessage(`The connection to the CXXRTL server has failed: ${err.code}.`); 104 | } 105 | this.stopSession(); 106 | }); 107 | socket.on('close', (hadError) => { 108 | if (!hadError) { 109 | vscode.window.showInformationMessage('Disconnected from the CXXRTL server.'); 110 | } 111 | this.stopSession(); 112 | }); 113 | }, 500); // FIXME 114 | } else { 115 | const OpenSettings = 'Open Settings'; 116 | const selection = await vscode.window.showErrorMessage('Configure the launch command to start a debug session.', OpenSettings); 117 | if (selection === OpenSettings) { 118 | vscode.commands.executeCommand('workbench.action.openSettings', 'rtlDebugger.command'); 119 | } 120 | } 121 | } 122 | 123 | stopSession() { 124 | this._onDidChangeSession.fire(null); 125 | 126 | this.terminal?.dispose(); 127 | this.terminal = null; 128 | 129 | this.session?.dispose(); 130 | this.session = null; 131 | 132 | this.setSessionStatus(CXXRTLSessionStatus.Absent); 133 | this.setSimulationStatus(CXXRTLSimulationStatus.Finished); 134 | } 135 | 136 | private setSessionStatus(sessionState: CXXRTLSessionStatus): void { 137 | if (this._sessionStatus !== sessionState) { 138 | this._sessionStatus = sessionState; 139 | this._onDidChangeSessionStatus.fire(sessionState); 140 | } 141 | } 142 | 143 | private setSimulationStatus(simulationState: CXXRTLSimulationStatus): void { 144 | if (this._simulationStatus !== simulationState) { 145 | this._simulationStatus = simulationState; 146 | this._onDidChangeSimulationStatus.fire(simulationState); 147 | } 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /src/extension.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | 3 | import { CXXRTLDebugger } from './debugger'; 4 | import * as sidebar from './ui/sidebar'; 5 | import { globalWatchList } from './debug/watch'; 6 | import { globalVariableOptions } from './debug/options'; 7 | import { HoverProvider } from './ui/hover'; 8 | import { DiagnosticProvider } from './ui/diagnostic'; 9 | import { inputTime } from './ui/input'; 10 | 11 | export function activate(context: vscode.ExtensionContext) { 12 | const rtlDebugger = new CXXRTLDebugger(); 13 | 14 | const sidebarTreeDataProvider = new sidebar.TreeDataProvider(rtlDebugger); 15 | const sidebarTreeView = vscode.window.createTreeView('rtlDebugger.sidebar', { 16 | treeDataProvider: sidebarTreeDataProvider 17 | }); 18 | context.subscriptions.push(sidebarTreeView); 19 | 20 | const hoverProvider = new HoverProvider(rtlDebugger); 21 | for (const language of HoverProvider.SUPPORTED_LANGUAGES) { 22 | context.subscriptions.push(vscode.languages.registerHoverProvider(language, hoverProvider)); 23 | } 24 | 25 | const diagnosticCollection = vscode.languages.createDiagnosticCollection('rtlDebugger'); 26 | const diagnosticProvider = new DiagnosticProvider(rtlDebugger, diagnosticCollection); 27 | context.subscriptions.push(diagnosticProvider); 28 | 29 | vscode.commands.executeCommand('setContext', 'rtlDebugger.sessionStatus', rtlDebugger.sessionStatus); 30 | context.subscriptions.push(rtlDebugger.onDidChangeSessionStatus((state) => 31 | vscode.commands.executeCommand('setContext', 'rtlDebugger.sessionStatus', state))); 32 | 33 | vscode.commands.executeCommand('setContext', 'rtlDebugger.simulationStatus', rtlDebugger.simulationStatus); 34 | context.subscriptions.push(rtlDebugger.onDidChangeSimulationStatus((state) => 35 | vscode.commands.executeCommand('setContext', 'rtlDebugger.simulationStatus', state))); 36 | 37 | context.subscriptions.push(vscode.commands.registerCommand('rtlDebugger.startSession', () => 38 | rtlDebugger.startSession())); 39 | context.subscriptions.push(vscode.commands.registerCommand('rtlDebugger.stopSession', () => 40 | rtlDebugger.stopSession())); 41 | context.subscriptions.push({ dispose: () => rtlDebugger.stopSession() }); 42 | 43 | context.subscriptions.push(vscode.commands.registerCommand('rtlDebugger.runSimulation', () => 44 | rtlDebugger.session!.runSimulation())); 45 | context.subscriptions.push(vscode.commands.registerCommand('rtlDebugger.pauseSimulation', () => 46 | rtlDebugger.session!.pauseSimulation())); 47 | context.subscriptions.push(vscode.commands.registerCommand('rtlDebugger.runPauseSimulation', () => { 48 | if (rtlDebugger.session!.isSimulationRunning) { 49 | rtlDebugger.session!.pauseSimulation(); 50 | } else { 51 | rtlDebugger.session!.runSimulation(); 52 | } 53 | })); 54 | context.subscriptions.push(vscode.commands.registerCommand('rtlDebugger.runSimulationUntil', async () => { 55 | const untilTime = await inputTime({ prompt: 'Enter the time to simulate until.' }); 56 | if (untilTime !== undefined) { 57 | rtlDebugger.session!.runSimulation({ untilTime }); 58 | } 59 | })); 60 | context.subscriptions.push(vscode.commands.registerCommand('rtlDebugger.stepBackward', () => 61 | rtlDebugger.session!.stepBackward())); 62 | context.subscriptions.push(vscode.commands.registerCommand('rtlDebugger.stepForward', () => 63 | rtlDebugger.session!.stepForward())); 64 | context.subscriptions.push(vscode.commands.registerCommand('rtlDebugger.continueForward', () => 65 | rtlDebugger.session!.continueForward())); 66 | context.subscriptions.push(vscode.commands.registerCommand('rtlDebugger.goToTime', async () => { 67 | const goToTime = await inputTime({ prompt: 'Enter the time to examine the state at.' }); 68 | if (goToTime !== undefined) { 69 | if (rtlDebugger.session!.simulationStatus.latestTime.lessThan(goToTime)) { 70 | vscode.window.showErrorMessage(`The simulation has not advanced to ${goToTime} yet.`); 71 | } else { 72 | rtlDebugger.session!.timeCursor = goToTime; 73 | } 74 | } 75 | })); 76 | 77 | context.subscriptions.push(vscode.commands.registerCommand('rtlDebugger.setRadix.2', (treeItem) => 78 | globalVariableOptions.update(treeItem.designation.variable.cxxrtlIdentifier, { radix: 2 }))); 79 | context.subscriptions.push(vscode.commands.registerCommand('rtlDebugger.setRadix.8', (treeItem) => 80 | globalVariableOptions.update(treeItem.designation.variable.cxxrtlIdentifier, { radix: 8 }))); 81 | context.subscriptions.push(vscode.commands.registerCommand('rtlDebugger.setRadix.10', (treeItem) => 82 | globalVariableOptions.update(treeItem.designation.variable.cxxrtlIdentifier, { radix: 10 }))); 83 | context.subscriptions.push(vscode.commands.registerCommand('rtlDebugger.setRadix.16', (treeItem) => 84 | globalVariableOptions.update(treeItem.designation.variable.cxxrtlIdentifier, { radix: 16 }))); 85 | 86 | context.subscriptions.push(vscode.commands.registerCommand('rtlDebugger.watchVariable', (treeItem) => 87 | globalWatchList.append(treeItem.getWatchItem()))); 88 | context.subscriptions.push(vscode.commands.registerCommand('rtlDebugger.unWatchVariable', (treeItem) => 89 | globalWatchList.remove(treeItem.metadata.index))); 90 | 91 | // For an unknown reason, the `vscode.open` command (which does the exact same thing) ignores the options. 92 | context.subscriptions.push(vscode.commands.registerCommand('rtlDebugger.openDocument', 93 | (uri: vscode.Uri, options: vscode.TextDocumentShowOptions) => { 94 | vscode.window.showTextDocument(uri, options); 95 | })); 96 | } 97 | -------------------------------------------------------------------------------- /src/model/sample.ts: -------------------------------------------------------------------------------- 1 | import * as proto from '../cxxrtl/proto'; 2 | import { Location } from './source'; 3 | import { TimePoint } from './time'; 4 | import { MemoryVariable, ScalarVariable, Variable } from './variable'; 5 | 6 | class DataRange { 7 | readonly stride: number; 8 | readonly count: number; 9 | 10 | constructor(variable: Variable, count: number = 1) { 11 | this.stride = 0 | ((variable.width + 31) / 32); 12 | this.count = count; 13 | } 14 | 15 | get size() { 16 | return this.stride * this.count; 17 | } 18 | 19 | bigintFromRaw(data: Uint32Array, offset: number = 0) { 20 | if (!(offset <= this.count)) { 21 | throw RangeError(`Offset ${offset} out of bounds for data range with ${this.count} elements`); 22 | } 23 | let value = 0n; 24 | for (let index = 0; index < this.stride; index++) { 25 | value = (value << 32n) + BigInt(data[this.stride * offset + index]); 26 | } 27 | return value; 28 | } 29 | } 30 | 31 | export abstract class Designation { 32 | abstract variable: Variable; 33 | 34 | abstract get canonicalKey(): string; 35 | 36 | abstract get cxxrtlItemDesignation(): proto.ItemDesignation; 37 | 38 | abstract dataRange(): DataRange; 39 | 40 | abstract extractFromRaw(data: Uint32Array): T; 41 | } 42 | 43 | export class ScalarDesignation extends Designation { 44 | constructor( 45 | readonly variable: ScalarVariable, 46 | ) { 47 | super(); 48 | } 49 | 50 | get canonicalKey(): string { 51 | return this.variable.fullName.join(' '); 52 | } 53 | 54 | get cxxrtlItemDesignation(): proto.ItemDesignation { 55 | return [this.variable.cxxrtlIdentifier]; 56 | } 57 | 58 | override dataRange(): DataRange { 59 | return new DataRange(this.variable); 60 | } 61 | 62 | override extractFromRaw(data: Uint32Array): bigint { 63 | return this.dataRange().bigintFromRaw(data); 64 | } 65 | } 66 | 67 | export class MemoryRowDesignation extends Designation { 68 | constructor( 69 | readonly variable: MemoryVariable, 70 | readonly index: number, 71 | ) { 72 | super(); 73 | } 74 | 75 | get canonicalKey(): string { 76 | return `${this.variable.fullName.join(' ')}\u0000${this.index}`; 77 | } 78 | 79 | get cxxrtlItemDesignation(): proto.ItemDesignation { 80 | return [this.variable.cxxrtlIdentifier, this.index, this.index]; 81 | } 82 | 83 | override dataRange(): DataRange { 84 | return new DataRange(this.variable); 85 | } 86 | 87 | override extractFromRaw(data: Uint32Array): bigint { 88 | return this.dataRange().bigintFromRaw(data); 89 | } 90 | } 91 | 92 | export class MemoryRangeDesignation extends Designation> { 93 | constructor( 94 | readonly variable: MemoryVariable, 95 | readonly first: number, 96 | readonly last: number, 97 | ) { 98 | super(); 99 | } 100 | 101 | get canonicalKey(): string { 102 | return `${this.variable.fullName.join(' ')}\u0000${this.first}\u0000${this.last}`; 103 | } 104 | 105 | get cxxrtlItemDesignation(): proto.ItemDesignation { 106 | return [this.variable.cxxrtlIdentifier, this.first, this.last]; 107 | } 108 | 109 | get count(): number { 110 | return (this.last >= this.first) ? (this.last - this.first + 1) : (this.first - this.last + 1); 111 | } 112 | 113 | override dataRange(): DataRange { 114 | return new DataRange(this.variable, this.count); 115 | } 116 | 117 | *extractFromRaw(data: Uint32Array): Iterable { 118 | for (let offset = 0; offset < this.count; offset++) { 119 | yield this.dataRange().bigintFromRaw(data, offset); 120 | } 121 | } 122 | } 123 | 124 | export class Handle { 125 | constructor( 126 | readonly designation: Designation, 127 | readonly reference: UnboundReference, 128 | readonly offset: number, 129 | ) {} 130 | 131 | extractFromRaw(data: Uint32Array): T { 132 | return this.designation.extractFromRaw(data.subarray(this.offset)); 133 | } 134 | } 135 | 136 | export class UnboundReference { 137 | private frozen: boolean = false; 138 | private offset: number = 0; // in chunks 139 | private handles: Handle[] = []; 140 | 141 | add(designation: Designation): Handle { 142 | if (this.frozen) { 143 | throw new Error('Cannot add variables to a reference that has been bound to a name'); 144 | } 145 | const handle = new Handle(designation, this, this.offset); 146 | this.handles.push(handle); 147 | this.offset += designation.dataRange().size; 148 | return handle; 149 | } 150 | 151 | freeze() { 152 | this.frozen = true; 153 | } 154 | 155 | *allHandles(): Iterable<[Designation, Handle]> { 156 | for (const handle of this.handles) { 157 | yield [handle.designation, handle]; 158 | } 159 | } 160 | 161 | cxxrtlItemDesignations(): proto.ItemDesignation[] { 162 | return this.handles.map((slice) => slice.designation.cxxrtlItemDesignation); 163 | } 164 | } 165 | 166 | export class Reference { 167 | constructor( 168 | readonly name: string, 169 | readonly epoch: number, 170 | readonly unbound: UnboundReference, 171 | ) { 172 | this.unbound.freeze(); 173 | } 174 | 175 | allHandles(): Iterable<[Designation, Handle]> { 176 | return this.unbound.allHandles(); 177 | } 178 | } 179 | 180 | export enum DiagnosticType { 181 | Break = 'break', 182 | Print = 'print', 183 | Assert = 'assert', 184 | Assume = 'assume', 185 | } 186 | 187 | export class Diagnostic { 188 | constructor( 189 | readonly type: DiagnosticType, 190 | readonly text: string, 191 | readonly location: Location | null, 192 | ) {} 193 | 194 | static fromCXXRTL(cxxrtlDiagnostic: proto.Diagnostic): Diagnostic { 195 | return new Diagnostic( 196 | cxxrtlDiagnostic.type, 197 | cxxrtlDiagnostic.text, 198 | Location.fromCXXRTL(cxxrtlDiagnostic.src) 199 | ); 200 | } 201 | } 202 | 203 | export class Sample { 204 | constructor( 205 | readonly time: TimePoint, 206 | readonly reference: UnboundReference | null, 207 | readonly variableData: Uint32Array | null, 208 | readonly diagnostics: Diagnostic[] | null, 209 | ) { 210 | if ((this.reference === null) !== (this.variableData === null)) { 211 | throw new Error('A sample must include both a reference and variable data, or neither'); 212 | } 213 | } 214 | 215 | extract(handle: Handle): T { 216 | if (handle.reference !== this.reference) { 217 | throw new Error('Handle is not bound to the same reference as the sample'); 218 | } 219 | if (this.variableData === null) { 220 | throw new Error('Sample does not include item values'); 221 | } 222 | return handle.extractFromRaw(this.variableData); 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /src/model/scope.ts: -------------------------------------------------------------------------------- 1 | import * as proto from '../cxxrtl/proto'; 2 | import { Location } from './source'; 3 | import { Variable } from './variable'; 4 | 5 | export abstract class Scope { 6 | constructor( 7 | readonly fullName: string[], 8 | readonly location: Location | null, 9 | readonly scopes: Scope[] | Thenable, 10 | readonly variables: Variable[] | Thenable, 11 | ) {} 12 | 13 | static fromCXXRTL( 14 | cxxrtlName: string, 15 | cxxrtlDesc: proto.ScopeDescription, 16 | nestedScopes: Scope[] | Thenable, 17 | nestedVariables: Variable[] | Thenable, 18 | ) { 19 | const fullName = cxxrtlName === '' ? [] : cxxrtlName.split(' '); 20 | if (cxxrtlDesc.type === 'module') { 21 | const moduleName = cxxrtlDesc.definition.name?.split(' ') || []; 22 | return new ModuleScope( 23 | fullName, 24 | Location.fromCXXRTL(cxxrtlDesc.instantiation.src), 25 | moduleName, 26 | Location.fromCXXRTL(cxxrtlDesc.definition.src), 27 | nestedScopes, 28 | nestedVariables, 29 | ); 30 | } else { 31 | throw new Error(`Unknown scope type in ${cxxrtlDesc}`); 32 | } 33 | } 34 | 35 | get name(): string { 36 | if (this.fullName.length === 0) { 37 | return ''; 38 | } else { 39 | return this.fullName[this.fullName.length - 1]; 40 | } 41 | } 42 | 43 | get parentFullName(): string[] | null { 44 | if (this.fullName.length === 0) { 45 | return null; 46 | } else { 47 | return this.fullName.slice(0, -1); 48 | } 49 | } 50 | 51 | get cxxrtlIdentifier(): string { 52 | return this.fullName.join(' '); 53 | } 54 | } 55 | 56 | export class ModuleScope extends Scope { 57 | constructor( 58 | fullName: string[], 59 | location: Location | null, 60 | readonly moduleFullName: string[], 61 | readonly moduleLocation: Location | null, 62 | scopes: Scope[] | Thenable, 63 | variables: Variable[] | Thenable, 64 | ) { 65 | super(fullName, location, scopes, variables); 66 | } 67 | 68 | get moduleName(): string { 69 | if (this.moduleFullName.length === 0) { 70 | return ''; 71 | } else { 72 | return this.moduleFullName[this.moduleFullName.length - 1]; 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/model/source.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | 3 | export class Location { 4 | constructor( 5 | readonly file: string, 6 | // All zero-based, like VS Code itself. 7 | readonly startLine: number, 8 | readonly startColumn?: number, 9 | readonly endLine?: number, 10 | readonly endColumn?: number, 11 | ) {} 12 | 13 | static fromCXXRTL(locationString: string | null): Location | null { 14 | if (locationString === null) { 15 | return null; 16 | } 17 | const matches = locationString.match(/^(.+?):(\d+)(?:\.(\d+)(?:-(\d+)\.(\d+)))?$/); 18 | if (!matches) { 19 | return null; 20 | } 21 | return new Location( 22 | matches[1], 23 | parseInt(matches[2]) - 1, 24 | matches[3] !== undefined ? parseInt(matches[3]) - 1 : undefined, 25 | matches[4] !== undefined ? parseInt(matches[4]) - 1 : undefined, 26 | matches[5] !== undefined ? parseInt(matches[5]) - 1 : undefined, 27 | ); 28 | } 29 | 30 | get fileUri(): vscode.Uri { 31 | return vscode.Uri.file(this.file); 32 | } 33 | 34 | get range(): vscode.Range { 35 | return new vscode.Range( 36 | this.startLine, 37 | this.startColumn ?? 0, 38 | this.endLine ?? this.startLine, 39 | this.endColumn ?? 4096 40 | ); 41 | } 42 | 43 | openCommandArguments(): [vscode.Uri, vscode.TextDocumentShowOptions] { 44 | const position = new vscode.Position(this.startLine, this.startColumn ?? 0); 45 | return [ 46 | this.fileUri, 47 | { 48 | selection: new vscode.Selection(position, position), 49 | preview: true, 50 | preserveFocus: false 51 | } 52 | ]; 53 | } 54 | 55 | asOpenCommand(): vscode.Command { 56 | return { 57 | title: 'Reveal in Editor', 58 | command: 'rtlDebugger.openDocument', 59 | arguments: this.openCommandArguments(), 60 | }; 61 | } 62 | 63 | // Really? *Three* different command representations depending on the API? VS Code please. 64 | asOpenCommandUri(): vscode.Uri { 65 | const args = this.openCommandArguments(); 66 | return vscode.Uri.parse(`command:rtlDebugger.openDocument?${encodeURIComponent(JSON.stringify(args))}`); 67 | } 68 | 69 | asMarkdownLink(): string { 70 | const sourceRelativePath = vscode.workspace.asRelativePath(this.file); 71 | return `[${sourceRelativePath}:${this.startLine + 1}](${this.asOpenCommandUri()})`; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/model/styling.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | 3 | import { MemoryVariable, ScalarVariable, Variable } from './variable'; 4 | import { globalVariableOptions } from '../debug/options'; 5 | 6 | export enum DisplayStyle { 7 | Python = 'Python', 8 | Verilog = 'Verilog', 9 | VHDL = 'VHDL', 10 | } 11 | 12 | export function languageForDisplayStyle(style: DisplayStyle): string { 13 | return style as string; 14 | } 15 | 16 | export function variableDescription(style: DisplayStyle, variable: Variable, { scalar = false } = {}): string { 17 | let result = ''; 18 | if (variable instanceof ScalarVariable && variable.lsbAt === 0 && variable.width === 1) { 19 | return result; 20 | } 21 | switch (style) { 22 | case DisplayStyle.Python: { 23 | if (variable instanceof MemoryVariable && !scalar) { 24 | if (variable.zeroAt === 0) { 25 | result += `[${variable.depth}] `; 26 | } else { 27 | result += `[${variable.zeroAt}:${variable.zeroAt + variable.depth}] `; 28 | } 29 | } 30 | if (variable.lsbAt === 0) { 31 | result += `[${variable.width}]`; 32 | } else { 33 | result += `[${variable.lsbAt}:${variable.lsbAt + variable.width}] `; 34 | } 35 | return result; 36 | } 37 | 38 | case DisplayStyle.Verilog: 39 | case DisplayStyle.VHDL: { 40 | result += `[${variable.lsbAt + variable.width - 1}:${variable.lsbAt}]`; 41 | if (variable instanceof MemoryVariable && !scalar) { 42 | result += ` [${variable.zeroAt}:${variable.zeroAt + variable.depth - 1}] `; 43 | } 44 | return result; 45 | } 46 | } 47 | } 48 | 49 | export function* variableBitIndices(style: DisplayStyle, variable: Variable): Generator { 50 | switch (style) { 51 | case DisplayStyle.Python: { 52 | for (let index = 0; index < variable.width; index++) { 53 | yield variable.lsbAt + index; 54 | } 55 | return; 56 | } 57 | 58 | case DisplayStyle.Verilog: 59 | case DisplayStyle.VHDL: { 60 | for (let index = variable.width - 1; index >= 0; index--) { 61 | yield variable.lsbAt + index; 62 | } 63 | } 64 | } 65 | } 66 | 67 | export function* memoryRowIndices(variable: MemoryVariable): Generator { 68 | for (let index = 0; index < variable.depth; index++) { 69 | yield variable.zeroAt + index; 70 | } 71 | } 72 | 73 | export function variableValue(style: DisplayStyle, variable: Variable, value: bigint | undefined, radix?: 2 | 8 | 10 | 16): string { 74 | if (value === undefined) { 75 | return '...'; 76 | } 77 | 78 | // There is a bug in CXXRTL that occasionally causes out-of-bounds bits to be set. 79 | // Ideally it should be fixed there, but for now let's work around it here, for usability. 80 | value &= (1n << BigInt(variable.width)) - 1n; 81 | 82 | if (variable.width === 1) { 83 | return value.toString(); 84 | } else { 85 | if (radix === undefined) { 86 | radix = globalVariableOptions.get(variable.cxxrtlIdentifier).radix ?? 10; 87 | } 88 | let stringValue; 89 | switch (radix) { 90 | case 2: stringValue = value.toString(2) .padStart(variable.width / 1, '0'); break; 91 | case 8: stringValue = value.toString(8) .padStart(variable.width / 3, '0'); break; 92 | case 10: stringValue = value.toString(10); break; 93 | case 16: stringValue = value.toString(16).padStart(variable.width / 4, '0'); break; 94 | } 95 | switch (style) { 96 | case DisplayStyle.Python: 97 | switch (radix) { 98 | case 2: return `0b${stringValue}`; 99 | case 8: return `0o${stringValue}`; 100 | case 10: return stringValue; 101 | case 16: return `0x${stringValue}`; 102 | } 103 | 104 | case DisplayStyle.Verilog: 105 | switch (radix) { 106 | case 2: return `${variable.width}'b${stringValue}`; 107 | case 8: return `${variable.width}'o${stringValue}`; 108 | case 10: return `${variable.width}'d${stringValue}`; 109 | case 16: return `${variable.width}'h${stringValue}`; 110 | } 111 | 112 | case DisplayStyle.VHDL: 113 | switch (radix) { 114 | case 2: return `B"${stringValue}"`; 115 | case 8: return `O"${stringValue}"`; 116 | case 10: return stringValue; 117 | case 16: return `X"${stringValue}"`; 118 | } 119 | } 120 | } 121 | } 122 | 123 | export function variableTooltip(variable: Variable): vscode.MarkdownString { 124 | const tooltip = new vscode.MarkdownString(variable.fullName.join('.')); 125 | tooltip.isTrusted = true; 126 | if (variable.location) { 127 | tooltip.appendMarkdown(`\n\n- ${variable.location.asMarkdownLink()}`); 128 | } 129 | return tooltip; 130 | } 131 | -------------------------------------------------------------------------------- /src/model/time.ts: -------------------------------------------------------------------------------- 1 | export class TimePoint { 2 | static RESOLUTION: bigint = 1_000_000_000_000_000n; // femto 3 | 4 | static ZERO: TimePoint = new TimePoint(0n, 0n); 5 | 6 | // Femtoseconds since beginning of time. 7 | readonly #raw: bigint = 0n; 8 | 9 | constructor(secs: bigint, femtos: bigint) { 10 | this.#raw = secs * TimePoint.RESOLUTION + femtos; 11 | } 12 | 13 | get secs(): bigint { 14 | return this.#raw / TimePoint.RESOLUTION; 15 | } 16 | 17 | get femtos(): bigint { 18 | return this.#raw % TimePoint.RESOLUTION; 19 | } 20 | 21 | equals(other: TimePoint): boolean { 22 | return this.#raw === other.#raw; 23 | } 24 | 25 | greaterThan(other: TimePoint): boolean { 26 | return this.#raw > other.#raw; 27 | } 28 | 29 | lessThan(other: TimePoint): boolean { 30 | return this.#raw < other.#raw; 31 | } 32 | 33 | offsetByFemtos(femtos: bigint): TimePoint { 34 | return new TimePoint(this.secs, this.femtos + femtos); 35 | } 36 | 37 | differenceInFemtos(other: TimePoint): bigint { 38 | return this.#raw - other.#raw; 39 | } 40 | 41 | toString(): string { 42 | function groupDecimals(num: bigint) { 43 | const groups: string[] = []; 44 | if (num === 0n) { 45 | groups.push('0'); 46 | } else { 47 | while (num !== 0n) { 48 | groups.push(`${num % 1000n}`); 49 | num /= 1000n; 50 | } 51 | } 52 | return groups 53 | .map((group, index) => index === groups.length - 1 ? group : group.padStart(3, '0')) 54 | .reverse() 55 | .join(','); 56 | } 57 | 58 | if (this.#raw % 1_000_000_000_000_000n === 0n) { 59 | return `${groupDecimals(this.#raw / 1_000_000_000_000_000n)}s`; 60 | } else if (this.#raw % 1_000_000_000_000n === 0n) { 61 | return `${groupDecimals(this.#raw / 1_000_000_000_000n)}ms`; 62 | } else if (this.#raw % 1_000_000_000n === 0n) { 63 | return `${groupDecimals(this.#raw / 1_000_000_000n)}us`; 64 | } else if (this.#raw % 1_000_000n === 0n) { 65 | return `${groupDecimals(this.#raw / 1_000_000n)}ns`; 66 | } else if (this.#raw % 1_000n === 0n) { 67 | return `${groupDecimals(this.#raw / 1_000n)}ps`; 68 | } else { 69 | return `${groupDecimals(this.#raw)}fs`; 70 | } 71 | } 72 | 73 | static fromString(value: string): TimePoint { 74 | const matches = value.match(/^(\d+)\s*(s|ms|us|ns|ps|fs)$/); 75 | if (matches === null) { 76 | throw new SyntaxError(`${JSON.stringify(value)} is not a valid time point`); 77 | } 78 | const mantissa = BigInt(matches[1].replaceAll(',', '')); 79 | switch (matches[2]) { 80 | case 's': 81 | return new TimePoint(mantissa, 0n); 82 | case 'ms': 83 | return new TimePoint(0n, mantissa * 1_000_000_000_000n); 84 | case 'us': 85 | return new TimePoint(0n, mantissa * 1_000_000_000n); 86 | case 'ns': 87 | return new TimePoint(0n, mantissa * 1_000_000n); 88 | case 'ps': 89 | return new TimePoint(0n, mantissa * 1_000n); 90 | case 'fs': 91 | return new TimePoint(0n, mantissa); 92 | default: 93 | throw new Error('unreachable'); 94 | } 95 | } 96 | 97 | static fromCXXRTL(value: string): TimePoint { 98 | const matches = value.match(/^(\d+)\.(\d+)$/); 99 | if (matches === null) { 100 | throw new SyntaxError(`${JSON.stringify(value)} is not a valid time point`); 101 | } 102 | return new TimePoint(BigInt(matches[1]), BigInt(matches[2])); 103 | } 104 | 105 | toCXXRTL(): string { 106 | return `${this.secs.toString()}.${this.femtos.toString().padStart(15, '0')}`; 107 | } 108 | } 109 | 110 | export class TimeInterval { 111 | constructor(public begin: TimePoint, public end: TimePoint) {} 112 | 113 | static fromCXXRTL([begin, end]: [string, string]): TimeInterval { 114 | return new TimeInterval(TimePoint.fromCXXRTL(begin), TimePoint.fromCXXRTL(end)); 115 | } 116 | 117 | toCXXRTL(): [string, string] { 118 | return [this.begin.toCXXRTL(), this.end.toCXXRTL()]; 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/model/variable.ts: -------------------------------------------------------------------------------- 1 | import * as proto from '../cxxrtl/proto'; 2 | import { MemoryRangeDesignation, MemoryRowDesignation, ScalarDesignation } from './sample'; 3 | import { Location } from './source'; 4 | 5 | export abstract class Variable { 6 | constructor( 7 | readonly fullName: string[], 8 | readonly location: Location | null, 9 | readonly lsbAt: number, 10 | readonly width: number, 11 | ) {} 12 | 13 | static fromCXXRTL(cxxrtlName: string, cxxrtlDesc: proto.ItemDescription): Variable { 14 | const fullName = cxxrtlName.split(' '); 15 | if (cxxrtlDesc.type === 'node') { 16 | return new ScalarVariable( 17 | fullName, 18 | Location.fromCXXRTL(cxxrtlDesc.src), 19 | cxxrtlDesc.lsb_at, 20 | cxxrtlDesc.width, 21 | ); 22 | } else if (cxxrtlDesc.type === 'memory') { 23 | return new MemoryVariable( 24 | fullName, 25 | Location.fromCXXRTL(cxxrtlDesc.src), 26 | cxxrtlDesc.lsb_at, 27 | cxxrtlDesc.width, 28 | cxxrtlDesc.zero_at, 29 | cxxrtlDesc.depth, 30 | ); 31 | } else { 32 | throw new Error(`Unknown item type in ${cxxrtlDesc}`); 33 | } 34 | } 35 | 36 | get name(): string { 37 | return this.fullName.at(-1)!; 38 | } 39 | 40 | get scopeFullName(): string[] { 41 | return this.fullName.slice(0, -1); 42 | } 43 | 44 | get cxxrtlIdentifier(): string { 45 | return this.fullName.join(' '); 46 | } 47 | } 48 | 49 | export class ScalarVariable extends Variable { 50 | designation(): ScalarDesignation { 51 | return new ScalarDesignation(this); 52 | } 53 | } 54 | 55 | export class MemoryVariable extends Variable { 56 | constructor( 57 | fullName: string[], 58 | location: Location | null, 59 | lsbAt: number, 60 | width: number, 61 | readonly zeroAt: number, 62 | readonly depth: number, 63 | ) { 64 | super(fullName, location, lsbAt, width); 65 | } 66 | 67 | designation(index: number): MemoryRowDesignation; 68 | designation(first: number, last: number): MemoryRangeDesignation; 69 | designation(): MemoryRangeDesignation; 70 | 71 | designation(firstOrIndex?: number, last?: number): MemoryRowDesignation | MemoryRangeDesignation { 72 | if (firstOrIndex !== undefined && last === undefined) { 73 | return new MemoryRowDesignation(this, firstOrIndex); 74 | } else if (firstOrIndex !== undefined && last !== undefined) { 75 | return new MemoryRangeDesignation(this, firstOrIndex, last); 76 | } else { 77 | return new MemoryRangeDesignation(this, 0, this.depth - 1); 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/ui/diagnostic.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | 3 | import { CXXRTLDebugger } from '../debugger'; 4 | import { Diagnostic, DiagnosticType } from '../model/sample'; 5 | import { Session } from '../debug/session'; 6 | 7 | export class DiagnosticProvider { 8 | constructor( 9 | private rtlDebugger: CXXRTLDebugger, 10 | private diagnosticCollection: vscode.DiagnosticCollection, 11 | ) { 12 | rtlDebugger.onDidChangeSession((session) => { 13 | if (session === null) { 14 | this.clear(); 15 | } else { 16 | session.onDidChangeSimulationStatus((simulationStatus) => { 17 | if (simulationStatus.status === 'running') { 18 | this.clear(); 19 | } 20 | }); 21 | session.onDidChangeTimeCursor((_timeCursor) => { 22 | this.request(session); 23 | }); 24 | this.request(session); 25 | } 26 | }); 27 | } 28 | 29 | dispose() { 30 | this.diagnosticCollection.dispose(); 31 | } 32 | 33 | private async clear() { 34 | this.apply([]); 35 | } 36 | 37 | private async request(session: Session) { 38 | const sample = await session.queryAtCursor({ diagnostics: true }); 39 | this.apply(sample.diagnostics!); 40 | } 41 | 42 | private apply(diagnostics: Diagnostic[]) { 43 | const diagnosticMap = new Map(); 44 | let mostImportantDiagnostic = null; 45 | let mostImportantDiagnosticSeverity = vscode.DiagnosticSeverity.Hint; 46 | let multipleImportantDiagnostics = false; 47 | for (const diagnostic of diagnostics) { 48 | if (diagnostic.location === null) { 49 | continue; 50 | } 51 | const uri = diagnostic.location.fileUri; 52 | const range = diagnostic.location.range; 53 | if (!diagnosticMap.has(uri)) { 54 | diagnosticMap.set(uri, []); 55 | } 56 | 57 | console.warn('[RTL Debugger]', diagnostic); 58 | 59 | let message; 60 | let severity; 61 | switch (diagnostic.type) { 62 | case DiagnosticType.Break: 63 | if (diagnostic.text !== '') { 64 | message = `breakpoint: ${diagnostic.text}`; 65 | } else { 66 | message = 'breakpoint'; 67 | } 68 | severity = vscode.DiagnosticSeverity.Warning; 69 | break; 70 | 71 | case DiagnosticType.Print: 72 | message = diagnostic.text; 73 | severity = vscode.DiagnosticSeverity.Information; 74 | break; 75 | 76 | case DiagnosticType.Assert: 77 | if (diagnostic.text !== '') { 78 | message = `assertion violated: ${diagnostic.text}`; 79 | } else { 80 | message = 'assertion violated'; 81 | } 82 | severity = vscode.DiagnosticSeverity.Error; 83 | break; 84 | 85 | case DiagnosticType.Assume: 86 | if (diagnostic.text !== '') { 87 | message = `assumption violated: ${diagnostic.text}`; 88 | } else { 89 | message = 'assumption violated'; 90 | } 91 | severity = vscode.DiagnosticSeverity.Error; 92 | break; 93 | 94 | default: 95 | message = `(unknown diagnostic type ${diagnostic.type}): ${diagnostic.text}`; 96 | severity = vscode.DiagnosticSeverity.Error; 97 | break; 98 | } 99 | if (severity !== vscode.DiagnosticSeverity.Information && diagnostic.location !== null) { 100 | // Prioritize assertions/assumptions over breakpoints. (It's unclear whether this 101 | // specific prioritization is the best one, but one of them should probably take 102 | // priority over the other.) 103 | multipleImportantDiagnostics = (mostImportantDiagnostic !== null); 104 | if (severity < mostImportantDiagnosticSeverity) { 105 | mostImportantDiagnostic = diagnostic; 106 | mostImportantDiagnosticSeverity = severity; 107 | } 108 | } 109 | 110 | if (message !== '') { 111 | const mappedDiagnostic = new vscode.Diagnostic(range, message, severity); 112 | mappedDiagnostic.code = diagnostic.type; 113 | mappedDiagnostic.source = 'RTL Debug'; 114 | diagnosticMap.get(uri).push(mappedDiagnostic); 115 | } 116 | } 117 | 118 | this.diagnosticCollection.clear(); 119 | this.diagnosticCollection.set(Array.from(diagnosticMap.entries())); 120 | 121 | if (mostImportantDiagnostic !== null) { 122 | this.focus(mostImportantDiagnostic, multipleImportantDiagnostics); 123 | } 124 | } 125 | 126 | private async focus(diagnostic: Diagnostic, showDiagnosticsPane: boolean = false) { 127 | if (showDiagnosticsPane) { 128 | await vscode.commands.executeCommand('workbench.actions.view.problems'); 129 | } 130 | // 2024-11-14: Upsettingly, this is the best (and, more or less, only) way to expand a diagnostic. 131 | await vscode.window.showTextDocument(...diagnostic.location!.openCommandArguments()); 132 | await vscode.commands.executeCommand('editor.action.marker.next'); 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/ui/hover.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | 3 | import { CXXRTLDebugger } from '../debugger'; 4 | import { UnboundReference } from '../model/sample'; 5 | import { ScalarVariable, Variable } from '../model/variable'; 6 | import { DisplayStyle, languageForDisplayStyle, variableDescription, variableValue } from '../model/styling'; 7 | import { Session } from '../debug/session'; 8 | 9 | export class HoverProvider implements vscode.HoverProvider { 10 | static readonly SUPPORTED_LANGUAGES: string[] = ['verilog', 'systemverilog']; 11 | 12 | constructor( 13 | private rtlDebugger: CXXRTLDebugger 14 | ) {} 15 | 16 | private async hoverForVariables(session: Session, variables: Variable[]): Promise { 17 | if (variables.length === 0) { 18 | return null; 19 | } 20 | const displayStyle = vscode.workspace.getConfiguration('rtlDebugger').get('displayStyle') as DisplayStyle; 21 | const hoverText = new vscode.MarkdownString(); 22 | const unboundReference = new UnboundReference(); 23 | for (const variable of variables) { 24 | if (variable instanceof ScalarVariable) { 25 | unboundReference.add(variable.designation()); 26 | } 27 | } 28 | const reference = session.bindReference('hover', unboundReference); 29 | const sample = await session.queryAtCursor({ reference }); 30 | for (const [designation, handle] of reference.allHandles()) { 31 | const variable = designation.variable; 32 | const descriptionText = variableDescription(displayStyle, variable); 33 | const valueText = variableValue(displayStyle, variable, sample.extract(handle)); 34 | hoverText.appendCodeblock( 35 | `${variable.fullName.join('.')}${descriptionText} = ${valueText}`, 36 | languageForDisplayStyle(displayStyle) 37 | ); 38 | } 39 | return new vscode.Hover(hoverText); 40 | } 41 | 42 | async provideHover(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken): Promise { 43 | const session = this.rtlDebugger.session; 44 | if (session !== null) { 45 | const definitions = await ( 46 | vscode.commands.executeCommand('vscode.executeDefinitionProvider', document.uri, position) as 47 | vscode.ProviderResult 48 | ); 49 | let definition: vscode.Location | undefined; 50 | if (definitions instanceof vscode.Location) { 51 | definition = definitions; 52 | } else if (definitions instanceof Array && definitions.length === 1 && definitions[0] instanceof vscode.Location) { 53 | definition = definitions[0]; 54 | } else { 55 | console.warn('vscode.executeDefinitionProvider did not return a single Location: ', definition); 56 | return null; 57 | } 58 | const variables = await session.getVariablesForLocation(definition.uri.fsPath, definition.range.start); 59 | return await this.hoverForVariables(session, variables); 60 | } 61 | return null; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/ui/input.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { TimePoint } from '../model/time'; 3 | 4 | export async function inputTime(options: { prompt?: string } = {}): Promise { 5 | const inputValue = await vscode.window.showInputBox({ 6 | placeHolder: '10 ms', 7 | prompt: options.prompt, 8 | validateInput(value) { 9 | try { 10 | TimePoint.fromString(value); 11 | return null; 12 | } catch (e) { 13 | if (e instanceof SyntaxError) { 14 | return e.message; 15 | } else { 16 | throw e; 17 | } 18 | } 19 | }, 20 | }); 21 | 22 | if (inputValue !== undefined) { 23 | return TimePoint.fromString(inputValue); 24 | } else { 25 | return undefined; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/ui/sidebar.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | 3 | import { ModuleScope, Scope } from '../model/scope'; 4 | import { MemoryVariable, ScalarVariable, Variable } from '../model/variable'; 5 | import { DisplayStyle, variableDescription, variableBitIndices, memoryRowIndices, variableValue, variableTooltip } from '../model/styling'; 6 | import { CXXRTLDebugger } from '../debugger'; 7 | import { Observer } from '../debug/observer'; 8 | import { Designation, MemoryRangeDesignation, MemoryRowDesignation, ScalarDesignation } from '../model/sample'; 9 | import { IWatchItem, globalWatchList } from '../debug/watch'; 10 | import { Session } from '../debug/session'; 11 | 12 | abstract class TreeItem { 13 | // Currently used only for removing watch items, where knowing the index is necessary. 14 | metadata: any; 15 | 16 | constructor( 17 | readonly provider: TreeDataProvider 18 | ) {} 19 | 20 | abstract getTreeItem(): vscode.TreeItem | Thenable; 21 | 22 | getChildren(): vscode.ProviderResult { 23 | return []; 24 | } 25 | 26 | get displayStyle(): DisplayStyle { 27 | return this.provider.displayStyle; 28 | } 29 | 30 | getValue(designation: Designation): T | undefined { 31 | return this.provider.getValue(this, designation); 32 | } 33 | } 34 | 35 | class BitTreeItem extends TreeItem { 36 | constructor( 37 | provider: TreeDataProvider, 38 | readonly designation: ScalarDesignation | MemoryRowDesignation, 39 | readonly bitIndex: number, 40 | readonly contextValue: string = '', 41 | ) { 42 | super(provider); 43 | } 44 | 45 | get variable(): Variable { 46 | return this.designation.variable; 47 | } 48 | 49 | override getTreeItem(): vscode.TreeItem { 50 | const variable = this.designation.variable; 51 | const treeItem = new vscode.TreeItem(variable.name); 52 | if (this.designation instanceof MemoryRowDesignation) { 53 | treeItem.label += `[${this.designation.index}]`; 54 | } 55 | treeItem.label += `[${this.bitIndex}]`; 56 | treeItem.iconPath = new vscode.ThemeIcon('symbol-boolean'); 57 | const value = this.getValue(this.designation); 58 | if (value === undefined) { 59 | treeItem.description = '= ...'; 60 | } else { 61 | treeItem.description = ((value & (1n << BigInt(this.bitIndex))) !== 0n) 62 | ? '= 1' 63 | : '= 0'; 64 | } 65 | treeItem.tooltip = variableTooltip(variable); 66 | treeItem.contextValue = this.contextValue; 67 | return treeItem; 68 | } 69 | 70 | getWatchItem(): IWatchItem { 71 | return { 72 | id: this.designation.variable.cxxrtlIdentifier, 73 | row: (this.designation instanceof MemoryRowDesignation) 74 | ? this.designation.index : undefined, 75 | bit: this.bitIndex, 76 | }; 77 | } 78 | } 79 | 80 | class ScalarTreeItem extends TreeItem { 81 | constructor( 82 | provider: TreeDataProvider, 83 | readonly designation: ScalarDesignation | MemoryRowDesignation, 84 | readonly contextValue: string = '', 85 | ) { 86 | super(provider); 87 | } 88 | 89 | override getTreeItem(): vscode.TreeItem { 90 | const variable = this.designation.variable; 91 | const treeItem = new vscode.TreeItem(variable.name); 92 | if (this.designation instanceof MemoryRowDesignation) { 93 | treeItem.label += `[${this.designation.index}]`; 94 | } 95 | if (variable.width > 1) { 96 | treeItem.collapsibleState = vscode.TreeItemCollapsibleState.Collapsed; 97 | } 98 | treeItem.iconPath = (variable.width === 1) 99 | ? new vscode.ThemeIcon('symbol-boolean') 100 | : new vscode.ThemeIcon('symbol-variable'); 101 | const value = this.getValue(this.designation); 102 | treeItem.description = variableDescription(this.displayStyle, variable, { scalar: true }); 103 | treeItem.description += (treeItem.description !== '') ? ' = ' : '= '; 104 | treeItem.description += variableValue(this.displayStyle, variable, value); 105 | treeItem.tooltip = variableTooltip(variable); 106 | treeItem.command = variable.location?.asOpenCommand(); 107 | treeItem.contextValue = this.contextValue; 108 | return treeItem; 109 | } 110 | 111 | override getChildren(): TreeItem[] { 112 | const variable = this.designation.variable; 113 | return Array.from(variableBitIndices(this.displayStyle, variable)).map((index) => 114 | new BitTreeItem(this.provider, this.designation, index, 'canWatch')); 115 | } 116 | 117 | getWatchItem(): IWatchItem { 118 | return { 119 | id: this.designation.variable.cxxrtlIdentifier, 120 | row: (this.designation instanceof MemoryRowDesignation) 121 | ? this.designation.index : undefined, 122 | }; 123 | } 124 | } 125 | 126 | class ArrayTreeItem extends TreeItem { 127 | constructor( 128 | provider: TreeDataProvider, 129 | readonly designation: MemoryRangeDesignation, 130 | readonly contextValue: string = '', 131 | ) { 132 | super(provider); 133 | } 134 | 135 | override getTreeItem(): vscode.TreeItem { 136 | const variable = this.designation.variable; 137 | const treeItem = new vscode.TreeItem(variable.name); 138 | treeItem.collapsibleState = vscode.TreeItemCollapsibleState.Collapsed; 139 | treeItem.iconPath = new vscode.ThemeIcon('symbol-array'); 140 | treeItem.description = variableDescription(this.displayStyle, variable); 141 | treeItem.tooltip = variableTooltip(variable); 142 | treeItem.command = variable.location?.asOpenCommand(); 143 | treeItem.contextValue = this.contextValue; 144 | return treeItem; 145 | } 146 | 147 | override getChildren(): TreeItem[] { 148 | const variable = this.designation.variable; 149 | return Array.from(memoryRowIndices(variable)).map((index) => 150 | new ScalarTreeItem(this.provider, variable.designation(index), 'canWatch')); 151 | } 152 | 153 | getWatchItem(): IWatchItem { 154 | return { 155 | id: this.designation.variable.cxxrtlIdentifier 156 | }; 157 | } 158 | } 159 | 160 | class ScopeTreeItem extends TreeItem { 161 | constructor( 162 | provider: TreeDataProvider, 163 | readonly scope: Scope, 164 | readonly hasSiblings: boolean, 165 | ) { 166 | super(provider); 167 | } 168 | 169 | override async getTreeItem(): Promise { 170 | if (this.scope.name === '') { 171 | return new vscode.TreeItem('ʜɪᴇʀᴀʀᴄʜʏ', vscode.TreeItemCollapsibleState.Expanded); 172 | } else { 173 | const treeItem = new vscode.TreeItem(this.scope.name); 174 | treeItem.iconPath = new vscode.ThemeIcon('symbol-module'); 175 | if (this.scope instanceof ModuleScope) { 176 | treeItem.description = this.scope.moduleFullName.join('.'); 177 | } 178 | treeItem.tooltip = new vscode.MarkdownString(this.scope.fullName.join('.')); 179 | treeItem.tooltip.isTrusted = true; 180 | if (this.scope.location) { 181 | treeItem.tooltip.appendMarkdown(`\n\n- ${this.scope.location.asMarkdownLink()}`); 182 | if (this.scope instanceof ModuleScope && this.scope.moduleLocation) { 183 | treeItem.tooltip.appendMarkdown(`\n- ${this.scope.moduleLocation.asMarkdownLink()}`); 184 | } 185 | treeItem.command = this.scope.location.asOpenCommand(); 186 | } 187 | const [subScopes, variables] = await Promise.all([this.scope.scopes, this.scope.variables]); 188 | if (subScopes.length > 0 || variables.length > 0) { 189 | treeItem.collapsibleState = this.hasSiblings ? 190 | vscode.TreeItemCollapsibleState.Collapsed : 191 | vscode.TreeItemCollapsibleState.Expanded; 192 | } 193 | return treeItem; 194 | } 195 | } 196 | 197 | override async getChildren(): Promise { 198 | const [subScopes, variables] = await Promise.all([this.scope.scopes, this.scope.variables]); 199 | const children = []; 200 | for (const scope of subScopes) { 201 | children.push(new ScopeTreeItem(this.provider, scope, subScopes.length > 1)); 202 | } 203 | for (const variable of variables) { 204 | if (variable instanceof ScalarVariable) { 205 | children.push(new ScalarTreeItem(this.provider, variable.designation(), 206 | variable.width > 1 ? 'canWatch|canSetRadix' : 'canWatch')); 207 | } 208 | if (variable instanceof MemoryVariable) { 209 | children.push(new ArrayTreeItem(this.provider, variable.designation(), 'canWatch|canSetRadix')); 210 | } 211 | } 212 | return children; 213 | } 214 | } 215 | 216 | class WatchTreeItem extends TreeItem { 217 | constructor( 218 | provider: TreeDataProvider 219 | ) { 220 | super(provider); 221 | } 222 | 223 | override async getTreeItem(): Promise { 224 | if (globalWatchList.get().length > 0) { 225 | return new vscode.TreeItem('ᴡᴀᴛᴄʜ', vscode.TreeItemCollapsibleState.Expanded); 226 | } else { 227 | return new vscode.TreeItem('ᴡᴀᴛᴄʜ (empty)'); 228 | } 229 | } 230 | 231 | override async getChildren(): Promise { 232 | const children = []; 233 | for (const [index, watchItem] of globalWatchList.get().entries()) { 234 | const variable = await this.provider.getVariable(watchItem.id); 235 | if (variable === null) { 236 | continue; 237 | } 238 | let designation; 239 | if (variable instanceof ScalarVariable) { 240 | designation = variable.designation(); 241 | } else if (variable instanceof MemoryVariable) { 242 | if (watchItem.row === undefined) { 243 | designation = variable.designation(); 244 | } else { 245 | designation = variable.designation(watchItem.row); 246 | } 247 | } 248 | let treeItem; 249 | if (designation instanceof MemoryRangeDesignation) { 250 | treeItem = new ArrayTreeItem(this.provider, designation, 'inWatchList|canSetRadix'); 251 | } else if (designation instanceof ScalarDesignation || designation instanceof MemoryRowDesignation) { 252 | if (watchItem.bit === undefined) { 253 | treeItem = new ScalarTreeItem(this.provider, designation, 254 | designation.variable.width > 1 ? 'inWatchList|canSetRadix' : 'inWatchList'); 255 | } else { 256 | treeItem = new BitTreeItem(this.provider, designation, watchItem.bit, 'inWatchList'); 257 | } 258 | } 259 | if (treeItem !== undefined) { 260 | treeItem.metadata = { index }; 261 | children.push(treeItem); 262 | } 263 | } 264 | return children; 265 | } 266 | } 267 | 268 | export class TreeDataProvider implements vscode.TreeDataProvider { 269 | private _onDidChangeTreeData: vscode.EventEmitter = new vscode.EventEmitter(); 270 | readonly onDidChangeTreeData: vscode.Event = this._onDidChangeTreeData.event; 271 | 272 | private session: Session | null = null; 273 | private observer: Observer | null = null; 274 | private watchTreeItem: WatchTreeItem | null = null; 275 | private scopeTreeItem: ScopeTreeItem | null = null; 276 | 277 | constructor(rtlDebugger: CXXRTLDebugger) { 278 | vscode.workspace.onDidChangeConfiguration((event) => { 279 | if (event.affectsConfiguration('rtlDebugger.displayStyle') || 280 | event.affectsConfiguration('rtlDebugger.variableOptions')) { 281 | this._onDidChangeTreeData.fire(null); 282 | } 283 | }); 284 | rtlDebugger.onDidChangeSession(async (session) => { 285 | this.session = session; 286 | if (session !== null) { 287 | this.observer = new Observer(session, 'sidebar'); 288 | this.watchTreeItem = new WatchTreeItem(this); 289 | this.scopeTreeItem = new ScopeTreeItem(this, await session.getRootScope(), false); 290 | } else { 291 | this.observer?.dispose(); 292 | this.observer = null; 293 | this.watchTreeItem = null; 294 | this.scopeTreeItem = null; 295 | } 296 | this._onDidChangeTreeData.fire(null); 297 | }); 298 | globalWatchList.onDidChange((_items) => { 299 | if (this.watchTreeItem !== null) { 300 | this._onDidChangeTreeData.fire(this.watchTreeItem); 301 | } 302 | }); 303 | } 304 | 305 | getTreeItem(element: TreeItem): vscode.TreeItem | Thenable { 306 | return element.getTreeItem(); 307 | } 308 | 309 | async getChildren(element?: TreeItem): Promise { 310 | if (element !== undefined) { 311 | return await element.getChildren(); 312 | } 313 | const children = []; 314 | if (this.watchTreeItem !== null) { 315 | children.push(this.watchTreeItem); 316 | } 317 | if (this.scopeTreeItem !== null) { 318 | children.push(this.scopeTreeItem); 319 | } 320 | return children; 321 | } 322 | 323 | get displayStyle(): DisplayStyle { 324 | const displayStyle = vscode.workspace.getConfiguration('rtlDebugger').get('displayStyle'); 325 | return displayStyle as DisplayStyle; 326 | } 327 | 328 | getVariable(identifier: string): Promise { 329 | return this.session!.getVariable(identifier); 330 | } 331 | 332 | getValue(element: TreeItem, designation: Designation): T | undefined { 333 | if (this.observer === null) { 334 | return; 335 | } 336 | this.observer.observe(designation, (_value) => { 337 | this._onDidChangeTreeData.fire(element); 338 | return false; // one-shot 339 | }); 340 | return this.observer.query(designation); 341 | } 342 | } 343 | -------------------------------------------------------------------------------- /src/ui/status.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { CXXRTLDebugger } from '../debugger'; 3 | import { Session } from '../debug/session'; 4 | 5 | export class StatusBarItem { 6 | private statusItem: vscode.StatusBarItem; 7 | 8 | constructor(rtlDebugger: CXXRTLDebugger) { 9 | this.statusItem = vscode.window.createStatusBarItem('rtlDebugger', vscode.StatusBarAlignment.Left, 10); 10 | this.statusItem.tooltip = 'RTL Debugger Status'; 11 | this.statusItem.command = 'rtlDebugger.runPauseSimulation'; 12 | rtlDebugger.onDidChangeSession((session) => { 13 | this.update(session); 14 | if (session !== null) { 15 | session.onDidChangeSimulationStatus((_simulationStatus) => this.update(session)); 16 | session.onDidChangeTimeCursor((_timeCursor) => this.update(session)); 17 | } 18 | }); 19 | } 20 | 21 | dispose() { 22 | this.statusItem.dispose(); 23 | } 24 | 25 | private update(session: Session | null) { 26 | if (session === null) { 27 | this.statusItem.hide(); 28 | } else { 29 | this.statusItem.show(); 30 | if (session.simulationStatus.status === 'running') { 31 | this.statusItem.text = '$(debug-pause) '; 32 | this.statusItem.tooltip = 'RTL Debugger: Running'; 33 | } else if (session.simulationStatus.status === 'paused') { 34 | this.statusItem.text = '$(debug-continue) '; 35 | this.statusItem.tooltip = 'RTL Debugger: Paused'; 36 | } else if (session.simulationStatus.status === 'finished') { 37 | this.statusItem.text = ''; 38 | this.statusItem.tooltip = 'RTL Debugger: Finished'; 39 | } 40 | this.statusItem.text += `${session.timeCursor} / ${session.simulationStatus.latestTime}`; 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "CommonJS", 4 | "esModuleInterop": true, 5 | "target": "ES2022", 6 | "moduleResolution": "Node", 7 | "rootDir": "src", 8 | "noEmit": true, 9 | "strict": true, 10 | "noImplicitReturns": true, 11 | "noImplicitAny": true, 12 | "noImplicitOverride": true, 13 | "noImplicitThis": true 14 | } 15 | } 16 | --------------------------------------------------------------------------------