├── .cargo └── config.toml ├── .github ├── dependabot.yml.bak └── workflows │ ├── build.yml │ ├── docs.yml │ ├── release.yml │ └── vscode.yml ├── .gitignore ├── CHANGELOG.md ├── Cargo.lock ├── Cargo.toml ├── Cross.toml ├── LICENSE ├── README.md ├── docs ├── .gitignore ├── .npmignore ├── package-lock.json ├── package.json └── src │ ├── .vuepress │ ├── config.js │ ├── enhanceApp.js │ ├── public │ │ └── mos.png │ └── styles │ │ ├── index.styl │ │ └── palette.styl │ ├── guide │ ├── advanced.md │ ├── assembler.md │ ├── cli │ │ └── index.md │ ├── ide │ │ ├── index.md │ │ ├── mos-vscode.jpg │ │ └── vscode.md │ ├── index.md │ ├── project-setup.md │ └── unit-testing.md │ └── index.md ├── examples ├── atari800 │ └── colors │ │ ├── atari800.asm │ │ ├── main.asm │ │ └── mos.toml └── c64 │ ├── cartridge │ ├── main.asm │ └── mos.toml │ ├── scroller │ ├── main.asm │ └── mos.toml │ ├── shared │ └── c64.asm │ └── unit-testing │ ├── main.asm │ └── mos.toml ├── mos-core ├── Cargo.toml ├── src │ ├── cbm │ │ ├── mod.rs │ │ └── petscii.rs │ ├── codegen │ │ ├── analysis.rs │ │ ├── config_extractor.rs │ │ ├── config_validator.rs │ │ ├── evaluator.rs │ │ ├── mod.rs │ │ ├── opcodes.rs │ │ ├── program_counter.rs │ │ ├── segment.rs │ │ ├── source_map.rs │ │ ├── symbols.rs │ │ └── text_encoding.rs │ ├── errors.rs │ ├── formatting │ │ └── mod.rs │ ├── io │ │ ├── binary_writer.rs │ │ ├── listing.rs │ │ ├── mod.rs │ │ └── vice.rs │ ├── lib.rs │ ├── parser │ │ ├── ast.rs │ │ ├── code_map.rs │ │ ├── config_map.rs │ │ ├── identifier.rs │ │ ├── mnemonic.rs │ │ ├── mod.rs │ │ ├── source.rs │ │ └── testing.rs │ └── testing.rs └── test-data │ ├── build │ └── include.bin │ └── format │ ├── mos-default-formatting.toml │ ├── valid-formatted.asm │ └── valid-unformatted.asm ├── mos-testing ├── Cargo.toml └── src │ └── lib.rs ├── mos ├── Cargo.toml ├── src │ ├── commands │ │ ├── build.rs │ │ ├── format.rs │ │ ├── init.rs │ │ ├── init.toml │ │ ├── lsp.rs │ │ ├── mod.rs │ │ ├── test.rs │ │ └── version.rs │ ├── config.rs │ ├── debugger │ │ ├── adapters │ │ │ ├── mod.rs │ │ │ ├── test_runner │ │ │ │ └── mod.rs │ │ │ └── vice │ │ │ │ ├── mod.rs │ │ │ │ └── protocol.rs │ │ ├── connection.rs │ │ ├── mod.rs │ │ ├── protocol.rs │ │ └── types.rs │ ├── diagnostic_emitter.rs │ ├── lsp │ │ ├── code_lens.rs │ │ ├── completion.rs │ │ ├── documents.rs │ │ ├── formatting.rs │ │ ├── hover.rs │ │ ├── mod.rs │ │ ├── references.rs │ │ ├── rename.rs │ │ ├── semantic_highlighting.rs │ │ ├── symbols.rs │ │ ├── testing.rs │ │ └── traits.rs │ ├── main.rs │ ├── memory_accessor.rs │ ├── test_runner │ │ └── mod.rs │ └── utils.rs └── test-data │ ├── build │ ├── include.asm │ ├── include.bin │ ├── include.prg │ ├── multiple_segments.asm │ ├── multiple_segments.prg │ ├── valid.asm │ └── valid.prg │ └── test │ └── some-tests.asm └── vscode ├── .gitignore ├── .vscodeignore ├── README.md ├── bundle.js ├── clean.sh ├── icon.png ├── language-configuration.json ├── mos-version.js ├── package-lock.json ├── package.json ├── src ├── auto-update │ ├── download-binary.ts │ └── net.ts ├── build-task-provider.ts ├── extension.ts ├── log.ts └── run-all-tests-task-provider.ts ├── syntaxes └── asm.tmLanguage.json └── tsconfig.json /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [profile.release] 2 | lto = true 3 | 4 | [target.x86_64-pc-windows-msvc] 5 | rustflags = ["-Ctarget-feature=+crt-static"] -------------------------------------------------------------------------------- /.github/dependabot.yml.bak: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: cargo 4 | directory: "/" 5 | schedule: 6 | interval: weekly 7 | open-pull-requests-limit: 99 8 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | pull_request: 5 | 6 | jobs: 7 | check: 8 | name: Check 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | - uses: actions-rs/toolchain@v1 13 | with: 14 | profile: minimal 15 | toolchain: stable 16 | override: true 17 | - uses: Swatinem/rust-cache@v1 18 | - uses: actions-rs/cargo@v1 19 | with: 20 | command: check 21 | 22 | fmt: 23 | name: Rustfmt 24 | runs-on: ubuntu-latest 25 | steps: 26 | - uses: actions/checkout@v2 27 | - uses: actions-rs/toolchain@v1 28 | with: 29 | profile: minimal 30 | toolchain: stable 31 | override: true 32 | - run: rustup component add rustfmt 33 | - uses: Swatinem/rust-cache@v1 34 | - uses: actions-rs/cargo@v1 35 | with: 36 | command: fmt 37 | args: --all -- --check 38 | 39 | clippy: 40 | name: Clippy 41 | runs-on: ubuntu-latest 42 | steps: 43 | - uses: actions/checkout@v2 44 | - uses: actions-rs/toolchain@v1 45 | with: 46 | profile: minimal 47 | toolchain: stable 48 | override: true 49 | - run: rustup component add clippy 50 | - uses: Swatinem/rust-cache@v1 51 | - uses: actions-rs/cargo@v1 52 | with: 53 | command: clippy 54 | args: -- -D warnings 55 | 56 | build: 57 | name: build 58 | needs: ['check', 'fmt', 'clippy'] 59 | runs-on: ${{ matrix.os }} 60 | env: 61 | # For some builds, we use cross to test on 32-bit and big-endian 62 | # systems. 63 | CARGO: cargo 64 | # When CARGO is set to CROSS, this is set to `--target matrix.target`. 65 | TARGET_FLAGS: 66 | # When CARGO is set to CROSS, TARGET_DIR includes matrix.target. 67 | TARGET_DIR: ./target 68 | # Emit backtraces on panics. 69 | RUST_BACKTRACE: 1 70 | strategy: 71 | matrix: 72 | build: [linux, macos, win-msvc] 73 | include: 74 | - build: linux 75 | os: ubuntu-18.04 76 | rust: stable 77 | target: x86_64-unknown-linux-musl 78 | - build: macos 79 | os: macos-latest 80 | rust: stable 81 | target: x86_64-apple-darwin 82 | - build: win-msvc 83 | os: windows-2019 84 | rust: stable 85 | target: x86_64-pc-windows-msvc 86 | 87 | steps: 88 | - name: Checkout repository 89 | uses: actions/checkout@v2 90 | with: 91 | fetch-depth: 1 92 | 93 | - name: Install Rust 94 | uses: actions-rs/toolchain@v1 95 | with: 96 | toolchain: ${{ matrix.rust }} 97 | profile: minimal 98 | override: true 99 | target: ${{ matrix.target }} 100 | 101 | - uses: Swatinem/rust-cache@v1 102 | 103 | - name: Use Cross 104 | if: matrix.os != 'windows-2019' 105 | run: | 106 | cargo install cross 107 | echo "CARGO=cross" >> $GITHUB_ENV 108 | echo "TARGET_FLAGS=--target ${{ matrix.target }}" >> $GITHUB_ENV 109 | echo "TARGET_DIR=./target/${{ matrix.target }}" >> $GITHUB_ENV 110 | 111 | - name: Show command used for Cargo 112 | run: | 113 | echo "cargo command is: ${{ env.CARGO }}" 114 | echo "target flag is: ${{ env.TARGET_FLAGS }}" 115 | echo "target dir is: ${{ env.TARGET_DIR }}" 116 | 117 | - name: Run tests 118 | run: ${{ env.CARGO }} test --verbose --release ${{ env.TARGET_FLAGS }} 119 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: Publish on Netlify 2 | on: 3 | workflow_dispatch: 4 | push: 5 | tags: 6 | - '[0-9]+.[0-9]+.[0-9]+' 7 | 8 | jobs: 9 | publish: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@master 14 | - uses: actions/setup-node@v2 15 | - run: npm install 16 | working-directory: docs 17 | 18 | - name: Build docs 19 | working-directory: docs 20 | run: npm run build 21 | 22 | - name: Deploy to netlify 23 | uses: netlify/actions/cli@master 24 | env: 25 | NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} 26 | NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }} 27 | with: 28 | args: deploy --dir=docs/src/.vuepress/dist --prod -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # Shamelessly stolen from https://raw.githubusercontent.com/BurntSushi/ripgrep/master/.github/workflows/release.yml 2 | # 3 | # The way this works is a little weird. But basically, the create-release job 4 | # runs purely to initialize the GitHub release itself. Once done, the upload 5 | # URL of the release is saved as an artifact. 6 | # 7 | # The build-release job runs only once create-release is finished. It gets 8 | # the release upload URL by downloading the corresponding artifact (which was 9 | # uploaded by create-release). It then builds the release executables for each 10 | # supported platform and attaches them as release assets to the previously 11 | # created release. 12 | # 13 | # The key here is that we create the release only once. 14 | 15 | name: Release 16 | on: 17 | push: 18 | branches: 19 | - dev-workflows 20 | tags: 21 | - '[0-9]+.[0-9]+.[0-9]+' 22 | jobs: 23 | create-release: 24 | name: create-release 25 | runs-on: ubuntu-latest 26 | steps: 27 | - name: Force release version number on the dev-workflows branch 28 | run: | 29 | if [[ $GITHUB_REF = 'refs/heads/dev-workflows' ]]; then 30 | echo "MOS_VERSION=TEST-0.0.0" >> "$GITHUB_ENV" 31 | fi 32 | 33 | - name: Create artifacts directory 34 | run: mkdir artifacts 35 | 36 | - name: Get the release version from the tag 37 | if: env.MOS_VERSION == '' 38 | run: | 39 | # Apparently, this is the right way to get a tag name. Really? 40 | # 41 | # See: https://github.community/t5/GitHub-Actions/How-to-get-just-the-tag-name/m-p/32167/highlight/true#M1027 42 | echo "MOS_VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV 43 | echo "version is: ${{ env.MOS_VERSION }}" 44 | 45 | - name: Create GitHub release 46 | id: release 47 | uses: actions/create-release@v1 48 | env: 49 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 50 | with: 51 | tag_name: ${{ env.MOS_VERSION }} 52 | release_name: ${{ env.MOS_VERSION }} 53 | body: See [changelog](https://github.com/datatrash/mos/blob/${{ env.MOS_VERSION }}/CHANGELOG.md) for details. 54 | 55 | - name: Save release upload URL to artifact 56 | run: echo "${{ steps.release.outputs.upload_url }}" > artifacts/release-upload-url 57 | 58 | - name: Save version number to artifact 59 | run: echo "${{ env.MOS_VERSION }}" > artifacts/release-version 60 | 61 | - name: Upload artifacts 62 | uses: actions/upload-artifact@v1 63 | with: 64 | name: artifacts 65 | path: artifacts 66 | 67 | build-release: 68 | name: build-release 69 | needs: ['create-release'] 70 | runs-on: ${{ matrix.os }} 71 | env: 72 | # For some builds, we use cross to test on 32-bit and big-endian 73 | # systems. 74 | CARGO: cargo 75 | # When CARGO is set to CROSS, this is set to `--target matrix.target`. 76 | TARGET_FLAGS: 77 | # When CARGO is set to CROSS, TARGET_DIR includes matrix.target. 78 | TARGET_DIR: ./target 79 | # Emit backtraces on panics. 80 | RUST_BACKTRACE: 1 81 | strategy: 82 | matrix: 83 | build: [linux, macos, win-msvc] 84 | include: 85 | - build: linux 86 | os: ubuntu-18.04 87 | rust: stable 88 | target: x86_64-unknown-linux-musl 89 | - build: macos 90 | os: macos-latest 91 | rust: stable 92 | target: x86_64-apple-darwin 93 | - build: win-msvc 94 | os: windows-2019 95 | rust: stable 96 | target: x86_64-pc-windows-msvc 97 | 98 | steps: 99 | - name: Checkout repository 100 | uses: actions/checkout@v2 101 | with: 102 | fetch-depth: 1 103 | 104 | - name: Install Rust 105 | uses: actions-rs/toolchain@v1 106 | with: 107 | toolchain: ${{ matrix.rust }} 108 | profile: minimal 109 | override: true 110 | target: ${{ matrix.target }} 111 | 112 | - name: Use Cross 113 | if: matrix.os != 'windows-2019' 114 | run: | 115 | cargo install cross 116 | echo "CARGO=cross" >> $GITHUB_ENV 117 | echo "TARGET_FLAGS=--target ${{ matrix.target }}" >> $GITHUB_ENV 118 | echo "TARGET_DIR=./target/${{ matrix.target }}" >> $GITHUB_ENV 119 | 120 | - name: Show command used for Cargo 121 | run: | 122 | echo "cargo command is: ${{ env.CARGO }}" 123 | echo "target flag is: ${{ env.TARGET_FLAGS }}" 124 | echo "target dir is: ${{ env.TARGET_DIR }}" 125 | 126 | - name: Get release download URL 127 | uses: actions/download-artifact@v1 128 | with: 129 | name: artifacts 130 | path: artifacts 131 | 132 | - name: Set release upload URL and release version 133 | shell: bash 134 | run: | 135 | release_upload_url="$(cat artifacts/release-upload-url)" 136 | echo "RELEASE_UPLOAD_URL=$release_upload_url" >> $GITHUB_ENV 137 | echo "release upload url: $RELEASE_UPLOAD_URL" 138 | release_version="$(cat artifacts/release-version)" 139 | echo "RELEASE_VERSION=$release_version" >> $GITHUB_ENV 140 | echo "release version: $RELEASE_VERSION" 141 | 142 | - name: Build release binary 143 | run: ${{ env.CARGO }} build --verbose --release ${{ env.TARGET_FLAGS }} 144 | 145 | - name: Strip release binary (linux and macos) 146 | if: matrix.build == 'linux' || matrix.build == 'macos' 147 | run: strip "${{ env.TARGET_DIR }}/release/mos" 148 | 149 | - name: Build archive 150 | shell: bash 151 | run: | 152 | staging="mos-${{ env.RELEASE_VERSION }}-${{ matrix.target }}" 153 | mkdir -p "$staging" 154 | 155 | cp {CHANGELOG.md,LICENSE,README.md} "$staging/" 156 | cp -R examples "$staging/" 157 | 158 | if [ "${{ matrix.os }}" = "windows-2019" ]; then 159 | cp "${{ env.TARGET_DIR }}/release/mos.exe" "$staging/" 160 | 7z a "$staging.zip" .\\"$staging"\\* 161 | echo "ASSET=$staging.zip" >> $GITHUB_ENV 162 | else 163 | cp "${{ env.TARGET_DIR }}/release/mos" "$staging/" 164 | cd "$staging" 165 | tar czf "../$staging.tar.gz" . 166 | cd .. 167 | echo "ASSET=$staging.tar.gz" >> $GITHUB_ENV 168 | fi 169 | 170 | - name: Upload release archive 171 | uses: actions/upload-release-asset@v1.0.1 172 | env: 173 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 174 | with: 175 | upload_url: ${{ env.RELEASE_UPLOAD_URL }} 176 | asset_path: ${{ env.ASSET }} 177 | asset_name: ${{ env.ASSET }} 178 | asset_content_type: application/octet-stream -------------------------------------------------------------------------------- /.github/workflows/vscode.yml: -------------------------------------------------------------------------------- 1 | name: Publish VSCode Extension 2 | on: 3 | workflow_dispatch: 4 | inputs: 5 | mos_version: 6 | description: 'The version of the MOS binary to download' 7 | required: true 8 | extension_version: 9 | description: 'The version of the extension to release' 10 | required: true 11 | 12 | jobs: 13 | publish: 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@master 18 | - uses: actions/setup-node@v2 19 | - run: npm install 20 | working-directory: vscode 21 | 22 | - name: Publish 23 | working-directory: vscode 24 | env: 25 | VSCE_PAT: ${{ secrets.VSCE_PAT }} 26 | MOS_VERSION: ${{ github.event.inputs.mos_version }} 27 | run: npm run publish -- ${{ github.event.inputs.extension_version }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /**/target 2 | /**/.vscode 3 | /.idea 4 | .DS_Store -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 0.8.2 (2022-10-06) 4 | 5 | ### New features 6 | - Allow setting registers when debugging, once again thanks to [@themkat](https://github.com/themkat)! ([#110](https://github.com/datatrash/mos/issues/110)) 7 | 8 | ## 0.8.1 (2022-09-02) 9 | 10 | ### New features 11 | * Autocompletion for mnemonics, thanks again [@themkat](https://github.com/themkat)! ([#196](https://github.com/datatrash/mos/issues/196)) 12 | 13 | ## 0.8.0 (2022-08-31) 14 | 15 | ### New features 16 | * There is now also code completion in the debugger, thanks [@themkat](https://github.com/themkat)! ([#166](https://github.com/datatrash/mos/issues/166)) 17 | 18 | ## 0.7.5 (2021-11-16) 19 | 20 | ### Bugfixes 21 | * 'Branch too far' errors could trigger based on label positions in previous passes. 22 | 23 | ## 0.7.4 (2021-07-22) 24 | 25 | ### Bugfixes 26 | * `Unknown identifier` errors were emitted too often. ([#211](https://github.com/datatrash/mos/issues/211)) 27 | 28 | ## 0.7.3 (2021-07-14) 29 | 30 | ### Bugfixes 31 | * When setting breakpoints, the breakpoint should be set at the location the code is ultimately designed to go to. Usually this is the same as the location it is emitted, but this is not the case for code that is designed to be moved to another location. ([#206](https://github.com/datatrash/mos/issues/206)) 32 | * A unexpected closing brace would break parsing of the rest of the file.([#205](https://github.com/datatrash/mos/issues/205)) 33 | 34 | ## 0.7.2 (2021-07-08) 35 | 36 | ### New features 37 | * The `mos.path` property may now be quoted, easing use on Windows. ([#204](https://github.com/datatrash/mos/issues/204)) 38 | 39 | ### Bugfixes 40 | * When launching the debugger no files have to be open in the editor. ([#203](https://github.com/datatrash/mos/issues/203)) 41 | 42 | ## 0.7.1 (2021-07-01) 43 | 44 | ### Bugfixes 45 | * Trying to invoke an undefined macro correctly did not report the macro as undefined ([#195](https://github.com/datatrash/mos/issues/195)) 46 | 47 | ## 0.7.0 (2021-06-18) 48 | 49 | ### New features 50 | * "Banks" can be used to control how the final binary is laid out (e.g. to create cartridge files) 51 | * Strings can be used as symbols, with support for string interpolation and concatenation 52 | * Error formatting is improved and made configurable through the `--error-style` option 53 | * A listing file can be generated for every segment by specifying `listing = true` in the `[build]` section of `mos.toml` 54 | * Instead of just generating a Commodore-compatible `.prg` file, it is now also possible to generate a raw binary or a raw binary per segment 55 | 56 | ### Notable bugfixes 57 | * Nested macros are now looked up correctly ([#168](https://github.com/datatrash/mos/issues/168)) 58 | * Symbol resolution doesn't return incorrect symbols when lookups fail halfway through 59 | * Error reporting shows correct paths on Windows now, instead of UNC paths ([#192](https://github.com/datatrash/mos/issues/192)) 60 | * Files imported from subdirectories are resolved in a platform-independent way (i.e. forward or backslashes may be used) 61 | * Uninvoked macros and untaken if/else paths now get proper refactoring support ([#180](https://github.com/datatrash/mos/issues/180)) 62 | * When attempting to debug with an unsupported version of VICE a proper error message is created ([#186](https://github.com/datatrash/mos/issues/186)) 63 | * Syntax grammar files were not packaged in the VSCode extension ([#185](https://github.com/datatrash/mos/issues/185)) 64 | 65 | For a full list of changes please refer to the [0.7.0 milestone on GitHub](https://github.com/datatrash/mos/milestone/8?closed=1). 66 | 67 | ## 0.6.0 (2021-06-02) 68 | 69 | ### New features 70 | * Support for writing unit tests 71 | * Support for running unit tests in an emulated 6502 72 | * Support for running and debugging unit tests from within Visual Studio Code 73 | * Symbols may be annotated with markdown comments, which will show on hover 74 | 75 | ### Bugfixes 76 | * Various scoping issues have been resolved 77 | 78 | ## 0.5.0 (2021-05-25) 79 | 80 | ### New features 81 | * Support for debugging in Visual Studio Code (via the Debug Adapter protocol) 82 | * Formatting now works with label/code/comment columns 83 | * Can now use - and + to refer to start and end of scopes 84 | * Visual Studio Code: Now provides a 'build' task and associated problem matcher 85 | 86 | ### Bugfixes 87 | * Fixed a number of edge cases related to code completion 88 | * Code completion doesn't seem to work inside a (segment) block 89 | * Fix forward references to other segments 90 | * Fix infinite loop with label before end of block 91 | * Formatting: No empty lines after a non-block label 92 | * Formatting: Empty line after a block 93 | * C/C++-style comments are not highlighted nicely 94 | * LSP not shutting down correctly 95 | * Error output is not sorted by line/file 96 | 97 | ## 0.4.1 (2021-03-26) 98 | 99 | ### Bugfixes 100 | * Formatting now works correctly for projects containing multiple files 101 | 102 | ## 0.4.0 (2021-03-24) 103 | 104 | ## 0.3.1 (2021-03-21) 105 | 106 | ### Bugfixes 107 | * Linux version now shows correct version information 108 | 109 | ## 0.3.0 (2021-03-16) 110 | 111 | ### New features 112 | * Support for macros 113 | * Support for loops 114 | * Can now use `-` and `+` to refer to start and end of scopes 115 | * Visual Studio Code: Now provides a 'build' task and associated problem matcher 116 | 117 | ### Bugfixes 118 | * Rewrote code generation to be both simpler and more accurate 119 | * The `no-color` parameter now works as expected 120 | 121 | ## 0.2.0 (2021-03-11) 122 | 123 | ### New features 124 | * Project configuration file is now required in lieu of command-line arguments 125 | * Language Server support 126 | 127 | ## 0.1.0 (2021-02-22) 128 | 129 | First public alpha release! 130 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "mos", 4 | "mos-core", 5 | "mos-testing" 6 | ] 7 | 8 | [profile.dev] 9 | split-debuginfo = "unpacked" 10 | -------------------------------------------------------------------------------- /Cross.toml: -------------------------------------------------------------------------------- 1 | [build.env] 2 | passthrough = [ 3 | "RELEASE_VERSION" 4 | ] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Roy Jacobs 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mos' dev! 2 | 3 | MOS logo 4 | 5 | **mos** is a tool suite for the MOS 6502 (and compatible) CPU written in Rust. More details on the [official site](https://mos.datatra.sh). 6 | 7 | *** 8 | 9 | ### Building the assembler from source (Linux/MacOS/Windows): 10 | 11 | * Ensure this is green: [![Build status](https://github.com/datatrash/mos/workflows/CI/badge.svg)](https://github.com/datatrash/mos/actions) 12 | * Install [Rust](https://rustup.rs/) 13 | * Clone this repository 14 | * `cargo build --release` 15 | * The `mos` executable will be in `target/release` 16 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | pids 2 | logs 3 | node_modules 4 | npm-debug.log 5 | coverage/ 6 | run 7 | dist 8 | .DS_Store 9 | .nyc_output 10 | .basement 11 | config.local.js 12 | basement_dist 13 | -------------------------------------------------------------------------------- /docs/.npmignore: -------------------------------------------------------------------------------- 1 | pids 2 | logs 3 | node_modules 4 | npm-debug.log 5 | coverage/ 6 | run 7 | dist 8 | .DS_Store 9 | .nyc_output 10 | .basement 11 | config.local.js 12 | basement_dist 13 | -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mos", 3 | "version": "0.0.1", 4 | "description": "MOS - 6502, modernized", 5 | "main": "index.js", 6 | "authors": { 7 | "name": "Roy Jacobs", 8 | "email": "roy.jacobs@gmail.com" 9 | }, 10 | "repository": "https://github.com/datatrash/mos/mos", 11 | "scripts": { 12 | "dev": "vuepress dev src", 13 | "build": "vuepress build src" 14 | }, 15 | "license": "MIT", 16 | "devDependencies": { 17 | "@vuepress/plugin-active-header-links": "^1.8.2", 18 | "vuepress": "^1.5.3", 19 | "vuepress-plugin-sitemap": "^2.3.1" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /docs/src/.vuepress/config.js: -------------------------------------------------------------------------------- 1 | const { description } = require('../../package') 2 | 3 | module.exports = { 4 | /** 5 | * Ref:https://v1.vuepress.vuejs.org/config/#title 6 | */ 7 | title: 'MOS', 8 | /** 9 | * Ref:https://v1.vuepress.vuejs.org/config/#description 10 | */ 11 | description: description, 12 | 13 | /** 14 | * Extra tags to be injected to the page HTML `` 15 | * 16 | * ref:https://v1.vuepress.vuejs.org/config/#head 17 | */ 18 | head: [ 19 | ['meta', { name: 'theme-color', content: '#af3e3e' }], 20 | ['meta', { name: 'apple-mobile-web-app-capable', content: 'yes' }], 21 | ['meta', { name: 'apple-mobile-web-app-status-bar-style', content: 'black' }] 22 | ], 23 | 24 | /** 25 | * Theme configuration, here is the default theme configuration for VuePress. 26 | * 27 | * ref:https://v1.vuepress.vuejs.org/theme/default-theme-config.html 28 | */ 29 | themeConfig: { 30 | repo: 'https://github.com/datatrash/mos', 31 | editLinks: false, 32 | docsDir: 'docs/src', 33 | editLinkText: '', 34 | lastUpdated: false, 35 | nav: [ 36 | { 37 | text: 'Guide', 38 | link: '/guide/', 39 | } 40 | ], 41 | sidebar: { 42 | '/guide/': [ 43 | { 44 | title: 'Getting started', 45 | collapsable: false, 46 | children: [ 47 | '', 48 | 'project-setup', 49 | 'assembler', 50 | 'advanced', 51 | 'unit-testing' 52 | ] 53 | }, 54 | { 55 | title: 'IDE support', 56 | collapsable: false, 57 | children: [ 58 | 'ide/', 59 | 'ide/vscode' 60 | ] 61 | }, 62 | { 63 | title: 'CLI', 64 | collapsable: false, 65 | children: [ 66 | 'cli/' 67 | ] 68 | } 69 | ], 70 | } 71 | }, 72 | 73 | markdown: { 74 | lineNumbers: true 75 | }, 76 | 77 | /** 78 | * Apply plugins,ref:https://v1.vuepress.vuejs.org/zh/plugin/ 79 | */ 80 | plugins: [ 81 | '@vuepress/plugin-back-to-top', 82 | '@vuepress/plugin-medium-zoom', 83 | '@vuepress/active-header-links', 84 | [ 'sitemap', { hostname: 'http://mos.datatra.sh' } ] 85 | ] 86 | } 87 | -------------------------------------------------------------------------------- /docs/src/.vuepress/enhanceApp.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Client app enhancement file. 3 | * 4 | * https://v1.vuepress.vuejs.org/guide/basic-config.html#app-level-enhancements 5 | */ 6 | 7 | export default ({ 8 | Vue, // the version of Vue being used in the VuePress app 9 | options, // the options for the root Vue instance 10 | router, // the router instance for the app 11 | siteData // site metadata 12 | }) => { 13 | // ...apply enhancements for the site. 14 | } 15 | -------------------------------------------------------------------------------- /docs/src/.vuepress/public/mos.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datatrash/mos/12aa86cb635c89d7a920d36c533dc70ae6ebbf38/docs/src/.vuepress/public/mos.png -------------------------------------------------------------------------------- /docs/src/.vuepress/styles/index.styl: -------------------------------------------------------------------------------- 1 | /** 2 | * Custom Styles here. 3 | * 4 | * ref:https://v1.vuepress.vuejs.org/config/#index-styl 5 | */ 6 | 7 | .home .hero img 8 | max-width 450px 9 | -------------------------------------------------------------------------------- /docs/src/.vuepress/styles/palette.styl: -------------------------------------------------------------------------------- 1 | /** 2 | * Custom palette here. 3 | * 4 | * ref:https://v1.vuepress.vuejs.org/zh/config/#palette-styl 5 | */ 6 | 7 | $accentColor = #af3e3e 8 | $textColor = #2c3e50 9 | $borderColor = #eaecef 10 | $codeBgColor = #282c34 11 | -------------------------------------------------------------------------------- /docs/src/guide/assembler.md: -------------------------------------------------------------------------------- 1 | # Assembler syntax 2 | The assembler allows you to write regular 6502 assembly instructions. However, some more powerful features are of course also available. 3 | 4 | ## Labels 5 | Labels can be defined to make it easier to refer to memory locations. Labels should consist of a valid identifier followed by a colon. A valid identifier starts with a character or underscore and may contain only characters, underscores or numbers. 6 | 7 | For example, a label can be used to loop: 8 | ```asm6502 9 | ldx #$20 10 | label: 11 | dex 12 | bne label 13 | ``` 14 | 15 | You can also add braces after the label to aid formatting: 16 | ```asm6502 17 | ldx #$20 18 | label: { 19 | dex 20 | bne label 21 | } 22 | ``` 23 | 24 | ## Variables and constants 25 | You can define variables and constants using the `.var` and `.const` directives respectively. They can then be used in expressions. 26 | 27 | For example: 28 | ```asm6502 29 | .const BORDER_COLOUR = $d020 30 | 31 | lda #7 32 | sta BORDER_COLOUR 33 | ``` 34 | 35 | Variables may be redefined, constants may not. 36 | 37 | ## Symbol scopes 38 | Labels, variables and constants are _symbols_. Symbols are defined in _scopes_. A scope is defined by a _block_ (starting with `{` and ending with `}`). As long as symbols reside in different scopes they can have duplicate names. You can use the `super` keyword to access symbols in outer scopes. 39 | 40 | Sounds complicated perhaps, so here's an example: 41 | ```asm6502 42 | label: { 43 | label: { 44 | jmp label 45 | jmp super.label 46 | } 47 | } 48 | ``` 49 | 50 | The first `jmp` will jump to the innermost label. The second `jmp` will jump to the outermost label. 51 | 52 | ## Automatic symbols 53 | Some symbols are generated for you automatically. 54 | 55 | ### `-` and `+` 56 | You can use `-` or `+` to refer to the start or the end of a block. 57 | 58 | For instance, the following code loops 64 times: 59 | ```asm6502 60 | ldx #$40 61 | { 62 | dex 63 | bne - 64 | } 65 | ``` 66 | 67 | ## Loops 68 | Loops may be generated using the `.loop` directive: 69 | 70 | ```asm6502 71 | .loop 5 { 72 | lda #$00 73 | sta $0400 + index 74 | } 75 | ``` 76 | 77 | The `index` symbol contains the current loop index (zero-based). 78 | 79 | ## Expressions 80 | Simple calculations may be performed. 81 | 82 | This program: 83 | ```asm6502 84 | lda #5 + 10 85 | ``` 86 | 87 | Is equal to: 88 | ```asm6502 89 | lda #15 90 | ``` 91 | 92 | ### Operators 93 | Supported operators are: 94 | - `*` Multiplication 95 | - `/` Division 96 | - `%` Modulo 97 | - `<<` Shift left 98 | - `>>` Shift right 99 | - `^` Exclusive or (XOR) 100 | - `+` Addition 101 | - `-` Subtraction 102 | 103 | You can also use parentheses, for example: 104 | 105 | ```asm6502 106 | lda #(3 + 4) * 5 107 | ``` 108 | 109 | ### Equality tests 110 | Equality tests may also be performed. They will evaluate to `0` when false and `1` when true: 111 | - `==` Equality 112 | - `!=` Inequality 113 | - `>` Greater than 114 | - `>=` Greater than or equal 115 | - `<` Less than 116 | - `<=` Less than or equal 117 | - `&&` And 118 | - `||` Or 119 | 120 | ### Word operators 121 | Additionally, the high or low byte of 16-bit variables may be accessed using the `<` and `>` modifiers, e.g.: 122 | 123 | ```asm6502 124 | .const ADDRESS = $1234 125 | lda #
ADDRESS // x will now contain '$12' 127 | ``` 128 | 129 | ### Modifiers 130 | There are two ways to modify a factor in an expression. 131 | 132 | - Prefix with `!` to convert `0` into `1` and any positive number into `0` 133 | - Prefix with `-` to negate the value 134 | 135 | ### Built-in functions 136 | Currently the only built-in function is `defined` which evaluates to `1` if a variable or constant is defined and to `0` otherwise. 137 | 138 | ```asm6502 139 | .const ADDRESS = $1234 140 | lda defined(ADDRESS) // a will now contain '1' 141 | lda !defined(ADDRESS) // a will now contain '0' 142 | ``` 143 | 144 | ## String handling 145 | It is possible to use strings in expressions and in variable definitions, e.g.: 146 | 147 | ```asm6502 148 | .const MY_HELLO = "hello" 149 | .const MY_WORLD = " world" 150 | .const GREETING = MY_HELLO + MY_WORLD 151 | ``` 152 | 153 | It is also possible to do string interpolation using curly braces, e.g.: 154 | 155 | ```asm6502 156 | .const MY_HELLO = "hello" 157 | .const GREETING = "{MY_HELLO} world" // <-- results in "hello world" 158 | ``` 159 | 160 | ## Data definition 161 | You may include data inline like so: 162 | 163 | ```asm6502 164 | .byte 1, 2, 3 165 | ``` 166 | 167 | Supported data types are `.byte`, `.word` and `.dword`. 168 | 169 | ## Text definition 170 | You may include text inline like so: 171 | 172 | ```asm6502 173 | .text "hello" 174 | ``` 175 | 176 | The default encoding is `ascii`. You may also use the encodings `petscii` or `petscreen`. The latter emits Commodore-compatible screen codes. For example: 177 | 178 | ```asm6502 179 | .text petscreen "abc" // This emits $01, $02, $03 180 | ``` 181 | 182 | ### Including from files 183 | It is also possible to include data from files, like so: 184 | 185 | ```asm6502 186 | .file "foo.bin" 187 | ``` 188 | 189 | The file is located relative to the source file that contains the `.file` directive. 190 | 191 | ## Comments 192 | Lines may end with a C++-style `//` comment, like so: 193 | 194 | ```asm6502 195 | nop // hello, I am a comment 196 | ``` 197 | 198 | C-style comment blocks are also supported: 199 | ```asm6502 200 | /* 201 | hello there! 202 | */ 203 | nop 204 | ``` 205 | 206 | C-style comments may also be nested. 207 | 208 | ## Conditional assembly 209 | It is possible to conditionally assemble chunks of code by wrapping them in an `.if` block: 210 | 211 | ```asm6502 212 | .const FOO = 1 213 | 214 | .if defined(FOO) { 215 | nop 216 | } else { 217 | brk 218 | } 219 | ``` 220 | 221 | The `else` clause is optional. 222 | 223 | ## Program counter 224 | During assembly it is possible to change the current program counter (i.e. the location where instructions are assembled to). 225 | 226 | ### Setting 227 | You can set the program counter with the `*` directive, like so: 228 | 229 | ```asm6502 230 | * = $0800 231 | ``` 232 | 233 | ### Aligning 234 | You can move the program counter forward to make it align on a certain number of bytes, using the `.align` directive: 235 | 236 | ```asm6502 237 | .align 256 238 | nop // This will always be assembled to $xx00 239 | ``` 240 | -------------------------------------------------------------------------------- /docs/src/guide/cli/index.md: -------------------------------------------------------------------------------- 1 | # Command-line interface 2 | 3 | The MOS binary has a command-line interface. `mos` can be invoked with various subcommands. 4 | 5 | Many of these subcommands don't have any parameters, since all configuration is read from the `mos.toml` project configuration file. 6 | 7 | ## build 8 | To build your application call `mos build`. 9 | 10 | ## format 11 | To format the source code of your application in-place, call `mos format`. 12 | 13 | ## init 14 | To create an empty project configuration, file, call `mos init`. 15 | 16 | ## lsp 17 | To launch the [Language Server](https://microsoft.github.io/language-server-protocol/), call `mos lsp`. Typically you will only do this if you are developing an IDE plugin. -------------------------------------------------------------------------------- /docs/src/guide/ide/index.md: -------------------------------------------------------------------------------- 1 | # IDE support 2 | 3 | MOS provides a language server and debug adapter that can be used by IDEs to provide tooling. 4 | 5 | It is used to provide a Visual Studio Code extension, and also [an Emacs package](https://github.com/themkat/mos-mode). The Emacs package provides the same features as the Visual Studio Code extension, and you can refer to the VSCode documentation for details. 6 | 7 | ![Showing debugging in action](./mos-vscode.jpg) -------------------------------------------------------------------------------- /docs/src/guide/ide/mos-vscode.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datatrash/mos/12aa86cb635c89d7a920d36c533dc70ae6ebbf38/docs/src/guide/ide/mos-vscode.jpg -------------------------------------------------------------------------------- /docs/src/guide/ide/vscode.md: -------------------------------------------------------------------------------- 1 | # Visual Studio Code extension 2 | 3 | MOS has a Visual Studio Code extension. Follow the [project setup guide](../#visual-studio-code-extension) to install it. 4 | 5 | Once you have it installed, the following features will become available: 6 | * Building your program 7 | * Launching and debugging your program in VICE 8 | * Syntax highlighting 9 | * Find usages 10 | * Go to definition 11 | * Format document 12 | * Format on-type 13 | * Automatic indentation 14 | * Show function documentation on hover 15 | 16 | The debugger supports breakpoints, local symbols, watches, setting variable values and evaluating expressions. 17 | 18 | ::: danger 19 | The debugger requires VICE 3.5+, which introduces the `-binarymonitor` command line argument. 20 | ::: 21 | 22 | ## Building and launching 23 | To build your application from within VSCode, use the `mos: Build` build command. 24 | 25 | You can also automatically create a `launch.json` configuration file, but here is a manual configuration: 26 | 27 | ```json 28 | { 29 | "version": "0.2.0", 30 | "configurations": [ 31 | { 32 | "type": "mos", 33 | "request": "launch", 34 | "name": "Launch", 35 | "workspace": "${workspaceFolder}", 36 | "preLaunchTask": "mos: Build", 37 | "vicePath": "", 38 | } 39 | ] 40 | } 41 | ``` 42 | 43 | If you specify a valid path to a VICE executable in the `vicePath` option you can launch or debug your application from within Visual Studio Code. 44 | 45 | ## Running tests 46 | The extension provides a "run all tests" extension that allows you to run all tests in your project. 47 | 48 | When you define a test (using the `.test` directive) a CodeLens will appear which will allow you to run or debug that specific test using an emulated 6502. 49 | 50 | ## Showing documentation on hover 51 | You can annotate a symbol with a special `///` comment syntax. When you hover over the symbol somewhere in your code the comment will be shown as markdown. 52 | 53 | So, for instance: 54 | ```asm6502 55 | /// This subroutine is **awesome**. 56 | my_subroutine: { 57 | rts 58 | } 59 | 60 | jsr my_subroutine // <-- hovering over my_subroutine will show the documentation 61 | ``` 62 | 63 | ## Watch expressions 64 | You can add watch expressions to keep track of the value of symbols whenever you hit a breakpoint. 65 | 66 | You can also access a few extra symbols that allow you to access CPU registers and memory locations: 67 | 68 | | Key | Description | 69 | |-----------------------------|-------------------------------------------| 70 | | cpu.a | The accumulator (A) register | 71 | | cpu.x | The X register | 72 | | cpu.y | The Y register | 73 | | cpu.sp | The stack pointer | 74 | | * | The program counter | 75 | | cpu.flags.zero | The Z flag | 76 | | cpu.flags.carry | The C flag | 77 | | cpu.flags.interrupt_disable | The I flag | 78 | | cpu.flags.decimal | The D flag | 79 | | cpu.flags.overflow | The V flag | 80 | | cpu.flags.negative | The N flag | 81 | | ram(...) | Read a byte from ram, e.g. `ram($d020)` | 82 | | ram16(...) | Read a word from ram, e.g. `ram16($0314)` | 83 | 84 | ## Set variable values during debugging 85 | ::: danger 86 | Currently you are only able to set register values. Other types of variables will give an error! 87 | ::: 88 | 89 | You can set the value of variables, either as decimal-, hexadecimal- or binary value. You specify the type or value with a prefix. 90 | 91 | | Prefix | Type | Example | 92 | |--------|-------------|-----------| 93 | | | Decimal | 123 | 94 | | $ | Hexadecimal | $d01 | 95 | | % | Binary | %00010010 | 96 | 97 | 98 | 99 | ## Options 100 | The following plugin options are available: 101 | 102 | | Key | Type | Description | 103 | | --- | ---- | ----------- | 104 | | `mos.path` | Path | By default, the extension will automatically download and update the `mos` executable. If for some reason you want to use your own `mos` executable you can fill in the path to this executable here. | -------------------------------------------------------------------------------- /docs/src/guide/index.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | MOS is an assembler targeting the MOS 6502 CPU. 3 | 4 | ::: danger 5 | **This documentation is preliminary.** 6 | 7 | It's just here to make sure current features at least have _some_ documentation. Everything is subject to change and will hopefully improve significantly :smile: 8 | ::: 9 | 10 | # Getting started 11 | Let's dive into the **MOS** pit :metal: (...sorry). 12 | 13 | ## Getting the tools 14 | There are two ways to work with MOS. You can download the CLI executable, but a quicker way is to install the Visual Studio Code extension. 15 | 16 | ### Visual Studio Code extension 17 | Installing the extension consists of these steps: 18 | * Open Visual Studio Code 19 | * In the `Extensions` menu, install the extension called `mos`. 20 | * Open a folder and place an empty `mos.toml` in this folder. This is the main configuration file for `mos` and the presence of this file will activate the extension. 21 | * The extension will ask if you want to install the `mos` executable automatically. Allow it to do so. 22 | * Done! If you'd like to know more, you can find more detailed documentation [here](./ide/vscode). 23 | 24 | ### Command line 25 | You can download the latest release of MOS [here](https://github.com/datatrash/mos/releases). 26 | 27 | Extracting the archive will leave you with a binary called `mos`. You can put it in some easily accessible place. The rest of the documentation will assume it's in your `PATH`. 28 | 29 | The current version of MOS only generates Commodore-compatible `.prg` files. So, let's try to build a tiny Commodore program. 30 | 31 | ## Building a sample application 32 | Let's create a new file called `main.asm` and fill it with these contents: 33 | ```asm6502 34 | inc $d020 35 | rts 36 | ``` 37 | This is a tiny program that changes the border colour on a Commodore 64. 38 | 39 | Next, you can build it like so: 40 | ``` 41 | > mos build 42 | ``` 43 | 44 | If everything went well you will see no output, but a `main.prg` file will have been created in a directory called `target`. You can load this `.prg` in your favourite C64 emulator. Run it by typing `sys 8192` in the BASIC prompt. The border colour will change, whoo! 45 | 46 | ::: tip 47 | You may be wondering why MOS is compiling `main.asm` when no input filename was passed in. Well, you can configure the input filename and many other things by creating a configuration file called `mos.toml`. For now, let's just keep the defaults. 48 | ::: 49 | 50 | ## Errors in your code 51 | What happens when there is an error :boom:? Well, let's edit `main.asm` and change it to this: 52 | ```asm6502 53 | inx $d020 54 | rts 55 | ``` 56 | `inx $d020` is not a valid instruction, and this is what you will see: 57 | ```{2} 58 | > mos build 59 | main.asm:1:5: error: unexpected '$d020' 60 | ``` 61 | 62 | The error indicates that it is located in file `main.asm`, on line 1, column 5. In this case, it didn't expect an operand because `inx` does not need any. 63 | 64 | ## What next? 65 | Alright, that was the quickest possible introduction! The remainder of the documentation will go over all the features in greater detail. -------------------------------------------------------------------------------- /docs/src/guide/project-setup.md: -------------------------------------------------------------------------------- 1 | # Project setup 2 | 3 | You can create a new MOS project by creating a file called `mos.toml` in the root of your project. This is the MOS **configuration file** and it allows you to customize things like how MOS assembles and formats your project. 4 | 5 | The simplest way to create this file is via MOS itself: 6 | ``` 7 | > mos init 8 | ``` 9 | 10 | This creates a `mos.toml` file with a few basic settings. This is enough to get started, but you can customize these settings too. 11 | 12 | ## Build options 13 | These are the default options under the `build` section in `mos.toml`: 14 | 15 | ```toml 16 | [build] 17 | entry = "main.asm" 18 | target-directory = "target" 19 | listing = false 20 | symbols = [] 21 | output-format = "prg" 22 | ``` 23 | 24 | | Key | Type | Description | 25 | | --- | ---- | ----------- | 26 | | `entry` | filename | The source file from which MOS should start assembling | 27 | | `target-directory` | directory name | The directory in which all output (binaries, symbols) is placed 28 | | `listing` | boolean | Generate listing files, containing disassembled code? | 29 | | `symbols` | array | Which symbol files to generate. Currently only `"vice"` is supported. 30 | | `output-filename` | string | In case the output format results in a single file, you can specify the output filename here. Otherwise the name of the entry source file will be used. (with a different extension). | 31 | | `output-format` | `"prg`" or `"bin`" | "prg" adds a 2-byte header that contains the loading position (used for Commodore emulators) | 32 | 33 | So, if you want to leave all defaults as-is, but would want to generate symbols for Vice, the `build` section in your `mos.toml` would look like this: 34 | 35 | ```toml 36 | [build] 37 | symbols = ["vice"] 38 | ``` 39 | 40 | ## Formatting options 41 | The formatter has a few options you can tweak, but it is not extensive yet. The following `mos.toml` represents the default formatting options: 42 | 43 | ```toml 44 | [formatting] 45 | mnemonics.casing = 'lowercase' 46 | mnemonics.register-casing = 'lowercase' 47 | braces.position = 'same-line' 48 | whitespace.indent = 4 49 | whitespace.label-margin = 20 50 | whitespace.label-alignment = right 51 | whitespace.code-margin = 30 52 | listing.num-bytes-per-line = 8 53 | ``` 54 | 55 | | Key | Type | Description | 56 | | --- | ---- | ----------- | 57 | | mnemonics.casing | `uppercase`, `lowercase` | The casing of mnemonics (e.g. `NOP`) | 58 | | mnemonics.register-casing | `uppercase`, `lowercase` | The casing of register suffixes (e.g. the `x` in `lda $fb,x`) | 59 | | braces.position | `same_line`, `new_line` | Where to place the braces in things like if statements | 60 | | whitespace.indent | number | How many spaces (no tabs yet... :innocent:) to indent in block statements | 61 | | whitespace.label-margin | number | How many characters to reserve for labels | 62 | | whitespace.label-alignment | `left`, `right` | How to align labels | 63 | | whitespace.code-margin | number | How many characters to reserve for code (the rest is reserved for comments) | 64 | | listing.num-bytes-per-line | number | When generating a listing file, this specifies how many bytes should be emitted per line | -------------------------------------------------------------------------------- /docs/src/guide/unit-testing.md: -------------------------------------------------------------------------------- 1 | # Unit testing 2 | 3 | When writing code, it can be very convenient to be able to test individual subroutines in isolation. For instance, when writing a sorting routine, it is nice to be able to test that in a few different scenarios. This will give you extra confidence when refactoring the sorting algorithm, since if the tests still pass you know you didn't break anything. 4 | 5 | ## Defining a test 6 | A test can be defined anywhere in your source files, and looks like a bit like this: 7 | 8 | ```asm6502 9 | .test "test_name" { 10 | // Do some setup 11 | lda #123 12 | sta foo 13 | 14 | jsr my_subroutine 15 | 16 | // Check the output 17 | .assert cpu.x == 7 18 | .assert ram(foo) = 123 19 | 20 | // Exit the test 21 | brk 22 | } 23 | ``` 24 | 25 | ## Running tests 26 | You can run tests by invoking the `mos test` command. Every test will be assembled individually and run on an emulated 6502. 27 | 28 | Please note that if you have defined [banks](advanced.html#banks) then the unit test will only have access to the bank it is defined in. 29 | 30 | ## Assertions 31 | In every part of your test (and even the subroutines you are testing) you can add `.assert` directives. The expression provided should be true, otherwise the test fails. 32 | 33 | ### Available flags 34 | Since the tests are run on an emulated 6502 you can access CPU and memory information in your assertions as you well: 35 | 36 | | Key | Description | 37 | |-----------------------------|-------------------------------------------| 38 | | cpu.a | The accumulator (A) register | 39 | | cpu.x | The X register | 40 | | cpu.y | The Y register | 41 | | cpu.sp | The stack pointer | 42 | | * | The program counter | 43 | | cpu.flags.zero | The Z flag | 44 | | cpu.flags.carry | The C flag | 45 | | cpu.flags.interrupt_disable | The I flag | 46 | | cpu.flags.decimal | The D flag | 47 | | cpu.flags.overflow | The V flag | 48 | | cpu.flags.negative | The N flag | 49 | | ram(...) | Read a byte from ram, e.g. `ram($d020)` | 50 | | ram16(...) | Read a word from ram, e.g. `ram16($0314)` | 51 | 52 | ### Custom failure message 53 | When an assertion fails, it will log the expression that was being tested. You can optionally also provide a custom message, like so: 54 | 55 | ```asm6502 56 | .assert cpu.y == 123 "this is a custom message" 57 | ``` 58 | 59 | ## Tracing 60 | You can add `.trace` directives to simply log some information during the test run, e.g.: 61 | 62 | ```asm6502 63 | .trace (cpu.x, cpu.y, ram($d020)) 64 | ``` 65 | 66 | Tracing only appears for failing tests, so if your test succeeds the tracing will not pollute your output. 67 | 68 | If you don't pass in any parameters you will get a detailed CPU dump, e.g.: 69 | 70 | ```asm6502 71 | .trace 72 | ``` 73 | 74 | Will print something like: `* = $2000, SP = $FD, flags = -----I--, A = $00, X = $00, Y = $00` -------------------------------------------------------------------------------- /docs/src/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | home: true 3 | heroText: 6502, modernized. 4 | heroImage: mos.png 5 | tagline: MOS helps you to build applications that target a MOS 6502 CPU. 6 | actionText: Get started → 7 | actionLink: /guide/ 8 | features: 9 | - title: Assembler 10 | details: A very fast multipass macro assembler that can quickly rip through any amount of code. 11 | - title: Formatter 12 | details: Automatic source code formatting to make sure your code always looks its shiniest. 13 | - title: IDE-ready 14 | details: Debugging, syntax highlighting, unit testing, automatic indentation, refactoring support. 15 | footer: MIT Licensed | Copyright © 2021-present datatra.sh 16 | --- -------------------------------------------------------------------------------- /examples/atari800/colors/atari800.asm: -------------------------------------------------------------------------------- 1 | .macro xex_load_header() { 2 | .define bank { 3 | name = "xex-load-header" 4 | create-segment = true 5 | } 6 | 7 | .segment "xex-load-header" { 8 | .byte $ff, $ff 9 | } 10 | } 11 | 12 | .macro xex_segment_header(name, start, end) { 13 | // the end of a segment is inclusive, so the last emitted byte is actually at end - 1 14 | .const end_offset = end - 1 15 | 16 | .define bank { 17 | name = "${name}-header" 18 | create-segment = true 19 | } 20 | 21 | .segment "${name}-header" { 22 | .byte start, end_offset 23 | } 24 | } 25 | 26 | .macro xex_segment_ini(name, addr) { 27 | .define bank { 28 | name = "${name}-ini" 29 | create-segment = true 30 | } 31 | 32 | .segment "${name}-ini" { 33 | .byte $e2, $02, $e3, $02, addr 34 | } 35 | } 36 | 37 | .macro xex_segment_run(name, addr) { 38 | .define bank { 39 | name = "${name}-run" 40 | create-segment = true 41 | } 42 | 43 | .segment "${name}-run" { 44 | .byte $e0, $02, $e1, $02, addr 45 | } 46 | } -------------------------------------------------------------------------------- /examples/atari800/colors/main.asm: -------------------------------------------------------------------------------- 1 | /// Thanks to F#READY for help in preparing this example 2 | /// After loading, hit 'start', then wait two seconds and hit 'select'. Colors should appear. 3 | .import * from "atari800.asm" 4 | 5 | xex_load_header() 6 | 7 | /////////////////////////////////////////////////// 8 | // First XEX segment 9 | /////////////////////////////////////////////////// 10 | xex_segment_header("first", segments.first.start, segments.first.end) 11 | 12 | .define bank { 13 | name = "first" 14 | } 15 | 16 | .define segment { 17 | name = "first" 18 | bank = "first" 19 | start = $0600 20 | } 21 | 22 | .segment "first" { 23 | first: lda #0 24 | sta 710 25 | 26 | wait_start: lda $d01f 27 | cmp #6 28 | bne wait_start 29 | rts // continue loading 30 | } 31 | 32 | xex_segment_ini("first", first) 33 | 34 | /////////////////////////////////////////////////// 35 | // Second XEX segment 36 | /////////////////////////////////////////////////// 37 | xex_segment_header("second", segments.second.start, segments.second.end) 38 | 39 | .define bank { 40 | name = "second" 41 | } 42 | 43 | .define segment { 44 | name = "second" 45 | bank = "second" 46 | start = segments.first.end 47 | } 48 | 49 | .segment "second" { 50 | second: lda #34 51 | sta 710 52 | 53 | lda #0 54 | sta 20 55 | wait_2sec: lda 20 56 | cmp #100 57 | bne wait_2sec 58 | 59 | wait_select: lda $d01f 60 | cmp #5 61 | bne wait_select 62 | rts // continue loading 63 | } 64 | 65 | xex_segment_ini("second", second) 66 | 67 | /////////////////////////////////////////////////// 68 | // Main XEX segment 69 | /////////////////////////////////////////////////// 70 | xex_segment_header("main", segments.main.start, segments.main.end) 71 | 72 | .define bank { 73 | name = "main" 74 | } 75 | 76 | .define segment { 77 | name = "main" 78 | bank = "main" 79 | start = segments.second.end 80 | } 81 | 82 | .segment "main" { 83 | main: lda $d40b 84 | adc 20 85 | asl 86 | sta $d40a 87 | sta $d018 88 | jmp main 89 | } 90 | 91 | xex_segment_run("main", main) -------------------------------------------------------------------------------- /examples/atari800/colors/mos.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | entry = "main.asm" -------------------------------------------------------------------------------- /examples/c64/cartridge/main.asm: -------------------------------------------------------------------------------- 1 | .import * from "../shared/c64.asm" 2 | 3 | .macro cart_header() { 4 | .define bank { 5 | name = "cart_header" 6 | create-segment = true 7 | size = 64 8 | } 9 | 10 | .segment "cart_header" { 11 | // signature 12 | .text "C64 CARTRIDGE " 13 | // header size 14 | .byte 0, 0, 0, $40 15 | // version 16 | .word 1 17 | // hardware type 'MAGIC DESK' 18 | .byte 0, 19 19 | // EXROM line status 20 | .byte 1 21 | // GAME line status 22 | .byte 0 23 | // reserved 24 | .byte 0, 0, 0, 0, 0, 0 25 | // cartridge name and padding 26 | .text "EXAMPLE CARTRIDGE FOR MOS" 27 | .byte 0, 0, 0, 0, 0, 0, 0 28 | } 29 | } 30 | 31 | .macro bank_header(bank_idx) { 32 | .define bank { 33 | name = "bank_header_{bank_idx}" 34 | size = 16 35 | create-segment = true 36 | } 37 | 38 | .segment "bank_header_{bank_idx}" { 39 | // signature 40 | .text "CHIP" 41 | // packet length 42 | .byte 0, 0, $20, $10 43 | // chip type (0 = ROM, 1 = RAM / no ROM data, 2 = Flash ROM) 44 | .byte 0, 0 45 | // bank number 46 | .byte 0, bank_idx 47 | // starting load address 48 | .byte $80, $00 49 | // ROM image size 50 | .byte $20, $00 51 | } 52 | 53 | .define bank { 54 | name = "bank_{bank_idx}" 55 | size = 8192 56 | fill = 0 57 | } 58 | 59 | .define segment { 60 | name = "bank_{bank_idx}" 61 | bank = "bank_{bank_idx}" 62 | start = $8000 63 | } 64 | } 65 | 66 | cart_header() 67 | bank_header(0) 68 | 69 | .segment "bank_0" { 70 | .word coldstart 71 | .word warmstart 72 | .byte $C3, $C2, $CD, $38, $30 73 | 74 | coldstart: sei 75 | stx $d016 76 | jsr $fda3 77 | jsr $fd50 78 | jsr $fd15 79 | jsr $ff5b 80 | cli 81 | 82 | warmstart: inc $d020 83 | jmp warmstart 84 | } 85 | 86 | .segment "bank_0" { 87 | .test "header_is_in_the_right_place" { 88 | .assert ram($8004) == $C3 89 | 90 | } 91 | } -------------------------------------------------------------------------------- /examples/c64/cartridge/mos.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | entry = "main.asm" 3 | output-format = "bin" 4 | output-filename = "cart.crt" -------------------------------------------------------------------------------- /examples/c64/scroller/main.asm: -------------------------------------------------------------------------------- 1 | .import * from "../shared/c64.asm" 2 | 3 | basic_start(start) 4 | 5 | start: lda #colors.gray 6 | sta cursor_color 7 | jsr kernal.clrscr 8 | lda #0 9 | sta vic.background 10 | sta vic.foreground 11 | 12 | // Set 38-column mode 13 | lda vic.xscroll 14 | and #%11110111 15 | sta vic.xscroll 16 | 17 | { 18 | // Run all this code once per frame 19 | lda vic.raster_pos 20 | cmp #$80 21 | bne - 22 | 23 | // Do per-pixel soft-scroll 24 | dec xscroll 25 | bpl apply_xscroll 26 | 27 | // the xscroll has wrapped around, so reset it 28 | // and shift the screen by one char 29 | lda #7 30 | sta xscroll 31 | 32 | ldx #0 33 | 34 | { 35 | lda $0401, x 36 | sta $0400, x 37 | inx 38 | cpx #39 39 | bne - 40 | } 41 | 42 | // Read a new character from our scroller. 43 | // If it's $ff we should wrap around. 44 | ldx text_pos 45 | lda text, x 46 | cmp #$ff 47 | bne write_char 48 | 49 | lda #$00 50 | sta text_pos 51 | jmp apply_xscroll 52 | 53 | write_char: sta $0427 54 | inc text_pos 55 | 56 | // Apply our soft-scroll value to VIC's xscroll register 57 | apply_xscroll: lda vic.xscroll 58 | and #%11111000 59 | ora xscroll 60 | sta vic.xscroll 61 | 62 | // Back to top 63 | jmp - 64 | } 65 | 66 | xscroll: .byte 0 67 | text_pos: .byte 0 68 | 69 | text: { 70 | .text petscreen "mos says hello! " 71 | .byte $ff 72 | } -------------------------------------------------------------------------------- /examples/c64/scroller/mos.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | entry = "main.asm" 3 | listing = true 4 | output-format = "prg" 5 | -------------------------------------------------------------------------------- /examples/c64/shared/c64.asm: -------------------------------------------------------------------------------- 1 | /// Color of the cursor (taken into effect when clearing screen) 2 | .const cursor_color = $0286 3 | 4 | vic: { 5 | /// The low-byte of the raster position 6 | .const raster_pos = $d012 7 | 8 | /// Bits 7-6: Unused 9 | /// 10 | /// Bit 5: Reset-Bit 11 | /// 12 | /// Bit 4: Multi-Color Mode 13 | /// 14 | /// Bit 3: 38/40 column (1 = 40 cols) 15 | /// 16 | /// Bit 2-0: Smooth scroll 17 | .const xscroll = $d016 18 | 19 | /// Background color 20 | .const background = $d020 21 | 22 | /// Foreground color 23 | .const foreground = $d021 24 | } 25 | 26 | colors: { 27 | .const black = 0 28 | .const white = 1 29 | .const red = 2 30 | .const cyan = 3 31 | .const purple = 4 32 | .const green = 5 33 | .const blue = 6 34 | .const yellow = 7 35 | .const orange = 8 36 | .const brown = 9 37 | .const light_red = 10 38 | .const dark_gray = 11 39 | .const gray = 12 40 | .const light_green = 13 41 | .const light_blue = 14 42 | .const light_gray = 15 43 | } 44 | 45 | kernal: { 46 | /// Clears the screen in the current cursor color 47 | .const clrscr = $e544 48 | } 49 | 50 | /// Constructs a `0 sys*` basic line 51 | .macro basic_start(address) { 52 | * = $0801 53 | 54 | .byte $0c, $08, $00, $00, $9e 55 | 56 | .if address >= 10000 { 57 | .byte $30 + (address / 10000) % 10 58 | } 59 | 60 | .if address >= 1000 { 61 | .byte $30 + (address / 1000) % 10 62 | } 63 | 64 | .if address >= 100 { 65 | .byte $30 + (address / 100) % 10 66 | } 67 | 68 | .if address >= 10 { 69 | .byte $30 + (address / 10) % 10 70 | } 71 | 72 | .byte $30 + address % 10 73 | .byte 0, 0, 0 74 | } -------------------------------------------------------------------------------- /examples/c64/unit-testing/main.asm: -------------------------------------------------------------------------------- 1 | // Based on a c64unit example 2 | .test "stack_pointer" { 3 | lda #6 4 | pha 5 | lda #4 6 | pha 7 | 8 | pla 9 | pla 10 | 11 | tsx 12 | 13 | .assert cpu.sp == $fd 14 | 15 | brk 16 | } 17 | 18 | .test "will_fail" { 19 | .loop 2 { 20 | .trace (index, *, ram($2000)) 21 | } 22 | .trace 23 | .assert * == $1234 24 | 25 | // will never be reached 26 | nop 27 | } 28 | 29 | .test "will_succeed" { 30 | brk 31 | } -------------------------------------------------------------------------------- /examples/c64/unit-testing/mos.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | entry = "main.asm" -------------------------------------------------------------------------------- /mos-core/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "mos-core" 3 | version = "0.0.0" # unused 4 | authors = ["Roy Jacobs "] 5 | edition = "2018" 6 | repository = "https://github.com/datatrash/mos" 7 | license = "MIT" 8 | keywords = ["retro", "6502", "assembler"] 9 | 10 | [lib] 11 | 12 | [dependencies] 13 | ansi_term = "0.12" 14 | bitflags = "1" 15 | codespan-reporting = "0.11" 16 | derive_more = "0.99" 17 | fs-err = "2" 18 | indexmap = "1.7" 19 | itertools = "0.10" 20 | log = { version = "0.4", features = ["max_level_trace", "release_max_level_info"] } 21 | loggerv = "0.7" 22 | nom = "7" 23 | nom_locate = "4" 24 | once_cell = "1.8" 25 | path-absolutize = "3" 26 | path-dedot = "3" 27 | pathdiff = "0.2" 28 | petgraph = "0.6" 29 | serde = { version = "1", features = ["derive"] } 30 | serde_json = "1" 31 | smallvec = "1" 32 | strum = { version = "0.23", features = ["derive"] } 33 | toml = "0.5" 34 | 35 | [dev-dependencies] 36 | mos-testing = { path = "../mos-testing" } 37 | tempfile = "3" -------------------------------------------------------------------------------- /mos-core/src/cbm/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod petscii; 2 | -------------------------------------------------------------------------------- /mos-core/src/codegen/config_extractor.rs: -------------------------------------------------------------------------------- 1 | use crate::codegen::CodegenContext; 2 | use crate::errors::CoreResult; 3 | use crate::parser::code_map::Span; 4 | use crate::parser::{Expression, Located, Token}; 5 | use codespan_reporting::diagnostic::Diagnostic; 6 | 7 | pub struct ConfigExtractor<'a> { 8 | config_span: Span, 9 | kvps: Vec<(&'a Located, &'a Located)>, 10 | } 11 | 12 | impl<'a> ConfigExtractor<'a> { 13 | pub fn new(config_span: Span, kvps: &[(&'a Located, &'a Located)]) -> Self { 14 | Self { 15 | config_span, 16 | kvps: kvps.to_vec(), 17 | } 18 | } 19 | 20 | pub fn get_string(&self, ctx: &mut CodegenContext, key: &str) -> CoreResult { 21 | if let Some(str) = self.try_get_string(ctx, key)? { 22 | Ok(str) 23 | } else { 24 | let span = self 25 | .try_get_kvp(key) 26 | .map(|(k, v)| k.span.merge(v.span)) 27 | .unwrap_or(self.config_span); 28 | Err(Diagnostic::error() 29 | .with_message(format!("could not evaluate configuration key '{}'", key)) 30 | .with_labels(vec![span.to_label()]) 31 | .into()) 32 | } 33 | } 34 | 35 | pub fn try_get_string( 36 | &self, 37 | ctx: &mut CodegenContext, 38 | key: &str, 39 | ) -> CoreResult> { 40 | match self.try_get_expression(key) { 41 | Some(expr) => ctx.evaluate_expression_as_string(&expr, true), 42 | None => Ok(None), 43 | } 44 | } 45 | 46 | pub fn try_get_i64(&self, ctx: &mut CodegenContext, key: &str) -> CoreResult> { 47 | match self.try_get_expression(key) { 48 | Some(expr) => ctx.evaluate_expression_as_i64(&expr, true), 49 | None => Ok(None), 50 | } 51 | } 52 | 53 | pub fn try_get_expression(&self, key: &str) -> Option> { 54 | let expr = self.try_get_located_token(key).map(|lt| { 55 | lt.map(|tok| match tok { 56 | Token::Expression(expr) => Some(expr.clone()), 57 | _ => None, 58 | }) 59 | }); 60 | 61 | match expr { 62 | Some(expr) if expr.data.is_some() => Some(Located::new_with_trivia( 63 | expr.span, 64 | expr.data.unwrap(), 65 | expr.trivia, 66 | )), 67 | _ => None, 68 | } 69 | } 70 | 71 | fn try_get_located_token(&self, wanted: &str) -> Option<&Located> { 72 | for (key, value) in &self.kvps { 73 | if key.data == wanted { 74 | return Some(value); 75 | } 76 | } 77 | None 78 | } 79 | 80 | fn try_get_kvp(&self, wanted: &str) -> Option<(&Located, &Located)> { 81 | for (key, value) in &self.kvps { 82 | if key.data == wanted { 83 | return Some((key, value)); 84 | } 85 | } 86 | None 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /mos-core/src/codegen/config_validator.rs: -------------------------------------------------------------------------------- 1 | use crate::codegen::config_extractor::ConfigExtractor; 2 | use crate::errors::CoreResult; 3 | use crate::parser::code_map::Span; 4 | use crate::parser::{Located, Token}; 5 | use codespan_reporting::diagnostic::Diagnostic; 6 | use itertools::Itertools; 7 | use std::collections::HashSet; 8 | 9 | pub struct ConfigValidator { 10 | required: HashSet, 11 | allowed: HashSet, 12 | } 13 | 14 | impl ConfigValidator { 15 | pub fn new() -> Self { 16 | Self { 17 | required: HashSet::new(), 18 | allowed: HashSet::new(), 19 | } 20 | } 21 | 22 | pub fn required(mut self, key: &str) -> Self { 23 | self.required.insert(key.into()); 24 | self 25 | } 26 | 27 | pub fn allowed(mut self, key: &str) -> Self { 28 | self.allowed.insert(key.into()); 29 | self 30 | } 31 | 32 | pub fn extract<'a>( 33 | self, 34 | config_span: Span, 35 | kvps: &'a [(&'a Located, &'a Located)], 36 | ) -> CoreResult> { 37 | let mut errors = vec![]; 38 | 39 | let mut req = self.required.clone(); 40 | for (key, _) in kvps.iter().sorted_by_key(|(k, _)| &k.data) { 41 | if req.contains(&key.data) { 42 | req.remove(&key.data); 43 | } else if !self.allowed.contains(&key.data) { 44 | errors.push((Some(key.span), format!("field not allowed: {}", key.data))); 45 | } 46 | } 47 | 48 | if !req.is_empty() { 49 | let r = req.iter().sorted().join(", "); 50 | errors.push((None, format!("missing required fields: {}", r))); 51 | } 52 | 53 | let errors = errors 54 | .into_iter() 55 | .map(|(span, message)| { 56 | Diagnostic::error() 57 | .with_message(message) 58 | .with_labels(vec![span.unwrap_or(config_span).to_label()]) 59 | }) 60 | .collect_vec(); 61 | match errors.is_empty() { 62 | true => Ok(ConfigExtractor::new(config_span, kvps)), 63 | false => Err(errors.into()), 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /mos-core/src/codegen/program_counter.rs: -------------------------------------------------------------------------------- 1 | use derive_more::{Add, From, Into, Sub, UpperHex}; 2 | use std::fmt::{Display, Formatter}; 3 | use std::ops::{Add, Deref, Range}; 4 | 5 | /// A simple newtype that wraps a program counter 6 | #[derive( 7 | Debug, Default, PartialEq, Eq, Hash, Clone, Copy, PartialOrd, From, Add, Sub, Into, UpperHex, 8 | )] 9 | pub struct ProgramCounter(usize); 10 | 11 | impl ProgramCounter { 12 | pub fn new(pc: usize) -> Self { 13 | Self(pc) 14 | } 15 | 16 | pub fn to_le_bytes(self) -> [u8; 2] { 17 | (self.0 as u16).to_le_bytes() 18 | } 19 | 20 | pub fn as_usize(&self) -> usize { 21 | self.0 22 | } 23 | 24 | pub fn as_u16(&self) -> u16 { 25 | self.0 as u16 26 | } 27 | 28 | pub fn as_i64(&self) -> i64 { 29 | self.0 as i64 30 | } 31 | 32 | pub fn as_empty_range(&self) -> Range { 33 | self.0..self.0 34 | } 35 | } 36 | 37 | impl Add for ProgramCounter { 38 | type Output = ProgramCounter; 39 | 40 | fn add(self, rhs: usize) -> Self::Output { 41 | Self(self.0 + rhs) 42 | } 43 | } 44 | 45 | impl Deref for ProgramCounter { 46 | type Target = usize; 47 | 48 | fn deref(&self) -> &Self::Target { 49 | &self.0 50 | } 51 | } 52 | 53 | impl Display for ProgramCounter { 54 | fn fmt(&self, f: &mut Formatter) -> std::fmt::Result { 55 | write!(f, "${:04x}", self.0) 56 | } 57 | } 58 | 59 | impl From for ProgramCounter { 60 | fn from(val: i32) -> Self { 61 | Self(val as usize) 62 | } 63 | } 64 | 65 | impl From for ProgramCounter { 66 | fn from(val: i64) -> Self { 67 | Self(val as usize) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /mos-core/src/codegen/segment.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | use crate::codegen::ProgramCounter; 3 | use crate::parser::Identifier; 4 | use once_cell::sync::OnceCell; 5 | use std::ops::Range; 6 | 7 | pub struct Segment { 8 | pc: ProgramCounter, 9 | data: Vec, 10 | range: Range, 11 | options: SegmentOptions, 12 | } 13 | 14 | pub struct SegmentOptions { 15 | pub bank: Option, 16 | pub initial_pc: ProgramCounter, 17 | pub write: bool, 18 | pub target_address: ProgramCounter, 19 | } 20 | 21 | impl Default for SegmentOptions { 22 | fn default() -> Self { 23 | Self { 24 | bank: None, 25 | initial_pc: 0x2000.into(), 26 | write: true, 27 | target_address: 0x2000.into(), 28 | } 29 | } 30 | } 31 | 32 | static EMPTY_DATA: OnceCell> = OnceCell::new(); 33 | 34 | impl Segment { 35 | pub fn new(options: SegmentOptions) -> Self { 36 | let pc = options.initial_pc; 37 | let range = pc.as_empty_range(); 38 | 39 | Self { 40 | pc, 41 | data: vec![], 42 | range, 43 | options, 44 | } 45 | } 46 | 47 | pub fn reset(&mut self) { 48 | self.pc = self.options.initial_pc; 49 | self.range = self.pc.as_empty_range(); 50 | self.data = vec![]; 51 | } 52 | 53 | pub fn pc(&self) -> ProgramCounter { 54 | self.pc 55 | } 56 | 57 | pub fn set_pc(&mut self, pc: impl Into) { 58 | self.pc = pc.into(); 59 | } 60 | 61 | pub fn target_pc(&self) -> ProgramCounter { 62 | ((self.pc.as_i64() + self.target_offset()) as usize).into() 63 | } 64 | 65 | pub fn options(&self) -> &SegmentOptions { 66 | &self.options 67 | } 68 | 69 | pub fn options_mut(&mut self) -> &mut SegmentOptions { 70 | &mut self.options 71 | } 72 | 73 | pub fn range(&self) -> Range { 74 | self.range.clone() 75 | } 76 | 77 | pub fn target_offset(&self) -> i64 { 78 | self.options.target_address.as_i64() - self.options.initial_pc.as_i64() 79 | } 80 | 81 | pub fn range_data(&self) -> &[u8] { 82 | if self.data.is_empty() { 83 | EMPTY_DATA.get_or_init(Vec::new) 84 | } else { 85 | &self.data[self.range()] 86 | } 87 | } 88 | 89 | pub fn emit(&mut self, bytes: &[u8]) -> bool { 90 | let start = self.pc; 91 | let end = self.pc + bytes.len(); 92 | if start.as_usize() > 0xffff || end.as_usize() > 0x10000 { 93 | return false; 94 | } 95 | 96 | if start.as_usize() < self.range.start || self.data.is_empty() { 97 | self.range.start = start.as_usize(); 98 | log::trace!("Extending start of range to: {}", self.range.start); 99 | } 100 | if end.as_usize() > self.range.end || self.data.is_empty() { 101 | self.range.end = end.as_usize(); 102 | log::trace!("Extending end of range to: {}", self.range.end); 103 | } 104 | 105 | if self.data.is_empty() { 106 | self.data = [0; 65536].into(); 107 | } 108 | 109 | self.data.splice(*start..*end, bytes.to_vec()); 110 | self.pc = end; 111 | 112 | true 113 | } 114 | } 115 | 116 | #[cfg(test)] 117 | mod tests { 118 | use super::*; 119 | 120 | #[test] 121 | fn can_emit_to_segment() { 122 | let mut seg = Segment::new(SegmentOptions { 123 | initial_pc: 0xc000.into(), 124 | target_address: 0xb000.into(), 125 | ..Default::default() 126 | }); 127 | seg.emit(&[1, 2, 3]); 128 | assert_eq!(seg.pc(), 0xc003.into()); 129 | assert_eq!(seg.data[0xc000..0xc003], [1, 2, 3]); 130 | assert_eq!(seg.range_data(), &[1, 2, 3]); 131 | assert_eq!(seg.range(), 0xc000..0xc003); 132 | assert_eq!(seg.target_offset(), -0x1000); 133 | 134 | seg.pc = 0x2000.into(); 135 | seg.emit(&[4]); 136 | assert_eq!(seg.pc, 0x2001.into()); 137 | assert_eq!(seg.data[0xc000..0xc003], [1, 2, 3]); 138 | assert_eq!(seg.data[0x2000..0x2001], [4]); 139 | assert_eq!(seg.range(), 0x2000..0xc003); 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /mos-core/src/codegen/source_map.rs: -------------------------------------------------------------------------------- 1 | use crate::codegen::symbols::SymbolIndex; 2 | use crate::codegen::ProgramCounter; 3 | use crate::parser::code_map::{CodeMap, Span}; 4 | use std::ops::Range; 5 | 6 | #[derive(Debug, Default)] 7 | pub struct SourceMap { 8 | offsets: Vec, 9 | } 10 | 11 | #[derive(Debug, PartialEq, Eq)] 12 | pub struct SourceMapOffset { 13 | pub scope: SymbolIndex, 14 | pub span: Span, 15 | pub pc: Range, 16 | } 17 | 18 | impl SourceMap { 19 | pub fn clear(&mut self) { 20 | self.offsets.clear(); 21 | } 22 | 23 | pub fn offsets(&self) -> &Vec { 24 | &self.offsets 25 | } 26 | 27 | pub fn add(&mut self, scope: SymbolIndex, span: Span, pc: ProgramCounter, len: usize) { 28 | let offset = SourceMapOffset { 29 | scope, 30 | span, 31 | pc: pc.as_usize()..(pc.as_usize() + len), 32 | }; 33 | self.offsets.push(offset); 34 | } 35 | 36 | pub fn address_to_offset>(&self, pc: PC) -> Option<&SourceMapOffset> { 37 | let pc = pc.into().as_usize(); 38 | self.offsets 39 | .iter() 40 | .find(|offset| pc >= offset.pc.start && pc < offset.pc.end) 41 | } 42 | 43 | // A single line may map to multiple addresses, since it could be an import compiled with different parameters 44 | pub fn line_col_to_offsets>>( 45 | &self, 46 | code_map: &CodeMap, 47 | filename: &str, 48 | line: usize, 49 | column: C, 50 | ) -> Vec<&SourceMapOffset> { 51 | let column = column.into(); 52 | self.offsets 53 | .iter() 54 | .filter(|o| { 55 | let sl = code_map.look_up_span(o.span); 56 | if sl.file.name() != filename { 57 | return false; 58 | } 59 | 60 | // Somewhere within begin and end, so fine regardless of column 61 | if line > sl.begin.line && line < sl.end.line { 62 | return true; 63 | } 64 | 65 | match column { 66 | Some(column) => { 67 | // Begin and end on same line 68 | if line == sl.begin.line && line == sl.end.line { 69 | // So make sure our column is in the middle 70 | if column >= sl.begin.column && column < sl.end.column { 71 | return true; 72 | } 73 | } else { 74 | // Begin and end on different lines, so we need to be either after the beginning column 75 | // or before the ending column 76 | if (line == sl.begin.line && column >= sl.begin.column) 77 | || (line == sl.end.line && column < sl.end.column) 78 | { 79 | return true; 80 | } 81 | } 82 | } 83 | None => { 84 | // No column specified, so we're fine if we're at the beginning line 85 | if line == sl.begin.line { 86 | return true; 87 | } 88 | } 89 | } 90 | 91 | false 92 | }) 93 | .collect() 94 | } 95 | 96 | pub fn move_offsets(&mut self, scope: SymbolIndex, new_scope: SymbolIndex, new_span: Span) { 97 | log::trace!( 98 | "Trying to move offset from scope '{:?}' to scope '{:?}'", 99 | scope, 100 | new_scope 101 | ); 102 | self.offsets.iter_mut().for_each(|offset| { 103 | if offset.scope == scope { 104 | log::trace!("Moved"); 105 | offset.scope = new_scope; 106 | offset.span = new_span; 107 | } 108 | }); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /mos-core/src/codegen/text_encoding.rs: -------------------------------------------------------------------------------- 1 | use crate::cbm::petscii::Petscii; 2 | use crate::parser::TextEncoding; 3 | 4 | pub fn encode_text(str: &str, encoding: TextEncoding) -> Vec { 5 | match encoding { 6 | TextEncoding::Ascii | TextEncoding::Unspecified => str.as_bytes().to_vec(), 7 | TextEncoding::Petscii => Petscii::from_str(str).as_bytes().to_vec(), 8 | TextEncoding::Petscreen => Petscii::from_str(str) 9 | .as_bytes() 10 | .iter() 11 | .map(|c| match c { 12 | 0x00..=0x1f => c + 128, 13 | 0x20..=0x3f => *c, 14 | 0x40..=0x5f => c - 64, 15 | 0x60..=0x7f => c - 32, 16 | 0x80..=0x9f => c + 64, 17 | 0xa0..=0xbf => c - 64, 18 | 0xc0..=0xfe => c - 128, 19 | 0xff => 0x5e, 20 | }) 21 | .collect(), 22 | } 23 | } 24 | 25 | #[cfg(test)] 26 | mod tests { 27 | use super::*; 28 | 29 | #[test] 30 | fn can_encode_text() { 31 | assert_eq!(encode_text("abc", TextEncoding::Ascii), &[97, 98, 99]); 32 | assert_eq!(encode_text("abc", TextEncoding::Petscii), &[65, 66, 67]); 33 | assert_eq!(encode_text("abc", TextEncoding::Petscreen), &[1, 2, 3]); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /mos-core/src/errors.rs: -------------------------------------------------------------------------------- 1 | use crate::parser::code_map::{CodeMap, Span, SpanLoc}; 2 | use codespan_reporting::diagnostic::{Diagnostic, Severity}; 3 | use codespan_reporting::files::{Error, Files}; 4 | use itertools::Itertools; 5 | use pathdiff::diff_paths; 6 | use std::fmt::{Display, Formatter}; 7 | use std::ops::Range; 8 | 9 | pub type CoreResult = Result; 10 | 11 | #[derive(Debug, Default)] 12 | pub struct Diagnostics { 13 | diags: Vec>, 14 | code_map: Option, 15 | } 16 | 17 | impl PartialEq for Diagnostics { 18 | fn eq(&self, other: &Self) -> bool { 19 | self.diags == other.diags 20 | } 21 | } 22 | 23 | impl std::error::Error for Diagnostics {} 24 | 25 | impl Diagnostics { 26 | pub fn iter(&self) -> impl Iterator> { 27 | self.diags.iter() 28 | } 29 | 30 | pub fn is_empty(&self) -> bool { 31 | self.diags.is_empty() 32 | } 33 | 34 | pub fn len(&self) -> usize { 35 | self.diags.len() 36 | } 37 | 38 | pub fn first(&self) -> Option<&Diagnostic> { 39 | self.diags.first() 40 | } 41 | 42 | pub fn push(&mut self, diag: Diagnostic) { 43 | self.diags.push(diag); 44 | } 45 | 46 | pub fn extend(&mut self, other: Diagnostics) { 47 | self.diags.extend(other.diags); 48 | } 49 | 50 | pub fn code_map(&self) -> Option<&CodeMap> { 51 | self.code_map.as_ref() 52 | } 53 | 54 | pub fn with_code_map(mut self, code_map: &CodeMap) -> Self { 55 | self.code_map = Some(code_map.clone()); 56 | self 57 | } 58 | 59 | pub fn location(&self) -> Option { 60 | self.code_map.as_ref().and_then(|cm| { 61 | self.diags.first().and_then(|diag| { 62 | diag.labels 63 | .first() 64 | .map(|label| cm.look_up_span(label.file_id)) 65 | }) 66 | }) 67 | } 68 | 69 | fn format(&self) -> String { 70 | self.diags 71 | .iter() 72 | .sorted_by_key(|s| s.labels.first().map(|l| l.file_id)) 73 | .map(|diag| { 74 | let mut msg = "".to_string(); 75 | if let Some(cm) = &self.code_map { 76 | if let Some(label) = diag.labels.first() { 77 | let sl = cm.look_up_span(label.file_id); 78 | msg += format!( 79 | "{}:{}:{}: ", 80 | sl.file.name(), 81 | sl.begin.line + 1, 82 | sl.begin.column + 1 83 | ) 84 | .as_str(); 85 | } 86 | } 87 | let severity = match diag.severity { 88 | Severity::Error => "error", 89 | _ => unimplemented!(), 90 | }; 91 | msg += format!("{}: ", severity).as_str(); 92 | msg += diag.message.as_str(); 93 | msg 94 | }) 95 | .collect_vec() 96 | .join("\n") 97 | } 98 | } 99 | 100 | impl Display for Diagnostics { 101 | fn fmt(&self, f: &mut Formatter) -> std::fmt::Result { 102 | write!(f, "{}", self.format()) 103 | } 104 | } 105 | 106 | impl From>> for Diagnostics { 107 | fn from(diags: Vec>) -> Self { 108 | Diagnostics { 109 | diags, 110 | code_map: None, 111 | } 112 | } 113 | } 114 | 115 | impl From> for Diagnostics { 116 | fn from(diag: Diagnostic) -> Self { 117 | Diagnostics { 118 | diags: vec![diag], 119 | code_map: None, 120 | } 121 | } 122 | } 123 | 124 | pub fn map_io_error(e: std::io::Error) -> Diagnostics { 125 | Diagnostic::error().with_message(e.to_string()).into() 126 | } 127 | 128 | pub fn map_generic_error(e: impl Display) -> Diagnostics { 129 | Diagnostic::error().with_message(e.to_string()).into() 130 | } 131 | 132 | impl<'a> Files<'a> for &'a CodeMap { 133 | type FileId = Span; 134 | type Name = String; 135 | type Source = &'a str; 136 | 137 | fn name(&'a self, file: Span) -> Result { 138 | let mut filename = self.find_file(file.low()).name().to_string(); 139 | if let Some(relative_from) = &self.paths_relative_from { 140 | if let Some(path) = diff_paths(&filename, relative_from) { 141 | filename = path.to_str().unwrap().into(); 142 | } 143 | } 144 | Ok(filename) 145 | } 146 | 147 | fn source(&'a self, file: Span) -> Result { 148 | Ok(self.find_file(file.low()).source()) 149 | } 150 | 151 | fn line_index(&'a self, file: Span, byte_in_file: usize) -> Result { 152 | let file = self.look_up_span(file).file; 153 | let pos = file.span.low() + byte_in_file as u64; 154 | Ok(file.find_line(pos)) 155 | } 156 | 157 | fn line_range(&'a self, file: Span, line_index: usize) -> Result, Error> { 158 | let file = self.look_up_span(file).file; 159 | let line_span = file.line_span(line_index); 160 | let low = line_span.low().as_usize() - file.span.low().as_usize(); 161 | let high = line_span.high().as_usize() - file.span.low().as_usize(); 162 | Ok(low..high) 163 | } 164 | } 165 | 166 | #[cfg(test)] 167 | mod tests { 168 | use crate::parser::code_map::CodeMap; 169 | use codespan_reporting::files::Files; 170 | 171 | #[test] 172 | fn files() { 173 | let mut codemap = CodeMap::default(); 174 | let a = codemap.add_file("a".into(), "1234567890\nabcde".into()); 175 | let b = codemap.add_file("b".into(), "ABCDE\nFGHIJKLMNO".into()); 176 | 177 | let cm = &codemap; 178 | assert_eq!(cm.line_index(a.span, 5).unwrap(), 0); 179 | assert_eq!(cm.line_index(a.span, 15).unwrap(), 1); 180 | assert_eq!(cm.line_index(b.span, 3).unwrap(), 0); 181 | assert_eq!(cm.line_index(b.span, 8).unwrap(), 1); 182 | 183 | assert_eq!(cm.line_range(a.span, 0).unwrap(), 0..11); 184 | assert_eq!(cm.line_range(a.span, 1).unwrap(), 11..16); 185 | assert_eq!(cm.line_range(b.span, 0).unwrap(), 0..6); 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /mos-core/src/io/listing.rs: -------------------------------------------------------------------------------- 1 | use crate::codegen::CodegenContext; 2 | use crate::errors::CoreResult; 3 | use crate::LINE_ENDING; 4 | use itertools::Itertools; 5 | use std::collections::HashMap; 6 | use std::path::PathBuf; 7 | 8 | pub fn to_listing( 9 | ctx: &CodegenContext, 10 | num_bytes_per_line: usize, 11 | ) -> CoreResult> { 12 | let mut listing = HashMap::new(); 13 | 14 | for file in ctx.tree().code_map.files() { 15 | let mut result = vec![]; 16 | for line_idx in 0..file.num_lines() { 17 | let offsets = ctx.source_map().line_col_to_offsets( 18 | &ctx.tree().code_map, 19 | file.name(), 20 | line_idx, 21 | None, 22 | ); 23 | 24 | let mut data = vec![]; 25 | for offset in &offsets { 26 | for segment in ctx.segments().values() { 27 | if segment.range().start <= offset.pc.start 28 | && segment.range().end >= offset.pc.end 29 | { 30 | let mut start = offset.pc.start - segment.range().start; 31 | let end = start + (offset.pc.end - offset.pc.start); 32 | 33 | let mut pc = offset.pc.start; 34 | while start < end { 35 | data.push((pc, segment.range_data()[start])); 36 | start += 1; 37 | pc += 1; 38 | } 39 | break; 40 | } 41 | } 42 | } 43 | 44 | if data.is_empty() { 45 | let mut line = vec![]; 46 | line.push(format!("{:>5}", line_idx + 1)); 47 | line.push(format!("{:5}", "")); 48 | line.push(format!("{:width$}", "", width = num_bytes_per_line * 3)); 49 | line.push(file.source_line(line_idx).to_string()); 50 | result.push(line.join(" ")); 51 | } else { 52 | let mut source_line_emitted = false; 53 | 54 | let chunks = data.chunks(num_bytes_per_line); 55 | for chunk in chunks { 56 | let pc = chunk.iter().next().unwrap().0; 57 | let bytes = chunk.iter().map(|(_, bytes)| bytes).collect_vec(); 58 | let formatted_bytes = bytes.into_iter().map(|b| format!("{:02X}", b)).join(" "); 59 | 60 | let mut line = vec![]; 61 | line.push(format!("{:>5}", line_idx + 1)); 62 | line.push(format!("{:04X}:", pc)); 63 | line.push(format!( 64 | "{:width$}", 65 | formatted_bytes, 66 | width = num_bytes_per_line * 3 67 | )); 68 | if !source_line_emitted { 69 | line.push(file.source_line(line_idx).to_string()); 70 | source_line_emitted = true; 71 | } 72 | result.push(line.join(" ").trim_end().into()); 73 | } 74 | } 75 | } 76 | 77 | let result = result.join(LINE_ENDING); 78 | let result = result.trim_end().into(); 79 | listing.insert(PathBuf::from(file.name()), result); 80 | } 81 | 82 | Ok(listing) 83 | } 84 | 85 | #[cfg(test)] 86 | mod tests { 87 | use super::*; 88 | use crate::codegen::tests::{test_codegen, test_codegen_with_options}; 89 | use crate::codegen::CodegenOptions; 90 | 91 | #[test] 92 | fn create_listing() -> CoreResult<()> { 93 | let ctx = test_codegen( 94 | r#"lda $d020 95 | // hello 96 | jmp foo 97 | foo: nop 98 | .byte 1,2,3,4,5,6,7,8,9,10 99 | .text "hello there""#, 100 | )?; 101 | assert_eq!( 102 | to_listing(&ctx, 8)? 103 | .get(&PathBuf::from("test.asm")) 104 | .unwrap(), 105 | &r#" 1 C000: AD 20 D0 lda $d020 106 | 2 // hello 107 | 3 C003: 4C 06 C0 jmp foo 108 | 4 C006: EA foo: nop 109 | 5 C007: 01 02 03 04 05 06 07 08 .byte 1,2,3,4,5,6,7,8,9,10 110 | 5 C00F: 09 0A 111 | 6 C011: 68 65 6C 6C 6F 20 74 68 .text "hello there" 112 | 6 C019: 65 72 65"# 113 | .replace('\n', LINE_ENDING) 114 | ); 115 | 116 | Ok(()) 117 | } 118 | 119 | #[test] 120 | fn create_macro_listing() -> CoreResult<()> { 121 | let ctx = test_codegen_with_options( 122 | r#".macro outer() { 123 | inner() 124 | brk 125 | } 126 | .macro inner() { 127 | nop 128 | } 129 | outer()"#, 130 | CodegenOptions { 131 | move_macro_source_map_to_invocation: true, 132 | ..Default::default() 133 | }, 134 | )?; 135 | assert_eq!( 136 | to_listing(&ctx, 8)? 137 | .get(&PathBuf::from("test.asm")) 138 | .unwrap(), 139 | &r#" 1 .macro outer() { 140 | 2 inner() 141 | 3 brk 142 | 4 } 143 | 5 .macro inner() { 144 | 6 nop 145 | 7 } 146 | 8 C000: EA 00 outer()"# 147 | .replace('\n', LINE_ENDING) 148 | ); 149 | 150 | Ok(()) 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /mos-core/src/io/mod.rs: -------------------------------------------------------------------------------- 1 | pub use binary_writer::*; 2 | pub use listing::*; 3 | pub use vice::*; 4 | 5 | /// The main entry point for writing generated code to file(s) 6 | mod binary_writer; 7 | /// Listing files, containing disassembled code 8 | mod listing; 9 | /// IO with the VICE emulator 10 | mod vice; 11 | -------------------------------------------------------------------------------- /mos-core/src/io/vice.rs: -------------------------------------------------------------------------------- 1 | use crate::codegen::{Symbol, SymbolTable, SymbolType}; 2 | use crate::LINE_ENDING; 3 | use itertools::Itertools; 4 | 5 | pub fn to_vice_symbols(table: &SymbolTable) -> String { 6 | table 7 | .all() 8 | .into_iter() 9 | .filter_map(|(path, (_, symbol))| match symbol.ty { 10 | SymbolType::Label => Some(format!("al C:{:X} .{}", symbol.data.as_i64(), path)), 11 | _ => None, 12 | }) 13 | .sorted() 14 | .join(LINE_ENDING) 15 | } 16 | 17 | #[cfg(test)] 18 | mod tests { 19 | use super::*; 20 | use crate::codegen::SymbolData; 21 | use itertools::Itertools; 22 | 23 | #[test] 24 | fn can_generate_vice_symbols() { 25 | let mut st = SymbolTable::default(); 26 | st.insert( 27 | st.root, 28 | "foo", 29 | Symbol { 30 | span: None, 31 | segment: None, 32 | pass_idx: 0, 33 | data: SymbolData::Number(0x1234), 34 | ty: SymbolType::Label, 35 | }, 36 | ); 37 | assert_eq!( 38 | to_vice_symbols(&st).lines().collect_vec(), 39 | &["al C:1234 .foo"] 40 | ); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /mos-core/src/lib.rs: -------------------------------------------------------------------------------- 1 | /// Contains everything related to transforming the [AST](parser::ast) into actual code 2 | pub mod codegen; 3 | 4 | /// Contains code related to IO, file formats, and so on 5 | pub mod io; 6 | 7 | /// Parses source files and translates them into a stream of [parser::Token] 8 | pub mod parser; 9 | 10 | /// Everything related to Commodore handling 11 | pub mod cbm; 12 | 13 | /// Some testing helpers 14 | #[cfg(test)] 15 | pub mod testing; 16 | 17 | /// The main error and result types 18 | pub mod errors; 19 | /// Source code formatting 20 | pub mod formatting; 21 | 22 | /// Path to the MOS user guide 23 | pub const GUIDE_URL: &str = "https://mos.datatra.sh/guide"; 24 | 25 | #[cfg(windows)] 26 | /// A platform-specific newline. 27 | pub const LINE_ENDING: &str = "\r\n"; 28 | #[cfg(not(windows))] 29 | /// A platform-specific newline 30 | pub const LINE_ENDING: &str = "\n"; 31 | -------------------------------------------------------------------------------- /mos-core/src/parser/config_map.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | use nom::combinator::map; 3 | use nom::multi::many0; 4 | 5 | /// Tries to parse a single key-value pair within the map 6 | fn kvp(input: LocatedSpan) -> IResult { 7 | let value = alt((config_map, |input| { 8 | map(expression, |expr| Token::Expression(expr.data))(input) 9 | })); 10 | 11 | map_once( 12 | tuple((mws(config_key), mws(char('=')), mws(value))), 13 | move |(key, eq, value)| { 14 | let key = key.map(|k| k.as_str().to_string()); 15 | let value = Box::new(value); 16 | Token::ConfigPair { key, eq, value } 17 | }, 18 | )(input) 19 | } 20 | 21 | /// Tries to parse a config key 22 | fn config_key(input: LocatedSpan) -> IResult { 23 | map_once( 24 | recognize(pair( 25 | alt((alpha1, tag("-"))), 26 | many0(alt((alphanumeric1, tag("-")))), 27 | )), 28 | move |id: LocatedSpan| id.fragment().to_string(), 29 | )(input) 30 | } 31 | 32 | /// Tries to parse a config map 33 | pub fn config_map(input: LocatedSpan) -> IResult { 34 | map_once( 35 | tuple((mws(char('{')), many0(kvp), mws(char('}')))), 36 | move |(lparen, inner, rparen)| { 37 | Token::Config(Block { 38 | lparen, 39 | inner, 40 | rparen, 41 | }) 42 | }, 43 | )(input) 44 | } 45 | 46 | #[cfg(test)] 47 | mod tests { 48 | use super::config_map::config_map; 49 | use super::{LocatedSpan, State}; 50 | use crate::parser::source::InMemoryParsingSource; 51 | use crate::parser::ParserInstance; 52 | use std::sync::{Arc, Mutex}; 53 | 54 | #[test] 55 | fn parse_config_object() { 56 | check( 57 | r"/* */ 58 | { 59 | num = 123 60 | path = a.b 61 | nested = { 62 | nested-id = nested-v 63 | } 64 | }", 65 | r"/* */ 66 | { 67 | num = 123 68 | path = a.b 69 | nested = { 70 | nested-id = nested-v 71 | } 72 | }", 73 | ); 74 | } 75 | 76 | fn check(source: &str, expected: &str) { 77 | let state = State::new( 78 | None, 79 | InMemoryParsingSource::new().add("test.asm", source).into(), 80 | ); 81 | let state = Arc::new(Mutex::new(state)); 82 | let current_file = state.lock().unwrap().add_file("test.asm").unwrap(); 83 | let instance = ParserInstance::new(state, current_file); 84 | let input = LocatedSpan::new_extra(source, instance); 85 | let (_, expr) = config_map(input).ok().unwrap(); 86 | assert_eq!(format!("{}", expr), expected.to_string()); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /mos-core/src/parser/identifier.rs: -------------------------------------------------------------------------------- 1 | use itertools::Itertools; 2 | use std::fmt::{Debug, Display, Formatter}; 3 | 4 | #[macro_export] 5 | macro_rules! id { 6 | ($s:expr) => { 7 | $crate::parser::identifier::Identifier::from($s) 8 | }; 9 | } 10 | 11 | #[macro_export] 12 | macro_rules! idpath { 13 | ($s:expr) => { 14 | $crate::parser::identifier::IdentifierPath::from($s) 15 | }; 16 | } 17 | 18 | /// A Rust-style identifier that can be used to, well, identify things 19 | #[derive(Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] 20 | pub struct Identifier(String); 21 | 22 | impl Identifier { 23 | pub fn new>(s: S) -> Self { 24 | let s = s.into(); 25 | assert!( 26 | !s.contains('.'), 27 | "Identifiers may not contain periods. Use an IdentifierPath instead." 28 | ); 29 | Self(s) 30 | } 31 | 32 | pub fn anonymous(index: usize) -> Self { 33 | Identifier::new(format!("$scope_{}", index)) 34 | } 35 | 36 | pub fn sup() -> Self { 37 | Identifier::new("super") 38 | } 39 | 40 | pub fn as_str(&self) -> &str { 41 | &self.0 42 | } 43 | 44 | pub fn is_empty(&self) -> bool { 45 | self.0.is_empty() 46 | } 47 | 48 | pub fn is_super(&self) -> bool { 49 | self.0.to_lowercase().eq("super") 50 | } 51 | 52 | pub fn is_special(&self) -> bool { 53 | self.0 == "-" || self.0 == "+" || self.0.starts_with('$') 54 | } 55 | 56 | pub fn len(&self) -> usize { 57 | self.0.len() 58 | } 59 | } 60 | 61 | impl PartialEq for Identifier { 62 | fn eq(&self, other: &str) -> bool { 63 | self.0.as_str() == other 64 | } 65 | } 66 | 67 | impl From<&str> for Identifier { 68 | fn from(id: &str) -> Self { 69 | Identifier(id.to_string()) 70 | } 71 | } 72 | 73 | impl<'a> From<&'a Identifier> for &'a str { 74 | fn from(id: &'a Identifier) -> Self { 75 | id.0.as_str() 76 | } 77 | } 78 | 79 | impl<'a> From<&'a Identifier> for Identifier { 80 | fn from(id: &'a Identifier) -> Self { 81 | id.clone() 82 | } 83 | } 84 | 85 | impl PartialEq<&str> for Identifier { 86 | fn eq(&self, other: &&str) -> bool { 87 | other.eq(&self.0) 88 | } 89 | } 90 | 91 | impl Debug for Identifier { 92 | fn fmt(&self, f: &mut Formatter) -> std::fmt::Result { 93 | write!(f, "\"{}\"", self) 94 | } 95 | } 96 | 97 | impl Display for Identifier { 98 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 99 | write!(f, "{}", self.0) 100 | } 101 | } 102 | 103 | /// A path of multiple identifiers, usually written as being separated by dots (e.g. `foo.bar.baz`) 104 | #[derive(Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] 105 | pub struct IdentifierPath(Vec); 106 | 107 | impl Debug for IdentifierPath { 108 | fn fmt(&self, f: &mut Formatter) -> std::fmt::Result { 109 | write!(f, "\"{}\"", self) 110 | } 111 | } 112 | 113 | impl Display for IdentifierPath { 114 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 115 | write!( 116 | f, 117 | "{}", 118 | self.0.iter().map(|i| i.0.as_str()).collect_vec().join(".") 119 | ) 120 | } 121 | } 122 | 123 | impl From for IdentifierPath { 124 | fn from(id: Identifier) -> Self { 125 | IdentifierPath::new(&[id]) 126 | } 127 | } 128 | 129 | impl From<&Identifier> for IdentifierPath { 130 | fn from(id: &Identifier) -> Self { 131 | IdentifierPath::new(&[id.clone()]) 132 | } 133 | } 134 | 135 | impl From<&IdentifierPath> for IdentifierPath { 136 | fn from(path: &IdentifierPath) -> Self { 137 | IdentifierPath::new(&path.0.clone()) 138 | } 139 | } 140 | 141 | impl From<&str> for IdentifierPath { 142 | fn from(strs: &str) -> Self { 143 | IdentifierPath::new( 144 | &strs 145 | .split_terminator('.') 146 | .map(Identifier::from) 147 | .collect_vec(), 148 | ) 149 | } 150 | } 151 | 152 | impl IdentifierPath { 153 | pub fn new(ids: &[Identifier]) -> Self { 154 | Self(ids.to_vec()) 155 | } 156 | 157 | pub fn empty() -> Self { 158 | Self(vec![]) 159 | } 160 | 161 | pub fn is_empty(&self) -> bool { 162 | self.0.is_empty() 163 | } 164 | 165 | pub fn push(&mut self, id: &Identifier) { 166 | self.0.push(id.clone()); 167 | } 168 | 169 | pub fn pop(&mut self) -> Option { 170 | self.0.pop() 171 | } 172 | 173 | pub fn split(mut self) -> (IdentifierPath, Identifier) { 174 | let id = self.pop().unwrap(); 175 | (self, id) 176 | } 177 | 178 | pub fn pop_front(&mut self) -> Option { 179 | match self.0.len() { 180 | 0 => None, 181 | _ => Some(self.0.remove(0)), 182 | } 183 | } 184 | 185 | pub fn join>(&self, other: I) -> IdentifierPath { 186 | let other = other.into(); 187 | let mut p = self.0.clone(); 188 | p.extend(other.0); 189 | IdentifierPath(p) 190 | } 191 | 192 | pub fn len(&self) -> usize { 193 | self.0.len() 194 | } 195 | 196 | pub fn first(&self) -> Option<&Identifier> { 197 | self.0.first() 198 | } 199 | 200 | pub fn contains_super(&self) -> bool { 201 | self.0.iter().any(|id| id.is_super()) 202 | } 203 | 204 | pub fn is_special(&self) -> bool { 205 | self.0.first().map(|id| id.is_special()).unwrap_or_default() 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /mos-core/src/parser/mnemonic.rs: -------------------------------------------------------------------------------- 1 | use super::{IResult, LocatedSpan}; 2 | use nom::branch::alt; 3 | use nom::bytes::complete::tag_no_case; 4 | use nom::combinator::map; 5 | use strum::{EnumIter, EnumString, EnumVariantNames}; 6 | 7 | /// The available 6502 instructions. 8 | #[derive(Debug, Clone, Copy, PartialEq, Eq, EnumVariantNames, EnumIter, EnumString)] 9 | #[strum(serialize_all = "lowercase")] 10 | pub enum Mnemonic { 11 | Adc, 12 | And, 13 | Asl, 14 | Bcc, 15 | Bcs, 16 | Beq, 17 | Bit, 18 | Bmi, 19 | Bne, 20 | Bpl, 21 | Brk, 22 | Bvc, 23 | Bvs, 24 | Clc, 25 | Cld, 26 | Cli, 27 | Clv, 28 | Cmp, 29 | Cpx, 30 | Cpy, 31 | Dec, 32 | Dex, 33 | Dey, 34 | Eor, 35 | Inc, 36 | Inx, 37 | Iny, 38 | Jmp, 39 | Jsr, 40 | Lda, 41 | Ldx, 42 | Ldy, 43 | Lsr, 44 | Nop, 45 | Ora, 46 | Pha, 47 | Php, 48 | Pla, 49 | Plp, 50 | Rol, 51 | Ror, 52 | Rti, 53 | Rts, 54 | Sbc, 55 | Sec, 56 | Sed, 57 | Sei, 58 | Sta, 59 | Stx, 60 | Sty, 61 | Tax, 62 | Tay, 63 | Tsx, 64 | Txa, 65 | Txs, 66 | Tya, 67 | } 68 | 69 | macro_rules! parse_mnemonic { 70 | ( $ input : expr , $ expected : expr ) => { 71 | map(tag_no_case($input), |_| $expected) 72 | }; 73 | } 74 | 75 | pub(super) fn implied_mnemonic(input: LocatedSpan) -> IResult { 76 | alt(( 77 | alt(( 78 | parse_mnemonic!("asl", Mnemonic::Asl), 79 | parse_mnemonic!("brk", Mnemonic::Brk), 80 | parse_mnemonic!("clc", Mnemonic::Clc), 81 | parse_mnemonic!("cld", Mnemonic::Cld), 82 | parse_mnemonic!("cli", Mnemonic::Cli), 83 | parse_mnemonic!("clv", Mnemonic::Clv), 84 | parse_mnemonic!("dex", Mnemonic::Dex), 85 | parse_mnemonic!("dey", Mnemonic::Dey), 86 | parse_mnemonic!("inx", Mnemonic::Inx), 87 | parse_mnemonic!("iny", Mnemonic::Iny), 88 | parse_mnemonic!("lsr", Mnemonic::Lsr), 89 | parse_mnemonic!("nop", Mnemonic::Nop), 90 | parse_mnemonic!("pha", Mnemonic::Pha), 91 | parse_mnemonic!("php", Mnemonic::Php), 92 | parse_mnemonic!("pla", Mnemonic::Pla), 93 | parse_mnemonic!("plp", Mnemonic::Plp), 94 | parse_mnemonic!("rol", Mnemonic::Rol), 95 | parse_mnemonic!("ror", Mnemonic::Ror), 96 | parse_mnemonic!("rti", Mnemonic::Rti), 97 | parse_mnemonic!("rts", Mnemonic::Rts), 98 | parse_mnemonic!("sec", Mnemonic::Sec), 99 | )), 100 | alt(( 101 | parse_mnemonic!("sed", Mnemonic::Sed), 102 | parse_mnemonic!("sei", Mnemonic::Sei), 103 | parse_mnemonic!("tax", Mnemonic::Tax), 104 | parse_mnemonic!("tay", Mnemonic::Tay), 105 | parse_mnemonic!("tsx", Mnemonic::Tsx), 106 | parse_mnemonic!("txa", Mnemonic::Txa), 107 | parse_mnemonic!("txs", Mnemonic::Txs), 108 | parse_mnemonic!("tya", Mnemonic::Tya), 109 | )), 110 | ))(input) 111 | } 112 | 113 | /// Tries to parse a 6502 mnemonic 114 | pub(super) fn mnemonic(input: LocatedSpan) -> IResult { 115 | alt(( 116 | alt(( 117 | parse_mnemonic!("adc", Mnemonic::Adc), 118 | parse_mnemonic!("and", Mnemonic::And), 119 | parse_mnemonic!("asl", Mnemonic::Asl), 120 | parse_mnemonic!("bcc", Mnemonic::Bcc), 121 | parse_mnemonic!("bcs", Mnemonic::Bcs), 122 | parse_mnemonic!("beq", Mnemonic::Beq), 123 | parse_mnemonic!("bit", Mnemonic::Bit), 124 | parse_mnemonic!("bmi", Mnemonic::Bmi), 125 | parse_mnemonic!("bne", Mnemonic::Bne), 126 | parse_mnemonic!("bpl", Mnemonic::Bpl), 127 | parse_mnemonic!("bvc", Mnemonic::Bvc), 128 | parse_mnemonic!("bvs", Mnemonic::Bvs), 129 | parse_mnemonic!("cmp", Mnemonic::Cmp), 130 | parse_mnemonic!("cpx", Mnemonic::Cpx), 131 | parse_mnemonic!("cpy", Mnemonic::Cpy), 132 | parse_mnemonic!("dec", Mnemonic::Dec), 133 | parse_mnemonic!("eor", Mnemonic::Eor), 134 | parse_mnemonic!("inc", Mnemonic::Inc), 135 | parse_mnemonic!("jmp", Mnemonic::Jmp), 136 | parse_mnemonic!("jsr", Mnemonic::Jsr), 137 | parse_mnemonic!("lda", Mnemonic::Lda), 138 | )), 139 | alt(( 140 | parse_mnemonic!("ldx", Mnemonic::Ldx), 141 | parse_mnemonic!("ldy", Mnemonic::Ldy), 142 | parse_mnemonic!("lsr", Mnemonic::Lsr), 143 | parse_mnemonic!("ora", Mnemonic::Ora), 144 | parse_mnemonic!("rol", Mnemonic::Rol), 145 | parse_mnemonic!("ror", Mnemonic::Ror), 146 | parse_mnemonic!("sbc", Mnemonic::Sbc), 147 | parse_mnemonic!("sta", Mnemonic::Sta), 148 | parse_mnemonic!("stx", Mnemonic::Stx), 149 | parse_mnemonic!("sty", Mnemonic::Sty), 150 | )), 151 | ))(input) 152 | } 153 | -------------------------------------------------------------------------------- /mos-core/src/parser/source.rs: -------------------------------------------------------------------------------- 1 | use crate::errors::{map_io_error, CoreResult}; 2 | use codespan_reporting::diagnostic::Diagnostic; 3 | use std::collections::HashMap; 4 | use std::io::Read; 5 | use std::path::Path; 6 | use std::sync::{Arc, Mutex}; 7 | 8 | /// A source of data for the parser. Maps paths to their contents. 9 | pub trait ParsingSource { 10 | fn get_contents(&self, path: &Path) -> CoreResult; 11 | 12 | fn try_get_contents(&self, path: &Path) -> Option { 13 | self.get_contents(path).ok() 14 | } 15 | 16 | fn exists(&self, path: &Path) -> bool { 17 | self.try_get_contents(path).is_some() 18 | } 19 | } 20 | 21 | pub struct FileSystemParsingSource {} 22 | 23 | impl FileSystemParsingSource { 24 | pub fn new() -> Self { 25 | Self {} 26 | } 27 | } 28 | 29 | impl Default for FileSystemParsingSource { 30 | fn default() -> Self { 31 | Self::new() 32 | } 33 | } 34 | 35 | impl From for Arc> { 36 | fn from(t: FileSystemParsingSource) -> Self { 37 | Arc::new(Mutex::new(t)) 38 | } 39 | } 40 | 41 | impl ParsingSource for FileSystemParsingSource { 42 | fn get_contents(&self, path: &Path) -> CoreResult { 43 | let mut file = fs_err::File::open(&path).map_err(map_io_error)?; 44 | let mut source = String::new(); 45 | file.read_to_string(&mut source).map_err(map_io_error)?; 46 | Ok(source) 47 | } 48 | } 49 | 50 | pub struct InMemoryParsingSource { 51 | files: HashMap, 52 | } 53 | 54 | impl Default for InMemoryParsingSource { 55 | fn default() -> Self { 56 | Self::new() 57 | } 58 | } 59 | 60 | impl From for Arc> { 61 | fn from(t: InMemoryParsingSource) -> Self { 62 | Arc::new(Mutex::new(t)) 63 | } 64 | } 65 | 66 | impl InMemoryParsingSource { 67 | pub fn new() -> Self { 68 | Self { 69 | files: HashMap::new(), 70 | } 71 | } 72 | 73 | pub fn add>(mut self, filename: F, src: &str) -> Self { 74 | let filename = filename.into(); 75 | self.files.insert(filename, src.into()); 76 | self 77 | } 78 | } 79 | 80 | impl ParsingSource for InMemoryParsingSource { 81 | fn get_contents(&self, path: &Path) -> CoreResult { 82 | match self.files.get(path.to_str().unwrap()) { 83 | Some(data) => Ok(data.to_string()), 84 | None => Err(Diagnostic::error() 85 | .with_message(format!("file not found: '{}'", path.to_str().unwrap())) 86 | .into()), 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /mos-core/src/parser/testing.rs: -------------------------------------------------------------------------------- 1 | use crate::parser::ParseTree; 2 | 3 | impl ParseTree { 4 | #[allow(dead_code)] 5 | pub fn trace(&self) { 6 | log::trace!("==== ParseTree AST trace start ================================"); 7 | for item in self.main_file().tokens.iter() { 8 | log::trace!("{:#?}", item); 9 | } 10 | log::trace!("==== ParseTree AST trace end =================================="); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /mos-core/src/testing.rs: -------------------------------------------------------------------------------- 1 | use crate::parser::code_map::{CodeMap, Span}; 2 | use crate::LINE_ENDING; 3 | use itertools::Itertools; 4 | 5 | pub fn empty_span() -> Span { 6 | let mut codemap = CodeMap::default(); 7 | let f1 = codemap.add_file("test1.rs".to_string(), "abcd\nefghij\nqwerty".to_string()); 8 | f1.span 9 | } 10 | 11 | // Cross-platform eq 12 | pub fn xplat_eq, T: AsRef>(actual: S, expected: T) { 13 | // Split the result into lines to work around cross-platform line ending normalization issues 14 | assert_eq!( 15 | actual.as_ref().lines().join(LINE_ENDING), 16 | expected.as_ref().lines().join(LINE_ENDING) 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /mos-core/test-data/build/include.bin: -------------------------------------------------------------------------------- 1 |    ! -------------------------------------------------------------------------------- /mos-core/test-data/format/mos-default-formatting.toml: -------------------------------------------------------------------------------- 1 | [mnemonics] 2 | casing = 'lowercase' 3 | register_casing = 'lowercase' 4 | 5 | [braces] 6 | position = 'same_line' 7 | 8 | [whitespace] 9 | indent = 4 10 | -------------------------------------------------------------------------------- /mos-core/test-data/format/valid-formatted.asm: -------------------------------------------------------------------------------- 1 | // woof 2 | .define segment { 3 | name /*hello*/ = default 4 | start = $2000 + 4 - %00100 5 | } 6 | 7 | .file "foo.bin" 8 | 9 | .const test /* test value */ = 1 10 | .var test2 = 5 11 | 12 | // first comment 13 | // second comment 14 | * = $1000 15 | 16 | { 17 | lda data // interesting 18 | sta data 19 | stx data 20 | 21 | .if test { 22 | nop 23 | } 24 | 25 | .if test { 26 | sta $d020, x 27 | asl 28 | } else { 29 | nop 30 | } 31 | 32 | foo: rts 33 | } 34 | 35 | .segment "default" { 36 | lda #"] 5 | edition = "2018" 6 | repository = "https://github.com/datatrash/mos" 7 | license = "MIT" 8 | keywords = ["retro", "6502", "assembler"] 9 | 10 | [lib] 11 | 12 | [dependencies] 13 | log = { version = "0.4", features = ["max_level_trace", "release_max_level_info"] } 14 | simple_logger = "1" -------------------------------------------------------------------------------- /mos-testing/src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashSet; 2 | use std::fmt::Debug; 3 | use std::hash::Hash; 4 | 5 | pub fn assert_unordered_eq(a: &[T], b: &[T]) 6 | where 7 | T: Debug + Eq + Hash, 8 | { 9 | let a: HashSet<_> = a.iter().collect(); 10 | let b: HashSet<_> = b.iter().collect(); 11 | 12 | assert_eq!(a, b) 13 | } 14 | 15 | pub fn enable_tracing simple_logger::SimpleLogger>( 16 | customizer: F, 17 | ) { 18 | use simple_logger::*; 19 | let logger = SimpleLogger::new().with_level(log::LevelFilter::Off); 20 | let logger = customizer(logger); 21 | logger.init().unwrap(); 22 | } 23 | 24 | pub fn enable_default_tracing() { 25 | use simple_logger::*; 26 | let _ = SimpleLogger::new() 27 | .with_level(log::LevelFilter::Off) 28 | .with_module_level("mos", log::LevelFilter::Trace) 29 | .init(); 30 | } 31 | -------------------------------------------------------------------------------- /mos/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "mos" 3 | version = "0.0.0" # unused 4 | authors = ["Roy Jacobs "] 5 | edition = "2018" 6 | repository = "https://github.com/datatrash/mos" 7 | license = "MIT" 8 | keywords = ["retro", "6502", "assembler"] 9 | 10 | [dependencies] 11 | ansi_term = "0.12" 12 | anyhow = "1" 13 | byteorder = "1.4" 14 | argh = "0.1" 15 | codespan-reporting = "0.11" 16 | crossbeam-channel = "0.5" 17 | dissimilar = "1" 18 | emulator_6502 = { version = "1.1.0", features = ["implementation_transparency"] } 19 | fs-err = "2" 20 | itertools = "0.10" 21 | log = { version = "0.4", features = ["max_level_trace", "release_max_level_info"] } 22 | loggerv = "0.7" 23 | lsp-types = "0.89" 24 | lsp-server = "0.5" 25 | mos-core = { path = "../mos-core" } 26 | once_cell = "1.8" 27 | path-absolutize = "3" 28 | serde = { version = "1", features = ["derive"] } 29 | serde_json = "1" 30 | strum = { version = "0.23", features = ["derive"] } 31 | toml = "0.5" 32 | 33 | [dev-dependencies] 34 | mos-testing = { path = "../mos-testing" } 35 | tempfile = "3" 36 | -------------------------------------------------------------------------------- /mos/src/commands/format.rs: -------------------------------------------------------------------------------- 1 | use crate::config::Config; 2 | use crate::diagnostic_emitter::MosResult; 3 | use fs_err::OpenOptions; 4 | use mos_core::errors::map_io_error; 5 | use mos_core::formatting::format; 6 | use mos_core::parser::parse_or_err; 7 | use mos_core::parser::source::FileSystemParsingSource; 8 | use mos_core::LINE_ENDING; 9 | use std::io::Write; 10 | 11 | /// Formats input file(s) 12 | #[derive(argh::FromArgs, PartialEq, Eq, Debug)] 13 | #[argh(subcommand, name = "format")] 14 | pub struct FormatArgs {} 15 | 16 | pub fn format_command(cfg: &Config) -> MosResult<()> { 17 | let input_name = cfg.build.entry.clone(); 18 | let tree = parse_or_err(input_name.as_ref(), FileSystemParsingSource::new().into())?; 19 | 20 | for file in tree.files.keys() { 21 | let formatted = format(file, tree.clone(), cfg.formatting); 22 | let formatted = formatted.replace('\n', LINE_ENDING); 23 | let mut output_file = OpenOptions::new() 24 | .truncate(true) 25 | .write(true) 26 | .open(file) 27 | .map_err(map_io_error)?; 28 | output_file 29 | .write_all(formatted.as_bytes()) 30 | .map_err(map_io_error)?; 31 | } 32 | 33 | Ok(()) 34 | } 35 | -------------------------------------------------------------------------------- /mos/src/commands/init.rs: -------------------------------------------------------------------------------- 1 | use crate::config::Config; 2 | use crate::diagnostic_emitter::MosResult; 3 | use codespan_reporting::diagnostic::Diagnostic; 4 | use fs_err as fs; 5 | use mos_core::errors::{map_io_error, Diagnostics}; 6 | use std::path::Path; 7 | 8 | /// Creates a new MOS project configuration file 9 | #[derive(argh::FromArgs, PartialEq, Eq, Debug)] 10 | #[argh(subcommand, name = "init")] 11 | pub struct InitArgs {} 12 | 13 | pub fn init_command(root: &Path, _cfg: &Config) -> MosResult<()> { 14 | if root.join("mos.toml").exists() { 15 | return Err(Diagnostics::from( 16 | Diagnostic::error().with_message("`mos init` cannot be run in existing MOS projects"), 17 | ) 18 | .into()); 19 | } 20 | 21 | fs::write("mos.toml", get_init_toml()).map_err(map_io_error)?; 22 | 23 | Ok(()) 24 | } 25 | 26 | fn get_init_toml() -> &'static str { 27 | include_str!("init.toml") 28 | } 29 | 30 | #[cfg(test)] 31 | mod tests { 32 | use super::get_init_toml; 33 | use crate::config::Config; 34 | use crate::diagnostic_emitter::MosResult; 35 | 36 | #[test] 37 | fn create_init_mos_toml() -> MosResult<()> { 38 | let cfg = Config::from_toml(get_init_toml())?; 39 | assert_eq!(cfg.build.entry, "main.asm"); 40 | 41 | Ok(()) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /mos/src/commands/init.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | entry = "main.asm" -------------------------------------------------------------------------------- /mos/src/commands/lsp.rs: -------------------------------------------------------------------------------- 1 | use crate::debugger::DebugServer; 2 | use crate::diagnostic_emitter::MosResult; 3 | use crate::lsp::{LspContext, LspServer}; 4 | 5 | /// Starts a Language Server 6 | #[derive(argh::FromArgs, PartialEq, Eq, Debug)] 7 | #[argh(subcommand, name = "lsp")] 8 | pub struct LspArgs { 9 | /// the port on which the debug adapter server should listen 10 | #[argh(option, default = "6503", short = 'p')] 11 | debug_adapter_port: u16, 12 | } 13 | 14 | pub fn lsp_command(args: &LspArgs) -> MosResult<()> { 15 | let mut ctx = LspContext::new(); 16 | ctx.listen_stdio(); 17 | let lsp = LspServer::new(ctx); 18 | let mut dbg = DebugServer::new(lsp.context()); 19 | dbg.start(args.debug_adapter_port)?; 20 | 21 | lsp.start()?; 22 | log::info!("LSP ended"); 23 | dbg.join()?; 24 | log::info!("DBG ended"); 25 | 26 | Ok(()) 27 | } 28 | 29 | #[cfg(test)] 30 | mod tests { 31 | use super::*; 32 | use crate::lsp::LspContext; 33 | use crossbeam_channel::{Receiver, Sender}; 34 | use lsp_server::{Message, Notification, Request}; 35 | use lsp_types::InitializeParams; 36 | use serde::Serialize; 37 | use std::thread; 38 | 39 | #[test] 40 | fn can_shutdown() -> MosResult<()> { 41 | let mut ctx = LspContext::new(); 42 | let conn = ctx.listen_memory(); 43 | 44 | let sender = conn.sender; 45 | let receiver = conn.receiver; 46 | 47 | // Some other thread will be listening to the LSP shutdown as well (used by the debug adapter) 48 | let shutdown_receiver = ctx.add_shutdown_handler(); 49 | let some_other_thread = thread::spawn(move || { 50 | // expect this shutdown receiver to be called 51 | shutdown_receiver.receiver().recv().unwrap(); 52 | log::info!("Received shutdown callback!"); 53 | }); 54 | 55 | thread::spawn(move || { 56 | let lsp = LspServer::new(ctx); 57 | lsp.start().unwrap(); 58 | }); 59 | 60 | // Hey LSP, get ready to initialize 61 | let params = serde_json::from_str::(r#"{ "capabilities": {} }"#).unwrap(); 62 | send_req(&sender, "initialize", params); 63 | // Receive capabilities response 64 | expect_msg(&receiver, |_| true); 65 | // Great, we're now initialized 66 | send_empty_not(&sender, "initialized"); 67 | 68 | // Let's shutdown now 69 | send_req(&sender, "shutdown", ()); 70 | // Receive shutdown response 71 | expect_msg(&receiver, |_| true); 72 | 73 | some_other_thread.join().unwrap(); 74 | Ok(()) 75 | } 76 | 77 | fn send_req(sender: &Sender, ty: &str, params: P) { 78 | sender 79 | .send(Message::Request(Request::new(0.into(), ty.into(), params))) 80 | .unwrap(); 81 | } 82 | 83 | fn send_empty_not(sender: &Sender, ty: &str) { 84 | sender 85 | .send(Message::Notification(Notification::new(ty.into(), ()))) 86 | .unwrap(); 87 | } 88 | 89 | fn expect_msg bool>(receiver: &Receiver, pred: F) { 90 | let msg = receiver.recv().unwrap(); 91 | if !pred(&msg) { 92 | panic!("Did not expect: {:?}", msg); 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /mos/src/commands/mod.rs: -------------------------------------------------------------------------------- 1 | pub use build::*; 2 | pub use format::*; 3 | pub use init::*; 4 | pub use lsp::*; 5 | pub use test::*; 6 | pub use version::*; 7 | 8 | mod build; 9 | mod format; 10 | mod init; 11 | mod lsp; 12 | mod test; 13 | mod version; 14 | -------------------------------------------------------------------------------- /mos/src/commands/test.rs: -------------------------------------------------------------------------------- 1 | use crate::config::Config; 2 | use crate::diagnostic_emitter::{DiagnosticEmitter, MosResult}; 3 | use crate::test_runner::{enumerate_test_cases, format_cpu_details, ExecuteResult, TestRunner}; 4 | use crate::utils::paint; 5 | use crate::Args; 6 | use ansi_term::Colour; 7 | use mos_core::parser::source::{FileSystemParsingSource, ParsingSource}; 8 | use serde::Deserialize; 9 | use std::path::{Path, PathBuf}; 10 | use std::sync::{Arc, Mutex}; 11 | 12 | /// Runs unit test(s) 13 | #[derive(argh::FromArgs, PartialEq, Eq, Debug)] 14 | #[argh(subcommand, name = "test")] 15 | pub struct TestArgs {} 16 | 17 | #[derive(Debug, Default, Clone, Deserialize, PartialEq, Eq)] 18 | #[serde(default, deny_unknown_fields, rename_all = "kebab-case")] 19 | pub struct TestOptions { 20 | pub name: Option, 21 | pub filter: Option, 22 | } 23 | 24 | pub fn test_command(args: &Args, root: &Path, cfg: &Config) -> MosResult { 25 | let use_color = !args.no_color; 26 | let src: Arc> = FileSystemParsingSource::new().into(); 27 | let input_path = root.join(PathBuf::from(&cfg.build.entry)); 28 | let mut test_cases = enumerate_test_cases(src.clone(), &input_path)?; 29 | 30 | if let Some(test_name) = &cfg.test.name { 31 | test_cases.retain(|(_, c)| &c.to_string() == test_name); 32 | } 33 | if let Some(test_filter) = &cfg.test.filter { 34 | test_cases.retain(|(_, c)| c.to_string().contains(test_filter)); 35 | } 36 | 37 | let mut failed = vec![]; 38 | let mut num_passed = 0; 39 | for (_, test_case) in test_cases { 40 | let mut runner = TestRunner::new(src.clone(), &input_path, &test_case)?; 41 | let (num_cycles, failure) = match runner.run()? { 42 | ExecuteResult::Running => panic!(), 43 | ExecuteResult::TestFailed(num_cycles, failure) => (num_cycles, Some(failure)), 44 | ExecuteResult::TestSuccess(num_cycles) => (num_cycles, None), 45 | }; 46 | let cycles = if use_color { 47 | paint( 48 | use_color, 49 | Colour::Yellow, 50 | format!(" ({} cycles)", num_cycles), 51 | ) 52 | } else { 53 | format!(" ({} cycles)", num_cycles).into() 54 | }; 55 | 56 | let msg = match failure { 57 | Some(failure) => { 58 | failed.push((test_case.to_string(), failure)); 59 | paint(use_color, Colour::Red, "failed") 60 | } 61 | None => { 62 | num_passed += 1; 63 | paint(use_color, Colour::Green, "ok") 64 | } 65 | }; 66 | log::info!("test '{}' ... {}{}", test_case, msg, cycles); 67 | } 68 | 69 | let test_result = if !failed.is_empty() { 70 | paint(use_color, Colour::Red, "FAILED") 71 | } else { 72 | paint(use_color, Colour::Green, "ok") 73 | }; 74 | 75 | log::info!(""); 76 | if !failed.is_empty() { 77 | log::info!("failed tests:"); 78 | log::info!(""); 79 | let mut emitter = DiagnosticEmitter::stdout(args); 80 | for (failed_test_name, failure) in &failed { 81 | log::info!("test: {}", failed_test_name); 82 | emitter.emit_diagnostics(&failure.diagnostic); 83 | log::info!("{}", format_cpu_details(&failure.cpu, use_color)); 84 | if !failure.traces.is_empty() { 85 | log::info!("traces:"); 86 | for trace in &failure.traces { 87 | log::info!("- {}", &trace); 88 | } 89 | } 90 | } 91 | log::info!(""); 92 | log::info!("failed test summary:"); 93 | for (failed_test_name, _) in &failed { 94 | log::info!(" {}", failed_test_name); 95 | } 96 | log::info!(""); 97 | } 98 | log::info!( 99 | "test result: {}. {} passed; {} failed", 100 | test_result, 101 | num_passed, 102 | failed.len() 103 | ); 104 | 105 | if !failed.is_empty() { 106 | Ok(1) 107 | } else { 108 | Ok(0) 109 | } 110 | } 111 | 112 | #[cfg(test)] 113 | mod tests { 114 | use super::*; 115 | use crate::commands::BuildOptions; 116 | use crate::{ErrorStyle, Subcommand}; 117 | use anyhow::Result; 118 | use std::path::PathBuf; 119 | 120 | #[test] 121 | fn can_invoke_ok_test() -> Result<()> { 122 | let entry = test_cli_build().join("some-tests.asm"); 123 | let cfg = Config { 124 | build: BuildOptions { 125 | entry: entry.to_string_lossy().into(), 126 | target_directory: target().to_string_lossy().into(), 127 | ..Default::default() 128 | }, 129 | test: TestOptions { 130 | name: Some("ok".into()), 131 | ..Default::default() 132 | }, 133 | ..Default::default() 134 | }; 135 | assert_eq!(test_command(&test_args(), root().as_path(), &cfg)?, 0); 136 | Ok(()) 137 | } 138 | 139 | #[test] 140 | fn can_invoke_failing_test() -> Result<()> { 141 | let entry = test_cli_build().join("some-tests.asm"); 142 | let cfg = Config { 143 | build: BuildOptions { 144 | entry: entry.to_string_lossy().into(), 145 | target_directory: target().to_string_lossy().into(), 146 | ..Default::default() 147 | }, 148 | test: TestOptions { 149 | name: Some("fail".into()), 150 | ..Default::default() 151 | }, 152 | ..Default::default() 153 | }; 154 | assert_eq!(test_command(&test_args(), root().as_path(), &cfg)?, 1); 155 | Ok(()) 156 | } 157 | 158 | fn test_args() -> Args { 159 | Args { 160 | subcommand: Subcommand::Test(TestArgs {}), 161 | no_color: false, 162 | verbosity: 0, 163 | error_style: ErrorStyle::Short, 164 | } 165 | } 166 | 167 | fn root() -> PathBuf { 168 | PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("..") 169 | } 170 | 171 | fn target() -> PathBuf { 172 | root().join(PathBuf::from("target")) 173 | } 174 | 175 | fn test_cli_build() -> PathBuf { 176 | root() 177 | .join(PathBuf::from("mos")) 178 | .join(PathBuf::from("test-data").join(PathBuf::from("test"))) 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /mos/src/commands/version.rs: -------------------------------------------------------------------------------- 1 | use crate::diagnostic_emitter::MosResult; 2 | 3 | /// Prints the version of the application 4 | #[derive(argh::FromArgs, PartialEq, Eq, Debug)] 5 | #[argh(subcommand, name = "version")] 6 | pub struct VersionArgs {} 7 | 8 | pub fn version_command() -> MosResult<()> { 9 | log::info!("{}", option_env!("RELEASE_VERSION").unwrap_or("unknown")); 10 | Ok(()) 11 | } 12 | -------------------------------------------------------------------------------- /mos/src/config.rs: -------------------------------------------------------------------------------- 1 | use crate::commands::{BuildOptions, TestOptions}; 2 | use crate::diagnostic_emitter::MosResult; 3 | use mos_core::formatting::FormattingOptions; 4 | use serde::Deserialize; 5 | 6 | #[derive(Clone, Debug, Default, Deserialize, PartialEq, Eq)] 7 | #[serde(default, deny_unknown_fields, rename_all = "kebab-case")] 8 | pub struct Config { 9 | pub build: BuildOptions, 10 | pub formatting: FormattingOptions, 11 | pub test: TestOptions, 12 | } 13 | 14 | impl Config { 15 | pub fn from_toml(toml: &str) -> MosResult { 16 | Ok(toml::from_str(toml)?) 17 | } 18 | } 19 | 20 | #[cfg(test)] 21 | mod tests { 22 | use super::*; 23 | 24 | #[test] 25 | fn can_parse_build_options() -> MosResult<()> { 26 | assert_eq!( 27 | Config::from_toml( 28 | r#"[build] 29 | entry = "main.asm" 30 | target-directory = "target" 31 | symbols = [] 32 | "# 33 | )?, 34 | Config { 35 | ..Default::default() 36 | } 37 | ); 38 | 39 | Ok(()) 40 | } 41 | 42 | #[test] 43 | fn can_parse_formatting_options() -> MosResult<()> { 44 | assert_eq!( 45 | Config::from_toml( 46 | r"[formatting] 47 | mnemonics.casing = 'lowercase' 48 | mnemonics.register-casing = 'lowercase' 49 | braces.position = 'same-line' 50 | whitespace.indent = 4 51 | whitespace.label-margin = 20 52 | whitespace.code-margin = 30 53 | " 54 | )?, 55 | Config { 56 | ..Default::default() 57 | } 58 | ); 59 | 60 | Ok(()) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /mos/src/debugger/adapters/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod test_runner; 2 | pub mod vice; 3 | 4 | use crate::debugger::types::LaunchRequestArguments; 5 | use crate::diagnostic_emitter::MosResult; 6 | use crate::memory_accessor::MemoryAccessor; 7 | use crossbeam_channel::{bounded, Receiver, TryRecvError}; 8 | use mos_core::codegen::{CodegenContext, ProgramCounter}; 9 | use mos_core::parser::code_map::SpanLoc; 10 | use std::collections::HashMap; 11 | use std::io::{BufReader, ErrorKind}; 12 | use std::net::TcpStream; 13 | use std::ops::Range; 14 | use std::path::PathBuf; 15 | use std::process::Command; 16 | use std::sync::{Arc, Mutex, RwLock, RwLockReadGuard, RwLockWriteGuard}; 17 | use std::thread; 18 | use std::thread::JoinHandle; 19 | use std::time::Duration; 20 | 21 | pub struct Machine { 22 | adapter: Arc>>, 23 | poller: JoinHandle>, 24 | } 25 | 26 | impl Machine { 27 | pub fn new(adapter: Arc>>) -> Self { 28 | let adap = adapter.clone(); 29 | let poller = thread::spawn(move || { 30 | log::debug!("Starting machine poller thread."); 31 | loop { 32 | { 33 | let mut adap = adap.write().unwrap(); 34 | if !adap.is_connected()? { 35 | break; 36 | } 37 | adap.poll()?; 38 | } 39 | 40 | thread::sleep(Duration::from_millis(50)); 41 | } 42 | log::debug!("Shutting down machine poller thread."); 43 | Ok(()) 44 | }); 45 | 46 | Self { adapter, poller } 47 | } 48 | 49 | pub fn adapter(&self) -> RwLockReadGuard> { 50 | self.adapter.read().unwrap() 51 | } 52 | 53 | pub fn adapter_mut(&self) -> RwLockWriteGuard> { 54 | self.adapter.write().unwrap() 55 | } 56 | 57 | pub fn join(self) { 58 | let _ = self.poller.join().unwrap(); 59 | } 60 | } 61 | 62 | #[derive(Debug, PartialEq, Eq)] 63 | pub enum MachineEvent { 64 | RunningStateChanged { 65 | old: MachineRunningState, 66 | new: MachineRunningState, 67 | }, 68 | Message { 69 | output: String, 70 | location: Option, 71 | }, 72 | Disconnected, 73 | } 74 | 75 | pub trait MachineAdapter: MemoryAccessor { 76 | /// If the adapter is doing its own code generation instead of the one that the LSP is doing, we can grab that here 77 | /// to generate the breakpoint mappings etc 78 | fn codegen(&self) -> Option>>; 79 | 80 | /// Poll the underlying machine for data and handle any events that may have occured 81 | fn poll(&mut self) -> MosResult<()>; 82 | /// The receiver for any events that may originate from the underlying machine 83 | fn receiver(&self) -> MosResult>; 84 | 85 | /// Transition the underlying machine from launching to running 86 | fn start(&mut self) -> MosResult<()>; 87 | /// Stops the underlying machine and kills any associated processes 88 | fn stop(&mut self) -> MosResult<()>; 89 | /// Is the underlying machine still connected? 90 | fn is_connected(&self) -> MosResult; 91 | /// What is the current running state? 92 | fn running_state(&self) -> MosResult; 93 | 94 | /// When paused, resume 95 | fn resume(&mut self) -> MosResult<()>; 96 | /// When running, pause 97 | fn pause(&mut self) -> MosResult<()>; 98 | /// When paused, go to the next instruction (skip subroutines) 99 | fn next(&mut self) -> MosResult<()>; 100 | /// When paused, step in to the next instruction (also step into subroutines) 101 | fn step_in(&mut self) -> MosResult<()>; 102 | /// When paused, step out of a subroutine 103 | fn step_out(&mut self) -> MosResult<()>; 104 | 105 | /// Set all breakpoints for a specific source path 106 | fn set_breakpoints( 107 | &mut self, 108 | source_path: &str, 109 | breakpoints: Vec, 110 | ) -> MosResult>; 111 | 112 | /// Gets the current register values 113 | fn registers(&self) -> MosResult>; 114 | 115 | /// Get the cpu flags 116 | fn flags(&self) -> MosResult; 117 | 118 | /// Sets a variable to a new value 119 | /// Note: Implementations currently only supports setting registers 120 | fn set_variable(&mut self, name: String, value: u8) -> MosResult<()>; 121 | } 122 | 123 | #[derive(Clone, Debug, PartialEq, Eq)] 124 | pub struct MachineBreakpoint { 125 | pub line: usize, 126 | pub column: Option, 127 | pub range: Range, 128 | } 129 | 130 | #[derive(Clone, Debug, PartialEq, Eq)] 131 | pub struct MachineValidatedBreakpoint { 132 | pub id: usize, 133 | pub source_path: String, 134 | pub requested: MachineBreakpoint, 135 | pub range: Range, 136 | } 137 | 138 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] 139 | pub enum MachineRunningState { 140 | Launching, 141 | Running, 142 | Stopped(ProgramCounter), 143 | } 144 | -------------------------------------------------------------------------------- /mos/src/debugger/connection.rs: -------------------------------------------------------------------------------- 1 | use crate::debugger::protocol::ProtocolMessage; 2 | use crate::diagnostic_emitter::MosResult; 3 | use crossbeam_channel::{bounded, Receiver, Sender}; 4 | use std::io::{BufRead, BufReader, Write}; 5 | use std::net::{TcpListener, TcpStream}; 6 | use std::{io, thread}; 7 | 8 | pub struct DebugConnection { 9 | pub sender: Sender, 10 | pub receiver: Receiver, 11 | } 12 | 13 | impl DebugConnection { 14 | pub fn tcp(address: &str) -> MosResult<(DebugConnection, DebugIoThreads)> { 15 | let listener = TcpListener::bind(address)?; 16 | let (stream, _) = listener.accept()?; 17 | let (reader_receiver, reader) = make_reader(stream.try_clone().unwrap()); 18 | let (writer_sender, writer) = make_write(stream.try_clone().unwrap()); 19 | let io_threads = DebugIoThreads { reader, writer }; 20 | Ok(( 21 | DebugConnection { 22 | sender: writer_sender, 23 | receiver: reader_receiver, 24 | }, 25 | io_threads, 26 | )) 27 | } 28 | } 29 | 30 | #[allow(dead_code)] 31 | pub struct DebugIoThreads { 32 | reader: thread::JoinHandle>, 33 | writer: thread::JoinHandle>, 34 | } 35 | 36 | impl ProtocolMessage { 37 | pub fn read(r: &mut impl BufRead) -> io::Result> { 38 | ProtocolMessage::_read(r) 39 | } 40 | fn _read(r: &mut dyn BufRead) -> io::Result> { 41 | let text = match read_msg_text(r)? { 42 | None => return Ok(None), 43 | Some(text) => text, 44 | }; 45 | let msg = serde_json::from_str(&text)?; 46 | Ok(Some(msg)) 47 | } 48 | pub fn write(self, w: &mut impl Write) -> io::Result<()> { 49 | let text = serde_json::to_string(&self)?; 50 | write_msg_text(w, &text) 51 | } 52 | } 53 | 54 | fn read_msg_text(inp: &mut dyn BufRead) -> io::Result> { 55 | fn invalid_data(error: impl Into>) -> io::Error { 56 | io::Error::new(io::ErrorKind::InvalidData, error) 57 | } 58 | macro_rules! invalid_data { 59 | ($($tt:tt)*) => (invalid_data(format!($($tt)*))) 60 | } 61 | 62 | let mut size = None; 63 | let mut buf = String::new(); 64 | loop { 65 | buf.clear(); 66 | if inp.read_line(&mut buf)? == 0 { 67 | return Ok(None); 68 | } 69 | if !buf.ends_with("\r\n") { 70 | return Err(invalid_data!("malformed header: {:?}", buf)); 71 | } 72 | let buf = &buf[..buf.len() - 2]; 73 | if buf.is_empty() { 74 | break; 75 | } 76 | let mut parts = buf.splitn(2, ": "); 77 | let header_name = parts.next().unwrap(); 78 | let header_value = parts 79 | .next() 80 | .ok_or_else(|| invalid_data!("malformed header: {:?}", buf))?; 81 | if header_name == "Content-Length" { 82 | size = Some(header_value.parse::().map_err(invalid_data)?); 83 | } 84 | } 85 | let size: usize = size.ok_or_else(|| invalid_data!("no Content-Length"))?; 86 | let mut buf = buf.into_bytes(); 87 | buf.resize(size, 0); 88 | inp.read_exact(&mut buf)?; 89 | let buf = String::from_utf8(buf).map_err(invalid_data)?; 90 | log::debug!("< {}", buf); 91 | Ok(Some(buf)) 92 | } 93 | 94 | fn write_msg_text(out: &mut dyn Write, msg: &str) -> io::Result<()> { 95 | log::debug!("> {}", msg); 96 | write!(out, "Content-Length: {}\r\n\r\n", msg.len())?; 97 | out.write_all(msg.as_bytes())?; 98 | out.flush()?; 99 | Ok(()) 100 | } 101 | 102 | fn make_reader( 103 | stream: TcpStream, 104 | ) -> ( 105 | Receiver, 106 | thread::JoinHandle>, 107 | ) { 108 | let (reader_sender, reader_receiver) = bounded::(0); 109 | let reader = thread::spawn(move || { 110 | let mut buf_read = BufReader::new(stream); 111 | while let Ok(Some(msg)) = ProtocolMessage::read(&mut buf_read) { 112 | let is_exit = match &msg { 113 | ProtocolMessage::Request(req) => req.command == "disconnect", 114 | _ => false, 115 | }; 116 | if let Err(e) = reader_sender.send(msg) { 117 | log::debug!("Could not send protocol message to reader: {:?}", e); 118 | } 119 | if is_exit { 120 | break; 121 | } 122 | } 123 | Ok(()) 124 | }); 125 | (reader_receiver, reader) 126 | } 127 | 128 | fn make_write( 129 | mut stream: TcpStream, 130 | ) -> (Sender, thread::JoinHandle>) { 131 | let (writer_sender, writer_receiver) = bounded::(0); 132 | let writer = thread::spawn(move || { 133 | if let Err(e) = writer_receiver 134 | .into_iter() 135 | .try_for_each(|it| it.write(&mut stream)) 136 | { 137 | log::debug!("Could not receive protocol message from writer: {:?}", e); 138 | } 139 | Ok(()) 140 | }); 141 | (writer_sender, writer) 142 | } 143 | -------------------------------------------------------------------------------- /mos/src/debugger/protocol.rs: -------------------------------------------------------------------------------- 1 | use serde::de::DeserializeOwned; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | #[derive(Debug, Serialize, Deserialize)] 5 | #[serde(tag = "type", rename_all = "camelCase")] 6 | pub enum ProtocolMessage { 7 | Request(RequestMessage), 8 | Event(EventMessage), 9 | Response(ResponseMessage), 10 | } 11 | 12 | impl ProtocolMessage { 13 | pub fn seq(&self) -> usize { 14 | match self { 15 | Self::Request(req) => req.seq, 16 | _ => unimplemented!(), 17 | } 18 | } 19 | } 20 | 21 | #[derive(Debug, Serialize, Deserialize, Clone)] 22 | #[serde(rename_all = "camelCase")] 23 | pub struct RequestMessage { 24 | pub seq: usize, 25 | pub command: String, 26 | #[serde(default = "serde_json::Value::default")] 27 | #[serde(skip_serializing_if = "serde_json::Value::is_null")] 28 | pub arguments: serde_json::Value, 29 | } 30 | 31 | #[derive(Debug, Serialize, Deserialize, Clone)] 32 | #[serde(rename_all = "camelCase")] 33 | pub struct EventMessage { 34 | pub seq: usize, 35 | pub event: String, 36 | #[serde(default = "serde_json::Value::default")] 37 | #[serde(skip_serializing_if = "serde_json::Value::is_null")] 38 | pub body: serde_json::Value, 39 | } 40 | 41 | #[derive(Debug, Serialize, Deserialize, Clone)] 42 | #[serde(rename_all = "camelCase")] 43 | pub struct ResponseMessage { 44 | pub seq: usize, 45 | #[serde(rename = "request_seq")] // wtf 46 | pub request_seq: usize, 47 | pub command: String, 48 | pub success: bool, 49 | #[serde(skip_serializing_if = "Option::is_none")] 50 | pub message: Option, 51 | #[serde(default = "serde_json::Value::default")] 52 | #[serde(skip_serializing_if = "serde_json::Value::is_null")] 53 | pub body: serde_json::Value, 54 | } 55 | 56 | pub trait Request { 57 | type Arguments: DeserializeOwned + Serialize; 58 | type Response: DeserializeOwned + Serialize; 59 | const COMMAND: &'static str; 60 | } 61 | 62 | pub trait Event { 63 | type Body: DeserializeOwned + Serialize; 64 | const EVENT: &'static str; 65 | } 66 | 67 | pub trait Response { 68 | type Body: DeserializeOwned + Serialize; 69 | const COMMAND: &'static str; 70 | } 71 | -------------------------------------------------------------------------------- /mos/src/diagnostic_emitter.rs: -------------------------------------------------------------------------------- 1 | use crate::{Args, ErrorStyle}; 2 | use codespan_reporting::diagnostic::{Diagnostic, Label}; 3 | use codespan_reporting::term::termcolor::{ColorChoice, ColorSpec, StandardStream, WriteColor}; 4 | use codespan_reporting::term::{Config, DisplayStyle}; 5 | use mos_core::errors::Diagnostics; 6 | use mos_core::parser::code_map::CodeMap; 7 | use std::io; 8 | 9 | pub type MosResult = anyhow::Result; 10 | 11 | pub struct DiagnosticEmitter { 12 | writer: Box, 13 | config: Config, 14 | } 15 | 16 | impl DiagnosticEmitter { 17 | pub fn stdout(args: &Args) -> Self { 18 | let color_choice = if args.no_color { 19 | ColorChoice::Never 20 | } else { 21 | ColorChoice::Auto 22 | }; 23 | 24 | let display_style = match args.error_style { 25 | ErrorStyle::Short => DisplayStyle::Short, 26 | ErrorStyle::Medium => DisplayStyle::Medium, 27 | ErrorStyle::Rich => DisplayStyle::Rich, 28 | }; 29 | 30 | let config = Config { 31 | display_style, 32 | ..Default::default() 33 | }; 34 | 35 | Self { 36 | writer: Box::new(StandardStream::stdout(color_choice)), 37 | config, 38 | } 39 | } 40 | 41 | pub fn emit(&mut self, error: anyhow::Error) { 42 | match error.downcast_ref::() { 43 | Some(d) => { 44 | self.emit_diagnostics(d); 45 | } 46 | None => { 47 | self.emit_diagnostics(&Diagnostic::error().with_message(error.to_string()).into()); 48 | } 49 | } 50 | } 51 | 52 | pub fn emit_diagnostics(&mut self, diagnostics: &Diagnostics) { 53 | let dummy_code_map = CodeMap::default(); 54 | let code_map = diagnostics.code_map().unwrap_or(&dummy_code_map); 55 | for diag in diagnostics.iter() { 56 | // Go over all labels and fix up the range, which needs to be the bytes inside the source file to extract 57 | let labels = diag 58 | .labels 59 | .iter() 60 | .map(|label| { 61 | let sl = code_map.look_up_span(label.file_id); 62 | let range = ((label.file_id.low() - sl.file.span.low()) as usize) 63 | ..((label.file_id.high() - sl.file.span.low()) as usize); 64 | 65 | Label { 66 | style: label.style, 67 | file_id: label.file_id, 68 | range, 69 | message: label.message.clone(), 70 | } 71 | }) 72 | .collect(); 73 | let mut diag = diag.clone(); 74 | diag.labels = labels; 75 | 76 | codespan_reporting::term::emit(&mut self.writer, &self.config, &code_map, &diag) 77 | .unwrap(); 78 | } 79 | } 80 | } 81 | 82 | #[derive(Default)] 83 | struct TestLogStream { 84 | bytes: Vec, 85 | } 86 | 87 | impl io::Write for TestLogStream { 88 | fn write(&mut self, buf: &[u8]) -> io::Result { 89 | self.bytes.extend(buf); 90 | Ok(buf.len()) 91 | } 92 | 93 | fn flush(&mut self) -> io::Result<()> { 94 | Ok(()) 95 | } 96 | } 97 | 98 | impl WriteColor for TestLogStream { 99 | fn supports_color(&self) -> bool { 100 | false 101 | } 102 | 103 | fn set_color(&mut self, _: &ColorSpec) -> io::Result<()> { 104 | Ok(()) 105 | } 106 | 107 | fn reset(&mut self) -> io::Result<()> { 108 | Ok(()) 109 | } 110 | } 111 | 112 | #[cfg(test)] 113 | mod tests { 114 | use crate::diagnostic_emitter::{DiagnosticEmitter, TestLogStream}; 115 | use mos_core::codegen::{codegen, CodegenContext, CodegenOptions}; 116 | use mos_core::errors::CoreResult; 117 | use mos_core::parser::parse_or_err; 118 | use mos_core::parser::source::{InMemoryParsingSource, ParsingSource}; 119 | use std::path::Path; 120 | use std::sync::{Arc, Mutex}; 121 | 122 | #[test] 123 | fn error_unknown_identifiers() { 124 | let src = InMemoryParsingSource::new() 125 | .add("test.asm", r#".import * from "wrong.asm""#) 126 | .add("wrong.asm", ".byte foo") 127 | .into(); 128 | let err = test_codegen_parsing_source(src, CodegenOptions::default()) 129 | .err() 130 | .unwrap(); 131 | assert_eq!( 132 | err.to_string(), 133 | "wrong.asm:1:7: error: unknown identifier: foo" 134 | ); 135 | let mut emitter = DiagnosticEmitter { 136 | writer: Box::new(TestLogStream::default()), 137 | config: Default::default(), 138 | }; 139 | emitter.emit_diagnostics(&err); 140 | } 141 | 142 | pub(super) fn test_codegen_parsing_source( 143 | src: Arc>, 144 | options: CodegenOptions, 145 | ) -> CoreResult { 146 | let ast = parse_or_err(Path::new("test.asm"), src)?; 147 | let (ctx, err) = codegen(ast, options); 148 | if err.is_empty() { 149 | Ok(ctx.unwrap()) 150 | } else { 151 | Err(err) 152 | } 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /mos/src/lsp/code_lens.rs: -------------------------------------------------------------------------------- 1 | use crate::diagnostic_emitter::MosResult; 2 | use crate::impl_request_handler; 3 | use crate::lsp::{to_range, LspContext, RequestHandler}; 4 | use crate::test_runner::enumerate_test_cases; 5 | use lsp_types::request::CodeLensRequest; 6 | use lsp_types::{CodeLens, CodeLensParams, Command}; 7 | 8 | pub struct CodeLensRequestHandler; 9 | 10 | impl_request_handler!(CodeLensRequestHandler); 11 | 12 | impl RequestHandler for CodeLensRequestHandler { 13 | fn handle( 14 | &self, 15 | ctx: &mut LspContext, 16 | params: CodeLensParams, 17 | ) -> MosResult>> { 18 | let tests = enumerate_test_cases( 19 | ctx.parsing_source(), 20 | ¶ms.text_document.uri.to_file_path().unwrap(), 21 | ) 22 | .unwrap_or_default(); 23 | 24 | let result = tests 25 | .into_iter() 26 | .flat_map(|(sl, test_case_path)| { 27 | let run = CodeLens { 28 | range: to_range(sl.clone()), 29 | command: Some(Command { 30 | title: "Run".to_string(), 31 | command: "mos.runSingleTest".to_string(), 32 | arguments: Some(vec![serde_json::Value::String( 33 | test_case_path.to_string(), 34 | )]), 35 | }), 36 | data: None, 37 | }; 38 | 39 | let debug = CodeLens { 40 | range: to_range(sl), 41 | command: Some(Command { 42 | title: "Debug".to_string(), 43 | command: "mos.debugSingleTest".to_string(), 44 | arguments: Some(vec![serde_json::Value::String( 45 | test_case_path.to_string(), 46 | )]), 47 | }), 48 | data: None, 49 | }; 50 | 51 | vec![run, debug] 52 | }) 53 | .collect(); 54 | 55 | Ok(Some(result)) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /mos/src/lsp/documents.rs: -------------------------------------------------------------------------------- 1 | use crate::diagnostic_emitter::MosResult; 2 | use crate::impl_notification_handler; 3 | use crate::lsp::{LspContext, NotificationHandler}; 4 | use itertools::Itertools; 5 | use lsp_types::notification::{ 6 | DidChangeTextDocument, DidCloseTextDocument, DidOpenTextDocument, PublishDiagnostics, 7 | }; 8 | use lsp_types::{ 9 | Diagnostic, DidChangeTextDocumentParams, DidCloseTextDocumentParams, DidOpenTextDocumentParams, 10 | Position, PublishDiagnosticsParams, Range, Url, 11 | }; 12 | use mos_core::errors::Diagnostics; 13 | use std::collections::HashMap; 14 | 15 | pub struct DidOpenTextDocumentHandler; 16 | pub struct DidChangeTextDocumentHandler; 17 | pub struct DidCloseTextDocumentHandler; 18 | 19 | impl_notification_handler!(DidOpenTextDocumentHandler); 20 | impl_notification_handler!(DidChangeTextDocumentHandler); 21 | impl_notification_handler!(DidCloseTextDocumentHandler); 22 | 23 | impl NotificationHandler for DidOpenTextDocumentHandler { 24 | fn handle(&self, ctx: &mut LspContext, params: DidOpenTextDocumentParams) -> MosResult<()> { 25 | register_document(ctx, ¶ms.text_document.uri, ¶ms.text_document.text); 26 | publish_diagnostics(ctx)?; 27 | Ok(()) 28 | } 29 | } 30 | 31 | impl NotificationHandler for DidChangeTextDocumentHandler { 32 | fn handle(&self, ctx: &mut LspContext, params: DidChangeTextDocumentParams) -> MosResult<()> { 33 | let text_changes = params.content_changes.first().unwrap(); 34 | register_document(ctx, ¶ms.text_document.uri, &text_changes.text); 35 | publish_diagnostics(ctx)?; 36 | Ok(()) 37 | } 38 | } 39 | 40 | impl NotificationHandler for DidCloseTextDocumentHandler { 41 | fn handle(&self, ctx: &mut LspContext, params: DidCloseTextDocumentParams) -> MosResult<()> { 42 | ctx.parsing_source() 43 | .lock() 44 | .unwrap() 45 | .remove(¶ms.text_document.uri.to_file_path().unwrap()); 46 | Ok(()) 47 | } 48 | } 49 | 50 | fn register_document(ctx: &mut LspContext, uri: &Url, source: &str) { 51 | let path = uri.to_file_path().unwrap(); 52 | ctx.parsing_source().lock().unwrap().insert(&path, source); 53 | ctx.perform_codegen(); 54 | } 55 | 56 | fn publish_diagnostics(ctx: &LspContext) -> MosResult<()> { 57 | log::trace!("Publish diagnostics"); 58 | 59 | let mut result: HashMap> = 60 | to_diagnostics(&ctx.error).into_iter().into_group_map(); 61 | 62 | // Grab all the files in the project 63 | if let Some(tree) = ctx.tree.as_ref() { 64 | let filenames = tree 65 | .code_map 66 | .files() 67 | .iter() 68 | .map(|file| file.name().to_string()) 69 | .collect_vec(); 70 | 71 | // Publish errors (or no errors!) for every file 72 | for filename in filenames { 73 | let diags = result.remove(filename.as_str()).unwrap_or_default(); 74 | let params = PublishDiagnosticsParams::new( 75 | Url::from_file_path(filename).unwrap(), 76 | diags, 77 | None, // todo: handle document version 78 | ); 79 | ctx.publish_notification::(params)?; 80 | } 81 | } 82 | Ok(()) 83 | } 84 | 85 | fn to_diagnostics(error: &Diagnostics) -> Vec<(String, Diagnostic)> { 86 | error 87 | .iter() 88 | .filter_map(|diag| { 89 | diag.labels 90 | .first() 91 | .map(|label| { 92 | error 93 | .code_map() 94 | .as_ref() 95 | .unwrap() 96 | .look_up_span(label.file_id) 97 | }) 98 | .map(|location| { 99 | let start = 100 | Position::new(location.begin.line as u32, location.begin.column as u32); 101 | let end = Position::new(location.end.line as u32, location.end.column as u32); 102 | let range = Range::new(start, end); 103 | let mut d = Diagnostic::new_simple(range, diag.message.clone()); 104 | d.source = Some("mos".into()); 105 | (location.file.name().to_string(), d) 106 | }) 107 | }) 108 | .collect() 109 | } 110 | 111 | #[cfg(test)] 112 | mod tests { 113 | #[cfg(windows)] 114 | #[test] 115 | fn can_parse_windows_uris() { 116 | use lsp_types::Url; 117 | let url = Url::parse("file:///g%3A/code/mos/vscode/test-workspace/main.asm").unwrap(); 118 | let _ = url.to_file_path().unwrap(); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /mos/src/lsp/hover.rs: -------------------------------------------------------------------------------- 1 | use crate::diagnostic_emitter::MosResult; 2 | use crate::impl_request_handler; 3 | use crate::lsp::{LspContext, RequestHandler}; 4 | use lsp_types::request::HoverRequest; 5 | use lsp_types::{Hover, HoverContents, HoverParams, MarkupContent, MarkupKind}; 6 | 7 | pub struct HoverRequestHandler; 8 | 9 | impl_request_handler!(HoverRequestHandler); 10 | 11 | impl RequestHandler for HoverRequestHandler { 12 | fn handle(&self, ctx: &mut LspContext, params: HoverParams) -> MosResult> { 13 | if ctx.codegen.is_none() { 14 | return Ok(None); 15 | } 16 | let codegen = ctx.codegen().unwrap(); 17 | let codegen = codegen.lock().unwrap(); 18 | let analysis = codegen.analysis(); 19 | 20 | let defs = ctx.find_definitions(analysis, ¶ms.text_document_position_params); 21 | if let Some((_, def)) = defs.first() { 22 | if let Some(location) = def.location.as_ref() { 23 | let mut comments = vec![]; 24 | let sl = analysis.look_up(location.span); 25 | let mut line = sl.begin.line; 26 | while line > 0 { 27 | line -= 1; 28 | let source_line = sl.file.source_line(line).trim(); 29 | if source_line.starts_with("///") { 30 | let (_prefix, rest) = source_line.split_at(3); 31 | comments.push(rest.trim()); 32 | } else { 33 | break; 34 | } 35 | } 36 | comments.reverse(); 37 | 38 | return if comments.is_empty() { 39 | Ok(None) 40 | } else { 41 | let value = comments.join("\n"); 42 | 43 | Ok(Some(Hover { 44 | contents: HoverContents::Markup(MarkupContent { 45 | kind: MarkupKind::Markdown, 46 | value, 47 | }), 48 | range: None, 49 | })) 50 | }; 51 | } 52 | } 53 | 54 | Ok(None) 55 | } 56 | } 57 | 58 | #[cfg(test)] 59 | mod tests { 60 | use super::*; 61 | use crate::lsp::testing::test_root; 62 | use crate::lsp::LspServer; 63 | use lsp_types::Position; 64 | 65 | #[test] 66 | fn get_hover_coments() -> MosResult<()> { 67 | let mut server = LspServer::new(LspContext::new()); 68 | server.did_open_text_document( 69 | test_root().join("main.asm"), 70 | "/// some other comment\n\n/// hello\n/// foo\nlabel: nop\n\nlda label", 71 | )?; 72 | let response = server.hover(test_root().join("main.asm"), Position::new(6, 4))?; 73 | assert_eq!( 74 | response, 75 | Some(Hover { 76 | contents: HoverContents::Markup(MarkupContent { 77 | kind: MarkupKind::Markdown, 78 | value: "hello\nfoo".to_string() 79 | }), 80 | range: None 81 | }) 82 | ); 83 | Ok(()) 84 | } 85 | 86 | #[test] 87 | fn cannot_get_invalid_hover_coments() -> MosResult<()> { 88 | let mut server = LspServer::new(LspContext::new()); 89 | server.did_open_text_document(test_root().join("main.asm"), "label: nop\n\nlda label")?; 90 | assert_eq!( 91 | server.hover(test_root().join("main.asm"), Position::new(2, 4))?, 92 | None 93 | ); 94 | assert_eq!( 95 | server.hover(test_root().join("main.asm"), Position::new(0, 0))?, 96 | None 97 | ); 98 | Ok(()) 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /mos/src/lsp/testing.rs: -------------------------------------------------------------------------------- 1 | use crate::diagnostic_emitter::MosResult; 2 | use crate::lsp::{LspContext, LspServer}; 3 | use lsp_types::notification::{DidOpenTextDocument, Notification}; 4 | use lsp_types::request::{ 5 | Completion, GotoDefinition, HoverRequest, PrepareRenameRequest, References, Rename, Request, 6 | SemanticTokensFullRequest, 7 | }; 8 | use lsp_types::{ 9 | CompletionParams, CompletionResponse, DidOpenTextDocumentParams, GotoDefinitionParams, 10 | GotoDefinitionResponse, Hover, HoverParams, Position, ReferenceContext, ReferenceParams, 11 | RenameParams, SemanticTokensParams, SemanticTokensResult, TextDocumentIdentifier, 12 | TextDocumentItem, TextDocumentPositionParams, Url, 13 | }; 14 | use serde::de::DeserializeOwned; 15 | use std::path::{Path, PathBuf}; 16 | 17 | impl LspContext { 18 | fn pop_response(&self) -> T { 19 | let response = self 20 | .responses 21 | .lock() 22 | .unwrap() 23 | .pop() 24 | .unwrap() 25 | .result 26 | .unwrap(); 27 | serde_json::from_value(response).unwrap() 28 | } 29 | } 30 | 31 | impl LspServer { 32 | pub fn did_open_text_document>( 33 | &mut self, 34 | path: P, 35 | source: &str, 36 | ) -> MosResult<()> { 37 | self.handle_message(notification::( 38 | DidOpenTextDocumentParams { 39 | text_document: TextDocumentItem { 40 | uri: Url::from_file_path(path).unwrap(), 41 | language_id: "".to_string(), 42 | version: 0, 43 | text: source.to_string(), 44 | }, 45 | }, 46 | )) 47 | } 48 | 49 | pub fn prepare_rename>(&mut self, path: P, position: Position) -> MosResult<()> { 50 | self.handle_message(request::( 51 | TextDocumentPositionParams { 52 | text_document: TextDocumentIdentifier { 53 | uri: Url::from_file_path(path).unwrap(), 54 | }, 55 | position, 56 | }, 57 | )) 58 | } 59 | 60 | pub fn rename>( 61 | &mut self, 62 | path: P, 63 | position: Position, 64 | new_name: &str, 65 | ) -> MosResult<()> { 66 | self.handle_message(request::(RenameParams { 67 | text_document_position: TextDocumentPositionParams { 68 | text_document: TextDocumentIdentifier { 69 | uri: Url::from_file_path(path).unwrap(), 70 | }, 71 | position, 72 | }, 73 | new_name: new_name.to_string(), 74 | work_done_progress_params: Default::default(), 75 | })) 76 | } 77 | 78 | pub fn go_to_definition>( 79 | &mut self, 80 | path: P, 81 | position: Position, 82 | ) -> MosResult { 83 | self.handle_message(request::(GotoDefinitionParams { 84 | text_document_position_params: TextDocumentPositionParams { 85 | text_document: TextDocumentIdentifier { 86 | uri: Url::from_file_path(path).unwrap(), 87 | }, 88 | position, 89 | }, 90 | partial_result_params: Default::default(), 91 | work_done_progress_params: Default::default(), 92 | }))?; 93 | Ok(self.lock_context().pop_response()) 94 | } 95 | 96 | pub fn find_references>( 97 | &mut self, 98 | path: P, 99 | position: Position, 100 | include_declaration: bool, 101 | ) -> MosResult>> { 102 | self.handle_message(request::(ReferenceParams { 103 | text_document_position: TextDocumentPositionParams { 104 | text_document: TextDocumentIdentifier { 105 | uri: Url::from_file_path(path).unwrap(), 106 | }, 107 | position, 108 | }, 109 | partial_result_params: Default::default(), 110 | work_done_progress_params: Default::default(), 111 | context: ReferenceContext { 112 | include_declaration, 113 | }, 114 | }))?; 115 | Ok(self.lock_context().pop_response()) 116 | } 117 | 118 | pub fn hover>( 119 | &mut self, 120 | path: P, 121 | position: Position, 122 | ) -> MosResult> { 123 | self.handle_message(request::(HoverParams { 124 | text_document_position_params: TextDocumentPositionParams { 125 | text_document: TextDocumentIdentifier { 126 | uri: Url::from_file_path(path).unwrap(), 127 | }, 128 | position, 129 | }, 130 | work_done_progress_params: Default::default(), 131 | }))?; 132 | Ok(self.lock_context().pop_response()) 133 | } 134 | 135 | pub fn semantic_tokens>( 136 | &mut self, 137 | path: P, 138 | ) -> MosResult> { 139 | self.handle_message(request::(SemanticTokensParams { 140 | work_done_progress_params: Default::default(), 141 | partial_result_params: Default::default(), 142 | text_document: TextDocumentIdentifier { 143 | uri: Url::from_file_path(path).unwrap(), 144 | }, 145 | }))?; 146 | Ok(self.lock_context().pop_response()) 147 | } 148 | 149 | pub fn completion>( 150 | &mut self, 151 | path: P, 152 | position: Position, 153 | ) -> MosResult> { 154 | self.handle_message(request::(CompletionParams { 155 | text_document_position: TextDocumentPositionParams { 156 | text_document: TextDocumentIdentifier { 157 | uri: Url::from_file_path(path).unwrap(), 158 | }, 159 | position, 160 | }, 161 | partial_result_params: Default::default(), 162 | work_done_progress_params: Default::default(), 163 | context: None, 164 | }))?; 165 | Ok(self.lock_context().pop_response()) 166 | } 167 | } 168 | 169 | fn request(params: T::Params) -> lsp_server::Message { 170 | let params = serde_json::to_value(¶ms).unwrap(); 171 | lsp_server::Request { 172 | id: 1.into(), 173 | method: T::METHOD.to_string(), 174 | params, 175 | } 176 | .into() 177 | } 178 | 179 | pub fn response(result: T::Result) -> lsp_server::Response { 180 | let result = serde_json::to_value(&result).unwrap(); 181 | lsp_server::Response { 182 | id: 1.into(), 183 | result: Some(result), 184 | error: None, 185 | } 186 | } 187 | 188 | fn notification(params: T::Params) -> lsp_server::Message { 189 | let params = serde_json::to_value(¶ms).unwrap(); 190 | lsp_server::Notification { 191 | method: T::METHOD.to_string(), 192 | params, 193 | } 194 | .into() 195 | } 196 | 197 | pub fn range( 198 | start_line: usize, 199 | start_col: usize, 200 | end_line: usize, 201 | end_col: usize, 202 | ) -> lsp_types::Range { 203 | lsp_types::Range { 204 | start: Position { 205 | line: start_line as u32, 206 | character: start_col as u32, 207 | }, 208 | end: Position { 209 | line: end_line as u32, 210 | character: end_col as u32, 211 | }, 212 | } 213 | } 214 | 215 | #[cfg(not(windows))] 216 | pub fn test_root() -> PathBuf { 217 | PathBuf::from("/") 218 | } 219 | 220 | #[cfg(windows)] 221 | pub fn test_root() -> PathBuf { 222 | PathBuf::from("C:\\") 223 | } 224 | -------------------------------------------------------------------------------- /mos/src/lsp/traits.rs: -------------------------------------------------------------------------------- 1 | use crate::diagnostic_emitter::MosResult; 2 | use crate::lsp::LspContext; 3 | use lsp_types::notification::Notification; 4 | use lsp_types::request::Request; 5 | 6 | pub trait UntypedRequestHandler { 7 | fn method(&self) -> &'static str; 8 | fn handle(&self, ctx: &mut LspContext, req: lsp_server::Request) -> MosResult<()>; 9 | } 10 | 11 | pub trait UntypedNotificationHandler { 12 | fn method(&self) -> &'static str; 13 | fn handle(&self, ctx: &mut LspContext, req: lsp_server::Notification) -> MosResult<()>; 14 | } 15 | 16 | pub trait RequestHandler { 17 | fn method(&self) -> &'static str { 18 | R::METHOD 19 | } 20 | fn handle(&self, ctx: &mut LspContext, params: R::Params) -> MosResult; 21 | } 22 | 23 | pub trait NotificationHandler { 24 | fn method(&self) -> &'static str { 25 | N::METHOD 26 | } 27 | fn handle(&self, ctx: &mut LspContext, params: N::Params) -> MosResult<()>; 28 | } 29 | 30 | #[macro_export] 31 | macro_rules! impl_request_handler { 32 | ($ty:ty) => { 33 | impl $crate::lsp::traits::UntypedRequestHandler for $ty { 34 | fn method(&self) -> &'static str { 35 | RequestHandler::method(self) 36 | } 37 | 38 | fn handle(&self, ctx: &mut LspContext, req: lsp_server::Request) -> MosResult<()> { 39 | let method = RequestHandler::method(self); 40 | let (id, params) = req.extract(method).unwrap(); 41 | let result = RequestHandler::handle(self, ctx, params)?; 42 | ctx.send_response(id, result)?; 43 | Ok(()) 44 | } 45 | } 46 | }; 47 | } 48 | 49 | #[macro_export] 50 | macro_rules! impl_notification_handler { 51 | ($ty:ty) => { 52 | impl $crate::lsp::traits::UntypedNotificationHandler for $ty { 53 | fn method(&self) -> &'static str { 54 | NotificationHandler::method(self) 55 | } 56 | 57 | fn handle(&self, ctx: &mut LspContext, req: lsp_server::Notification) -> MosResult<()> { 58 | let method = NotificationHandler::method(self); 59 | let params = req.extract(method).unwrap(); 60 | NotificationHandler::handle(self, ctx, params) 61 | } 62 | } 63 | }; 64 | } 65 | -------------------------------------------------------------------------------- /mos/src/main.rs: -------------------------------------------------------------------------------- 1 | //! MOS is a toolkit for building applications that target the MOS 6502 CPU. 2 | 3 | use std::path::{Path, PathBuf}; 4 | 5 | use fs_err as fs; 6 | 7 | use crate::commands::*; 8 | use crate::config::Config; 9 | use crate::diagnostic_emitter::*; 10 | 11 | /// Contains the available CLI commands and their associated logic 12 | mod commands; 13 | /// Configuration file handling 14 | mod config; 15 | /// Debug Adapter Protocol implementation 16 | mod debugger; 17 | /// Error handling 18 | mod diagnostic_emitter; 19 | /// Language Server Protocol implementation 20 | mod lsp; 21 | /// MemoryAccessor trait 22 | mod memory_accessor; 23 | /// Unit Test runner 24 | mod test_runner; 25 | /// Miscellaneous utility methods 26 | mod utils; 27 | 28 | #[derive(argh::FromArgs, PartialEq, Eq, Debug)] 29 | /// mos - https://mos.datatra.sh 30 | pub struct Args { 31 | #[argh(subcommand)] 32 | subcommand: Subcommand, 33 | /// disables colorized output 34 | #[argh(switch)] 35 | no_color: bool, 36 | /// logging verbosity 37 | #[argh(switch, short = 'v')] 38 | verbosity: u8, 39 | /// error style 40 | #[argh(option, short = 'e', default = "ErrorStyle::Rich")] 41 | error_style: ErrorStyle, 42 | } 43 | 44 | #[derive(PartialEq, Eq, Debug, strum::EnumString)] 45 | pub enum ErrorStyle { 46 | Short, 47 | Medium, 48 | Rich, 49 | } 50 | 51 | #[derive(argh::FromArgs, PartialEq, Eq, Debug)] 52 | #[argh(subcommand)] 53 | pub enum Subcommand { 54 | Init(InitArgs), 55 | Build(BuildArgs), 56 | Format(FormatArgs), 57 | Test(TestArgs), 58 | Lsp(LspArgs), 59 | Version(VersionArgs), 60 | } 61 | 62 | fn mos_toml_path>( 63 | root: Option<&Path>, 64 | starting_path: P, 65 | ) -> MosResult> { 66 | let starting_path = starting_path.into().canonicalize().unwrap(); 67 | let mut path = starting_path.as_path(); 68 | loop { 69 | let toml = path.join("mos.toml"); 70 | if toml.exists() { 71 | return Ok(Some(toml)); 72 | } 73 | 74 | // Are we at the root? Then we didn't find anything. 75 | if Some(path) == root { 76 | return Ok(None); 77 | } 78 | 79 | match path.parent() { 80 | Some(parent) => { 81 | path = parent; 82 | } 83 | None => { 84 | return Ok(None); 85 | } 86 | } 87 | } 88 | } 89 | 90 | fn run(args: &Args) -> MosResult<()> { 91 | let mos_toml = mos_toml_path(None, &Path::new("."))?; 92 | let (root, cfg) = match mos_toml { 93 | Some(path) => { 94 | log::trace!("Using configuration from: {}", &path.to_str().unwrap()); 95 | let toml = fs::read_to_string(&path)?; 96 | ( 97 | path.parent().unwrap().to_path_buf(), 98 | Config::from_toml(&toml)?, 99 | ) 100 | } 101 | None => { 102 | log::trace!("No configuration file found. Using defaults."); 103 | (PathBuf::from("."), Config::default()) 104 | } 105 | }; 106 | 107 | match &args.subcommand { 108 | Subcommand::Build(_) => build_command(&root, &cfg), 109 | Subcommand::Format(_) => format_command(&cfg), 110 | Subcommand::Init(_) => init_command(&root, &cfg), 111 | Subcommand::Lsp(subargs) => lsp_command(subargs), 112 | Subcommand::Test(_) => { 113 | let exit_code = test_command(args, &root, &cfg)?; 114 | if exit_code > 0 { 115 | std::process::exit(exit_code); 116 | } else { 117 | Ok(()) 118 | } 119 | } 120 | Subcommand::Version(_) => version_command(), 121 | } 122 | } 123 | 124 | fn main() { 125 | #[cfg(windows)] 126 | ansi_term::enable_ansi_support().unwrap(); 127 | 128 | let args: Args = argh::from_env(); 129 | 130 | loggerv::Logger::new() 131 | .verbosity((1 + args.verbosity) as u64) // show 'info' by default 132 | .colors(!args.no_color) 133 | .module_path(false) 134 | .init() 135 | .unwrap(); 136 | 137 | if let Err(e) = run(&args) { 138 | match &args.subcommand { 139 | Subcommand::Lsp(_) => { 140 | // Don't try to emit to stdout since it's probably gone by this time in the LSP 141 | } 142 | _ => DiagnosticEmitter::stdout(&args).emit(e), 143 | } 144 | std::process::exit(1); 145 | } 146 | } 147 | 148 | #[cfg(test)] 149 | mod tests { 150 | use std::path::Path; 151 | 152 | use fs_err as fs; 153 | use tempfile::tempdir; 154 | 155 | use crate::diagnostic_emitter::MosResult; 156 | use crate::mos_toml_path; 157 | 158 | #[test] 159 | fn can_locate_mos_toml() -> MosResult<()> { 160 | let root = tempdir()?; 161 | fs::create_dir_all(root.path().join("test/test2/test3"))?; 162 | let file = fs::File::create(root.path().join("test/mos.toml"))?; 163 | 164 | let is_present = |path: &str| { 165 | let path = Path::new(path); 166 | mos_toml_path(Some(root.path()), root.path().join(path)) 167 | .ok() 168 | .flatten() 169 | .is_some() 170 | }; 171 | 172 | assert!(is_present("./test/test2/test3")); 173 | assert!(is_present("./test/test2")); 174 | assert!(is_present("./test")); 175 | assert!(!is_present(".")); 176 | 177 | drop(file); 178 | root.close()?; 179 | 180 | Ok(()) 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /mos/src/memory_accessor.rs: -------------------------------------------------------------------------------- 1 | use mos_core::codegen::{ 2 | CodegenContext, EvaluationResult, Evaluator, FunctionCallback, SymbolData, 3 | }; 4 | use mos_core::parser::{Expression, Located}; 5 | use std::sync::{Arc, Mutex}; 6 | 7 | pub trait MemoryAccessor { 8 | fn read(&mut self, address: u16, len: usize) -> Vec; 9 | fn write(&mut self, address: u16, bytes: &[u8]); 10 | } 11 | 12 | pub fn ensure_ram_fn( 13 | ctx: &mut CodegenContext, 14 | memory_accessor: Box, 15 | ) { 16 | let memory_accessor = Arc::new(Mutex::new(memory_accessor)); 17 | 18 | struct RamFn { 19 | memory_accessor: Arc>>, 20 | word: bool, 21 | } 22 | 23 | impl FunctionCallback for RamFn { 24 | fn expected_args(&self) -> usize { 25 | 1 26 | } 27 | 28 | fn apply( 29 | &mut self, 30 | ctx: &Evaluator, 31 | args: &[&Located], 32 | ) -> EvaluationResult> { 33 | let arg = args.first().unwrap(); 34 | let address = ctx 35 | .evaluate_expression(arg, false)? 36 | .and_then(|d| d.try_as_i64()); 37 | let val = address.and_then(|a| { 38 | let len = if self.word { 2 } else { 1 }; 39 | let bytes = self.memory_accessor.lock().unwrap().read(a as u16, len); 40 | if self.word { 41 | let lo = bytes.first(); 42 | let hi = bytes.get(1); 43 | match (lo, hi) { 44 | (Some(lo), Some(hi)) => Some(256 * (*hi as i64) + (*lo as i64)), 45 | _ => None, 46 | } 47 | } else { 48 | bytes.first().map(|b| *b as i64) 49 | } 50 | }); 51 | Ok(val.map(SymbolData::Number)) 52 | } 53 | } 54 | 55 | ctx.register_fn( 56 | "ram", 57 | RamFn { 58 | memory_accessor: memory_accessor.clone(), 59 | word: false, 60 | }, 61 | ); 62 | ctx.register_fn( 63 | "ram16", 64 | RamFn { 65 | memory_accessor, 66 | word: true, 67 | }, 68 | ); 69 | } 70 | -------------------------------------------------------------------------------- /mos/src/utils.rs: -------------------------------------------------------------------------------- 1 | use ansi_term::ANSIGenericString; 2 | use std::borrow::Cow; 3 | 4 | pub fn paint<'a, I, S: 'a + ToOwned + ?Sized>( 5 | use_color: bool, 6 | colour: ansi_term::Colour, 7 | input: I, 8 | ) -> ANSIGenericString<'a, S> 9 | where 10 | I: Into>, 11 | ::Owned: std::fmt::Debug, 12 | { 13 | if use_color { 14 | colour.paint(input) 15 | } else { 16 | let input = input.into(); 17 | input.into() 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /mos/test-data/build/include.asm: -------------------------------------------------------------------------------- 1 | ldx #$00 2 | loop: 3 | lda data, x 4 | beq end 5 | sta $0400, x 6 | inx 7 | jmp loop 8 | end: 9 | rts 10 | 11 | data: 12 | .file "include.bin" 13 | .byte 0 14 | -------------------------------------------------------------------------------- /mos/test-data/build/include.bin: -------------------------------------------------------------------------------- 1 |    ! -------------------------------------------------------------------------------- /mos/test-data/build/include.prg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datatrash/mos/12aa86cb635c89d7a920d36c533dc70ae6ebbf38/mos/test-data/build/include.prg -------------------------------------------------------------------------------- /mos/test-data/build/multiple_segments.asm: -------------------------------------------------------------------------------- 1 | // Segment 'a' is assembled as if it is run from $4000, but it is stored in the target from $1000 2 | .define segment { 3 | name = "a" 4 | start = $1000 5 | pc = $4000 6 | } 7 | 8 | // Segment 'b' starts right after segment a 9 | .define segment { 10 | name = "b" 11 | start = segments.a.end 12 | } 13 | 14 | // Segment 'c' starts right after segment b 15 | .define segment { 16 | name = "c" 17 | start = segments.b.end 18 | } 19 | 20 | lda data 21 | sta $d020 22 | rts 23 | 24 | .segment "b" { nop } 25 | .segment "c" { asl } 26 | 27 | data: 28 | .byte 1 29 | -------------------------------------------------------------------------------- /mos/test-data/build/multiple_segments.prg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datatrash/mos/12aa86cb635c89d7a920d36c533dc70ae6ebbf38/mos/test-data/build/multiple_segments.prg -------------------------------------------------------------------------------- /mos/test-data/build/valid.asm: -------------------------------------------------------------------------------- 1 | lda data 2 | sta $d020 3 | rts 4 | 5 | data: 6 | .byte 1 -------------------------------------------------------------------------------- /mos/test-data/build/valid.prg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datatrash/mos/12aa86cb635c89d7a920d36c533dc70ae6ebbf38/mos/test-data/build/valid.prg -------------------------------------------------------------------------------- /mos/test-data/test/some-tests.asm: -------------------------------------------------------------------------------- 1 | .test "ok" { 2 | .assert 1 == 1 "nice" 3 | brk 4 | } 5 | 6 | .test "fail" { 7 | .assert 1 == 2 "whoops" 8 | } -------------------------------------------------------------------------------- /vscode/.gitignore: -------------------------------------------------------------------------------- 1 | out 2 | dist 3 | node_modules 4 | test-workspace/ 5 | *.vsix 6 | MOS_VERSION -------------------------------------------------------------------------------- /vscode/.vscodeignore: -------------------------------------------------------------------------------- 1 | ** 2 | !language-configuration.json 3 | !syntaxes 4 | !syntaxes/asm.tmLanguage.json 5 | !out/extension.js 6 | !out/MOS_VERSION 7 | !package.json 8 | !package-lock.json 9 | !icon.png 10 | !README.md -------------------------------------------------------------------------------- /vscode/README.md: -------------------------------------------------------------------------------- 1 | # MOS 2 | 3 | 4 | 5 | MOS helps you write applications targeting the MOS 6502 CPU. 6 | 7 | To activate the extension, make sure your workspace contains a `mos.toml` file in its root. For more information, check out the [documentation](https://mos.datatra.sh/guide/). -------------------------------------------------------------------------------- /vscode/bundle.js: -------------------------------------------------------------------------------- 1 | require('esbuild').build({ 2 | entryPoints: ['src/extension.ts'], 3 | bundle: true, 4 | platform: 'node', 5 | external: ['vscode'], 6 | minify: true, 7 | treeShaking: true, 8 | outdir: 'out', 9 | }).catch(e => { 10 | console.log(e); 11 | process.exit(1); 12 | }) -------------------------------------------------------------------------------- /vscode/clean.sh: -------------------------------------------------------------------------------- 1 | rm -rfv ~/Library/Application\ Support/Code/CachedExtensionVSIXs/datatrash* 2 | rm -rfv ~/Library/Application\ Support/Code/User/globalStorage/datatrash.mos 3 | rm -rfv ~/.vscode/extensions/datatrash* -------------------------------------------------------------------------------- /vscode/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datatrash/mos/12aa86cb635c89d7a920d36c533dc70ae6ebbf38/vscode/icon.png -------------------------------------------------------------------------------- /vscode/language-configuration.json: -------------------------------------------------------------------------------- 1 | { 2 | "comments": { 3 | "lineComment": "//", 4 | "blockComment": [ "/*", "*/" ] 5 | }, 6 | "brackets": [ 7 | ["{", "}"], 8 | ["[", "]"], 9 | ["(", ")"] 10 | ], 11 | "autoClosingPairs": [ 12 | { "open": "{", "close": "}" }, 13 | { "open": "[", "close": "]" }, 14 | { "open": "(", "close": ")" }, 15 | { "open": "\"", "close": "\"", "notIn": ["string"] }, 16 | { "open": "/*", "close": " */" } 17 | ], 18 | "autoCloseBefore": ";:.,=}])>` \n\t", 19 | "surroundingPairs": [ 20 | ["{", "}"], 21 | ["[", "]"], 22 | ["(", ")"], 23 | ["\"", "\""], 24 | ["'", "'"] 25 | ], 26 | "indentationRules": { 27 | "increaseIndentPattern": "^.*\\{[^}\"']*$|^.*\\([^\\)\"']*$", 28 | "decreaseIndentPattern": "^\\s*(\\s*\\/[*].*[*]\\/\\s*)*[})]" 29 | } 30 | } -------------------------------------------------------------------------------- /vscode/mos-version.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const path = require("path"); 3 | 4 | let version = process.env["MOS_VERSION"]; 5 | if (!version) { 6 | console.log("MOS_VERSION environment variable was not set!"); 7 | process.exit(1); 8 | } 9 | if (!fs.existsSync("out")) { 10 | fs.mkdirSync("out"); 11 | } 12 | console.log(`MOS Version to install: ${version}`); 13 | fs.writeFileSync(path.join("out", "MOS_VERSION"), version); -------------------------------------------------------------------------------- /vscode/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mos", 3 | "publisher": "datatrash", 4 | "displayName": "mos", 5 | "description": "An extension for the MOS 6502 assembler.", 6 | "categories": [ 7 | "Programming Languages", 8 | "Formatters" 9 | ], 10 | "version": "0.0.1", 11 | "preview": true, 12 | "license": "MIT", 13 | "homepage": "README.md", 14 | "galleryBanner": { 15 | "color": "#668", 16 | "theme": "dark" 17 | }, 18 | "icon": "icon.png", 19 | "bugs": { 20 | "url": "https://github.com/datatrash/mos/issues" 21 | }, 22 | "repository": { 23 | "url": "https://github.com/datatrash/mos.git", 24 | "type": "git" 25 | }, 26 | "engines": { 27 | "vscode": "^1.60.0" 28 | }, 29 | "activationEvents": [ 30 | "workspaceContains:**/mos.toml" 31 | ], 32 | "main": "./out/extension.js", 33 | "contributes": { 34 | "grammars": [ 35 | { 36 | "language": "asm", 37 | "scopeName": "source.asm", 38 | "path": "./syntaxes/asm.tmLanguage.json" 39 | } 40 | ], 41 | "taskDefinitions": [ 42 | { 43 | "type": "build" 44 | } 45 | ], 46 | "problemMatchers": [ 47 | { 48 | "name": "mos", 49 | "owner": "mos", 50 | "source": "mos", 51 | "fileLocation": [ 52 | "autoDetect", 53 | "${workspaceFolder}" 54 | ], 55 | "pattern": { 56 | "regexp": "^(.*?):(\\d+):(\\d*):?\\s+?(warning|error):\\s+(.*)$", 57 | "file": 1, 58 | "line": 2, 59 | "column": 3, 60 | "severity": 4, 61 | "message": 5 62 | } 63 | } 64 | ], 65 | "languages": [ 66 | { 67 | "id": "asm", 68 | "extensions": [ 69 | ".asm" 70 | ], 71 | "aliases": [ 72 | "6502 Assembly" 73 | ], 74 | "configuration": "language-configuration.json" 75 | } 76 | ], 77 | "configuration": { 78 | "title": "MOS", 79 | "properties": { 80 | "mos.path": { 81 | "type": "string", 82 | "description": "Path to the mos executable", 83 | "markdownDescription": "Path to the `mos` executable" 84 | } 85 | } 86 | }, 87 | "breakpoints": [ 88 | { 89 | "language": "asm" 90 | } 91 | ], 92 | "debuggers": [ 93 | { 94 | "type": "mos", 95 | "label": "MOS Debug with VICE", 96 | "configurationAttributes": { 97 | "launch": { 98 | "properties": { 99 | "workspace": { 100 | "type": "string", 101 | "description": "Absolute path to workspace containing mos.toml", 102 | "default": "${workspaceFolder}" 103 | }, 104 | "vicePath": { 105 | "type": "string", 106 | "description": "Absolute path to a VICE binary" 107 | }, 108 | "trace": { 109 | "type": "boolean", 110 | "description": "Enable logging of the Debug Adapter Protocol.", 111 | "default": true 112 | } 113 | } 114 | } 115 | }, 116 | "initialConfigurations": [ 117 | { 118 | "type": "mos", 119 | "request": "launch", 120 | "name": "Launch", 121 | "workspace": "${workspaceFolder}", 122 | "preLaunchTask": "mos: Build" 123 | } 124 | ] 125 | } 126 | ] 127 | }, 128 | "scripts": { 129 | "check-unix": "code --disable-extensions --extensionDevelopmentPath=$INIT_CWD $INIT_CWD/test-workspace", 130 | "check-win": "code --disable-extensions --extensionDevelopmentPath=%INIT_CWD% %INIT_CWD%/test-workspace", 131 | "compile": "node bundle", 132 | "publish": "vsce publish", 133 | "mos-version": "node mos-version", 134 | "vscode:prepublish": "npm run mos-version && node bundle" 135 | }, 136 | "dependencies": { 137 | "decompress": "^4.2.1", 138 | "https-proxy-agent": "^5.0.0", 139 | "node-fetch": "^3.0.0", 140 | "rimraf": "^3.0.2", 141 | "semver": "^7.3.4", 142 | "smart-buffer": "^4.2.0", 143 | "unquote": "^1.1.1", 144 | "vscode-debugadapter": "^1.49.0", 145 | "vscode-debugprotocol": "^1.49.0", 146 | "vscode-languageclient": "^7.0.0" 147 | }, 148 | "devDependencies": { 149 | "@types/decompress": "^4.2.4", 150 | "@types/glob": "^7.1.4", 151 | "@types/node": "^16.9.4", 152 | "@types/rimraf": "^3.0.2", 153 | "@types/semver": "^7.3.8", 154 | "@types/vscode": "^1.60.0", 155 | "esbuild": "^0.12.28", 156 | "glob": "^7.1.6", 157 | "typescript": "^4.4.3", 158 | "vsce": "^1.99.0" 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /vscode/src/auto-update/download-binary.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import * as path from "path"; 3 | import {promises as fs, renameSync} from "fs"; 4 | import {download, fetchRelease} from "./net"; 5 | import * as os from "os"; 6 | import rimraf from "rimraf"; 7 | import {promisify} from "util"; 8 | import decompress from "decompress"; 9 | import * as semver from 'semver'; 10 | const unquote: any = require('unquote'); 11 | 12 | export async function getMosBinary(ctx: vscode.ExtensionContext): Promise { 13 | const mos_version = await fs.readFile(path.join(vscode.extensions.getExtension("datatrash.mos")!!.extensionPath, "out", "MOS_VERSION"), "utf8"); 14 | 15 | let cfg = vscode.workspace.getConfiguration("mos"); 16 | const explicitPath = unquote(cfg.get("path")); 17 | if (explicitPath) { 18 | if (explicitPath.startsWith("~/")) { 19 | return os.homedir() + explicitPath.slice("~".length); 20 | } 21 | 22 | if (!path.isAbsolute(explicitPath) && vscode.workspace.workspaceFolders !== undefined) { 23 | // Relative path, so make it absolute from the workspace folder 24 | let workspacePath = vscode.workspace.workspaceFolders[0].uri.fsPath; 25 | return path.join(workspacePath, explicitPath); 26 | } else { 27 | return explicitPath; 28 | } 29 | } 30 | 31 | const platforms: { [key: string]: string } = { 32 | "ia32 win32": "x86_64-pc-windows-msvc", 33 | "x64 win32": "x86_64-pc-windows-msvc", 34 | "x64 linux": "x86_64-unknown-linux-musl", 35 | "x64 darwin": "x86_64-apple-darwin", 36 | }; 37 | let platform = platforms[`${process.arch} ${process.platform}`]; 38 | if (platform === undefined) { 39 | await vscode.window.showErrorMessage("Unfortunately we don't ship binaries for your platform yet."); 40 | return undefined; 41 | } 42 | 43 | const bin_path = path.join(ctx.globalStorageUri.fsPath, "bin"); 44 | const bin_extract_path = path.join(ctx.globalStorageUri.fsPath, "extract"); 45 | const ext = platform.indexOf("-windows-") !== -1 ? ".exe" : ""; 46 | const dest = path.join(bin_path, `mos${ext}`); 47 | const exists = await fs.stat(dest).then(() => true, () => false); 48 | if (exists) { 49 | const execFile = promisify(require('child_process').execFile); 50 | let mos_version_output = await execFile(dest, ["--version"]); 51 | let existing_version = mos_version_output.stdout.split(" ")[1].trim(); 52 | if (semver.gt(mos_version, existing_version)) { 53 | const updateResponse = await vscode.window.showInformationMessage( 54 | `There is a new version of MOS (v${mos_version}), you currently have v${existing_version} installed.`, 55 | "Update now", 56 | "Dismiss" 57 | ); 58 | if (updateResponse !== "Update now") return dest; 59 | } else { 60 | return dest; 61 | } 62 | } else { 63 | const userResponse = await vscode.window.showInformationMessage( 64 | `The mos extension requires the MOS binary, which is not yet installed.`, 65 | "Download now", 66 | "Dismiss" 67 | ); 68 | if (userResponse !== "Download now") return undefined; 69 | } 70 | 71 | let release = await downloadWithRetryDialog(async () => { 72 | return await fetchRelease(mos_version, null, null); 73 | }) 74 | const arch_ext = platform.indexOf("-windows-") !== -1 ? "zip" : "tar.gz"; 75 | const archive = `mos-${mos_version}-${platform}.${arch_ext}`; 76 | const artifact = release.assets.find(artifact => artifact.name === archive)!!; 77 | 78 | const full_archive_path = path.join(ctx.globalStorageUri.fsPath, archive); 79 | await downloadWithRetryDialog(async () => { 80 | await download({ 81 | url: artifact.browser_download_url, 82 | dest: full_archive_path, 83 | progressTitle: "Downloading MOS", 84 | mode: 0o755, 85 | httpProxy: undefined, 86 | }); 87 | }); 88 | 89 | await fs.mkdir(bin_extract_path, { recursive: true }); 90 | await decompress(full_archive_path, bin_extract_path); 91 | await fs.unlink(full_archive_path); 92 | 93 | // Binary path can be removed, and extracted binary path can be swapped 94 | rimraf.sync(bin_path); 95 | renameSync(bin_extract_path, bin_path); 96 | 97 | return dest; 98 | } 99 | 100 | async function downloadWithRetryDialog(fn: () => Promise): Promise { 101 | while (true) { 102 | try { 103 | return await fn(); 104 | } catch (e) { 105 | const selected = await vscode.window.showErrorMessage("Failed to download: " + e.message, {}, { 106 | title: "Retry", 107 | retry: true, 108 | }, { 109 | title: "Dismiss", 110 | }); 111 | 112 | if (selected?.retry) { 113 | continue; 114 | } 115 | throw e; 116 | } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /vscode/src/auto-update/net.ts: -------------------------------------------------------------------------------- 1 | // Taken from https://github.com/rust-analyzer/rust-analyzer under MIT license 2 | // 3 | import fetch from "node-fetch"; 4 | import * as vscode from "vscode"; 5 | import * as stream from "stream"; 6 | import * as crypto from "crypto"; 7 | import * as fs from "fs"; 8 | import * as zlib from "zlib"; 9 | import * as util from "util"; 10 | import * as path from "path"; 11 | import {log} from "../log"; 12 | 13 | var HttpsProxyAgent = require('https-proxy-agent'); 14 | 15 | const pipeline = util.promisify(stream.pipeline); 16 | 17 | const GITHUB_API_ENDPOINT_URL = "https://api.github.com"; 18 | const OWNER = "datatrash"; 19 | const REPO = "mos"; 20 | 21 | export async function fetchRelease( 22 | releaseTag: string, 23 | githubToken: string | null | undefined, 24 | httpProxy: string | null | undefined, 25 | ): Promise { 26 | 27 | const apiEndpointPath = `/repos/${OWNER}/${REPO}/releases/tags/${releaseTag}`; 28 | 29 | const requestUrl = GITHUB_API_ENDPOINT_URL + apiEndpointPath; 30 | 31 | const headers: Record = { Accept: "application/vnd.github.v3+json" }; 32 | if (githubToken != null) { 33 | headers.Authorization = "token " + githubToken; 34 | } 35 | 36 | const response = await (() => { 37 | if (httpProxy) { 38 | return fetch(requestUrl, { headers: headers, agent: new HttpsProxyAgent(httpProxy) }); 39 | } 40 | 41 | return fetch(requestUrl, { headers: headers }); 42 | })(); 43 | 44 | if (!response.ok) { 45 | throw new Error( 46 | `Got response ${response.status} when trying to fetch ` + 47 | `release info for ${releaseTag} release` 48 | ); 49 | } 50 | 51 | // We skip runtime type checks for simplicity (here we cast from `any` to `GithubRelease`) 52 | return await response.json(); 53 | } 54 | 55 | // We omit declaration of tremendous amount of fields that we are not using here 56 | export interface GithubRelease { 57 | name: string; 58 | id: number; 59 | // eslint-disable-next-line camelcase 60 | published_at: string; 61 | assets: Array<{ 62 | name: string; 63 | // eslint-disable-next-line camelcase 64 | browser_download_url: string; 65 | }>; 66 | } 67 | 68 | interface DownloadOpts { 69 | progressTitle: string; 70 | url: string; 71 | dest: string; 72 | mode?: number; 73 | httpProxy?: string; 74 | } 75 | 76 | export async function download(opts: DownloadOpts) { 77 | // Put artifact into a temporary file (in the same dir for simplicity) 78 | // to prevent partially downloaded files when user kills vscode 79 | // This also avoids overwriting running executables 80 | const dest = path.parse(opts.dest); 81 | const randomHex = crypto.randomBytes(5).toString("hex"); 82 | const tempFile = path.join(dest.dir, `${dest.name}${randomHex}`); 83 | 84 | await vscode.window.withProgress( 85 | { 86 | location: vscode.ProgressLocation.Notification, 87 | cancellable: false, 88 | title: opts.progressTitle 89 | }, 90 | async (progress, _cancellationToken) => { 91 | let lastPercentage = 0; 92 | await downloadFile(opts.url, tempFile, opts.mode, opts.httpProxy, (readBytes, totalBytes) => { 93 | const newPercentage = Math.round((readBytes / totalBytes) * 100); 94 | if (newPercentage !== lastPercentage) { 95 | progress.report({ 96 | message: `${newPercentage.toFixed(0)}%`, 97 | increment: newPercentage - lastPercentage 98 | }); 99 | 100 | lastPercentage = newPercentage; 101 | } 102 | }); 103 | } 104 | ); 105 | 106 | await fs.promises.rename(tempFile, opts.dest); 107 | } 108 | 109 | async function downloadFile( 110 | url: string, 111 | destFilePath: fs.PathLike, 112 | mode: number | undefined, 113 | httpProxy: string | null | undefined, 114 | onProgress: (readBytes: number, totalBytes: number) => void 115 | ): Promise { 116 | const res = await (() => { 117 | if (httpProxy) { 118 | return fetch(url, { agent: new HttpsProxyAgent(httpProxy) }); 119 | } 120 | 121 | return fetch(url); 122 | })(); 123 | 124 | if (!res.ok) { 125 | throw new Error(`Got response ${res.status} when trying to download a file.`); 126 | } 127 | 128 | const totalBytes = Number(res.headers.get('content-length')); 129 | 130 | let readBytes = 0; 131 | res.body.on("data", (chunk: Buffer) => { 132 | readBytes += chunk.length; 133 | onProgress(readBytes, totalBytes); 134 | }); 135 | 136 | const destFileStream = fs.createWriteStream(destFilePath, { mode }); 137 | const srcStream = res.body; 138 | 139 | await pipeline(srcStream, destFileStream); 140 | 141 | // Don't apply the workaround in fixed versions of nodejs, since the process 142 | // freezes on them, the process waits for no-longer emitted `close` event. 143 | // The fix was applied in commit 7eed9d6bcc in v13.11.0 144 | // See the nodejs changelog: 145 | // https://github.com/nodejs/node/blob/master/doc/changelogs/CHANGELOG_V13.md 146 | const [, major, minor] = /v(\d+)\.(\d+)\.(\d+)/.exec(process.version)!; 147 | if (+major > 13 || (+major === 13 && +minor >= 11)) return; 148 | 149 | await new Promise(resolve => { 150 | destFileStream.on("close", resolve); 151 | destFileStream.destroy(); 152 | // This workaround is awaiting to be removed when vscode moves to newer nodejs version: 153 | // https://github.com/rust-analyzer/rust-analyzer/issues/3167 154 | }); 155 | } 156 | -------------------------------------------------------------------------------- /vscode/src/build-task-provider.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import {TaskGroup} from "vscode"; 3 | import {State} from "./extension"; 4 | 5 | export interface MosTaskDefinition extends vscode.TaskDefinition { 6 | type: string 7 | } 8 | 9 | export class BuildTaskProvider implements vscode.TaskProvider { 10 | private state: State; 11 | 12 | constructor(state: State) { 13 | this.state = state; 14 | } 15 | 16 | public provideTasks(): vscode.Task[] { 17 | const definition: MosTaskDefinition = { 18 | type: "build" 19 | }; 20 | const exec = new vscode.ShellExecution(this.state.mosPath, ['build']); 21 | const task = new vscode.Task(definition, this.state.workspaceFolder, "Build", "mos", exec, ['$mos']); 22 | task.group = TaskGroup.Build; 23 | return [task]; 24 | } 25 | 26 | public resolveTask(_task: vscode.Task): vscode.Task | undefined { 27 | return undefined; 28 | } 29 | } -------------------------------------------------------------------------------- /vscode/src/extension.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import {existsSync, promises as fs} from "fs"; 3 | import {getMosBinary} from "./auto-update/download-binary"; 4 | import {log} from "./log"; 5 | import {LanguageClient, LanguageClientOptions, ServerOptions} from "vscode-languageclient/node"; 6 | import {BuildTaskProvider} from "./build-task-provider"; 7 | import { 8 | debug, 9 | DebugAdapterDescriptor, 10 | DebugAdapterDescriptorFactory, 11 | DebugAdapterExecutable, DebugAdapterServer, 12 | DebugSession, 13 | ProviderResult 14 | } from "vscode"; 15 | import * as net from "net"; 16 | import {AddressInfo} from "net"; 17 | import {RunAllTestsTaskProvider} from "./run-all-tests-task-provider"; 18 | 19 | let client: LanguageClient; 20 | 21 | let disposables: vscode.Disposable[] = []; 22 | 23 | export class State { 24 | workspaceFolder!: vscode.WorkspaceFolder; 25 | mosPath!: string; 26 | } 27 | 28 | export async function activate(ctx: vscode.ExtensionContext) { 29 | let state = await getState(ctx); 30 | if (!state) { 31 | return; 32 | } 33 | 34 | let debugAdapterPort = await findFreePort(); 35 | 36 | disposables.push(vscode.tasks.registerTaskProvider("build", new BuildTaskProvider(state))); 37 | disposables.push(vscode.tasks.registerTaskProvider("run all tests", new RunAllTestsTaskProvider(state))); 38 | disposables.push(vscode.commands.registerCommand("mos.runSingleTest", testRunnerCommandFactory(true))); 39 | disposables.push(vscode.commands.registerCommand("mos.debugSingleTest", testRunnerCommandFactory(false))); 40 | 41 | let serverOptions: ServerOptions = { 42 | command: state.mosPath, args: ["lsp", "--debug-adapter-port", debugAdapterPort.toString()], options: {} 43 | }; 44 | 45 | let clientOptions: LanguageClientOptions = { 46 | diagnosticCollectionName: "mos", 47 | documentSelector: [{scheme: 'file', language: 'asm'}], 48 | }; 49 | 50 | // Create the language client and start the client. 51 | client = new LanguageClient( 52 | 'mos', 53 | 'MOS Language Server', 54 | serverOptions, 55 | clientOptions 56 | ); 57 | 58 | client.start(); 59 | 60 | log.info(`Trying to start debug adapter on port ${debugAdapterPort}`); 61 | ctx.subscriptions.push(debug.registerDebugAdapterDescriptorFactory("mos", new DebuggerAdapter(debugAdapterPort))); 62 | } 63 | 64 | export type Cmd = (...args: any[]) => unknown; 65 | 66 | function testRunnerCommandFactory(noDebug: boolean): Cmd { 67 | return async (test_name: string) => { 68 | const workspaceFolders = vscode.workspace.workspaceFolders!; 69 | const workspaceFolder = workspaceFolders[0]; 70 | await vscode.debug.startDebugging(workspaceFolder, { 71 | type: "mos", 72 | request: "launch", 73 | name: `Test ${test_name}`, 74 | workspace: workspaceFolder.uri.path, 75 | noDebug, 76 | testRunner: { 77 | testCaseName: test_name 78 | } 79 | }); 80 | }; 81 | } 82 | 83 | class DebuggerAdapter implements DebugAdapterDescriptorFactory { 84 | constructor(private port: number) {} 85 | 86 | createDebugAdapterDescriptor(session: DebugSession, executable: DebugAdapterExecutable | undefined): ProviderResult { 87 | return new DebugAdapterServer(this.port); 88 | } 89 | } 90 | 91 | async function getState(ctx: vscode.ExtensionContext): Promise { 92 | const workspaceFolder = vscode.workspace.workspaceFolders?.[0]; 93 | if (!workspaceFolder) { 94 | return; 95 | } 96 | 97 | await fs.mkdir(ctx.globalStorageUri.fsPath, { recursive: true }); 98 | 99 | let mosPath; 100 | while (true) { 101 | mosPath = await getMosBinary(ctx); 102 | if (!mosPath) { 103 | // User chose not to locate or download a binary, so bail 104 | log.info("No MOS binary found or downloaded. Extension will not activate."); 105 | return; 106 | } 107 | 108 | if (existsSync(mosPath)) { 109 | break; 110 | } 111 | 112 | await vscode.window.showErrorMessage("Could not find MOS executable. Please configure the mos.path setting or leave it blank to automatically download the latest version.", "Retry"); 113 | } 114 | log.info(`Using mos executable: ${mosPath}`); 115 | 116 | let state = new State(); 117 | state.workspaceFolder = workspaceFolder; 118 | state.mosPath = mosPath; 119 | return state; 120 | } 121 | 122 | export function deactivate(): void { 123 | (async () => { 124 | disposables.forEach(d => d.dispose()); 125 | disposables = []; 126 | if (!client) { 127 | return; 128 | } 129 | await client.stop(); 130 | })(); 131 | } 132 | 133 | function findFreePort(): Promise { 134 | return new Promise(resolve => { 135 | const srv = net.createServer(sock => { 136 | sock.end(); 137 | }); 138 | srv.listen(0, () => { 139 | let address = srv.address()!; 140 | resolve(address.port); 141 | }); 142 | }); 143 | } -------------------------------------------------------------------------------- /vscode/src/log.ts: -------------------------------------------------------------------------------- 1 | import { inspect } from "util"; 2 | import * as vscode from "vscode"; 3 | 4 | export const log = new class { 5 | private enabled = true; 6 | private readonly output = vscode.window.createOutputChannel("MOS"); 7 | 8 | setEnabled(yes: boolean): void { 9 | log.enabled = yes; 10 | } 11 | 12 | // Hint: the type [T, ...T[]] means a non-empty array 13 | debug(...msg: [unknown, ...unknown[]]): void { 14 | if (!log.enabled) return; 15 | log.write("DEBUG", ...msg); 16 | } 17 | 18 | info(...msg: [unknown, ...unknown[]]): void { 19 | log.write("INFO", ...msg); 20 | } 21 | 22 | warn(...msg: [unknown, ...unknown[]]): void { 23 | debugger; 24 | log.write("WARN", ...msg); 25 | } 26 | 27 | error(...msg: [unknown, ...unknown[]]): void { 28 | debugger; 29 | log.write("ERROR", ...msg); 30 | log.output.show(true); 31 | } 32 | 33 | private write(label: string, ...messageParts: unknown[]): void { 34 | const message = messageParts.map(log.stringify).join(" "); 35 | const dateTime = new Date().toLocaleString(); 36 | log.output.appendLine(`${label} [${dateTime}]: ${message}`); 37 | } 38 | 39 | private stringify(val: unknown): string { 40 | if (typeof val === "string") return val; 41 | return inspect(val, { 42 | colors: false, 43 | depth: 6, // heuristic 44 | }); 45 | } 46 | }; 47 | -------------------------------------------------------------------------------- /vscode/src/run-all-tests-task-provider.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import {TaskGroup} from "vscode"; 3 | import {State} from "./extension"; 4 | import {MosTaskDefinition} from "./build-task-provider"; 5 | 6 | export class RunAllTestsTaskProvider implements vscode.TaskProvider { 7 | private state: State; 8 | 9 | constructor(state: State) { 10 | this.state = state; 11 | } 12 | 13 | public provideTasks(): vscode.Task[] { 14 | const definition: MosTaskDefinition = { 15 | type: "shell" 16 | }; 17 | const exec = new vscode.ShellExecution(this.state.mosPath, ['test']); 18 | const task = new vscode.Task(definition, this.state.workspaceFolder, "Run all tests", "mos", exec, ['$mos']); 19 | task.group = TaskGroup.Test; 20 | return [task]; 21 | } 22 | 23 | public resolveTask(_task: vscode.Task): vscode.Task | undefined { 24 | return undefined; 25 | } 26 | } -------------------------------------------------------------------------------- /vscode/syntaxes/asm.tmLanguage.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ASM", 3 | "scopeName": "source.asm", 4 | "patterns": [ 5 | { 6 | "include": "#comment" 7 | }, 8 | { 9 | "include": "#mnemonic" 10 | }, 11 | { 12 | "include": "#directive" 13 | }, 14 | { 15 | "include": "#label" 16 | }, 17 | { 18 | "include": "#hex-literal" 19 | }, 20 | { 21 | "include": "#dec-literal" 22 | }, 23 | { 24 | "include": "#bin-literal" 25 | } 26 | ], 27 | "repository": { 28 | "comment": { 29 | "patterns": [ 30 | { 31 | "name": "comment.block.asm", 32 | "begin": "/\\*", 33 | "beginCaptures": { 34 | "0": { 35 | "name": "punctuation.definition.comment.asm" 36 | } 37 | }, 38 | "end": "\\*/", 39 | "endCaptures": { 40 | "0": { 41 | "name": "punctuation.definition.comment.asm" 42 | } 43 | } 44 | }, 45 | { 46 | "begin": "//", 47 | "beginCaptures": { 48 | "0": { 49 | "name": "comment.line.double-slash.asm" 50 | } 51 | }, 52 | "end": "(?=$)", 53 | "contentName": "comment.line.double-slash.asm" 54 | } 55 | ] 56 | }, 57 | "mnemonic": { 58 | "patterns": [ 59 | { 60 | "name": "keyword.mnemonic.asm", 61 | "match": "(adc|and|asl|bcc|bcs|beq|bit|bmi|bne|bpl|brk|bvc|bvs|clc|cld|cli|clv|cmp|cpx|cpy|dec|dex|dey|eor|inc|inx|iny|jmp|jsr|lda|ldx|ldy|lsr|nop|ora|pha|php|pla|plp|rol|ror|rti|rts|sbc|sec|sed|sei|sta|stx|sty|tax|tay|tsx|txa|txs|tya)(\\b|\\z)" 62 | } 63 | ] 64 | }, 65 | "directive": { 66 | "patterns": [ 67 | { 68 | "name": "entity.name.function.preprocessor.asm", 69 | "match": "(\\W+|\\s|\\A)(.const|.var|.byte|.word|.dword|.macro|.define|.segment|.loop|.align|.if|else|as|.import|from|.text|ascii|petscii|petscreen|.file|.assert|.trace|.test)(\\s|\\z)" 70 | } 71 | ] 72 | }, 73 | "label": { 74 | "patterns": [ 75 | { 76 | "name": "support.type.asm", 77 | "match": "\\s*(\\w+)\\:" 78 | } 79 | ] 80 | }, 81 | "hex-literal": { 82 | "patterns": [ 83 | { 84 | "name": "constant.numeric.hexadecimal.asm", 85 | "match": "#?\\$[0-9A-Fa-f]+" 86 | } 87 | ] 88 | }, 89 | "dec-literal": { 90 | "patterns": [ 91 | { 92 | "name": "constant.numeric.decimal.asm", 93 | "match": "\\b#?[0-9]+" 94 | } 95 | ] 96 | }, 97 | "bin-literal": { 98 | "patterns": [ 99 | { 100 | "name": "constant.numeric.binary.asm", 101 | "match": "#?\\%[0-1]+" 102 | } 103 | ] 104 | } 105 | } 106 | } -------------------------------------------------------------------------------- /vscode/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "experimentalDecorators": true, 4 | "module": "commonjs", 5 | "target": "es6", 6 | "outDir": "out", 7 | "lib": [ 8 | "es6" 9 | ], 10 | "sourceMap": true, 11 | "rootDir": "src", 12 | "strict": true, 13 | "moduleResolution": "Node", 14 | /* enable all strict type-checking options */ 15 | /* Additional Checks */ 16 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 17 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 18 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 19 | "allowSyntheticDefaultImports": true 20 | }, 21 | "exclude": [ 22 | "node_modules", 23 | ".vscode-test" 24 | ] 25 | } 26 | --------------------------------------------------------------------------------