├── .github └── workflows │ ├── node.js.yml │ └── webui.yml ├── .gitignore ├── .gitlab-ci.yml ├── .posthtmlrc ├── Docs.md ├── LICENSE ├── Readme.md ├── markdown2html.js ├── package.json ├── src ├── cli.js ├── lib │ ├── asmNormalize.js │ ├── asmWriter.js │ ├── ast2asm.js │ ├── astCalcNormalizer.js │ ├── astNormalize.js │ ├── builtins │ │ ├── functions.js │ │ └── libdragon.js │ ├── dataTypes │ │ └── dataTypes.js │ ├── grammar.cjs │ ├── grammar.ne │ ├── intsructions │ │ └── asmWriter.js │ ├── memory │ │ └── memoryValidator.js │ ├── operations │ │ ├── branch.js │ │ ├── scalar.js │ │ ├── userFunction.js │ │ └── vector.js │ ├── optimizer │ │ ├── asmOptimizer.js │ │ ├── asmScanDeps.js │ │ ├── eval │ │ │ └── evalCost.js │ │ └── pattern │ │ │ ├── assertCompare.js │ │ │ ├── branchJump.js │ │ │ ├── commandAlias.js │ │ │ ├── dedupeImm.js │ │ │ ├── dedupeJumps.js │ │ │ ├── dedupeLabels.js │ │ │ ├── mergeSequence.js │ │ │ ├── removeDeadCode.js │ │ │ └── tailCall.js │ ├── preproc │ │ └── preprocess.js │ ├── state.js │ ├── syntax │ │ ├── annotations.js │ │ ├── registers.js │ │ └── swizzle.js │ ├── transpiler.js │ ├── types │ │ ├── asm.d.ts │ │ ├── ast.d.ts │ │ ├── astCalc.d.ts │ │ ├── opt.d.ts │ │ └── types.d.ts │ ├── utils.js │ └── workerThreads.js ├── tests │ ├── annotations.test.js │ ├── branchConst.test.js │ ├── branchVar.test.js │ ├── branchZero.test.js │ ├── builtins.test.js │ ├── compare.test.js │ ├── const.test.js │ ├── control.test.js │ ├── e2e │ │ └── e2e.test.js │ ├── examples.test.js │ ├── examples │ │ ├── 3d.S │ │ ├── 3d.rspl │ │ ├── mandelbrot.rspl │ │ └── squares2d.rspl │ ├── helper │ │ └── compileAndEmu.js │ ├── immediateScalar.test.js │ ├── labels.test.js │ ├── load.test.js │ ├── loop.test.js │ ├── macros.test.js │ ├── optimizer │ │ ├── depScan │ │ │ ├── optDepScanCtrl.test.js │ │ │ ├── optDepScanMem.test.js │ │ │ ├── optDepScanRegs.test.js │ │ │ └── optRegScan.test.js │ │ ├── e2e │ │ │ ├── optAssert.test.js │ │ │ ├── optBranchJump.test.js │ │ │ ├── optDeadCode.test.js │ │ │ ├── optDelaySlot.test.js │ │ │ ├── optJumpDedupe.test.js │ │ │ ├── optLabels.test.js │ │ │ └── optMergeSequence.test.js │ │ └── eval │ │ │ ├── evalCost.test.js │ │ │ └── evalCostExample.test.js │ ├── preproc │ │ ├── defineAsm.test.js │ │ └── preproc.test.js │ ├── scalarOps.test.js │ ├── scope.test.js │ ├── state.test.js │ ├── store.test.js │ ├── swizzle.test.js │ ├── syntaxExpansion.test.js │ ├── syntaxNumbers.test.js │ ├── syntaxVar.test.js │ └── vectorOps.test.js └── web │ ├── docs.html │ ├── fonts │ ├── FiraCode-Bold.ttf │ ├── FiraCode-Regular.ttf │ └── Roboto-Regular.ttf │ ├── img │ ├── copy.svg │ └── save.svg │ ├── index.html │ ├── js │ ├── editor.js │ ├── editorMode │ │ └── rspl_highlight_rules.js │ ├── exampleCode.js │ ├── logger.js │ ├── main.js │ ├── storage.js │ └── utils.js │ └── style │ └── main.css └── yarn.lock /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | branches: [ "main" ] 9 | pull_request: 10 | branches: [ "main" ] 11 | 12 | jobs: 13 | test: 14 | runs-on: ubuntu-latest 15 | 16 | strategy: 17 | matrix: 18 | node-version: [20.x] 19 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 20 | 21 | steps: 22 | - uses: actions/checkout@v4 23 | - name: Use Node.js ${{ matrix.node-version }} 24 | uses: actions/setup-node@v4 25 | with: 26 | node-version: ${{ matrix.node-version }} 27 | cache: 'npm' 28 | - run: yarn install 29 | - run: yarn test 30 | -------------------------------------------------------------------------------- /.github/workflows/webui.yml: -------------------------------------------------------------------------------- 1 | name: Deploy site to Pages 2 | 3 | on: 4 | # Runs on pushes targeting the default branch 5 | push: 6 | branches: ["main"] 7 | 8 | # Allows you to run this workflow manually from the Actions tab 9 | workflow_dispatch: 10 | 11 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 12 | permissions: 13 | contents: read 14 | pages: write 15 | id-token: write 16 | 17 | # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. 18 | # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. 19 | concurrency: 20 | group: "pages" 21 | cancel-in-progress: false 22 | 23 | jobs: 24 | # Build job 25 | build: 26 | runs-on: ubuntu-latest 27 | steps: 28 | - name: Checkout 29 | uses: actions/checkout@v4 30 | - name: Setup Node 31 | uses: actions/setup-node@v4 32 | with: 33 | node-version: "20" 34 | cache: 'yarn' 35 | - name: Install dependencies 36 | run: yarn install 37 | - name: Build with Next.js 38 | run: yarn build 39 | - name: Upload artifact 40 | uses: actions/upload-pages-artifact@v3 41 | with: 42 | path: ./dist 43 | 44 | # Deployment job 45 | deploy: 46 | environment: 47 | name: github-pages 48 | url: ${{ steps.deployment.outputs.page_url }} 49 | runs-on: ubuntu-latest 50 | needs: build 51 | steps: 52 | - name: Deploy to GitHub Pages 53 | id: deployment 54 | uses: actions/deploy-pages@v4 55 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .parcel-cache 2 | dist/ 3 | node_modules 4 | .vscode 5 | .idea 6 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | image: node:20.0.0 2 | 3 | cache: 4 | key: 5 | files: 6 | - yarn.lock 7 | paths: 8 | - .yarn 9 | - node_modules/ 10 | 11 | stages: 12 | - build 13 | - deploy 14 | 15 | build: 16 | stage: build 17 | rules: 18 | - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH 19 | script: 20 | - yarn install 21 | - yarn build 22 | artifacts: 23 | paths: 24 | - dist 25 | 26 | pages: 27 | image: alpine:3.18.0 28 | stage: deploy 29 | rules: 30 | - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH 31 | script: 32 | - rm -rf public 33 | - mv dist public 34 | 35 | artifacts: 36 | paths: 37 | - public 38 | -------------------------------------------------------------------------------- /.posthtmlrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": { 3 | "posthtml-include": {} 4 | } 5 | } -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # RSPL - Transpiler 2 | 3 | RSPL is a high-level language which can be transpiled into RSP MIPS assembly in text form.
4 | The output is a completely functional overlay usable by libdragon. 5 | 6 | It's intended to be a simple language staying close to the hardware, while proving a GLSL-like "swizzle" syntax to make use of the vector/lane instructions on the RSP. 7 | 8 | For a detailed documentation on the langue itself, see the language docs: [RSPL](Docs.md) 9 | 10 | ## Using RSPL 11 | 12 | This project provides both a CLI, and an interactive Web-App which can transpile code in real-time. 13 | 14 | A hosted version of the web-app is available at: [https://mbeboek.gitlab.io/rspl/](https://hailtododongo.github.io/rspl/)
15 | 16 | The CLI is currently not available as a pre-build, and needs to be build from sources.
17 | See the build section for more information. 18 | 19 | ## Building 20 | The only system requirement is [NodeJS](https://nodejs.org/), using version 20.0 or higher.
21 | 22 | After checking out the repo, install all NPM packages with: 23 | ```sh 24 | yarn install 25 | ``` 26 | This only needs to be done once, or when dependencies have changed. 27 | 28 | ### Web-App 29 | The webapp can be both build, and started in development mode (which should be preferred when working on the transpiler).
30 | To start it, run: 31 | ```sh 32 | yarn start 33 | ``` 34 | This starts a local web-server with hot-reloading support.
35 | You can access the page by opening the link it prints out. 36 | 37 | To make a production-ready build, run this instead: 38 | ```sh 39 | yarn build 40 | ``` 41 | This will create a minified, static website inside the `/dist` directory.
42 | If you want to host this page, make sure to set the base-path correctly (set in the `package.json`).
43 | By default, it's set to `/rspl` since that's what the GitLab page of this repo needs.
44 | 45 | ### CLI 46 | To build the CLI instead, run: 47 | ```sh 48 | yarn build:cli 49 | ``` 50 | A single file called `cli.mjs` inside the `/dist` directory will be build.
51 | This file is completely self-contained, meaning it no longer needs any external packages and can be moved into any other directory. 52 | 53 | > **Note**
54 | > If you switch between building the Web-App and CLI, make a clean build first.
55 | > To clean any build files, run: `yarn clean` 56 | 57 | #### Usage 58 | To transpile files using the CLI, start it with NodeJS: 59 | ```sh 60 | node cli.mjs inputFile.rspl 61 | ``` 62 | A new file in the same location as the input will be created, with the extension `.S`.
63 | This can then be used as-is in a libdragon project. 64 | 65 | ### Language Definition 66 | RSPL itself is defined in a grammar file, using the libraries [Nearly.js](https://nearley.js.org/) & [Moo](https://github.com/no-context/moo) for tokenization and lexing.
67 | It can be found in: `src/lib/grammar.ne`
68 | This is not directly used by the CLI/WebAPP, but rather nearly.js can be used to produce a JS file from it which acts as the lexer/parser.
69 | To regenerate that file, run: 70 | ```sh 71 | yarn build:lang 72 | ``` 73 | To efficiently make changes to the grammar file,
74 | i recommend using the online tool nearly provides: https://omrelli.ug/nearley-playground/ 75 | 76 | > **Note**
77 | > The produced JS file is checked into this repo.
78 | > You only need to run this step when making changes to `src/lib/grammar.ne`. 79 | 80 | ## Tests 81 | This project uses `jest` for automated testing.
82 | Tests are located in `src/tests` and mostly contain unit tests for syntax and operations, as well as a few complete example files.
83 | To execute them, run: 84 | ```sh 85 | yarn test 86 | ``` 87 | For an interactive mode, which watches for changes, run: 88 | ``` 89 | yarn test --watch 90 | ``` 91 | 92 | ## Types 93 | While this project is written in plain JS, it still makes use of type-hints.
94 | This is using a combination of [JSDoc](https://jsdoc.app/) and [TypeScript](https://www.typescriptlang.org/)-Definitions.
95 | Type-definition files are located in `src/lib/types`.
96 | Given an IDE that can understand these, it will provide auto-completion and type-checking. 97 | 98 | > **Note**
99 | > There is no need to install TypeScript, IDEs will understand the type-hints without it. 100 | 101 | ## License 102 | This software is licensed under "Apache License 2.0".
103 | 104 | Note that this license does NOT apply to any RSPL code written by users of this software, including the transpiled assembly output.
105 | I do NOT claim any copyright, nor do i enforce any restrictions on generated assembly code produced by this software. 106 | 107 | © Max Bebök (HailToDodongo) 108 | -------------------------------------------------------------------------------- /markdown2html.js: -------------------------------------------------------------------------------- 1 | import showdown from 'showdown'; 2 | import fs from 'fs'; 3 | 4 | const markdown = fs.readFileSync('./Docs.md', 'utf8'); 5 | 6 | const converter = new showdown.Converter(); 7 | const html = converter.makeHtml(markdown); 8 | 9 | fs.writeFileSync('./src/web/docs.html', html); 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rspl", 3 | "version": "1.0.0", 4 | "source": "src/web/index.html", 5 | "license": "MIT", 6 | "type": "module", 7 | "devDependencies": { 8 | "armips": "1.1.0", 9 | "jest": "^29.7.0", 10 | "parcel": "2.15.2", 11 | "posthtml-doctype": "^1.1.1", 12 | "posthtml-include": "^1.7.4", 13 | "rsp-wasm": "1.0.3", 14 | "showdown": "^2.1.0" 15 | }, 16 | "targets": { 17 | "cli": { 18 | "optimize": true, 19 | "context": "node", 20 | "isLibrary": false, 21 | "sourceMap": true, 22 | "outputFormat": "esmodule", 23 | "includeNodeModules": { 24 | "typescript": false, 25 | "vscode": false 26 | } 27 | } 28 | }, 29 | "scripts": { 30 | "start": "yarn parcel src/web/index.html", 31 | "build": "parcel build --public-url '/rspl'", 32 | "build:cli": "parcel build --no-cache --target cli src/cli.js && mv dist/cli.js dist/cli.mjs", 33 | "build:lang": "nearleyc src/lib/grammar.ne -o src/lib/grammar.cjs", 34 | "clean": "rm -rf .parcel-cache && rm -rf dist", 35 | "test": "NODE_OPTIONS=--experimental-vm-modules yarn jest" 36 | }, 37 | "dependencies": { 38 | "ace-builds": "^1.30.0", 39 | "highlight.js": "^11.9.0", 40 | "moo": "^0.5.2", 41 | "nearley": "^2.20.1" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/cli.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @copyright 2023 - Max Bebök 3 | * @license Apache-2.0 4 | */ 5 | 6 | import { transpileSource } from "./lib/transpiler"; 7 | import { readFileSync, writeFileSync, watch } from "fs"; 8 | import * as path from "path"; 9 | import {initWorker, registerTask, WorkerThreads} from "./lib/workerThreads.js"; 10 | 11 | import { fileURLToPath } from 'url'; 12 | import {reorderRound, reorderTask} from "./lib/optimizer/asmOptimizer.js"; 13 | const __filename = fileURLToPath(import.meta.url); 14 | 15 | /** 16 | * @type RSPLConfig 17 | */ 18 | let config = { 19 | optimize: true, 20 | optimizeTime: 1000 * 30, 21 | optimizeWorker: 4, 22 | reorder: false, 23 | rspqWrapper: true, 24 | defines: {}, 25 | patchFunctions: [], 26 | watch: false, 27 | debugInfo: true 28 | }; 29 | 30 | function getFunctionStartEnd(source, funcName) { 31 | const funcIdx = source.search(funcName + ":\n"); 32 | if(funcIdx === -1) { 33 | throw new Error("Function "+funcName+" not found in output file!"); 34 | } 35 | // search for end, each following line starts with 2 spaces and the function ends when a line has none 36 | const endIdx = source.substring(funcIdx).search(/\n[A-Za-z0-9]/); 37 | if(endIdx === -1) { 38 | throw new Error("Function end not found in output file!"); 39 | } 40 | return [funcIdx, funcIdx+endIdx]; 41 | } 42 | 43 | for(let i=3; i arg.includes(".mjs")); 85 | const worker = new WorkerThreads(config.optimizeWorker, selfPath); 86 | 87 | const sourceBaseDir = path.dirname(process.argv[2]); 88 | config.fileLoader = filePath => readFileSync(path.join(sourceBaseDir, filePath), "utf8"); 89 | 90 | const finalizeASM = (asmRes, lastRun = false) => { 91 | if(asmRes.warn && lastRun) { 92 | console.warn(asmRes.warn); 93 | asmRes.warn = ""; 94 | } 95 | if(asmRes.info && lastRun) { 96 | console.info(asmRes.info); 97 | asmRes.info = ""; 98 | } 99 | 100 | if(config.patchFunctions.length) { 101 | let oldSource = readFileSync(pathOut, "utf8"); 102 | for(const patchFunc of config.patchFunctions) { 103 | if(lastRun)console.log("Patching functions", config.patchFunctions.join(", ")); 104 | 105 | const posOld = getFunctionStartEnd(oldSource, patchFunc); 106 | const posNew = getFunctionStartEnd(asmRes.asm, patchFunc); 107 | oldSource = oldSource.substring(0, posOld[0]) 108 | + asmRes.asm.substring(posNew[0], posNew[1]) 109 | + oldSource.substring(posOld[1]); 110 | } 111 | writeFileSync(pathOut, oldSource); 112 | } else { 113 | writeFileSync(pathOut, asmRes.asm); 114 | } 115 | }; 116 | 117 | console.time("transpile"); 118 | const asmRes = await transpileSource(source, config, finalizeASM); 119 | console.timeEnd("transpile"); 120 | 121 | finalizeASM(asmRes, true); 122 | 123 | worker.stop(); 124 | } 125 | 126 | if(config.watch) { 127 | console.log("Watching for changes on: " + process.argv[2]); 128 | let lastChangeTime = 0; 129 | let isRunning = false; 130 | watch(process.argv[2], (eventType, filename) => 131 | { 132 | if(isRunning || eventType !== "change")return; 133 | if(Date.now() - lastChangeTime < 100)return; 134 | lastChangeTime = Date.now(); 135 | isRunning = true; 136 | main().catch(e => console.error(e)) 137 | .finally(() => isRunning = false); 138 | }); 139 | } else { 140 | main().catch(e => { 141 | console.error(e); 142 | process.exit(1); 143 | }); 144 | } -------------------------------------------------------------------------------- /src/lib/asmNormalize.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @copyright 2023 - Max Bebök 3 | * @license Apache-2.0 4 | */ 5 | 6 | import {REG} from "./syntax/registers.js"; 7 | import {ASM_TYPE} from "./intsructions/asmWriter.js"; 8 | import {READ_ONLY_OPS} from "./optimizer/asmScanDeps.js"; 9 | 10 | /** 11 | * Normalizes ASM output of a function. 12 | * 13 | * This is different to optimizing, as it's necessary to produce valid ASM. 14 | * E.g. some operations use assignments to the zero-register to keep the logic simple, 15 | * this gets removed here. 16 | * @param {ASMFunc} asmFunc 17 | */ 18 | export function normalizeASM(asmFunc) 19 | { 20 | /** @type {ASM[]} */ const asm = []; 21 | 22 | for(const line of asmFunc.asm) 23 | { 24 | if(line.type !== ASM_TYPE.OP 25 | || READ_ONLY_OPS.includes(line.op) 26 | || line.args.length === 0 27 | ) { 28 | asm.push(line); 29 | continue; 30 | } 31 | 32 | // ignore any write to the zero registers 33 | let targetReg = ["mtc2"].includes(line.op) ? line.args[1] : line.args[0]; 34 | if(targetReg.startsWith(REG.VZERO) || targetReg.startsWith(REG.ZERO)) { 35 | continue; 36 | } 37 | 38 | asm.push(line); 39 | } 40 | asmFunc.asm = asm; 41 | } -------------------------------------------------------------------------------- /src/lib/builtins/libdragon.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @copyright 2023 - Max Bebök 3 | * @license Apache-2.0 4 | */ 5 | 6 | // The flags here are either N64 hardware register/flags or composite values from libdragon. 7 | // They are mainly used directly to avoid macros that my expand into multiple instructions. 8 | // For all macros/flags see: include/rsp.inc in libdragon 9 | 10 | export const SP_STATUS = { 11 | HALTED : 1<<0, 12 | BROKE : 1<<1, 13 | DMA_BUSY : 1<<2, 14 | DMA_FULL : 1<<3, 15 | IO_FULL : 1<<4, 16 | SSTEP : 1<<5, 17 | INTR_BREAK: 1<<6, 18 | SIG0 : 1<<7, 19 | SIG1 : 1<<8, 20 | SIG2 : 1<<9, 21 | SIG3 : 1<<10, 22 | SIG4 : 1<<11, 23 | SIG5 : 1<<12, 24 | SIG6 : 1<<13, 25 | SIG7 : 1<<14, 26 | }; 27 | 28 | export const DMA_FLAGS = { 29 | DMA_IN_ASYNC : 0x00000000, 30 | DMA_OUT_ASYNC: 0xFFFF8000, 31 | DMA_IN : 0x00000000 | SP_STATUS.DMA_BUSY | SP_STATUS.DMA_FULL, 32 | DMA_OUT : 0xFFFF8000 | SP_STATUS.DMA_BUSY | SP_STATUS.DMA_FULL, 33 | }; 34 | 35 | export const LABEL_CMD_LOOP = "RSPQ_Loop"; 36 | export const LABEL_ASSERT = "assertion_failed"; -------------------------------------------------------------------------------- /src/lib/dataTypes/dataTypes.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @copyright 2023 - Max Bebök 3 | * @license Apache-2.0 4 | */ 5 | 6 | /** @type {Record} */ 7 | export const TYPE_SIZE = { 8 | s8 : 1, 9 | u8 : 1, 10 | s16 : 2, 11 | u16 : 2, 12 | s32 : 4, 13 | u32 : 4, 14 | vec16: 2*8, // 16bit per lane @ 8 lanes 15 | vec32: 4*8, // 16bit per lane @ 8 lanes, two register 16 | }; 17 | 18 | export const TYPE_ASM_DEF = { 19 | s8 : {type: "byte", count: 1}, 20 | u8 : {type: "byte", count: 1}, 21 | s16 : {type: "half", count: 1}, 22 | u16 : {type: "half", count: 1}, 23 | s32 : {type: "word", count: 1}, 24 | u32 : {type: "word", count: 1}, 25 | vec16: {type: "half", count: 8}, 26 | vec32: {type: "half", count: 16}, 27 | }; 28 | 29 | /** @type {Record} */ 30 | export const TYPE_ALIGNMENT = { 31 | s8 : 0, 32 | u8 : 0, 33 | s16 : 1, 34 | u16 : 1, 35 | s32 : 2, 36 | u32 : 2, 37 | vec16: 4, 38 | vec32: 4, 39 | }; 40 | 41 | /** @type {Record} */ 42 | export const TYPE_REG_COUNT = { 43 | s8 : 1, 44 | u8 : 1, 45 | s16 : 1, 46 | u16 : 1, 47 | s32 : 1, 48 | u32 : 1, 49 | vec16: 1, 50 | vec32: 2, 51 | }; 52 | 53 | export const SCALAR_TYPES = ["s8", "u8", "s16", "u16", "s32", "u32"]; 54 | export const VEC_CASTS = ["uint", "sint", "ufract", "sfract"]; 55 | 56 | /** 57 | * @param {DataType} type 58 | * @returns {boolean} 59 | */ 60 | export const isTwoRegType = type => type === "vec32"; 61 | 62 | /** 63 | * @param {DataType} type 64 | * @returns {boolean} 65 | */ 66 | export const isVecType = type => type.startsWith("vec"); 67 | 68 | /** 69 | * @param {string} type 70 | * @returns {boolean} 71 | */ 72 | export const isSigned = type => type.startsWith("s"); 73 | 74 | /** 75 | * @param {number} val 76 | * @param {number} pad 77 | * @returns {string} 78 | */ 79 | export const toHex = (val, pad = 2) => 80 | "0x" + val.toString(16).padStart(pad, '0').toUpperCase(); 81 | 82 | /** 83 | * @param {number} valueU32 84 | * @returns {boolean} 85 | */ 86 | export function u32InS16Range(valueU32) { 87 | return valueU32 <= 0x7FFF || valueU32 >= 0xFFFF8000; 88 | } 89 | 90 | /** 91 | * @param {number} valueU32 92 | * @returns {boolean} 93 | */ 94 | export function u32InU16Range(valueU32) { 95 | return valueU32 <= 0xFFFF; 96 | } 97 | 98 | /** 99 | * @param {number} valueF32 100 | * @returns {number} 101 | */ 102 | export function f32ToFP32(valueF32) { 103 | return Math.round(valueF32 * (1<<16)) >>> 0; 104 | } -------------------------------------------------------------------------------- /src/lib/intsructions/asmWriter.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @copyright 2023 - Max Bebök 3 | * @license Apache-2.0 4 | */ 5 | import state from "../state.js"; 6 | import { 7 | BRANCH_OPS, 8 | IMMOVABLE_OPS, 9 | LOAD_OPS, 10 | LOAD_OPS_SCALAR, 11 | LOAD_OPS_VECTOR, 12 | MEM_STALL_LOAD_OPS, 13 | MEM_STALL_STORE_OPS, OP_FLAG_CTC2_CFC2, 14 | OP_FLAG_IS_BRANCH, 15 | OP_FLAG_IS_IMMOVABLE, OP_FLAG_IS_LIKELY, 16 | OP_FLAG_IS_LOAD, 17 | OP_FLAG_IS_MEM_STALL_LOAD, OP_FLAG_IS_MEM_STALL_STORE, OP_FLAG_IS_NOP, 18 | OP_FLAG_IS_STORE, OP_FLAG_IS_VECTOR, OP_FLAG_LIKELY_BRANCH, 19 | STORE_OPS 20 | } from "../optimizer/asmScanDeps.js"; 21 | import {REG} from "../syntax/registers.js"; 22 | import {getAnnotationVal} from "../syntax/annotations.js"; 23 | 24 | export const ASM_TYPE = { 25 | OP: 0, 26 | LABEL: 1, 27 | // COMMENT: 2, 28 | INLINE: 3, 29 | } 30 | 31 | /** @param {string} op */ 32 | function getStallLatency(op) { 33 | if(op.startsWith("v") || op === "mtc2")return 4; 34 | if(LOAD_OPS_VECTOR.includes(op))return 4; 35 | if(LOAD_OPS_SCALAR.includes(op))return 3; 36 | if(["mfc0", "mfc2", "cfc2", "ctc2"].includes(op))return 3; 37 | return 0; 38 | } 39 | 40 | /** 41 | * @returns {ASMDebug} 42 | */ 43 | function getDebugData() { 44 | return { 45 | lineASM: 0, 46 | lineRSPL: state.line, 47 | lineASMOpt: 0, 48 | reorderCount: 0, 49 | reorderLineMin: 0, 50 | reorderLineMax: 0, 51 | cycle: 0, 52 | }; 53 | } 54 | 55 | /** 56 | * 57 | * @param op 58 | * @return {ASM} 59 | */ 60 | function getOpInfo(op, type = ASM_TYPE.OP) { 61 | const annotations = state.getAnnotations(); 62 | const res = { 63 | opFlags: (LOAD_OPS.includes(op) ? OP_FLAG_IS_LOAD : 0) 64 | | (STORE_OPS.includes(op) ? OP_FLAG_IS_STORE : 0) 65 | | (BRANCH_OPS.includes(op) ? OP_FLAG_IS_BRANCH : 0) 66 | | (IMMOVABLE_OPS.includes(op) ? OP_FLAG_IS_IMMOVABLE : 0) 67 | | (MEM_STALL_LOAD_OPS.includes(op) ? OP_FLAG_IS_MEM_STALL_LOAD : 0) 68 | | (MEM_STALL_STORE_OPS.includes(op) ? OP_FLAG_IS_MEM_STALL_STORE : 0) 69 | | (op.startsWith("v") ? OP_FLAG_IS_VECTOR : 0) 70 | | (op === "nop" ? OP_FLAG_IS_NOP : 0) 71 | | (!getAnnotationVal(annotations, "Unlikely") ? OP_FLAG_IS_LIKELY : 0) 72 | | ((op === "cfc2" || op === "ctc2") ? OP_FLAG_CTC2_CFC2 : 0) 73 | , 74 | stallLatency: getStallLatency(op), 75 | annotations, 76 | depsArgMask: 0n, 77 | type: type, 78 | }; 79 | if((res.opFlags & OP_FLAG_IS_BRANCH) && (res.opFlags & OP_FLAG_IS_LIKELY)) { 80 | res.opFlags |= OP_FLAG_LIKELY_BRANCH; 81 | } 82 | 83 | return res; 84 | } 85 | 86 | /** 87 | * Emits an assembly instruction 88 | * @param {string} op 89 | * @param {Array} args 90 | * @return {ASM} 91 | */ 92 | export function asm(op, args) { 93 | return {op, args, debug: getDebugData(), ...getOpInfo(op)}; 94 | } 95 | 96 | /** 97 | * Emits a function call 98 | * @param target 99 | * @param argRegs 100 | * @param {boolean} relative 101 | * @return {ASM} 102 | */ 103 | export function asmFunction(target, argRegs, relative = false) { 104 | return relative ? { 105 | op: "bgezal", args: [REG.ZERO, target], 106 | debug: getDebugData(), ...getOpInfo("bgezal"), 107 | funcArgs: argRegs 108 | } : { 109 | op: "jal", args: [target], 110 | debug: getDebugData(), ...getOpInfo("jal"), 111 | funcArgs: argRegs 112 | }; 113 | } 114 | 115 | export function asmBranch(op, args, labelEnd) { 116 | return {op, args, debug: getDebugData(), ...getOpInfo(op), labelEnd}; 117 | } 118 | 119 | export function asmInline(op, args = []) { 120 | return {op, args, debug: getDebugData(), ...getOpInfo(op, ASM_TYPE.INLINE)}; 121 | } 122 | 123 | /** @returns {ASM} */ 124 | export function asmNOP() { 125 | return {op: "nop", args: [], debug: getDebugData(), 126 | ...getOpInfo("nop"), 127 | }; 128 | } 129 | 130 | /** @returns {ASM} */ 131 | export function asmLabel(label) { 132 | return {label, op: "", args: [], debug: getDebugData(), 133 | ...getOpInfo("", ASM_TYPE.LABEL), 134 | }; 135 | } 136 | -------------------------------------------------------------------------------- /src/lib/memory/memoryValidator.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @copyright 2024 - Max Bebök 3 | * @license Apache-2.0 4 | */ 5 | 6 | import state from "../state.js"; 7 | import {TYPE_ALIGNMENT, TYPE_SIZE} from "../dataTypes/dataTypes.js"; 8 | 9 | const STATE_NAMES = ["state", "data", "bss"]; 10 | 11 | /** 12 | * validates state memory 13 | * @param {AST} ast 14 | * @param {Array<{name: string; vars: ASTState[]}>} states 15 | */ 16 | export const validateMemory = (ast, states) => 17 | { 18 | state.func = "state"; 19 | 20 | const names = states.map(s => s.name).flat(); 21 | if((new Set(names)).size !== names.length) { 22 | state.throwError("A type of state can only appear once!", names); 23 | } 24 | 25 | ast.state = []; 26 | ast.stateBss = []; 27 | ast.stateData = []; 28 | 29 | for(const s of states) { 30 | switch(s.name) { 31 | case 'state': ast.state = s.vars; break; 32 | case 'data': ast.stateData = s.vars; break; 33 | case 'temp_state': 34 | case 'bss': ast.stateBss = s.vars; break; 35 | default: 36 | state.throwError(`Invalid state name: ${s.name}, must be one of: ${STATE_NAMES.join(", ")}`); 37 | break; 38 | } 39 | } 40 | 41 | let stateVars = [...ast.state, ...ast.stateBss, ...ast.stateData] 42 | .filter(v => !v.extern); 43 | 44 | for(const m of ast.stateBss) { 45 | if(m.value) { 46 | state.throwError("Memory inside .BSS ("+m.varName+") can not have a value!"); 47 | } 48 | } 49 | 50 | if(stateVars.length === 0)return; 51 | 52 | let currentAddr = 0; 53 | let lastVar = stateVars[0]; 54 | for(const stateVar of stateVars) { 55 | const arraySize = stateVar.arraySize.reduce((a, b) => a * b, 1) || 1; 56 | const byteSize = TYPE_SIZE[stateVar.varType] * arraySize; 57 | let align = stateVar.align || (2 ** TYPE_ALIGNMENT[stateVar.varType]); 58 | 59 | let newAddr = currentAddr + byteSize; 60 | if(newAddr % align !== 0) { 61 | newAddr += align - (newAddr % align); 62 | } 63 | 64 | let alignDiff = (newAddr - currentAddr) % align; 65 | if(alignDiff > 0) { 66 | state.logInfo(`Info: Alignment gap (${alignDiff} bytes) between ${lastVar.varName} and ${stateVar.varName} at 0x${currentAddr.toString(16)}`); 67 | } 68 | 69 | currentAddr = newAddr; 70 | lastVar = stateVar; 71 | } 72 | }; 73 | -------------------------------------------------------------------------------- /src/lib/operations/branch.js: -------------------------------------------------------------------------------- 1 | import {REG} from "../syntax/registers.js"; 2 | import state from "../state.js"; 3 | import {asm, asmBranch, asmNOP} from "../intsructions/asmWriter.js"; 4 | import {isSigned, u32InS16Range} from "../dataTypes/dataTypes.js"; 5 | import opsScalar from "./scalar.js"; 6 | 7 | // "beq", "bne", "bgezal", "bltzal", "bgez", "bltz" 8 | const BRANCH_INVERT = { 9 | "beq": "bne", 10 | "bne": "beq", 11 | "bgezal": "bltzal", 12 | "bltzal": "bgezal", 13 | "bgez": "bltz", 14 | "bltz": "bgez", 15 | "blez": "bgtz", 16 | "bgtz": "blez", 17 | }; 18 | 19 | const ZERO_COMB_BRANCH = { 20 | "<": "bltz", 21 | "<=": "blez", 22 | ">": "bgtz", 23 | ">=": "bgez", 24 | }; 25 | 26 | export function invertBranchOp(op) { 27 | if(!BRANCH_INVERT[op])state.throwError("Cannot invert branch operation: " + op); 28 | return BRANCH_INVERT[op]; 29 | } 30 | 31 | /** 32 | * Creates a branch instruction with a comparison against a register or immediate. 33 | * Immediate values are loaded into a register first if necessary. 34 | * This will only jump if the comparison fails, so the "true" case is expected to follow the branch. 35 | * 36 | * @param {ASTCompare} compare operation and left/right values 37 | * @param labelElse label to jump to if the comparison fails (aka "else") 38 | * @returns {ASM[]} list of ASM instructions 39 | */ 40 | export function opBranch(compare, labelElse, invert = false) 41 | { 42 | compare = structuredClone(compare); // gets modified 43 | 44 | let isImmediate = compare.right.type === "num"; 45 | let regTestRes = isImmediate ? REG.AT 46 | : state.getRequiredVar(compare.right.value, "compare").reg; 47 | 48 | // use register for zero-checks (avoids potential imm. load instructions) 49 | if(isImmediate && compare.right.value === 0) { 50 | isImmediate = false; 51 | regTestRes = REG.ZERO; 52 | } 53 | 54 | let {reg: regLeft, type: baseType} = state.getRequiredVar(compare.left.value, "left"); 55 | 56 | // Easy case, just compare 57 | if(compare.op === "==" || compare.op === "!=") 58 | { 59 | let opBranch = compare.op === "==" ? "bne" : "beq"; 60 | if(invert)opBranch = invertBranchOp(opBranch); 61 | 62 | return [ 63 | ...(isImmediate ? opsScalar.loadImmediate(REG.AT, compare.right.value) : []), 64 | asmBranch(opBranch, [regLeft, regTestRes, labelElse], labelElse), 65 | asmNOP(), 66 | ]; 67 | } 68 | 69 | const opsLoad = []; 70 | let regOrValRight = isImmediate ? compare.right.value : regTestRes; 71 | let isZeroCompare = !isImmediate && regTestRes === REG.ZERO; 72 | 73 | 74 | if(isZeroCompare) { 75 | let op = ZERO_COMB_BRANCH[compare.op]; 76 | if(!invert)op = invertBranchOp(op); 77 | 78 | return [ 79 | asmBranch(op, [regLeft, labelElse], labelElse), // jump if "<" fails (aka ">=") 80 | asmNOP(), 81 | ]; 82 | } 83 | 84 | // Both ">" and "<=" are causing the biggest issues when inverted, so map them to the other two 85 | if(compare.op === ">" || compare.op === "<=") { 86 | if(isImmediate) { 87 | // add one to the immediate to add/remove the "=" part of the comparison. 88 | // Ignore overflows here (e.g. "x > 0xFFFFFFFF" would be stupid anyway) 89 | regOrValRight = (regOrValRight+1) >>> 0; 90 | compare.op = compare.op === ">" ? ">=" : "<"; 91 | } else { 92 | compare.op = compare.op === ">" ? "<" : ">="; // invert comparison ... 93 | [regLeft, regOrValRight] = [regOrValRight, regLeft]; //... and swap registers 94 | } 95 | } 96 | 97 | // All values from now on need signedness checks, first check if it can still be an immediate 98 | if(isImmediate && !u32InS16Range(regOrValRight >>> 0)) { 99 | // ...if it doesn't we load it and switch back to a register check 100 | opsLoad.push(...opsScalar.loadImmediate(REG.AT, regOrValRight)); 101 | isImmediate = false; 102 | regOrValRight = REG.AT; 103 | } 104 | 105 | const signed = isSigned(baseType); // Note: both the signed/unsigned 'slt' have a sign-extended immediate 106 | const opLessThan = "slt" + (isImmediate ? "i" : "") + (signed ? "" : "u"); 107 | 108 | if(compare.op === "<" || compare.op === ">=") 109 | { 110 | let opBranch = compare.op === "<" ? "beq" : "bne"; 111 | if(invert)opBranch = invertBranchOp(opBranch); 112 | 113 | return [ 114 | ...opsLoad, 115 | asm(opLessThan, [REG.AT, regLeft, regOrValRight]), 116 | asmBranch(opBranch, [REG.AT, REG.ZERO, labelElse], labelElse), // jump if "<" fails (aka ">=") 117 | asmNOP(), 118 | ]; 119 | } 120 | 121 | return state.throwError("Unknown comparison operator: " + compare.op, compare); 122 | } -------------------------------------------------------------------------------- /src/lib/operations/userFunction.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @copyright 2023 - Max Bebök 3 | * @license Apache-2.0 4 | */ 5 | import state from "../state.js"; 6 | import {asm, asmFunction, asmNOP} from "../intsructions/asmWriter.js"; 7 | import opsScalar from "./scalar.js"; 8 | 9 | /** 10 | * @param {string} name 11 | * @param {ASTFuncArg[]} args 12 | * @returns {ASM[]} 13 | */ 14 | export function callUserFunction(name, args) 15 | { 16 | let userFunc = state.getFunction(name); 17 | if(!userFunc) { 18 | if(!state.varExists(name)) { 19 | state.throwError("Function "+name+" not known!"); 20 | } 21 | userFunc = { 22 | name: state.getVarReg(name), 23 | args: [], 24 | isRelative: false 25 | }; 26 | } 27 | 28 | const res = []; 29 | 30 | if(userFunc.args.length !== args.length) { 31 | state.throwError(`Function ${name} expects ${userFunc.args.length} arguments, got ${args.length}!`, args); 32 | } 33 | 34 | for(let i = 0; i < args.length; ++i) { 35 | const argUser = args[i]; 36 | const argDef = userFunc.args[i]; 37 | if(argUser.type === "num") { 38 | res.push(...opsScalar.loadImmediate(argDef.reg, argUser.value)); 39 | } else { 40 | const argVar = state.getRequiredVar(argUser.value, "arg" + i); 41 | if(argVar.type !== argDef.type) { 42 | state.throwError(`Function ${name} expects argument ${i} to be of type ${argDef.type}, got ${argVar.type}!`, argUser); 43 | } 44 | if(argVar.reg !== argDef.reg) { 45 | state.throwError(`Function ${name} expects argument ${i} to be in register ${argDef.reg}, got ${argVar.reg}!`, argUser); 46 | } 47 | } 48 | } 49 | let isRelative = state.getAnnotations("Relative").length > 0; 50 | if(userFunc.isRelative) { 51 | isRelative = true; 52 | } 53 | 54 | const regsArg = userFunc.args.map(arg => arg.reg); 55 | res.push(asmFunction(userFunc.name, regsArg, isRelative), asmNOP()); 56 | return res; 57 | } -------------------------------------------------------------------------------- /src/lib/optimizer/eval/evalCost.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @copyright 2023 - Max Bebök 3 | * @license Apache-2.0 4 | */ 5 | 6 | import {ASM_TYPE, asmNOP} from "../../intsructions/asmWriter.js"; 7 | import { 8 | OP_FLAG_CTC2_CFC2, 9 | OP_FLAG_IS_BRANCH, OP_FLAG_IS_LIKELY, 10 | OP_FLAG_IS_MEM_STALL_LOAD, 11 | OP_FLAG_IS_MEM_STALL_STORE, 12 | OP_FLAG_IS_VECTOR, OP_FLAG_LIKELY_BRANCH 13 | } from "../asmScanDeps.js"; 14 | 15 | // Reference/Docs: https://n64brew.dev/wiki/Reality_Signal_Processor/CPU_Pipeline 16 | 17 | const BRANCH_STEP_NONE = 0; 18 | const BRANCH_STEP_BRANCH = 2; 19 | const BRANCH_STEP_DELAY = 1; 20 | 21 | /** 22 | * Evaluates the cost of a given function. 23 | * This performs cycle counting and calculates an overall score intended to compare optimizations. 24 | * Since it evaluates all possible branches, it will not reflect the real-world cycle count. 25 | * @param {ASMFunc} asmFunc 26 | * @return {number} score 27 | */ 28 | export function evalFunctionCost(asmFunc) 29 | { 30 | let execCount = 0; 31 | 32 | let branchStep = 0; 33 | let lastLoadPosMask = 0; 34 | let regCycleMap = []; // register to stall counter 35 | 36 | for(let i=0; i<64; ++i) { 37 | regCycleMap[i] = 0; 38 | } 39 | 40 | let didJump = false; 41 | let cycle = 0; 42 | let pc = 0; 43 | /** @type {ASM[]} */ 44 | let ops = asmFunc.asm.filter(asm => asm.type === ASM_TYPE.OP); 45 | 46 | const ticks = (count) => { 47 | for(let i=0; i<64; ++i)regCycleMap[i] -= count; 48 | lastLoadPosMask = lastLoadPosMask >>> count; 49 | cycle += count; 50 | }; 51 | 52 | while(pc < ops.length) 53 | { 54 | let lastCycle; 55 | do { 56 | lastCycle = cycle; 57 | for(let i=0; i 0)ticks(regCycleMap[regSrc]); 64 | } 65 | // if a store happens exactly two cycles after a load, stall it 66 | if(lastLoadPosMask & 0b001 && (execOp.opFlags & OP_FLAG_IS_MEM_STALL_STORE)) { 67 | ++execOp.debug.stall; 68 | ticks(1); 69 | } 70 | } 71 | } while (lastCycle !== cycle); // run until all stalls are resolved 72 | 73 | // now execute 74 | for(let i=0; i>>= 1; 81 | if(!branchStep && (execOp.opFlags & OP_FLAG_IS_BRANCH) !== 0)branchStep = BRANCH_STEP_BRANCH; 82 | 83 | // if branch is taken, an unavoidable bubble is inserted 84 | if(didJump && branchStep === BRANCH_STEP_DELAY) { 85 | ticks(1); didJump = false; 86 | } 87 | 88 | // now "execute" by marking the target regs with stalls 89 | execOp.debug.cycle = cycle; 90 | for(const regDst of execOp.depsStallTargetIdx) { 91 | regCycleMap[regDst] = execOp.stallLatency; 92 | } 93 | } 94 | 95 | // fetch next instructions to execute 96 | pc += execCount; 97 | const op = ops[pc]; 98 | const opNext = ops[pc+1]; 99 | 100 | // now check if we can dual-issue, this is always checked from the perspective of the current instruction 101 | // seeing if the next one can be used 102 | let canDualIssue = opNext // needs something after to dual issue 103 | && (op.opFlags & OP_FLAG_IS_VECTOR) !== (opNext.opFlags & OP_FLAG_IS_VECTOR) // can only do SU/VU or VU/SU 104 | && !(branchStep === BRANCH_STEP_BRANCH) // cannot dual issue if in delay slot 105 | && !(op.opFlags & OP_FLAG_IS_BRANCH) // cannot dual issue with the delay slot 106 | // if a vector reg is written to, and the next instr. reads from it, do not dual issue 107 | && (op.depsStallTargetMask0 & opNext.depsStallSourceMask0) === 0 108 | && (op.depsStallTargetMask1 & opNext.depsStallSourceMask1) === 0 109 | && (op.depsStallTargetMask0 & opNext.depsStallTargetMask0) === 0 110 | && (op.depsStallTargetMask1 & opNext.depsStallTargetMask1) === 0; 111 | 112 | if(canDualIssue) { 113 | let ctrlRWMask = opNext.depsSourceMask | opNext.depsTargetMask; // incl. control regs here as an exception 114 | if((opNext.opFlags & OP_FLAG_CTC2_CFC2) && (op.depsTargetMask & ctrlRWMask) !== 0n) { 115 | canDualIssue = false; 116 | } 117 | } 118 | 119 | execCount = canDualIssue ? 2 : 1; 120 | ticks(1); 121 | } 122 | return cycle; 123 | } -------------------------------------------------------------------------------- /src/lib/optimizer/pattern/assertCompare.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @copyright 2023 - Max Bebök 3 | * @license Apache-2.0 4 | */ 5 | 6 | import {ASM_TYPE} from "../../intsructions/asmWriter.js"; 7 | import {BRANCH_OPS, OP_FLAG_IS_BRANCH, OP_FLAG_IS_NOP} from "../asmScanDeps.js"; 8 | import {REG} from "../../syntax/registers.js"; 9 | import {invertBranchOp} from "../../operations/branch.js"; 10 | import {LABEL_ASSERT} from "../../builtins/libdragon.js"; 11 | 12 | /** 13 | * Asserts can be inside an if-condition. 14 | * This will lead to bad code since the inner content will set $at 15 | * preventing the branches to be merged. 16 | * 17 | * E.g.: "if(x > 4)assert(0xAB);" 18 | * becomes: 19 | * sltiu $at, $t0, 5 20 | * bne $at, $zero, LABEL_a_0001 21 | * nop 22 | * lui $at, 171 23 | * j assertion_failed 24 | * nop 25 | * LABEL_a_0001: 26 | * 27 | * But it should be merged into a single branch, pulling thr $at outside 28 | * and inverting the branch 29 | * 30 | * e.g.: 31 | * lui $at, 171 32 | * beq $at, $zero, assertion_failed 33 | * sltiu $at, $t0, 5 34 | * 35 | * 36 | * @param {ASMFunc} asmFunc 37 | */ 38 | export function assertCompare(asmFunc) 39 | { 40 | const asm = asmFunc.asm; 41 | for(let i=0; i BRANCH_OPS.includes(l.op) && l.args[l.args.length-1] === labelTemp); 50 | 51 | // if it was a jal, we need to manually assign the return register 52 | if(jumpOp === "jal") { 53 | lines.splice(i+2, 2); // remove jump, delay-slot 54 | 55 | if(!line.labelEnd)state.throwError("Missing labelEnd for branch, this is a bug in RSPL, please let me know"); 56 | lines.splice(i, 0, asm("ori", [REG.RA, REG.ZERO, line.labelEnd])); 57 | } else { 58 | if(labelUsed) { 59 | lines.splice(i+2, 2); // remove jump, delay-slot 60 | } else { 61 | lines.splice(i+2, 3); // remove jump, delay-slot, temp-label 62 | } 63 | } 64 | 65 | i-=2; 66 | } 67 | } 68 | 69 | } 70 | } -------------------------------------------------------------------------------- /src/lib/optimizer/pattern/commandAlias.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @copyright 2024 - Max Bebök 3 | * @license Apache-2.0 4 | */ 5 | 6 | import {ASM_TYPE} from "../../intsructions/asmWriter.js"; 7 | import {BRANCH_OPS, OP_FLAG_IS_NOP} from "../asmScanDeps.js"; 8 | 9 | /** 10 | * If a command only calls another function, we can simplify use that in the command table. 11 | * The command function can then be ignored. 12 | * 13 | * E.g: 14 | * T3DCmd_TriSync: 15 | * j RDPQ_Triangle_Send_End 16 | * nop 17 | * 18 | * Here we can remove it completely and put 'RDPQ_Triangle_Send_End' in the cmd. table 19 | * 20 | * @param {ASMFunc} asmFunc 21 | */ 22 | export function commandAlias(asmFunc) 23 | { 24 | const lines = asmFunc.asm; 25 | if(lines.length < 2 || asmFunc.type !== "command")return; 26 | 27 | const safeBranch = ["beq", "bne", "j", "jr"]; 28 | if(safeBranch.includes(lines[0].op) && (lines[1].opFlags & OP_FLAG_IS_NOP)) { 29 | asmFunc.nameOverride = lines[0].args[0]; 30 | 31 | if(lines.length == 2) { 32 | asmFunc.asm = []; // if nothing comes after, we can clear the function 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /src/lib/optimizer/pattern/dedupeImm.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @copyright 2023 - Max Bebök 3 | * @license Apache-2.0 4 | */ 5 | 6 | import {ASM_TYPE} from "../../intsructions/asmWriter.js"; 7 | import {REG} from "../../syntax/registers.js"; 8 | import {BRANCH_OPS, getTargetRegs} from "../asmScanDeps.js"; 9 | 10 | /** 11 | * De-duplicates immediate loads and labels 12 | * @param {ASMFunc} asmFunc 13 | */ 14 | export function dedupeImmediate(asmFunc) 15 | { 16 | let lastAT = undefined; 17 | // ...now keep the first one, remove the others and patch the references 18 | const asmNew = []; 19 | for(const asm of asmFunc.asm) 20 | { 21 | let keep = true; 22 | if(asm.type === ASM_TYPE.OP) 23 | { 24 | if(BRANCH_OPS.includes(asm.op))lastAT = undefined; 25 | 26 | const targetRegs = getTargetRegs(asm); 27 | if(targetRegs.includes(REG.AT)) 28 | { 29 | let newAT = undefined; 30 | // only handle "ori" for now, other instructions are assumed to set it in unknown ways 31 | if(asm.op === "ori") { 32 | newAT = asm.args[2]; 33 | if(lastAT === newAT) { 34 | keep = false; 35 | } 36 | } 37 | lastAT = newAT; 38 | } 39 | } else { 40 | lastAT = undefined; 41 | } 42 | 43 | if(keep)asmNew.push(asm); 44 | } 45 | asmFunc.asm = asmNew; 46 | } -------------------------------------------------------------------------------- /src/lib/optimizer/pattern/dedupeJumps.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @copyright 2023 - Max Bebök 3 | * @license Apache-2.0 4 | */ 5 | 6 | import {ASM_TYPE} from "../../intsructions/asmWriter.js"; 7 | import {BRANCH_OPS} from "../asmScanDeps.js"; 8 | 9 | /** 10 | * De-duplicates Jumps 11 | * This simplifies a chain of direct jumps (e.g. nested ifs) to a single jump. 12 | * If possible, the original jump label is even deleted. 13 | * @param {ASMFunc} asmFunc 14 | */ 15 | export function dedupeJumps(asmFunc) 16 | { 17 | // check for jumps/branches to labels that directly point to a jump itself 18 | const labelReplace = []; 19 | for(let i=0; i 1) { 27 | const newLabel = labels.pop(); 28 | labelsDelete.push(...labels); 29 | for(const label of labels)labelsReplace[label] = [newLabel]; 30 | } 31 | labels = []; 32 | } 33 | } 34 | 35 | // ...now keep the first one, remove the others and patch the references 36 | const asmNew = []; 37 | for(const asm of asmFunc.asm) 38 | { 39 | if(labelsDelete.includes(asm.label))continue; 40 | if(BRANCH_OPS.includes(asm.op)) { 41 | const label = asm.args[asm.args.length-1]; 42 | if(labelsReplace[label]) { 43 | asm.args[asm.args.length-1] = labelsReplace[label][0]; 44 | } 45 | } 46 | asmNew.push(asm); 47 | } 48 | asmFunc.asm = asmNew; 49 | } -------------------------------------------------------------------------------- /src/lib/optimizer/pattern/mergeSequence.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @copyright 2023 - Max Bebök 3 | * @license Apache-2.0 4 | */ 5 | 6 | import {ASM_TYPE} from "../../intsructions/asmWriter.js"; 7 | import {BRANCH_OPS} from "../asmScanDeps.js"; 8 | import {REG} from "../../syntax/registers.js"; 9 | 10 | /** 11 | * Sequence merger, simplifies redundant sequences & sequences that can be overlapped. 12 | * @param {ASMFunc} asmFunc 13 | */ 14 | export function mergeSequence(asmFunc) 15 | { 16 | const lines = asmFunc.asm; 17 | for(let i=0; i=0; --i) 39 | { 40 | const asm = lines[i]; 41 | 42 | if(["j", "jr"].includes(asm.op)) { 43 | lastSafeIndex = i; // incl. delay-slot 44 | break; 45 | } 46 | if(asm.opFlags & OP_FLAG_IS_NOP)continue; 47 | break; 48 | } 49 | 50 | if(lastSafeIndex < 0)return; 51 | lines.splice(lastSafeIndex+2); 52 | } -------------------------------------------------------------------------------- /src/lib/optimizer/pattern/tailCall.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @copyright 2024 - Max Bebök 3 | * @license Apache-2.0 4 | */ 5 | import {OP_FLAG_IS_NOP} from "../asmScanDeps.js"; 6 | 7 | const KNOWN_END_JUMPS = ["RSPQ_Loop"]; 8 | 9 | /** 10 | * If a return to the main loop happens right after a function call, 11 | * we can instead do a jump instead of a jump+link, removing the need for a return. 12 | * 13 | * E.g. 14 | * jal DMAExec; nop; 15 | * j RSPQ_Loop; nop; 16 | * 17 | * Can be turned into: 18 | * j DMAExec; nop; 19 | * 20 | * @param {ASMFunc} asmFunc 21 | */ 22 | export function tailCall(asmFunc) 23 | { 24 | if(asmFunc.type !== "command")return; // only works when one level deep 25 | const lines = asmFunc.asm; 26 | for(let i=0; i { 12 | const newlineCount = match.split('\n').length - 1; 13 | return '\n'.repeat(newlineCount); 14 | }); 15 | } 16 | 17 | /** 18 | * Runs a C-like preprocess on the source code. 19 | * This handles constants as well as ifdefs. 20 | * @param src input source code 21 | * @param defines this function will append all defines to this object 22 | * @param {(string) => string} fileLoader function to load included files 23 | * @return {string} processed source code 24 | */ 25 | export function preprocess(src, defines = {}, fileLoader = undefined) 26 | { 27 | const lines = src.split("\n"); 28 | 29 | let srcRes = ""; 30 | let insideIfdef = false; 31 | let ignoreLine = false; 32 | 33 | for (let i = 0; i < lines.length; i++) 34 | { 35 | let line = lines[i]; 36 | const lineTrimmed = line.trim(); 37 | let newLine = ""; 38 | state.func = "preprocessor"; 39 | state.line = i + 1; 40 | 41 | if (!ignoreLine && lineTrimmed.startsWith("#define")) 42 | { 43 | const parts = lineTrimmed.match(/#define\s+([a-zA-Z0-9_]+)\s+(.*)/); 44 | if(!parts)throw new Error(`Line ${i+1}: Invalid #define statement!`); 45 | let [_, name, value] = parts; 46 | 47 | for (const data of Object.values(defines)) { 48 | value = value.replace(data.regex, data.value); 49 | } 50 | 51 | defines[name] = { 52 | regex: new RegExp(`\\b${name}\\b`, "g"), 53 | value 54 | }; 55 | } 56 | else if (!ignoreLine && lineTrimmed.startsWith("#undef")) 57 | { 58 | const parts = lineTrimmed.match(/#undef\s+([a-zA-Z0-9_]+)/); 59 | if(!parts)throw new Error(`Line ${i+1}: Invalid #undef statement!`); 60 | const [_, name] = parts; 61 | 62 | delete defines[name]; 63 | } 64 | else if (lineTrimmed.startsWith("#ifdef") || lineTrimmed.startsWith("#ifndef")) 65 | { 66 | if(insideIfdef)throw new Error(`Line ${i+1}: Nested #ifdef statements are not allowed!`); 67 | insideIfdef = true; 68 | const parts = lineTrimmed.match(/#ifn?def\s+([a-zA-Z0-9_]+)/); 69 | if(!parts)throw new Error(`Line ${i+1}: Invalid #ifdef statement!`); 70 | const [_, name] = parts; 71 | 72 | const negate = lineTrimmed.startsWith("#ifdef"); 73 | ignoreLine = negate ? !defines[name] : defines[name]; 74 | } 75 | else if (lineTrimmed.startsWith("#else")) { 76 | ignoreLine = insideIfdef && !ignoreLine; 77 | } 78 | else if (lineTrimmed.startsWith("#endif")) { 79 | insideIfdef = false; 80 | ignoreLine = false; 81 | } 82 | else if (!ignoreLine && lineTrimmed.startsWith("#include")) 83 | { 84 | if(!fileLoader) { 85 | state.throwError(`Line ${i+1}: #include statement requires a fileLoader function!`); 86 | } 87 | const filePath = lineTrimmed.match(/#include\s+"(.*)"/)[1]; 88 | const fileContent = fileLoader(filePath); 89 | srcRes += preprocess(stripComments(fileContent), defines, fileLoader); 90 | 91 | } else if(!ignoreLine) { 92 | // replace all defines 93 | newLine = line; 94 | for (const data of Object.values(defines)) { 95 | newLine = newLine.replace(data.regex, data.value); 96 | } 97 | } 98 | 99 | srcRes += newLine + "\n"; 100 | } 101 | 102 | return srcRes; 103 | } 104 | -------------------------------------------------------------------------------- /src/lib/syntax/annotations.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @copyright 2024 - Max Bebök 3 | * @license Apache-2.0 4 | */ 5 | import state from "../state.js"; 6 | 7 | export const ANNOTATIONS = { 8 | Barrier: "Barrier", 9 | Relative: "Relative", 10 | Align: "Align", 11 | NoReturn: "NoReturn", 12 | Unlikely: "Unlikely", 13 | NoRegAlloc: "NoRegAlloc", 14 | }; 15 | 16 | export const KNOWN_ANNOTATIONS = Object.keys(ANNOTATIONS); 17 | 18 | /** 19 | * Checks if an annotation is known and valid. 20 | * @param {Annotation} anno 21 | * @throws {Error} if invalid 22 | */ 23 | export function validateAnnotation(anno) { 24 | if(!KNOWN_ANNOTATIONS.includes(anno.name)) { 25 | state.throwError("Unknown annotation '"+anno.name+"'!\nExpected on of: "+KNOWN_ANNOTATIONS.join(", ")+""); 26 | } 27 | 28 | // string annotations 29 | if(["Barrier"].includes(anno.name)) { 30 | if(typeof anno.value !== "string") { 31 | state.throwError("Annotation '"+anno.name+"' expects a string value!"); 32 | } 33 | } 34 | } 35 | 36 | export function getAnnotationVal(annotations, name) { 37 | const anno = annotations.find(anno => anno.name === name); 38 | return anno ? (anno.value || true) : undefined; 39 | } -------------------------------------------------------------------------------- /src/lib/syntax/registers.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @copyright 2023 - Max Bebök 3 | * @license Apache-2.0 4 | */ 5 | import {isTwoRegType} from "../dataTypes/dataTypes.js"; 6 | 7 | export const REG = { 8 | AT: "$at", ZERO: "$zero", 9 | V0: "$v0", V1: "$v1", 10 | A0: "$a0", A1: "$a1", A2: "$a2", A3: "$a3", 11 | T0: "$t0", T1: "$t1", T2: "$t2", T3: "$t3", T4: "$t4", T5: "$t5", T6: "$t6", T7: "$t7", T8: "$t8", T9: "$t9", 12 | S0: "$s0", S1: "$s1", S2: "$s2", S3: "$s3", S4: "$s4", S5: "$s5", S6: "$s6", S7: "$s7", 13 | K0: "$k0", K1: "$k1", 14 | GP: "$gp", SP: "$sp", FP: "$fp", RA: "$ra", 15 | V00: "$v00", V01: "$v01", V02: "$v02", V03: "$v03", V04: "$v04", V05: "$v05", V06: "$v06", V07: "$v07", 16 | V08: "$v08", V09: "$v09", V10: "$v10", V11: "$v11", V12: "$v12", V13: "$v13", V14: "$v14", V15: "$v15", 17 | V16: "$v16", V17: "$v17", V18: "$v18", V19: "$v19", V20: "$v20", V21: "$v21", V22: "$v22", V23: "$v23", 18 | V24: "$v24", V25: "$v25", V26: "$v26", V27: "$v27", V28: "$v28", V29: "$v29", V30: "$v30", V31: "$v31", 19 | 20 | VZERO: "$v00", 21 | //VTEMP0: "$v27", 22 | //VTEMP1: "$v28", 23 | VTEMP0: "$v29", 24 | VSHIFT: "$v30", 25 | VSHIFT8: "$v31", 26 | }; 27 | 28 | export const REG_COP0 = { 29 | DMA_BUSY: "COP0_DMA_BUSY", 30 | DP_START: "COP0_DP_START", 31 | DP_END: "COP0_DP_END", 32 | DP_CURRENT: "COP0_DP_CURRENT", 33 | DP_CLOCK: "COP0_DP_CLOCK", 34 | DMA_SPADDR: "COP0_DMA_SPADDR", 35 | DMA_RAMADDR: "COP0_DMA_RAMADDR", 36 | DMA_READ: "COP0_DMA_READ", 37 | DMA_WRITE: "COP0_DMA_WRITE", 38 | SP_STATUS: "COP0_SP_STATUS", 39 | DMA_FULL: "COP0_DMA_FULL", 40 | }; 41 | 42 | export const REG_COP2 = { 43 | VCO: "$vc0", 44 | VCC: "$vcc", 45 | VCE: "$vce", 46 | ACC_MD: "COP2_ACC_MD", 47 | ACC_HI: "COP2_ACC_HI", 48 | ACC_LO: "COP2_ACC_LO", 49 | }; 50 | 51 | // MIPS scalar register in correct order ($0 - $31) 52 | export const REGS_SCALAR = [ 53 | "$zero", "$at", "$v0", "$v1", "$a0", "$a1", "$a2", "$a3", 54 | "$t0", "$t1", "$t2", "$t3", "$t4", "$t5", "$t6", "$t7", 55 | "$s0", "$s1", "$s2", "$s3", "$s4", "$s5", "$s6", "$s7", 56 | "$t8", "$t9", 57 | "$k0", "$k1", "$gp", "$sp", "$fp", "$ra", 58 | ]; 59 | 60 | // MIPS vector register in correct order ($v00 - $v31) 61 | export const REGS_VECTOR = [ 62 | "$v00", "$v01", "$v02", "$v03", "$v04", "$v05", "$v06", "$v07", 63 | "$v08", "$v09", "$v10", "$v11", "$v12", "$v13", "$v14", "$v15", 64 | "$v16", "$v17", "$v18", "$v19", "$v20", "$v21", "$v22", "$v23", 65 | "$v24", "$v25", "$v26", "$v27", "$v28", "$v29", "$v30", "$v31", 66 | ]; 67 | 68 | // MIPS scalar register in allocation order for variables, excludes reserved regs 69 | export const REGS_ALLOC_SCALAR = [ 70 | "$t0", "$t1", "$t2", "$t3", "$t4", "$t5", "$t6", "$t7", "$t8", "$t9", 71 | "$k0", "$k1", "$sp", "$fp", 72 | "$s0", "$s1", "$s2", "$s3", "$s4", "$s5", "$s6", "$s7", 73 | ]; 74 | 75 | // MIPS vector register in allocation order for variables, excludes reserved regs 76 | export const REGS_ALLOC_VECTOR = [ 77 | "$v01", "$v02", "$v03", "$v04", "$v05", "$v06", "$v07", 78 | "$v08", "$v09", "$v10", "$v11", "$v12", "$v13", "$v14", "$v15", 79 | "$v16", "$v17", "$v18", "$v19", "$v20", "$v21", "$v22", "$v23", 80 | "$v24", "$v25", "$v26", "$v27", "$v28" 81 | ]; 82 | 83 | export const REGS_FORBIDDEN = [ 84 | REG.AT, REG.GP, 85 | REG.VTEMP0, 86 | // REG.VTEMP1, REG.VTEMP2 87 | ]; 88 | 89 | export const LABELS = { 90 | RSPQ_SCRATCH_MEM: "RSPQ_SCRATCH_MEM", 91 | }; 92 | 93 | /** 94 | * @param {string} regName 95 | * @returns {boolean} 96 | */ 97 | export function isVecReg(regName) { 98 | return REGS_VECTOR.includes(regName); 99 | } 100 | 101 | /** 102 | * @param {string} regName 103 | * @returns {string|null} 104 | */ 105 | export function nextVecReg(regName) { 106 | const idx = REGS_VECTOR.indexOf(regName); 107 | return idx === -1 ? null : REGS_VECTOR[idx+1]; 108 | } 109 | 110 | /** 111 | * @param {string} regName 112 | * @param {number} offset 113 | * @returns {string|null} 114 | */ 115 | export function nextReg(regName, offset = 1) { 116 | let idx = REGS_VECTOR.indexOf(regName); 117 | if(idx >= 0) { 118 | return REGS_VECTOR[idx+offset]; 119 | } 120 | idx = REGS_SCALAR.indexOf(regName); 121 | if(idx >= 0) { 122 | return REGS_SCALAR[idx+offset]; 123 | } 124 | return null; 125 | } 126 | 127 | /** 128 | * @param {ASTFuncArg} varRef 129 | * @returns {*} 130 | */ 131 | export function intReg(varRef) { 132 | return varRef.reg; 133 | } 134 | 135 | /** 136 | * @param {ASTFuncArg} varRef 137 | * @returns {*} 138 | */ 139 | export function fractReg(varRef) { 140 | return varRef.type === "vec32" ? nextVecReg(varRef.reg) : REG.VZERO; 141 | } 142 | 143 | /** 144 | * Returns 2 registers for all vector types, defaults to $zero. 145 | * This is used to handle vectors with casts (e.g. vec32 to vec32:ufract) 146 | * @param {ASTFuncArg} varRef 147 | * @returns {[string, string]} 148 | */ 149 | export function getVec32Regs(varRef) { 150 | if(varRef.type === "vec32") { 151 | return [varRef.reg, nextVecReg(varRef.reg)]; 152 | } 153 | if(varRef.castType === "ufract" || varRef.castType === "sfract") { 154 | return [REG.VZERO, varRef.reg]; 155 | } 156 | return [varRef.reg, REG.VZERO]; 157 | } 158 | 159 | /** 160 | * Shorthand for getVec32Regs() in the context of assigment-calculations. 161 | * Meaning the form: varRes = varLeft op varRight. 162 | * This correctly handles casted vec32 L/R vars with an assignment to a vec16 var. 163 | * @param varRes 164 | * @param varLeft 165 | * @param varRight 166 | * @return {[string,string][]} 167 | */ 168 | export function getVec32RegsResLR(varRes, varLeft, varRight) { 169 | const regsDst = getVec32Regs(varRes); 170 | const regsL = getVec32Regs(varLeft); 171 | const regsR = getVec32Regs(varRight); 172 | if(!isTwoRegType(varRes.type)) { 173 | regsL[0] = varLeft.reg; 174 | regsR[0] = varRight.reg; 175 | } 176 | return [regsDst, regsL, regsR]; 177 | } -------------------------------------------------------------------------------- /src/lib/syntax/swizzle.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @copyright 2023 - Max Bebök 3 | * @license Apache-2.0 4 | */ 5 | import {REG} from "./registers.js"; 6 | 7 | export const SWIZZLE_MAP = { 8 | "": ".v", 9 | "xyzwXYZW": ".v", 10 | "xxzzXXZZ": ".q0", 11 | "yywwYYWW": ".q1", 12 | "xxxxXXXX": ".h0", 13 | "yyyyYYYY": ".h1", 14 | "zzzzZZZZ": ".h2", 15 | "wwwwWWWW": ".h3", 16 | "xxxxxxxx": ".e0", 17 | "yyyyyyyy": ".e1", 18 | "zzzzzzzz": ".e2", 19 | "wwwwwwww": ".e3", 20 | "XXXXXXXX": ".e4", 21 | "YYYYYYYY": ".e5", 22 | "ZZZZZZZZ": ".e6", 23 | "WWWWWWWW": ".e7", 24 | 25 | "x": ".e0", 26 | "y": ".e1", 27 | "z": ".e2", 28 | "w": ".e3", 29 | "X": ".e4", 30 | "Y": ".e5", 31 | "Z": ".e6", 32 | "W": ".e7", 33 | }; 34 | 35 | export const SWIZZLE_LANE_MAP = { 36 | "v" : [0, 1, 2, 3, 4, 5, 6, 7], 37 | "q0": [0, 2, 4, 6], 38 | "q1": [1, 3, 5, 7], 39 | "h0": [0, 4], 40 | "h1": [1, 5], 41 | "h2": [2, 6], 42 | "h3": [3, 7], 43 | "e0": [0], 44 | "e1": [1], 45 | "e2": [2], 46 | "e3": [3], 47 | "e4": [4], 48 | "e5": [5], 49 | "e6": [6], 50 | "e7": [7], 51 | }; 52 | 53 | export const SWIZZLE_MAP_KEYS_STR = Object.keys(SWIZZLE_MAP).filter(Boolean).join(", "); 54 | 55 | export const SWIZZLE_SCALAR_IDX = { 56 | "x": 0, "y": 1, "z": 2, "w": 3, 57 | "X": 4, "Y": 5, "Z": 6, "W": 7, 58 | }; 59 | 60 | export const POW2_SWIZZLE_VAR = { 61 | 0: {type: 'vec16', reg: REG.VZERO, swizzle: 'x'}, 62 | 1: {type: 'vec16', reg: REG.VSHIFT, swizzle: 'W'}, 63 | 2: {type: 'vec16', reg: REG.VSHIFT, swizzle: 'Z'}, 64 | 4: {type: 'vec16', reg: REG.VSHIFT, swizzle: 'Y'}, 65 | 8: {type: 'vec16', reg: REG.VSHIFT, swizzle: 'X'}, 66 | 16: {type: 'vec16', reg: REG.VSHIFT, swizzle: 'w'}, 67 | 32: {type: 'vec16', reg: REG.VSHIFT, swizzle: 'z'}, 68 | 64: {type: 'vec16', reg: REG.VSHIFT, swizzle: 'y'}, 69 | 128: {type: 'vec16', reg: REG.VSHIFT, swizzle: 'x'}, 70 | 256: {type: 'vec16', reg: REG.VSHIFT8, swizzle: 'W'}, 71 | 512: {type: 'vec16', reg: REG.VSHIFT8, swizzle: 'Z'}, 72 | 1024: {type: 'vec16', reg: REG.VSHIFT8, swizzle: 'Y'}, 73 | 2048: {type: 'vec16', reg: REG.VSHIFT8, swizzle: 'X'}, 74 | 4096: {type: 'vec16', reg: REG.VSHIFT8, swizzle: 'w'}, 75 | 8192: {type: 'vec16', reg: REG.VSHIFT8, swizzle: 'z'}, 76 | 16384: {type: 'vec16', reg: REG.VSHIFT8, swizzle: 'y'}, 77 | 32768: {type: 'vec16', reg: REG.VSHIFT8, swizzle: 'x'}, 78 | }; 79 | 80 | /** 81 | * @param {string} swizzle 82 | * @returns {boolean} 83 | */ 84 | export const isScalarSwizzle = (swizzle) => { 85 | return swizzle.length === 1; 86 | } -------------------------------------------------------------------------------- /src/lib/transpiler.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @copyright 2023 - Max Bebök 3 | * @license Apache-2.0 4 | */ 5 | 6 | import { ast2asm } from "./ast2asm"; 7 | import { writeASM } from "./asmWriter"; 8 | import {astNormalizeFunctions} from "./astNormalize"; 9 | import nearly from "nearley"; 10 | import grammarDef from "./grammar.cjs"; 11 | import state from "./state.js"; 12 | import {normalizeASM} from "./asmNormalize.js"; 13 | import {asmOptimize, asmOptimizePattern} from "./optimizer/asmOptimizer.js"; 14 | import {asmInitDeps, asmScanDeps} from "./optimizer/asmScanDeps.js"; 15 | import {evalFunctionCost} from "./optimizer/eval/evalCost.js"; 16 | import {preprocess, stripComments} from "./preproc/preprocess.js"; 17 | import {validateMemory} from "./memory/memoryValidator.js"; 18 | 19 | const grammar = nearly.Grammar.fromCompiled(grammarDef); 20 | /** 21 | * @param {RSPLConfig} config 22 | */ 23 | function normalizeConfig(config) 24 | { 25 | if(config.rspqWrapper === undefined)config.rspqWrapper = true; 26 | if(config.optimize === undefined)config.optimize = false; 27 | if(config.patchFunctions === undefined)config.patchFunctions = []; 28 | } 29 | 30 | /** 31 | * @param {string} source 32 | * @param {RSPLConfig} config 33 | * @param {function} updateCb 34 | */ 35 | export async function transpileSource(source, config, updateCb = undefined) 36 | { 37 | source = stripComments(source); 38 | const parser = new nearly.Parser(grammar); 39 | 40 | //console.time("Preprocessor"); 41 | const defines = {}; 42 | if(config.defines) { 43 | for(const [key, value] of Object.entries(config.defines)) { 44 | defines[key] = { 45 | regex: new RegExp(`\\b${key}\\b`, "g"), 46 | value: value 47 | }; 48 | } 49 | } 50 | 51 | source = preprocess(source, defines, config.fileLoader); 52 | //console.timeEnd("Preprocessor"); 53 | 54 | state.sourceLines = source.split("\n").map((l => l.trim())); 55 | //console.time("parser"); 56 | const astList = parser.feed(source); 57 | //console.timeEnd("parser"); 58 | 59 | if(astList.results.length > 1) { 60 | throw Error("Warning: ambiguous syntax!"); 61 | } 62 | const ast = astList.results[0]; 63 | ast.defines = defines; 64 | 65 | try { 66 | return await transpile(ast, updateCb, config); 67 | } catch (e) { 68 | if(e.message.includes("Error in") && e.message.includes("line ")) { 69 | // add surrounding lines to error message 70 | const lineCount = 3; 71 | const lines = source.split("\n"); 72 | const line = parseInt(e.message.match(/line (\d+)/)?.[1]); 73 | const start = Math.max(0, line - lineCount); 74 | const end = Math.min(lines.length, line + lineCount); 75 | const context = lines.slice(start, end) 76 | .map((l, i) => 77 | `${line === (start+i+1) ? '>' : ' '}${(start + i + 1).toString().padStart(4, ' ')}: ${l}` 78 | ) 79 | .join("\n"); 80 | 81 | e.message += "\n\nSource:\n" + context + "\n"; 82 | } 83 | throw e; 84 | } 85 | } 86 | 87 | /** 88 | * @param {AST} ast 89 | * @param {function} updateCb 90 | * @param {RSPLConfig} config 91 | */ 92 | export async function transpile(ast, updateCb, config = {}) 93 | { 94 | state.reset(); 95 | normalizeConfig(config); 96 | 97 | validateMemory(ast, ast.states); 98 | 99 | ast.functions = astNormalizeFunctions(ast); 100 | const functionsAsm = ast2asm(ast); 101 | 102 | for(const func of functionsAsm) { 103 | normalizeASM(func); 104 | } 105 | 106 | let debugUnopt = {}; 107 | 108 | const generateASM = () => { 109 | const {asm, debug, sizeDMEM, sizeIMEM} = writeASM(ast, functionsAsm, config); 110 | //console.timeEnd("writeASM"); 111 | 112 | const usageImemPercent = sizeDMEM / 4096 * 100; 113 | state.logInfo(`Total state size: ${sizeDMEM} bytes (${usageImemPercent.toFixed(2)}%)`); 114 | const useageDmemPercent = sizeIMEM / 4096 * 100; 115 | state.logInfo(`Total text size: ${sizeIMEM} bytes (${useageDmemPercent.toFixed(2)}%)`); 116 | 117 | debug.lineDepMap = debugUnopt.lineDepMap; 118 | return { 119 | asm: asm.trimEnd(), 120 | asmUnoptimized, 121 | debug, 122 | warn: state.outWarn, 123 | info: state.outInfo, 124 | sizeDMEM, sizeIMEM, 125 | }; 126 | }; 127 | 128 | let asmUnoptimized = ""; 129 | if(config.optimize) 130 | { 131 | // pre-generate the first version of the ASM to get line numbers 132 | for(const func of functionsAsm) 133 | { 134 | if(config.patchFunctions.length && !config.patchFunctions.includes(func.name))continue; 135 | 136 | writeASM(ast, functionsAsm, config); 137 | asmInitDeps(func); 138 | asmScanDeps(func); // debugging only 139 | } 140 | 141 | const resUnopt = writeASM(ast, functionsAsm, config); 142 | asmUnoptimized = resUnopt.asm; 143 | debugUnopt = resUnopt.debug; 144 | 145 | for(const func of functionsAsm) 146 | { 147 | if(config.patchFunctions.length && !config.patchFunctions.includes(func.name))continue; 148 | 149 | func.cyclesBefore = evalFunctionCost(func); 150 | asmOptimizePattern(func); 151 | if(func.asm.length > 0) { 152 | asmInitDeps(func); 153 | 154 | if(config.reorder)console.time("asmOptimize"); 155 | await asmOptimize(func, updateCb ? (bestFunc) => { 156 | func.asm = structuredClone(bestFunc.asm); 157 | func.cyclesAfter = bestFunc.cyclesAfter; 158 | return updateCb(generateASM()); 159 | } : undefined, config); 160 | if(config.reorder)console.timeEnd("asmOptimize"); 161 | 162 | asmScanDeps(func); // debugging only 163 | func.cyclesAfter = evalFunctionCost(func); 164 | } else { 165 | func.cyclesAfter = 0; 166 | } 167 | } 168 | 169 | if(config.reorder) { 170 | console.log("==== Optimization Overview ===="); 171 | let longestFuncName = functionsAsm.reduce((a, b) => a.name.length > b.name.length ? a : b).name.length; 172 | for(const func of functionsAsm) { 173 | if(config.patchFunctions.length && !config.patchFunctions.includes(func.name))continue; 174 | console.log(`- ${func.name.padEnd(longestFuncName, ' ')}: ${func.cyclesBefore.toString().padStart(4, ' ')} -> ${func.cyclesAfter.toString().padStart(4, ' ')} cycles`); 175 | } 176 | console.log("==============================="); 177 | } 178 | } 179 | 180 | return generateASM(); 181 | } 182 | -------------------------------------------------------------------------------- /src/lib/types/asm.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * NOTE: this project does NOT use TypeScript. 3 | * Types defined here are solely used by JSDoc. 4 | */ 5 | import "./ast"; 6 | 7 | declare global 8 | { 9 | type ASMType = number; 10 | 11 | type ASMDebug = { 12 | lineASM: number; 13 | lineRSPL: number; 14 | lineASMOpt: number; 15 | reorderCount: number; 16 | reorderLineMin: number; 17 | reorderLineMax: number; 18 | cycle: number; 19 | stall: number; 20 | paired: boolean; 21 | }; 22 | 23 | type ASM = { 24 | type: ASMType; 25 | op: string; 26 | args: string[]; 27 | 28 | label?: string; 29 | comment?: string; 30 | 31 | depsSourceIdx: number[]; 32 | depsTargetIdx: number[]; 33 | barrierMask: number; 34 | 35 | depsArgMask: BigInt; 36 | depsSourceMask: BigInt; 37 | depsTargetMask: BigInt; 38 | 39 | depsBlockSourceMask: BigInt; 40 | depsBlockTargetMask: BigInt; 41 | 42 | depsStallSourceIdx: number[]; 43 | depsStallTargetIdx: number[]; 44 | 45 | depsStallSourceMask0: number; 46 | depsStallSourceMask1: number; 47 | depsStallTargetMask0: number; 48 | depsStallTargetMask1: number; 49 | opFlags: number; 50 | labelEnd: string; // sets the end for a block 51 | 52 | stallLatency: number; 53 | 54 | debug: ASMDebug; 55 | annotations: Annotation[]; 56 | }; 57 | 58 | type ASMFunc = ASTFunc | { 59 | asm: ASM[]; 60 | argSize: number; 61 | cyclesBefore: number; 62 | cyclesAfter: number; 63 | }; 64 | 65 | type ASMOutputDebug = { 66 | lineMap: Record; 67 | lineDepMap: Record; 68 | lineOptMap: Record; 69 | lineCycleMap: Record; 70 | lineStallMap: Record; 71 | }; 72 | 73 | type ASMOutput = { 74 | asm: string; 75 | debug: ASMOutputDebug; 76 | sizeIMEM: number; 77 | sizeDMEM: number; 78 | }; 79 | } -------------------------------------------------------------------------------- /src/lib/types/ast.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * NOTE: this project does NOT use TypeScript. 3 | * Types defined here are solely used by JSDoc. 4 | */ 5 | 6 | import './astCalc'; 7 | import './types'; 8 | import './asm'; 9 | 10 | declare global 11 | { 12 | type ASTStmType = 'scopedBlock' | 'varDef' | 'varAssign' | 'funcCall' | 'return' | 'if' | 'while' | 'for' | 'break' | 'continue' | 'asm'; 13 | type ASTFuncType = 'function' | 'macro' | 'command'; 14 | 15 | type ASTFuncArg = { 16 | type: 'num' | 'var' | 'string' | DataType; 17 | reg?: string; 18 | name?: string; 19 | value?: number | string; 20 | swizzle?: Swizzle; 21 | castType?: CastType; 22 | originalType?: DataType; 23 | }; 24 | 25 | type ASTFunc = { 26 | annotations: Annotation[]; 27 | type: ASTFuncType; 28 | resultType?: number; 29 | name: string; 30 | nameOverride?: string; 31 | body: ASTScopedBlock; 32 | args: ASTFuncArg[]; 33 | }; 34 | 35 | type ASTMacroMap = Record; 36 | 37 | type ASTState = { 38 | arraySize: number[]; 39 | extern: boolean; 40 | type: string; 41 | varName: string; 42 | varType: DataType; 43 | align: number; 44 | value: number[] | undefined; 45 | }; 46 | 47 | type ASTStatementBase = { line: number; }; 48 | 49 | type ASTScopedBlock = ASTStatementBase & { 50 | type: 'scopedBlock'; 51 | statements: ASTStatement[]; 52 | }; 53 | 54 | type ASTIf = ASTStatementBase & { 55 | type: 'if'; 56 | blockIf: ASTScopedBlock; 57 | blockElse?: ASTScopedBlock; 58 | compare: ASTCompare; 59 | }; 60 | 61 | type ASTWhile = ASTStatementBase & { 62 | type: 'while'; 63 | block: ASTScopedBlock; 64 | compare: ASTCompare; 65 | }; 66 | 67 | type ASTLoop = ASTStatementBase & { 68 | type: 'loop'; 69 | block: ASTScopedBlock; 70 | compare?: ASTCompare; 71 | }; 72 | 73 | type ASTDeclAssign = ASTStatementBase & { 74 | type: 'varDeclAssign'; 75 | varName: string; 76 | calc: ASTCalc; 77 | isConst: boolean; 78 | }; 79 | 80 | type ASTDeclMulti = ASTStatementBase & { 81 | type: 'varDeclMulti'; 82 | varNames: string[]; 83 | reg: string; 84 | varType: DataType; 85 | isConst: boolean; 86 | }; 87 | 88 | type ASTDecl = ASTStatementBase & { 89 | type: 'varDecl'; 90 | varName: string; 91 | reg: string; 92 | varType: DataType; 93 | isConst: boolean; 94 | }; 95 | 96 | type ASTDeclAlias = ASTStatementBase & { 97 | type: 'varDeclAlias'; 98 | varName: string; 99 | aliasName: string; 100 | }; 101 | 102 | type ASTNestedCalcPart = string | ASTNestedCalcPart[] | Object; 103 | 104 | type ASTNestedCalc = ASTStatementBase & { 105 | type: 'nestedCalc'; 106 | varName: string; 107 | swizzle?: Swizzle; 108 | parts: ASTNestedCalcPart[]; 109 | }; 110 | 111 | type ASTAssignCalc = ASTStatementBase & { 112 | type: 'varAssignCalc'; 113 | calc: ASTCalc; 114 | swizzle?: Swizzle; 115 | varName: string; 116 | assignType: "=" | "&&=" | "||=" | "&=" | "|=" | "<<=" | ">>=" | "+*=" | "+=" | "-=" | "*=" | "/=", 117 | }; 118 | 119 | type ASTFuncCall = ASTStatementBase & { 120 | type: 'funcCall'; 121 | func: any; 122 | args: ASTFuncArg[]; 123 | }; 124 | 125 | type ASTLabelDecl = ASTStatementBase & { 126 | type: 'labelDecl'; 127 | name: string; 128 | }; 129 | 130 | type ASTGoto = ASTStatementBase & { 131 | type: 'goto'; 132 | label: string; 133 | }; 134 | 135 | type ASTBreak = ASTStatementBase & { 136 | type: 'break'; 137 | label: string; 138 | }; 139 | 140 | type ASTExit = ASTStatementBase & { 141 | type: 'exit'; 142 | label: string; 143 | }; 144 | 145 | type ASTContinue = ASTStatementBase & { 146 | type: 'continue'; 147 | label: string; 148 | }; 149 | 150 | type ASTVarUndef = ASTStatementBase & { 151 | type: 'varUndef'; 152 | varName: string; 153 | }; 154 | 155 | type ASTAnnotation = ASTStatementBase & { 156 | type: 'annotation'; 157 | name: string; 158 | value: string | number; 159 | }; 160 | 161 | type ASTStatement = ASTScopedBlock | ASTIf | ASTWhile | ASTLoop | ASTDeclAssign | ASTDeclMulti 162 | | ASTDecl | ASTFuncCall | ASTDeclAlias | ASTAssignCalc | ASTNestedCalc 163 | | ASTLabelDecl | ASTGoto | ASTBreak | ASTExit | ASTContinue | ASTVarUndef | ASTAnnotation; 164 | 165 | type AST = { 166 | includes: string[]; 167 | state: ASTState[]; 168 | stateData: ASTState[]; 169 | stateBss: ASTState[]; 170 | functions: ASTFunc[]; 171 | postIncludes: string[]; 172 | defines: Record; 173 | }; 174 | } -------------------------------------------------------------------------------- /src/lib/types/astCalc.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * NOTE: this project does NOT use TypeScript. 3 | * Types defined here are solely used by JSDoc. 4 | */ 5 | import {ASTFuncArg} from "./ast"; 6 | 7 | declare global 8 | { 9 | type CalcOp = '+' | '-' | '*' | '+*' | '/' | '<<' | '>>' | '&' | '|' | '^' | '!' | '~'; 10 | type CompareOp = '==' | '!=' | '<' | '<=' | '>' | '>='; 11 | 12 | type ASTCompareArg = { 13 | type: 'num' | 'var'; 14 | value: number | string; 15 | }; 16 | 17 | type ASTCompare = { 18 | type: 'compare'; 19 | left: ASTCompareArg; 20 | op: CompareOp; 21 | right: ASTCompareArg; 22 | line: number; 23 | } 24 | 25 | type ASTExprNum = { 26 | type: 'num', 27 | value: number; 28 | }; 29 | type ASTExprVarName = { 30 | type: 'VarName', 31 | value: string; 32 | }; 33 | 34 | type ASTCalcNum = { 35 | type: 'calcNum'; 36 | right: ASTExprNum; 37 | }; 38 | 39 | type ASTCalcVar = { 40 | type: 'calcVar'; 41 | left?: string; 42 | op?: CalcOp; 43 | right: ASTExprVarName; 44 | swizzleLeft?: Swizzle; 45 | swizzleRight?: Swizzle; 46 | }; 47 | 48 | type ASTCalcMultiPart = { 49 | type: 'calcMultiPart'; 50 | op: CalcOp; 51 | right: ASTExprNum | ASTExprVarName; 52 | swizzleRight?: Swizzle; 53 | groupStart: number; 54 | groupEnd: number; 55 | }; 56 | 57 | interface ASTCalcMulti { 58 | type: 'calcMulti'; 59 | left: ASTExprNum | ASTExprVarName; 60 | swizzleLeft?: Swizzle; 61 | parts: ASTCalcMultiPart[]; 62 | groupStart: number; 63 | } 64 | 65 | type ASTCalcLR = { 66 | type: 'calcLR'; 67 | left: ASTExprNum | ASTExprVarName | ASTCalcLR; 68 | op: CalcOp; 69 | right: ASTExprNum | ASTExprVarName; 70 | swizzleLeft?: Swizzle; 71 | swizzleRight?: Swizzle; 72 | }; 73 | 74 | type ASTCalcFunc = { 75 | type: 'calcFunc'; 76 | funcName: string; 77 | args: ASTFuncArg[]; 78 | swizzleRight?: Swizzle; 79 | }; 80 | 81 | type ASTTernary = { 82 | left: string; 83 | right: string; 84 | swizzleRight?: Swizzle; 85 | }; 86 | 87 | type ASTCalcCompare = { 88 | type: 'calcCompare'; 89 | left: string; 90 | op: CalcOp; 91 | right: ASTExprNum | ASTExprVarName; 92 | swizzleRight?: Swizzle; 93 | ternary?: ASTTernary 94 | }; 95 | 96 | type ASTCalc = ASTCalcMulti | ASTCalcLR | ASTCalcNum | ASTCalcVar | ASTCalcFunc | ASTCalcCompare; 97 | } 98 | -------------------------------------------------------------------------------- /src/lib/types/opt.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * NOTE: this project does NOT use TypeScript. 3 | * Types defined here are solely used by JSDoc. 4 | */ 5 | 6 | import './types'; 7 | import './asm'; 8 | 9 | declare global 10 | { 11 | type OptInstruction = { 12 | latency: number; 13 | asm: ASM; 14 | }; 15 | } -------------------------------------------------------------------------------- /src/lib/types/types.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * NOTE: this project does NOT use TypeScript. 3 | * Types defined here are solely used by JSDoc. 4 | */ 5 | 6 | declare global { 7 | type DataType = 'u8' | 's8' | 'u16' | 's16' | 'u32' | 's32' | 'vec16' | 'vec32'; 8 | type CastType = DataType | 'uint' | 'sint' | 'ufract' | 'sfract'; 9 | type Swizzle = string; 10 | type AnnotationNames = 'Barrier'; 11 | 12 | type Annotation = { 13 | name: AnnotationNames; 14 | value: string | number; 15 | }; 16 | 17 | type VarRegDef = { 18 | reg: string; 19 | type: DataType; 20 | castType?: CastType; 21 | originalType?: DataType; 22 | isConst: boolean; 23 | modifyCount: number; 24 | }; 25 | 26 | type Scope = { 27 | varMap: Record; 28 | regVarMap: Record; 29 | varAliasMap: Record; 30 | annotations: Annotation[]; 31 | labelStart?: string; 32 | labelEnd?: string; 33 | }; 34 | 35 | type ScopeStack = Scope[]; 36 | 37 | type MemVarDef = { 38 | name: string; 39 | type: string; 40 | arraySize: number; 41 | }; 42 | 43 | type FuncArg = { 44 | type: DataType; 45 | reg?: string; 46 | name: string; 47 | }; 48 | 49 | type FuncDef = { 50 | name: string; 51 | args: FuncArg[]; 52 | isRelative: boolean; 53 | }; 54 | 55 | type RSPLConfig = { 56 | optimize: boolean; 57 | optimizeTime: number; 58 | optimizeWorker: number; 59 | reorder: boolean; 60 | rspqWrapper: boolean; 61 | fileLoader: (path: string) => string; 62 | defines: Record; 63 | patchFunctions: string[]; 64 | debugInfo: boolean; 65 | }; 66 | } -------------------------------------------------------------------------------- /src/lib/utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @copyright 2023 - Max Bebök 3 | * @license Apache-2.0 4 | */ 5 | 6 | /** 7 | * @param {Array} a 8 | * @param {Array} b 9 | * @returns {Array} 10 | */ 11 | export function intersect(a, b) 12 | { 13 | return a.filter(x => b.includes(x)); 14 | } 15 | 16 | /** 17 | * @param {Array} a 18 | * @param {Array} b 19 | * @returns {Array} 20 | */ 21 | export function difference(a, b) 22 | { 23 | return a.filter(x => !b.includes(x)); 24 | } 25 | 26 | export async function sleep(ms) { 27 | return new Promise(resolve => setTimeout(resolve, ms)); 28 | } -------------------------------------------------------------------------------- /src/lib/workerThreads.js: -------------------------------------------------------------------------------- 1 | import { Worker, isMainThread, workerData, parentPort, threadId } from 'worker_threads'; 2 | 3 | let instance = null; 4 | let tasks = {}; 5 | 6 | export class WorkerThreads { 7 | constructor(poolSize, scriptPath) { 8 | this.scriptPath = scriptPath; 9 | this.poolSize = poolSize; 10 | this.workers = []; 11 | this.workerPromises = []; 12 | 13 | if(isMainThread) { 14 | this.initWorkers(); 15 | } 16 | 17 | instance = this; 18 | } 19 | 20 | /** 21 | * @return {WorkerThreads} 22 | */ 23 | static getInstance() { 24 | if (!instance) { 25 | throw new Error("WorkerThreads instance not initialized"); 26 | } 27 | return instance; 28 | } 29 | 30 | initWorkers() { 31 | console.log("Init Workers"); 32 | for (let i = 0; i < this.poolSize; i++) { 33 | const worker = new Worker(this.scriptPath, { workerData: { id: i } }); 34 | worker.on('error', (error) => { 35 | console.error(`Worker ${i} error:`, error); 36 | }); 37 | worker.on('exit', (code) => { 38 | console.log(`Worker ${i} stopped with exit code ${code}`); 39 | }); 40 | worker.on('message', (message) => { 41 | if(this.workerPromises[i]) { 42 | const p = this.workerPromises[i]; 43 | this.workerPromises[i] = undefined; 44 | p.resolve(message); 45 | } else { 46 | console.warn(`Worker ${i} sent a message but no promise was waiting.`); 47 | } 48 | }); 49 | worker.on('error', (error) => { 50 | console.error(`Worker ${i} error:`, error); 51 | }); 52 | 53 | this.workers.push(worker); 54 | this.workerPromises.push(undefined); 55 | } 56 | } 57 | 58 | runTask(type, data) { 59 | let freeWorker = this.workerPromises.indexOf(undefined); 60 | if(freeWorker === -1) { 61 | console.log(this.workerPromises); 62 | throw new Error("No free workers available"); 63 | } 64 | 65 | //console.log(`Using worker ${freeWorker}`); 66 | 67 | const worker = this.workers[freeWorker]; 68 | return new Promise((resolve, reject) => { 69 | this.workerPromises[freeWorker] = { resolve, reject }; 70 | worker.postMessage({ type, data }); 71 | }); 72 | } 73 | 74 | stop() { 75 | this.workers.forEach(worker => worker.terminate()); 76 | } 77 | } 78 | 79 | export function initWorker() { 80 | if (!isMainThread) { 81 | parentPort.on('message', async (message) => { 82 | const { type, data } = message; 83 | if(!tasks[type]) { 84 | throw new Error(`Task type "${type}" not registered`); 85 | } 86 | try { 87 | parentPort.postMessage(tasks[type](data)); 88 | } catch (error) { 89 | console.error(error); 90 | parentPort.postMessage({ error: error.message }); 91 | } 92 | }); 93 | console.log(`Worker ${threadId} initialized`); 94 | } 95 | return !isMainThread; 96 | } 97 | 98 | export function registerTask(type, func) { 99 | tasks[type] = func; 100 | } -------------------------------------------------------------------------------- /src/tests/annotations.test.js: -------------------------------------------------------------------------------- 1 | import {transpileSource} from "../lib/transpiler"; 2 | 3 | const CONF = {rspqWrapper: false}; 4 | 5 | describe('Annotations', () => 6 | { 7 | test('Align (function)', async () => { 8 | const {asm, warn} = await transpileSource(` 9 | @Align(8) 10 | function test() 11 | { 12 | exit; 13 | }`, CONF); 14 | 15 | expect(warn).toBe(""); 16 | expect(asm).toBe( 17 | `.align 3 18 | test: 19 | j RSPQ_Loop 20 | nop 21 | jr $ra 22 | nop`); 23 | }); 24 | 25 | test('Relative (function)', async () => { 26 | const {asm, warn} = await transpileSource(` 27 | @Relative 28 | function target_rel() {} 29 | function target_abs() {} 30 | function caller() { 31 | target_rel(); 32 | target_abs(); 33 | } 34 | `, CONF); 35 | 36 | expect(warn).toBe(""); 37 | expect(asm).toBe( 38 | `target_rel: 39 | jr $ra 40 | nop 41 | target_abs: 42 | jr $ra 43 | nop 44 | caller: 45 | bgezal $zero, target_rel 46 | nop 47 | jal target_abs 48 | nop 49 | jr $ra 50 | nop`); 51 | }); 52 | 53 | test('Relative (caller)', async () => { 54 | const {asm, warn} = await transpileSource(` 55 | function target_rel() {} 56 | function target_abs() {} 57 | function caller() { 58 | @Relative target_rel(); 59 | target_abs(); 60 | } 61 | `, CONF); 62 | 63 | expect(warn).toBe(""); 64 | expect(asm).toBe( 65 | `target_rel: 66 | jr $ra 67 | nop 68 | target_abs: 69 | jr $ra 70 | nop 71 | caller: 72 | bgezal $zero, target_rel 73 | nop 74 | jal target_abs 75 | nop 76 | jr $ra 77 | nop`); 78 | }); 79 | }); -------------------------------------------------------------------------------- /src/tests/branchVar.test.js: -------------------------------------------------------------------------------- 1 | import {transpileSource} from "../lib/transpiler"; 2 | 3 | const CONF = {rspqWrapper: false}; 4 | 5 | describe('Branch (Var vs. Var)', () => 6 | { 7 | test('Equal - U32', async () => { 8 | const {asm, warn} = await transpileSource(`function test_if() { 9 | u32<$v0> a,b; 10 | if(a == b) { a += 1111; } else { a += 2222; } 11 | }`, CONF); 12 | 13 | expect(warn).toBe(""); 14 | expect(asm).toBe(`test_if: 15 | bne $v0, $v1, LABEL_test_if_0001 16 | nop 17 | addiu $v0, $v0, 1111 18 | beq $zero, $zero, LABEL_test_if_0002 19 | nop 20 | LABEL_test_if_0001: 21 | addiu $v0, $v0, 2222 22 | LABEL_test_if_0002: 23 | jr $ra 24 | nop`); 25 | }); 26 | 27 | test('Not-Equal - U32', async () => { 28 | const {asm, warn} = await transpileSource(`function test_if() { 29 | u32<$v0> a,b; 30 | if(a != b) { a += 1111; } else { a += 2222; } 31 | }`, CONF); 32 | 33 | expect(warn).toBe(""); 34 | expect(asm).toBe(`test_if: 35 | beq $v0, $v1, LABEL_test_if_0001 36 | nop 37 | addiu $v0, $v0, 1111 38 | beq $zero, $zero, LABEL_test_if_0002 39 | nop 40 | LABEL_test_if_0001: 41 | addiu $v0, $v0, 2222 42 | LABEL_test_if_0002: 43 | jr $ra 44 | nop`); 45 | }); 46 | 47 | test('Greater - U32', async () => { 48 | const {asm, warn} = await transpileSource(`function test_if() { 49 | u32<$v0> a,b; 50 | if(a > b) { a += 1111; } else { a += 2222; } 51 | }`, CONF); 52 | 53 | expect(warn).toBe(""); 54 | expect(asm).toBe(`test_if: 55 | sltu $at, $v1, $v0 56 | beq $at, $zero, LABEL_test_if_0001 57 | nop 58 | addiu $v0, $v0, 1111 59 | beq $zero, $zero, LABEL_test_if_0002 60 | nop 61 | LABEL_test_if_0001: 62 | addiu $v0, $v0, 2222 63 | LABEL_test_if_0002: 64 | jr $ra 65 | nop`); 66 | }); 67 | 68 | test('Less - U32', async () => { 69 | const {asm, warn} = await transpileSource(`function test_if() { 70 | u32<$v0> a,b; 71 | if(a < b) { a += 1111; } else { a += 2222; } 72 | }`, CONF); 73 | 74 | expect(warn).toBe(""); 75 | expect(asm).toBe(`test_if: 76 | sltu $at, $v0, $v1 77 | beq $at, $zero, LABEL_test_if_0001 78 | nop 79 | addiu $v0, $v0, 1111 80 | beq $zero, $zero, LABEL_test_if_0002 81 | nop 82 | LABEL_test_if_0001: 83 | addiu $v0, $v0, 2222 84 | LABEL_test_if_0002: 85 | jr $ra 86 | nop`); 87 | }); 88 | 89 | test('Greater-Than - U32', async () => { 90 | const {asm, warn} = await transpileSource(`function test_if() { 91 | u32<$v0> a,b; 92 | if(a >= b) { a += 1111; } else { a += 2222; } 93 | }`, CONF); 94 | 95 | expect(warn).toBe(""); 96 | expect(asm).toBe(`test_if: 97 | sltu $at, $v0, $v1 98 | bne $at, $zero, LABEL_test_if_0001 99 | nop 100 | addiu $v0, $v0, 1111 101 | beq $zero, $zero, LABEL_test_if_0002 102 | nop 103 | LABEL_test_if_0001: 104 | addiu $v0, $v0, 2222 105 | LABEL_test_if_0002: 106 | jr $ra 107 | nop`); 108 | }); 109 | 110 | test('Less-Than - U32', async () => { 111 | const {asm, warn} = await transpileSource(`function test_if() { 112 | u32<$v0> a,b; 113 | if(a <= b) { a += 1111; } else { a += 2222; } 114 | }`, CONF); 115 | 116 | expect(warn).toBe(""); 117 | expect(asm).toBe(`test_if: 118 | sltu $at, $v1, $v0 119 | bne $at, $zero, LABEL_test_if_0001 120 | nop 121 | addiu $v0, $v0, 1111 122 | beq $zero, $zero, LABEL_test_if_0002 123 | nop 124 | LABEL_test_if_0001: 125 | addiu $v0, $v0, 2222 126 | LABEL_test_if_0002: 127 | jr $ra 128 | nop`); 129 | }); 130 | 131 | // Signed 132 | 133 | test('Equal - S32', async () => { 134 | const {asm, warn} = await transpileSource(`function test_if() { 135 | s32<$v0> a,b; 136 | if(a == b) { a += 1111; } else { a += 2222; } 137 | }`, CONF); 138 | 139 | expect(warn).toBe(""); 140 | expect(asm).toBe(`test_if: 141 | bne $v0, $v1, LABEL_test_if_0001 142 | nop 143 | addiu $v0, $v0, 1111 144 | beq $zero, $zero, LABEL_test_if_0002 145 | nop 146 | LABEL_test_if_0001: 147 | addiu $v0, $v0, 2222 148 | LABEL_test_if_0002: 149 | jr $ra 150 | nop`); 151 | }); 152 | 153 | test('Not-Equal - S32', async () => { 154 | const {asm, warn} = await transpileSource(`function test_if() { 155 | s32<$v0> a,b; 156 | if(a != b) { a += 1111; } else { a += 2222; } 157 | }`, CONF); 158 | 159 | expect(warn).toBe(""); 160 | expect(asm).toBe(`test_if: 161 | beq $v0, $v1, LABEL_test_if_0001 162 | nop 163 | addiu $v0, $v0, 1111 164 | beq $zero, $zero, LABEL_test_if_0002 165 | nop 166 | LABEL_test_if_0001: 167 | addiu $v0, $v0, 2222 168 | LABEL_test_if_0002: 169 | jr $ra 170 | nop`); 171 | }); 172 | 173 | test('Greater - S32', async () => { 174 | const {asm, warn} = await transpileSource(`function test_if() { 175 | s32<$v0> a,b; 176 | if(a > b) { a += 1111; } else { a += 2222; } 177 | }`, CONF); 178 | 179 | expect(warn).toBe(""); 180 | expect(asm).toBe(`test_if: 181 | slt $at, $v1, $v0 182 | beq $at, $zero, LABEL_test_if_0001 183 | nop 184 | addiu $v0, $v0, 1111 185 | beq $zero, $zero, LABEL_test_if_0002 186 | nop 187 | LABEL_test_if_0001: 188 | addiu $v0, $v0, 2222 189 | LABEL_test_if_0002: 190 | jr $ra 191 | nop`); 192 | }); 193 | 194 | test('Less - S32', async () => { 195 | const {asm, warn} = await transpileSource(`function test_if() { 196 | s32<$v0> a,b; 197 | if(a < b) { a += 1111; } else { a += 2222; } 198 | }`, CONF); 199 | 200 | expect(warn).toBe(""); 201 | expect(asm).toBe(`test_if: 202 | slt $at, $v0, $v1 203 | beq $at, $zero, LABEL_test_if_0001 204 | nop 205 | addiu $v0, $v0, 1111 206 | beq $zero, $zero, LABEL_test_if_0002 207 | nop 208 | LABEL_test_if_0001: 209 | addiu $v0, $v0, 2222 210 | LABEL_test_if_0002: 211 | jr $ra 212 | nop`); 213 | }); 214 | 215 | test('Greater-Than - S32', async () => { 216 | const {asm, warn} = await transpileSource(`function test_if() { 217 | s32<$v0> a,b; 218 | if(a >= b) { a += 1111; } else { a += 2222; } 219 | }`, CONF); 220 | 221 | expect(warn).toBe(""); 222 | expect(asm).toBe(`test_if: 223 | slt $at, $v0, $v1 224 | bne $at, $zero, LABEL_test_if_0001 225 | nop 226 | addiu $v0, $v0, 1111 227 | beq $zero, $zero, LABEL_test_if_0002 228 | nop 229 | LABEL_test_if_0001: 230 | addiu $v0, $v0, 2222 231 | LABEL_test_if_0002: 232 | jr $ra 233 | nop`); 234 | }); 235 | 236 | test('Less-Than - S32', async () => { 237 | const {asm, warn} = await transpileSource(`function test_if() { 238 | s32<$v0> a,b; 239 | if(a <= b) { a += 1111; } else { a += 2222; } 240 | }`, CONF); 241 | 242 | expect(warn).toBe(""); 243 | expect(asm).toBe(`test_if: 244 | slt $at, $v1, $v0 245 | bne $at, $zero, LABEL_test_if_0001 246 | nop 247 | addiu $v0, $v0, 1111 248 | beq $zero, $zero, LABEL_test_if_0002 249 | nop 250 | LABEL_test_if_0001: 251 | addiu $v0, $v0, 2222 252 | LABEL_test_if_0002: 253 | jr $ra 254 | nop`); 255 | }); 256 | }); -------------------------------------------------------------------------------- /src/tests/branchZero.test.js: -------------------------------------------------------------------------------- 1 | import {transpileSource} from "../lib/transpiler"; 2 | 3 | const CONF = {rspqWrapper: false}; 4 | 5 | describe('Branch (Var vs. 0)', () => 6 | { 7 | test('Equal - U32', async () => { 8 | const {asm, warn} = await transpileSource(`function test_if() { 9 | u32<$v0> a; 10 | if(a == 0) { a += 1111; } else { a += 2222; } 11 | }`, CONF); 12 | 13 | expect(warn).toBe(""); 14 | expect(asm).toBe(`test_if: 15 | bne $v0, $zero, LABEL_test_if_0001 16 | nop 17 | addiu $v0, $v0, 1111 18 | beq $zero, $zero, LABEL_test_if_0002 19 | nop 20 | LABEL_test_if_0001: 21 | addiu $v0, $v0, 2222 22 | LABEL_test_if_0002: 23 | jr $ra 24 | nop`); 25 | }); 26 | 27 | test('Not-Equal - U32', async () => { 28 | const {asm, warn} = await transpileSource(`function test_if() { 29 | u32<$v0> a; 30 | if(a != 0) { a += 1111; } else { a += 2222; } 31 | }`, CONF); 32 | 33 | expect(warn).toBe(""); 34 | expect(asm).toBe(`test_if: 35 | beq $v0, $zero, LABEL_test_if_0001 36 | nop 37 | addiu $v0, $v0, 1111 38 | beq $zero, $zero, LABEL_test_if_0002 39 | nop 40 | LABEL_test_if_0001: 41 | addiu $v0, $v0, 2222 42 | LABEL_test_if_0002: 43 | jr $ra 44 | nop`); 45 | }); 46 | 47 | test('Greater - U32', async () => { 48 | const {asm, warn} = await transpileSource(`function test_if() { 49 | u32<$v0> a; 50 | if(a > 0) { a += 1111; } else { a += 2222; } 51 | }`, CONF); 52 | 53 | expect(warn).toBe(""); 54 | expect(asm).toBe(`test_if: 55 | blez $v0, LABEL_test_if_0001 56 | nop 57 | addiu $v0, $v0, 1111 58 | beq $zero, $zero, LABEL_test_if_0002 59 | nop 60 | LABEL_test_if_0001: 61 | addiu $v0, $v0, 2222 62 | LABEL_test_if_0002: 63 | jr $ra 64 | nop`); 65 | }); 66 | 67 | test('Less - U32', async () => { 68 | const {asm, warn} = await transpileSource(`function test_if() { 69 | u32<$v0> a; 70 | if(a < 0) { a += 1111; } else { a += 2222; } 71 | }`, CONF); 72 | 73 | expect(warn).toBe(""); 74 | expect(asm).toBe(`test_if: 75 | bgez $v0, LABEL_test_if_0001 76 | nop 77 | addiu $v0, $v0, 1111 78 | beq $zero, $zero, LABEL_test_if_0002 79 | nop 80 | LABEL_test_if_0001: 81 | addiu $v0, $v0, 2222 82 | LABEL_test_if_0002: 83 | jr $ra 84 | nop`); 85 | }); 86 | 87 | test('Greater-Equal - U32', async () => { 88 | const {asm, warn} = await transpileSource(`function test_if() { 89 | u32<$v0> a; 90 | if(a >= 0) { a += 1111; } else { a += 2222; } 91 | }`, CONF); 92 | 93 | expect(warn).toBe(""); 94 | expect(asm).toBe(`test_if: 95 | bltz $v0, LABEL_test_if_0001 96 | nop 97 | addiu $v0, $v0, 1111 98 | beq $zero, $zero, LABEL_test_if_0002 99 | nop 100 | LABEL_test_if_0001: 101 | addiu $v0, $v0, 2222 102 | LABEL_test_if_0002: 103 | jr $ra 104 | nop`); 105 | }); 106 | 107 | test('Less-Equal - U32', async () => { 108 | const {asm, warn} = await transpileSource(`function test_if() { 109 | u32<$v0> a; 110 | if(a <= 0) { a += 1111; } else { a += 2222; } 111 | }`, CONF); 112 | 113 | expect(warn).toBe(""); 114 | expect(asm).toBe(`test_if: 115 | bgtz $v0, LABEL_test_if_0001 116 | nop 117 | addiu $v0, $v0, 1111 118 | beq $zero, $zero, LABEL_test_if_0002 119 | nop 120 | LABEL_test_if_0001: 121 | addiu $v0, $v0, 2222 122 | LABEL_test_if_0002: 123 | jr $ra 124 | nop`); 125 | }); 126 | 127 | // Signed 128 | 129 | test('Equal - S32', async () => { 130 | const {asm, warn} = await transpileSource(`function test_if() { 131 | s32<$v0> a; 132 | if(a == 0) { a += 1111; } else { a += 2222; } 133 | }`, CONF); 134 | 135 | expect(warn).toBe(""); 136 | expect(asm).toBe(`test_if: 137 | bne $v0, $zero, LABEL_test_if_0001 138 | nop 139 | addiu $v0, $v0, 1111 140 | beq $zero, $zero, LABEL_test_if_0002 141 | nop 142 | LABEL_test_if_0001: 143 | addiu $v0, $v0, 2222 144 | LABEL_test_if_0002: 145 | jr $ra 146 | nop`); 147 | }); 148 | 149 | test('Not-Equal - S32', async () => { 150 | const {asm, warn} = await transpileSource(`function test_if() { 151 | s32<$v0> a; 152 | if(a != 0) { a += 1111; } else { a += 2222; } 153 | }`, CONF); 154 | 155 | expect(warn).toBe(""); 156 | expect(asm).toBe(`test_if: 157 | beq $v0, $zero, LABEL_test_if_0001 158 | nop 159 | addiu $v0, $v0, 1111 160 | beq $zero, $zero, LABEL_test_if_0002 161 | nop 162 | LABEL_test_if_0001: 163 | addiu $v0, $v0, 2222 164 | LABEL_test_if_0002: 165 | jr $ra 166 | nop`); 167 | }); 168 | 169 | test('Greater - S32', async () => { 170 | const {asm, warn} = await transpileSource(`function test_if() { 171 | s32<$v0> a; 172 | if(a > 0) { a += 1111; } else { a += 2222; } 173 | }`, CONF); 174 | 175 | expect(warn).toBe(""); 176 | expect(asm).toBe(`test_if: 177 | blez $v0, LABEL_test_if_0001 178 | nop 179 | addiu $v0, $v0, 1111 180 | beq $zero, $zero, LABEL_test_if_0002 181 | nop 182 | LABEL_test_if_0001: 183 | addiu $v0, $v0, 2222 184 | LABEL_test_if_0002: 185 | jr $ra 186 | nop`); 187 | }); 188 | 189 | test('Less - S32', async () => { 190 | const {asm, warn} = await transpileSource(`function test_if() { 191 | s32<$v0> a; 192 | if(a < 0) { a += 1111; } else { a += 2222; } 193 | }`, CONF); 194 | 195 | expect(warn).toBe(""); 196 | expect(asm).toBe(`test_if: 197 | bgez $v0, LABEL_test_if_0001 198 | nop 199 | addiu $v0, $v0, 1111 200 | beq $zero, $zero, LABEL_test_if_0002 201 | nop 202 | LABEL_test_if_0001: 203 | addiu $v0, $v0, 2222 204 | LABEL_test_if_0002: 205 | jr $ra 206 | nop`); 207 | }); 208 | 209 | test('Greater-Equal - S32', async () => { 210 | const {asm, warn} = await transpileSource(`function test_if() { 211 | s32<$v0> a; 212 | if(a >= 0) { a += 1111; } else { a += 2222; } 213 | }`, CONF); 214 | 215 | expect(warn).toBe(""); 216 | expect(asm).toBe(`test_if: 217 | bltz $v0, LABEL_test_if_0001 218 | nop 219 | addiu $v0, $v0, 1111 220 | beq $zero, $zero, LABEL_test_if_0002 221 | nop 222 | LABEL_test_if_0001: 223 | addiu $v0, $v0, 2222 224 | LABEL_test_if_0002: 225 | jr $ra 226 | nop`); 227 | }); 228 | 229 | test('Less-Equal - S32', async () => { 230 | const {asm, warn} = await transpileSource(`function test_if() { 231 | s32<$v0> a; 232 | if(a <= 0) { a += 1111; } else { a += 2222; } 233 | }`, CONF); 234 | 235 | expect(warn).toBe(""); 236 | expect(asm).toBe(`test_if: 237 | bgtz $v0, LABEL_test_if_0001 238 | nop 239 | addiu $v0, $v0, 1111 240 | beq $zero, $zero, LABEL_test_if_0002 241 | nop 242 | LABEL_test_if_0001: 243 | addiu $v0, $v0, 2222 244 | LABEL_test_if_0002: 245 | jr $ra 246 | nop`); 247 | }); 248 | }); -------------------------------------------------------------------------------- /src/tests/builtins.test.js: -------------------------------------------------------------------------------- 1 | import {transpileSource} from "../lib/transpiler"; 2 | 3 | const CONF = {rspqWrapper: false}; 4 | 5 | describe('Builtins', () => 6 | { 7 | test('swap() - scalar', async () => { 8 | const {asm, warn} = await transpileSource(`function test() { 9 | u32 v0, v1; 10 | swap(v0, v1); 11 | }`, CONF); 12 | 13 | expect(warn).toBe(""); 14 | expect(asm).toBe(`test: 15 | xor $t0, $t0, $t1 16 | xor $t1, $t0, $t1 17 | xor $t0, $t0, $t1 18 | jr $ra 19 | nop`); 20 | }); 21 | 22 | test('swap() - vec16', async () => { 23 | const {asm, warn} = await transpileSource(`function test() { 24 | vec16 v0, v1; 25 | swap(v0, v1); 26 | }`, CONF); 27 | 28 | expect(warn).toBe(""); 29 | expect(asm).toBe(`test: 30 | vxor $v01, $v01, $v02 31 | vxor $v02, $v01, $v02 32 | vxor $v01, $v01, $v02 33 | jr $ra 34 | nop`); 35 | }); 36 | 37 | test('swap() - vec32', async () => { 38 | const {asm, warn} = await transpileSource(`function test() { 39 | vec32 v0, v1; 40 | swap(v0, v1); 41 | }`, CONF); 42 | 43 | expect(warn).toBe(""); 44 | expect(asm).toBe(`test: 45 | vxor $v01, $v01, $v03 46 | vxor $v03, $v01, $v03 47 | vxor $v01, $v01, $v03 48 | vxor $v02, $v02, $v04 49 | vxor $v04, $v02, $v04 50 | vxor $v02, $v02, $v04 51 | jr $ra 52 | nop`); 53 | }); 54 | 55 | test('asm_include()', async () => { 56 | const {asm, warn} = await transpileSource(`function test() { 57 | u32 a = 4; 58 | asm_include("./test.inc"); 59 | a = 5; 60 | }`, CONF); 61 | 62 | expect(warn).toBe(""); 63 | expect(asm).toBe(`test: 64 | addiu $t0, $zero, 4 65 | #define zero $0 66 | #define v0 $2 67 | #define v1 $3 68 | #define a0 $4 69 | #define a1 $5 70 | #define a2 $6 71 | #define a3 $7 72 | #define t0 $8 73 | #define t1 $9 74 | #define t2 $10 75 | #define t3 $11 76 | #define t4 $12 77 | #define t5 $13 78 | #define t6 $14 79 | #define t7 $15 80 | #define s0 $16 81 | #define s1 $17 82 | #define s2 $18 83 | #define s3 $19 84 | #define s4 $20 85 | #define s5 $21 86 | #define s6 $22 87 | #define s7 $23 88 | #define t8 $24 89 | #define t9 $25 90 | #define k0 $26 91 | #define k1 $27 92 | #define gp $28 93 | #define sp $29 94 | #define fp $30 95 | #define ra $31 96 | .set at 97 | .set macro 98 | #include "./test.inc" 99 | .set noreorder 100 | .set noat 101 | .set nomacro 102 | #undef zero 103 | #undef at 104 | #undef v0 105 | #undef v1 106 | #undef a0 107 | #undef a1 108 | #undef a2 109 | #undef a3 110 | #undef t0 111 | #undef t1 112 | #undef t2 113 | #undef t3 114 | #undef t4 115 | #undef t5 116 | #undef t6 117 | #undef t7 118 | #undef s0 119 | #undef s1 120 | #undef s2 121 | #undef s3 122 | #undef s4 123 | #undef s5 124 | #undef s6 125 | #undef s7 126 | #undef t8 127 | #undef t9 128 | #undef k0 129 | #undef k1 130 | #undef gp 131 | #undef sp 132 | #undef fp 133 | #undef ra 134 | addiu $t0, $zero, 5 135 | jr $ra 136 | nop`); 137 | }); 138 | 139 | test('transpose() - 8x8 in-place', async () => { 140 | const {asm, warn} = await transpileSource(`function test() { 141 | vec16<$v08> v0; 142 | u16 buff; 143 | v0 = transpose(v0, buff, 8, 8); 144 | }`, CONF); 145 | 146 | expect(warn).toBe(""); 147 | expect(asm).toBe(`test: 148 | stv $v08, 2, 16, $t0 149 | stv $v08, 4, 32, $t0 150 | stv $v08, 6, 48, $t0 151 | stv $v08, 8, 64, $t0 152 | stv $v08, 10, 80, $t0 153 | stv $v08, 12, 96, $t0 154 | stv $v08, 14, 112, $t0 155 | ltv $v08, 14, 16, $t0 156 | ltv $v08, 12, 32, $t0 157 | ltv $v08, 10, 48, $t0 158 | ltv $v08, 8, 64, $t0 159 | ltv $v08, 6, 80, $t0 160 | ltv $v08, 4, 96, $t0 161 | ltv $v08, 2, 112, $t0 162 | jr $ra 163 | nop`); 164 | }); 165 | 166 | test('transpose() - 8x8 src-target', async () => { 167 | const {asm, warn} = await transpileSource(`function test() { 168 | vec16<$v08> v0; 169 | vec16<$v16> v1; 170 | u16 buff; 171 | v1 = transpose(v0, buff, 8, 8); 172 | }`, CONF); 173 | 174 | expect(warn).toBe(""); 175 | expect(asm).toBe(`test: 176 | stv $v08, 0, 0, $t0 177 | stv $v08, 2, 16, $t0 178 | stv $v08, 4, 32, $t0 179 | stv $v08, 6, 48, $t0 180 | stv $v08, 8, 64, $t0 181 | stv $v08, 10, 80, $t0 182 | stv $v08, 12, 96, $t0 183 | stv $v08, 14, 112, $t0 184 | ltv $v16, 14, 16, $t0 185 | ltv $v16, 12, 32, $t0 186 | ltv $v16, 10, 48, $t0 187 | ltv $v16, 8, 64, $t0 188 | ltv $v16, 6, 80, $t0 189 | ltv $v16, 4, 96, $t0 190 | ltv $v16, 2, 112, $t0 191 | ltv $v16, 0, 0, $t0 192 | jr $ra 193 | nop`); 194 | }); 195 | 196 | test('transpose() - 4x4 in-place', async () => { 197 | const {asm, warn} = await transpileSource(`function test() { 198 | vec16<$v08> v0; 199 | u16 buff; 200 | v0 = transpose(v0, buff, 4, 4); 201 | }`, CONF); 202 | 203 | expect(warn).toBe(""); 204 | expect(asm).toBe(`test: 205 | stv $v08, 2, 16, $t0 206 | stv $v08, 4, 32, $t0 207 | stv $v08, 6, 48, $t0 208 | stv $v08, 10, 80, $t0 209 | stv $v08, 12, 96, $t0 210 | stv $v08, 14, 112, $t0 211 | ltv $v08, 14, 16, $t0 212 | ltv $v08, 12, 32, $t0 213 | ltv $v08, 10, 48, $t0 214 | ltv $v08, 6, 80, $t0 215 | ltv $v08, 4, 96, $t0 216 | ltv $v08, 2, 112, $t0 217 | jr $ra 218 | nop`); 219 | }); 220 | 221 | test('transpose() - 4x4 src-target', async () => { 222 | const {asm, warn} = await transpileSource(`function test() { 223 | vec16<$v08> v0; 224 | vec16<$v16> v1; 225 | u16 buff; 226 | v1 = transpose(v0, buff, 4, 4); 227 | }`, CONF); 228 | 229 | expect(warn).toBe(""); 230 | expect(asm).toBe(`test: 231 | stv $v08, 0, 0, $t0 232 | stv $v08, 2, 16, $t0 233 | stv $v08, 4, 32, $t0 234 | stv $v08, 6, 48, $t0 235 | stv $v08, 10, 80, $t0 236 | stv $v08, 12, 96, $t0 237 | stv $v08, 14, 112, $t0 238 | ltv $v16, 14, 16, $t0 239 | ltv $v16, 12, 32, $t0 240 | ltv $v16, 10, 48, $t0 241 | ltv $v16, 6, 80, $t0 242 | ltv $v16, 4, 96, $t0 243 | ltv $v16, 2, 112, $t0 244 | ltv $v16, 0, 0, $t0 245 | jr $ra 246 | nop`); 247 | }); 248 | 249 | test('abs() - 16bit', async () => { 250 | const {asm, warn} = await transpileSource(`function test() { 251 | vec16<$v08> v0; 252 | vec16<$v16> v1; 253 | TEST_0: 254 | v0 = abs(v1); 255 | TEST_1: 256 | v1 = abs(v1); 257 | }`, CONF); 258 | 259 | expect(warn).toBe(""); 260 | expect(asm).toBe(`test: 261 | TEST_0: 262 | vabs $v08, $v16, $v16 263 | TEST_1: 264 | vabs $v16, $v16, $v16 265 | jr $ra 266 | nop`); 267 | }); 268 | 269 | test('Interface', async () => { 270 | const {asm, warn} = await transpileSource(`function test() { 271 | u32 a; 272 | a = get_ticks(); 273 | }`, CONF); 274 | 275 | expect(warn).toBe(""); 276 | expect(asm).toBe(`test: 277 | mfc0 $t0, COP0_DP_CLOCK 278 | jr $ra 279 | nop`); 280 | }); 281 | }); -------------------------------------------------------------------------------- /src/tests/compare.test.js: -------------------------------------------------------------------------------- 1 | import {transpileSource} from "../lib/transpiler"; 2 | 3 | const CONF = {rspqWrapper: false}; 4 | 5 | describe('Comparison', () => 6 | { 7 | test('Vector (vec16 vs vec16)', async () => { 8 | const {asm, warn} = await transpileSource(`function test() { 9 | vec16<$v01> res, a, b; 10 | res = a < b; 11 | res = a >= b; 12 | res = a == b; 13 | res = a != b; 14 | }`, CONF); 15 | 16 | expect(warn).toBe(""); 17 | expect(asm).toBe(`test: 18 | vlt $v01, $v02, $v03 19 | vge $v01, $v02, $v03 20 | veq $v01, $v02, $v03 21 | vne $v01, $v02, $v03 22 | jr $ra 23 | nop`); 24 | }); 25 | 26 | test('Vector (vec16 vs const)', async () => { 27 | const {asm, warn} = await transpileSource(`function test() { 28 | vec16<$v01> res, a, b; 29 | res = a < 0; 30 | res = a >= 2; 31 | res = a == 32; 32 | res = a != 256; 33 | }`, CONF); 34 | 35 | expect(warn).toBe(""); 36 | expect(asm).toBe(`test: 37 | vlt $v01, $v02, $v00.e0 38 | vge $v01, $v02, $v30.e6 39 | veq $v01, $v02, $v30.e2 40 | vne $v01, $v02, $v31.e7 41 | jr $ra 42 | nop`); 43 | }); 44 | 45 | test('Vector-Select (vec16)', async () => { 46 | const {asm, warn} = await transpileSource(`function test() { 47 | vec16<$v01> res, a, b; 48 | res = select(a, b); 49 | res = select(a, 32); 50 | // res = select(64, b); // INVALID 51 | // res = select(2, 4); // INVALID 52 | }`, CONF); 53 | 54 | expect(warn).toBe(""); 55 | expect(asm).toBe(`test: 56 | vmrg $v01, $v02, $v03 57 | vmrg $v01, $v02, $v30.e2 58 | jr $ra 59 | nop`); 60 | }); 61 | 62 | test('Vector-Select (vec32)', async () => { 63 | const {asm, warn} = await transpileSource(`function test() { 64 | vec32<$v01> res, a, b; 65 | A: 66 | res = select(a, b); 67 | B: 68 | res = select(a, b.y); 69 | C: 70 | res = select(a, 32); 71 | // res = select(64, b); // INVALID 72 | // res = select(2, 4); // INVALID 73 | }`, CONF); 74 | 75 | expect(warn).toBe(""); 76 | expect(asm).toBe(`test: 77 | A: 78 | vmrg $v01, $v03, $v05 79 | vmrg $v02, $v04, $v06 80 | B: 81 | vmrg $v01, $v03, $v05.e1 82 | vmrg $v02, $v04, $v06.e1 83 | C: 84 | vmrg $v01, $v03, $v30.e2 85 | vmrg $v02, $v04, $v00.e2 86 | jr $ra 87 | nop`); 88 | }); 89 | 90 | test('Vector-Select (vec32 cast)', async () => { 91 | const {asm, warn} = await transpileSource(`function test() { 92 | vec32<$v01> res, a, b; 93 | 94 | res:sint = select(a, b:sfract); 95 | res:sfract = select(a, 32); 96 | 97 | // res = select(64, b); // INVALID 98 | // res = select(2, 4); // INVALID 99 | }`, CONF); 100 | 101 | expect(warn).toBe(""); 102 | expect(asm).toBe(`test: 103 | vmrg $v01, $v03, $v06 104 | vmrg $v02, $v04, $v00.e2 105 | jr $ra 106 | nop`); 107 | }); 108 | 109 | test('Vector-Ternary (vec16)', async () => { 110 | const {asm, warn} = await transpileSource(`function test() { 111 | vec16<$v01> res, a, b; 112 | vec16<$v10> x, y; 113 | 114 | A: 115 | res = x != y ? a : b; 116 | B: 117 | res = x != 4 ? a : 32; 118 | }`, CONF); 119 | 120 | expect(warn).toBe(""); 121 | expect(asm).toBe(`test: 122 | A: 123 | vne $v29, $v10, $v11 124 | vmrg $v01, $v02, $v03 125 | B: 126 | vne $v29, $v10, $v30.e5 127 | vmrg $v01, $v02, $v30.e2 128 | jr $ra 129 | nop`); 130 | }); 131 | 132 | test('Vector-Ternary (vec32)', async () => { 133 | const {asm, warn} = await transpileSource(`function test() { 134 | vec32<$v01> res, a, b; 135 | vec16<$v10> x, y; 136 | 137 | A: 138 | res = x != y ? a : b; 139 | B: 140 | res = x != 4 ? a : 32; 141 | }`, CONF); 142 | 143 | expect(warn).toBe(""); 144 | expect(asm).toBe(`test: 145 | A: 146 | vne $v29, $v10, $v11 147 | vmrg $v01, $v03, $v05 148 | vmrg $v02, $v04, $v06 149 | B: 150 | vne $v29, $v10, $v30.e5 151 | vmrg $v01, $v03, $v30.e2 152 | vmrg $v02, $v04, $v00.e2 153 | jr $ra 154 | nop`); 155 | }); 156 | 157 | test('Vector-Ternary (swizzle)', async () => { 158 | const {asm, warn} = await transpileSource(`function test() { 159 | vec16<$v01> res, a, b; 160 | A: 161 | res = a == b ? a : b.y; 162 | B: 163 | res = a >= b.z ? a : b.y; 164 | C: 165 | res = a == b.z ? a : b; 166 | }`, CONF); 167 | 168 | expect(warn).toBe(""); 169 | expect(asm).toBe(`test: 170 | A: 171 | veq $v29, $v02, $v03 172 | vmrg $v01, $v02, $v03.e1 173 | B: 174 | vge $v29, $v02, $v03.e2 175 | vmrg $v01, $v02, $v03.e1 176 | C: 177 | veq $v29, $v02, $v03.e2 178 | vmrg $v01, $v02, $v03 179 | jr $ra 180 | nop`); 181 | }); 182 | }); -------------------------------------------------------------------------------- /src/tests/const.test.js: -------------------------------------------------------------------------------- 1 | import {transpileSource} from "../lib/transpiler"; 2 | 3 | const CONF = {rspqWrapper: false}; 4 | 5 | describe('Const', () => 6 | { 7 | test('Const Declaration', async () => { 8 | const {asm, warn} = await transpileSource(`function test() 9 | { 10 | const u32<$t0> a = 1234; 11 | const u32<$t1> b = a + a; 12 | const vec16<$v01> c = 0; 13 | }`, CONF); 14 | 15 | expect(warn).toBe(""); 16 | expect(asm).toBe(`test: 17 | addiu $t0, $zero, 1234 18 | addu $t1, $t0, $t0 19 | vxor $v01, $v00, $v00.e0 20 | jr $ra 21 | nop`); 22 | }); 23 | 24 | test('Const invalid (scalar)', async () => 25 | { 26 | const src = `function test() { 27 | const u32<$t0> a = 1234; 28 | a += 1; 29 | }`; 30 | await expect(() => transpileSource(src, CONF)) 31 | .rejects.toThrowError(/line 3: Cannot assign to constant variable!/); 32 | }); 33 | 34 | test('Const invalid (vector)', async () => 35 | { 36 | const src = `function test() { 37 | const vec16<$v01> a = 0; 38 | a += a; 39 | }`; 40 | await expect(() => transpileSource(src, CONF)) 41 | .rejects.toThrowError(/line 3: Cannot assign to constant variable!/); 42 | }); 43 | }); -------------------------------------------------------------------------------- /src/tests/control.test.js: -------------------------------------------------------------------------------- 1 | import {transpileSource} from "../lib/transpiler"; 2 | 3 | const CONF = {rspqWrapper: false}; 4 | 5 | describe('Control', () => 6 | { 7 | test('Exit', async () => { 8 | const {asm, warn} = await transpileSource(`function test() 9 | { 10 | exit; 11 | }`, CONF); 12 | 13 | expect(warn).toBe(""); 14 | expect(asm).toBe(`test: 15 | j RSPQ_Loop 16 | nop 17 | jr $ra 18 | nop`); 19 | }); 20 | }); -------------------------------------------------------------------------------- /src/tests/e2e/e2e.test.js: -------------------------------------------------------------------------------- 1 | import {describe, expect, jest, test} from '@jest/globals'; 2 | import {compileAndCreateEmu} from "../helper/compileAndEmu.js"; 3 | 4 | describe('E2E', () => 5 | { 6 | // @TODO: write proper tests, add basic armips support 7 | test('Test', async () => 8 | { 9 | const rsp = await compileAndCreateEmu(` 10 | function main() { 11 | vec16<$v02> b = 2; 12 | b.x = 4; 13 | b *= 2; 14 | }`); 15 | expect(rsp.getVPR("$v02")).toEqual([0,0,0,0, 0,0,0,0]); 16 | rsp.step(3); 17 | expect(rsp.getVPR("$v02")).toEqual([8,4,4,4, 4,4,4,4]); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /src/tests/examples.test.js: -------------------------------------------------------------------------------- 1 | import {transpileSource} from "../lib/transpiler"; 2 | import {EXAMPLE_CODE} from "../web/js/exampleCode.js"; 3 | import {readFileSync} from "fs"; 4 | 5 | const CONF = {rspqWrapper: true}; 6 | 7 | describe('Examples', () => 8 | { 9 | test('Example code', async () => { 10 | const {asm, warn} = await transpileSource(EXAMPLE_CODE, CONF); 11 | 12 | expect(warn.toLowerCase()).not.toContain("error"); 13 | expect(asm).toBeDefined(); 14 | }); 15 | 16 | test('Example code - Squares 2D', async () => { 17 | const code = readFileSync("./src/tests/examples/squares2d.rspl", "utf8"); 18 | const {asm, warn} = await transpileSource(code, CONF); 19 | 20 | expect(warn.toLowerCase()).not.toContain("error"); 21 | expect(asm).toBeDefined(); 22 | }); 23 | 24 | test('Example code - 3D', async () => { 25 | const code = readFileSync("./src/tests/examples/3d.rspl", "utf8"); 26 | const expectedASM = readFileSync("./src/tests/examples/3d.S", "utf8"); 27 | 28 | const {asm, warn} = await transpileSource(code, {rspqWrapper: true, optimize: true}); 29 | expect(warn.toLowerCase()).not.toContain("error"); 30 | expect(asm).toEqual(expectedASM); 31 | }); 32 | 33 | test('Example code - Mandelbrot', async () => { 34 | const code = readFileSync("./src/tests/examples/mandelbrot.rspl", "utf8"); 35 | const {asm, warn} = await transpileSource(code, CONF); 36 | 37 | expect(warn.toLowerCase()).not.toContain("error"); 38 | expect(asm).toBeDefined(); 39 | }); 40 | 41 | test('Matrix x Vector', async () => { 42 | const {asm, warn} = await transpileSource(`include "rsp_queue.inc" 43 | state { 44 | vec32 VEC_SLOTS[20]; 45 | } 46 | 47 | command<0> VecCmd_Transform(u32 vec_out, u32 mat_in) 48 | { 49 | u32<$t0> trans_mtx = mat_in >> 16; 50 | trans_mtx &= 0xFF0; 51 | 52 | u32<$t1> trans_vec = mat_in & 0xFF0; 53 | u32<$t2> trans_out = vec_out & 0xFF0; 54 | 55 | trans_mtx += VEC_SLOTS; 56 | trans_vec += VEC_SLOTS; 57 | trans_out += VEC_SLOTS; 58 | 59 | vec32<$v01> mat0 = load(trans_mtx, 0).xyzwxyzw; 60 | vec32<$v03> mat1 = load(trans_mtx, 8).xyzwxyzw; 61 | vec32<$v05> mat2 = load(trans_mtx, 0x20).xyzwxyzw; 62 | vec32<$v07> mat3 = load(trans_mtx, 0x28).xyzwxyzw; 63 | 64 | vec32<$v09> vecIn = load(trans_vec); 65 | vec32<$v13> res; 66 | 67 | res = mat0 * vecIn.xxxxXXXX; 68 | res = mat1 +* vecIn.yyyyYYYY; 69 | res = mat2 +* vecIn.zzzzZZZZ; 70 | res = mat3 +* vecIn.wwwwWWWW; 71 | 72 | store(res, trans_out); 73 | } 74 | 75 | include "rsp_rdpq.inc" 76 | `, CONF); 77 | 78 | expect(warn.toLowerCase()).not.toContain("error"); 79 | expect(asm.trimEnd()).toBe(`## Auto-generated file, transpiled with RSPL 80 | #include 81 | 82 | .set noreorder 83 | .set noat 84 | .set nomacro 85 | 86 | #undef zero 87 | #undef at 88 | #undef v0 89 | #undef v1 90 | #undef a0 91 | #undef a1 92 | #undef a2 93 | #undef a3 94 | #undef t0 95 | #undef t1 96 | #undef t2 97 | #undef t3 98 | #undef t4 99 | #undef t5 100 | #undef t6 101 | #undef t7 102 | #undef s0 103 | #undef s1 104 | #undef s2 105 | #undef s3 106 | #undef s4 107 | #undef s5 108 | #undef s6 109 | #undef s7 110 | #undef t8 111 | #undef t9 112 | #undef k0 113 | #undef k1 114 | #undef gp 115 | #undef sp 116 | #undef fp 117 | #undef ra 118 | .equ hex.$zero, 0 119 | .equ hex.$at, 1 120 | .equ hex.$v0, 2 121 | .equ hex.$v1, 3 122 | .equ hex.$a0, 4 123 | .equ hex.$a1, 5 124 | .equ hex.$a2, 6 125 | .equ hex.$a3, 7 126 | .equ hex.$t0, 8 127 | .equ hex.$t1, 9 128 | .equ hex.$t2, 10 129 | .equ hex.$t3, 11 130 | .equ hex.$t4, 12 131 | .equ hex.$t5, 13 132 | .equ hex.$t6, 14 133 | .equ hex.$t7, 15 134 | .equ hex.$s0, 16 135 | .equ hex.$s1, 17 136 | .equ hex.$s2, 18 137 | .equ hex.$s3, 19 138 | .equ hex.$s4, 20 139 | .equ hex.$s5, 21 140 | .equ hex.$s6, 22 141 | .equ hex.$s7, 23 142 | .equ hex.$t8, 24 143 | .equ hex.$t9, 25 144 | .equ hex.$k0, 26 145 | .equ hex.$k1, 27 146 | .equ hex.$gp, 28 147 | .equ hex.$sp, 29 148 | .equ hex.$fp, 30 149 | .equ hex.$ra, 31 150 | #define vco 0 151 | #define vcc 1 152 | #define vce 2 153 | 154 | .data 155 | RSPQ_BeginOverlayHeader 156 | RSPQ_DefineCommand VecCmd_Transform, 8 157 | RSPQ_EndOverlayHeader 158 | 159 | RSPQ_BeginSavedState 160 | STATE_MEM_START: 161 | .align 4 162 | VEC_SLOTS: .ds.b 640 163 | STATE_MEM_END: 164 | RSPQ_EndSavedState 165 | 166 | .text 167 | OVERLAY_CODE_START: 168 | 169 | VecCmd_Transform: 170 | srl $t0, $a1, 16 171 | andi $t0, $t0, 4080 172 | andi $t1, $a1, 4080 173 | andi $t2, $a0, 4080 174 | addiu $t0, $t0, %lo(VEC_SLOTS) 175 | addiu $t1, $t1, %lo(VEC_SLOTS) 176 | addiu $t2, $t2, %lo(VEC_SLOTS) 177 | ldv $v01, 0, 0, $t0 178 | ldv $v01, 8, 0, $t0 179 | ldv $v02, 0, 8, $t0 180 | ldv $v02, 8, 8, $t0 181 | ldv $v03, 0, 8, $t0 182 | ldv $v03, 8, 8, $t0 183 | ldv $v04, 0, 16, $t0 184 | ldv $v04, 8, 16, $t0 185 | ldv $v05, 0, 32, $t0 186 | ldv $v05, 8, 32, $t0 187 | ldv $v06, 0, 40, $t0 188 | ldv $v06, 8, 40, $t0 189 | ldv $v07, 0, 40, $t0 190 | ldv $v07, 8, 40, $t0 191 | ldv $v08, 0, 48, $t0 192 | ldv $v08, 8, 48, $t0 193 | lqv $v09, 0, 0, $t1 194 | lqv $v10, 0, 16, $t1 195 | vmudl $v29, $v02, $v10.h0 196 | vmadm $v29, $v01, $v10.h0 197 | vmadn $v14, $v02, $v09.h0 198 | vmadh $v13, $v01, $v09.h0 199 | vmadl $v29, $v04, $v10.h1 200 | vmadm $v29, $v03, $v10.h1 201 | vmadn $v14, $v04, $v09.h1 202 | vmadh $v13, $v03, $v09.h1 203 | vmadl $v29, $v06, $v10.h2 204 | vmadm $v29, $v05, $v10.h2 205 | vmadn $v14, $v06, $v09.h2 206 | vmadh $v13, $v05, $v09.h2 207 | vmadl $v29, $v08, $v10.h3 208 | vmadm $v29, $v07, $v10.h3 209 | vmadn $v14, $v08, $v09.h3 210 | vmadh $v13, $v07, $v09.h3 211 | sqv $v13, 0, 0, $t2 212 | sqv $v14, 0, 16, $t2 213 | j RSPQ_Loop 214 | nop 215 | 216 | OVERLAY_CODE_END: 217 | 218 | #define zero $0 219 | #define v0 $2 220 | #define v1 $3 221 | #define a0 $4 222 | #define a1 $5 223 | #define a2 $6 224 | #define a3 $7 225 | #define t0 $8 226 | #define t1 $9 227 | #define t2 $10 228 | #define t3 $11 229 | #define t4 $12 230 | #define t5 $13 231 | #define t6 $14 232 | #define t7 $15 233 | #define s0 $16 234 | #define s1 $17 235 | #define s2 $18 236 | #define s3 $19 237 | #define s4 $20 238 | #define s5 $21 239 | #define s6 $22 240 | #define s7 $23 241 | #define t8 $24 242 | #define t9 $25 243 | #define k0 $26 244 | #define k1 $27 245 | #define gp $28 246 | #define sp $29 247 | #define fp $30 248 | #define ra $31 249 | 250 | .set at 251 | .set macro 252 | #include `); 253 | }); 254 | }); -------------------------------------------------------------------------------- /src/tests/examples/mandelbrot.rspl: -------------------------------------------------------------------------------- 1 | include "rsp_queue.inc" 2 | include "rdpq_macros.h" 3 | 4 | state 5 | { 6 | vec32 OFFSET; 7 | vec32 SCALE; 8 | 9 | vec16 COLOR_BUFF[128]; 10 | u32 ITERATIONS; 11 | } 12 | 13 | macro flushScreen(u32 ptrScreen, u32 copySize) 14 | { 15 | u32<$t0> dmaSize = copySize; 16 | // async, the next time this data-slice is touched is in the next frame 17 | dma_out_async(COLOR_BUFF, ptrScreen, dmaSize); 18 | ptrScreen += copySize; 19 | ptrScreen += copySize; // skip every other line 20 | } 21 | 22 | macro complexMul(vec32 res, vec16 mask) 23 | { 24 | const vec32 resSq = res * res; 25 | const vec32 resSqDiff = resSq - resSq.yywwYYWW; 26 | 27 | vec32 res2 = res * res.xxzzXXZZ; 28 | res2 += res2; 29 | 30 | res = mask != 0 ? res2 : resSqDiff; 31 | } 32 | 33 | macro mandelbrot(vec16 color, vec32 c, u32 maxIter, vec16 maskOddEven) 34 | { 35 | vec32 res = 0; 36 | 37 | // 1=unset, 0=done, pre-shifted for easy color values 38 | vec16 colorMaskAdd = 0b1'0000'0000; 39 | 40 | u32 i = maxIter - 1; 41 | u32 iEnd = 0xFFFF; 42 | vec16 isOutside; 43 | 44 | LOOP: // @TODO: implement do-while loop 45 | { 46 | i -= 1; 47 | complexMul(res, maskOddEven); 48 | res += c; 49 | 50 | // test if we can stop the loop 51 | isOutside = res:uint + res:uint.yywwYYWW; 52 | isOutside:sfract *= isOutside; //distance check 53 | 54 | // lanes .xzXZ become either 0x0000 or 0xFFFF 55 | isOutside = isOutside >= 1 ? maskOddEven : maskOddEven.w; 56 | 57 | // mask out color-addend (if set), and apply to the color 58 | colorMaskAdd &= isOutside; 59 | color += colorMaskAdd; 60 | 61 | const u32 maskA = colorMaskAdd.x; 62 | const u32 maskB = colorMaskAdd.z; 63 | const u32 maskC = colorMaskAdd.X; 64 | const u32 maskD = colorMaskAdd.Z; 65 | 66 | // if all pixels are set (mask=0), stop 67 | // otherwise use the large number as the loop condition 68 | iEnd = maskA | maskB; 69 | iEnd |= maskC; 70 | iEnd |= maskD; 71 | 72 | // vector version (slower) 73 | //vec16 maskAll = colorMaskAdd | colorMaskAdd.zzzzZZZZ; 74 | //maskAll |= maskAll.X; // result is in .x 75 | 76 | // Abuse undeflow here, once 'i' is below zero, it's bigger than 'iEnd'. 77 | // The other exit condition sets 'iEnd' itself to zero. 78 | if(i < iEnd)goto LOOP; 79 | } 80 | } 81 | 82 | command<0> Cmd_Render(u32 ptrScreen, u32 sizeXY, u32 isOddLine) 83 | { 84 | u32<$s4> _; // reserved for dma stuff 85 | 86 | const u32 maxIter = load(ITERATIONS); 87 | s32 sizeY = sizeXY & 0xFFFF; 88 | 89 | // bytes to copy per batch, a batch is a screen-line 90 | u32 copySize = sizeXY >> 16; 91 | copySize *= 2; // 2-bytes per pixel 92 | 93 | // internal buffer in DMEM, start/end for a single batch 94 | u32 colorBuff = COLOR_BUFF; 95 | const u32 colorBuffEnd = colorBuff + copySize; 96 | 97 | const vec32 posScale = load(SCALE); 98 | const vec32 offset = load(OFFSET); 99 | 100 | vec32 incX = 0; 101 | incX:sint.x = 4; 102 | incX:sint.z = 4; 103 | incX:sint.X = 4; 104 | incX:sint.Z = 4; 105 | 106 | vec32 incY = 0; 107 | incY:sint.y = 2; 108 | incY:sint.w = 2; 109 | incY:sint.Y = 2; 110 | incY:sint.W = 2; 111 | 112 | incX *= posScale; 113 | incY *= posScale; 114 | 115 | vec32 pos = 0; 116 | //pos:sint.x = 0; 117 | pos:sint.z = 1; 118 | pos:sint.X = 2; 119 | pos:sint.Z = 3; 120 | 121 | if(isOddLine) { 122 | ptrScreen += copySize; 123 | pos:sint.y = 1; 124 | pos:sint.w = 1; 125 | pos:sint.Y = 1; 126 | pos:sint.W = 1; 127 | } 128 | 129 | pos *= posScale; 130 | pos += offset; 131 | 132 | // mask used in complexMul() & mandelbrot loop as a color mask 133 | vec16 maskOddEven = 0; 134 | maskOddEven.y = 0xFFFF; 135 | maskOddEven.w = maskOddEven.y; 136 | maskOddEven.Y = maskOddEven.y; 137 | maskOddEven.W = maskOddEven.y; 138 | 139 | vec32 posOrgX = pos; 140 | 141 | while(sizeY != 0) 142 | { 143 | while(colorBuff != colorBuffEnd) 144 | { 145 | vec16 color = 0; 146 | mandelbrot(color, pos, maxIter, maskOddEven); 147 | 148 | store_vec_s8(color, colorBuff); 149 | 150 | colorBuff += 0x08; 151 | pos += incX; 152 | } 153 | 154 | flushScreen(ptrScreen, copySize); 155 | 156 | posOrgX += incY; 157 | colorBuff = COLOR_BUFF; 158 | sizeY -= 2; 159 | pos = posOrgX; 160 | } 161 | 162 | } 163 | 164 | command<1> Cmd_SetScale(u32 iter, s32 scaleXY, s32 offsetX, s32 offsetY) 165 | { 166 | vec32 scale = 0; 167 | scale.x = scaleXY; 168 | scale.y = scaleXY; 169 | scale.z = scale.x; 170 | scale.w = scale.y; 171 | scale.X = scale.x; 172 | scale.Y = scale.y; 173 | scale.Z = scale.x; 174 | scale.W = scale.y; 175 | 176 | store(scale, SCALE); 177 | 178 | vec32 offset = 0; 179 | offset.x = offsetX; 180 | offset.y = offsetY; 181 | offset.z = offset.x; 182 | offset.w = offset.y; 183 | offset.X = offset.x; 184 | offset.Y = offset.y; 185 | offset.Z = offset.x; 186 | offset.W = offset.y; 187 | 188 | store(offset, OFFSET); 189 | 190 | iter &= 0xFFFF; 191 | store(iter, ITERATIONS); 192 | } 193 | 194 | include "rsp_rdpq.inc" 195 | -------------------------------------------------------------------------------- /src/tests/examples/squares2d.rspl: -------------------------------------------------------------------------------- 1 | include "rsp_queue.inc" 2 | include "rdpq_macros.h" 3 | 4 | state 5 | { 6 | extern u32 RDPQ_CMD_STAGING; 7 | 8 | vec32 TRI2D_DATA[0x20]; 9 | u32 BUFF_END; 10 | u32 VER_OFFSET; 11 | 12 | vec32 CIRLCE_POS; 13 | vec32 CIRLCE_DIR; 14 | 15 | u32 RET_ADDR; 16 | u32 TIME; 17 | } 18 | 19 | function t() 20 | { 21 | u32<$t0> i=0; 22 | u32<$t1> j=0; 23 | while(i<10) { 24 | while(j<20) { 25 | j+=1; 26 | } 27 | i+=1; 28 | } 29 | } 30 | 31 | function RDPQ_Send(u32<$s4> buffStart, u32<$s3> buffEnd); 32 | 33 | function RDPQ_Triangle( 34 | u32<$a0> triCmd, 35 | u32<$a1> ptrVert0, u32<$a2> ptrVert1, u32<$a3> ptrVert2, 36 | u32<$v0> cull, u32<$s3>buffOut 37 | ); 38 | 39 | function DMAIn(u32<$t0> size, u32<$t1> pitch, u32<$s0> rdram, u32<$s4> dmem); 40 | 41 | macro storeVert(u32 ptrDest, vec16 pos, u32 rgba) 42 | { 43 | //u32<$t0> posZ = 1; 44 | store(pos, ptrDest); 45 | //store(posZ, ptrDest, 4); 46 | store(rgba, ptrDest, 8); 47 | } 48 | 49 | function storeTri(vec16<$v20> pos, u32<$t3> rgba) 50 | { 51 | store(RA, RET_ADDR); 52 | 53 | u32<$t9> vertOffset = load(VER_OFFSET); 54 | u32<$a1> vert0 = vertOffset + TRI2D_DATA; 55 | u32<$a2> vert1 = vert0 + 0x20; 56 | u32<$a3> vert2 = vert1 + 0x20; 57 | u32 vert3 = vert2 + 0x20; 58 | 59 | vec16 posLocal; 60 | posLocal.x = pos.x; 61 | posLocal.y = pos.y; 62 | 63 | vec16 offsetX = 0; 64 | offsetX.x = pos.z; 65 | vec16 offsetY = 0; 66 | offsetY.y = pos.z; 67 | 68 | rgba |= 0x000000FF; 69 | 70 | // store vert 1 - 4 71 | storeVert(vert0, posLocal, rgba); 72 | 73 | posLocal += offsetX; 74 | storeVert(vert1, posLocal, rgba); 75 | 76 | posLocal += offsetY; 77 | storeVert(vert2, posLocal, rgba); 78 | 79 | posLocal -= offsetX; 80 | storeVert(vert3, posLocal, rgba); 81 | 82 | // append tris to queue 83 | u32<$s3> buffEnd = load(BUFF_END); 84 | RDPQ_Triangle(0xCF00, vert0, vert1, vert2, 2, buffEnd); 85 | 86 | vert0 += 0x40; 87 | vert1 += 0x40; 88 | vert2 -= 0x40; 89 | RDPQ_Triangle(0xCF00, vert0, vert1, vert2, 2, buffEnd); 90 | store(buffEnd, BUFF_END); 91 | 92 | // store vertex data offset 93 | vertOffset = load(VER_OFFSET); 94 | vertOffset += 0x80; 95 | store(vertOffset, VER_OFFSET); 96 | 97 | // flush & draw vertex buffer 98 | if(vertOffset > 0x200) 99 | { 100 | u32<$s4> buffer = RDPQ_CMD_STAGING; 101 | RDPQ_Send(buffer, buffEnd); 102 | buffEnd = RDPQ_CMD_STAGING; 103 | store(buffEnd, BUFF_END); 104 | vertOffset = 0; 105 | store(vertOffset, VER_OFFSET); 106 | } 107 | 108 | RA = load(RET_ADDR); 109 | } 110 | 111 | function getPosColor(u32<$s6> idxX, u32<$s7> idxY) 112 | { 113 | vec32<$v20> tilePos; 114 | u32<$t3> rgba; 115 | 116 | u32 time = load(TIME); 117 | time &= 0xFF; 118 | 119 | // scaling factors / offsets 120 | vec32 scale; 121 | scale.x = 0.0015; // tile scale 122 | scale.y = 12.2; // cirlce scale 123 | 124 | // get tile postion in the grid 125 | u32 startX = idxX << 20; 126 | u32 startY = idxY << 20; 127 | tilePos.x = startX; 128 | tilePos.y = startY; 129 | 130 | // load circle pos and apply velocity 131 | vec32 circleDir = load(CIRLCE_DIR); 132 | vec32 centerDist = load(CIRLCE_POS); 133 | centerDist += circleDir; 134 | store(centerDist, CIRLCE_POS); 135 | 136 | // chek if an edge was hit, invert direction 137 | s32 checkX = centerDist:sint.x; 138 | s32 checkY = centerDist:sint.y; 139 | 140 | if(checkX < 0)circleDir.x = 0.006; 141 | if(checkX > 320)circleDir.x = -0.006; 142 | if(checkY < 0)circleDir.y = 0.006; 143 | if(checkY > 240)circleDir.y = -0.006; 144 | store(circleDir, CIRLCE_DIR); 145 | 146 | // get squared tile-to-circle distance 147 | centerDist -= tilePos; 148 | centerDist *= centerDist; 149 | centerDist *= scale.y; 150 | 151 | // output color 152 | startX <<= 7; 153 | rgba = startX; 154 | startX <<= 8; 155 | rgba |= 0xFF00; 156 | rgba -= startX; 157 | 158 | startY >>= 1; 159 | time <<= 16; 160 | rgba |= time; 161 | rgba += startY; 162 | 163 | // tile scale 164 | centerDist += centerDist.y; 165 | scale *= centerDist.x; 166 | scale.y = 2; 167 | scale += scale.y; 168 | 169 | // convert to screen-space 170 | tilePos *= 4; 171 | tilePos.z = scale.x; 172 | } 173 | 174 | command<0> T3DCmd_Init() 175 | { 176 | store(ZERO, VER_OFFSET); // @TODO: syntax for zero 177 | store(ZERO, TIME); 178 | 179 | vec32 vecData; 180 | vecData.x = 0.006; 181 | vecData.y = vecData.x; 182 | store(vecData, CIRLCE_DIR); 183 | 184 | vecData.x = 42.42; 185 | vecData.y = 12.12; 186 | store(vecData, CIRLCE_POS); 187 | } 188 | 189 | command<1> T3DCmd_Test() 190 | { 191 | u32<$s6> idxX = 0; 192 | u32<$s7> idxY = 0; 193 | 194 | u32 buffEnd = RDPQ_CMD_STAGING; 195 | store(buffEnd, BUFF_END); 196 | 197 | u32 time = load(TIME); // @TODO: syntax 198 | time += 1; 199 | store(time, TIME); 200 | 201 | while(idxY <= 15) 202 | { 203 | u32<$t3> rgba; 204 | vec16<$v20> pos; 205 | getPosColor(idxX, idxY); 206 | 207 | storeTri(pos, rgba); 208 | 209 | idxX += 1; 210 | if(idxX > 19) { 211 | idxX = 0; 212 | idxY += 1; 213 | } 214 | } 215 | } 216 | 217 | include "rsp_rdpq.inc" -------------------------------------------------------------------------------- /src/tests/helper/compileAndEmu.js: -------------------------------------------------------------------------------- 1 | import {transpileSource} from "../../lib/transpiler"; 2 | import {createRSP} from 'rsp-wasm'; 3 | import {assemble} from 'armips'; 4 | 5 | const CONF = {rspqWrapper: false}; 6 | 7 | /** 8 | * Compile RSPL into IMEM/DMEM buffers 9 | * @param {string} source 10 | * @return {Promise} 11 | */ 12 | export async function compileAndCreateEmu(source) 13 | { 14 | let {asm, warn} = await transpileSource(source, CONF); 15 | if(warn)throw new Error(warn); 16 | 17 | // @TODO: add proper armips support 18 | for(let i=0; i<8; ++i) { 19 | asm = asm.replaceAll(".e"+i, "["+i+"]"); 20 | } 21 | 22 | const asmSrc = ` 23 | .rsp 24 | 25 | .create "dmem", 0x0000 26 | TEST_CONST: .db 0x11 27 | .db 0x22 28 | .db 0x33 29 | .db 0x44 30 | .align 2 31 | .close 32 | 33 | .create "imem", 0x1000 34 | ${asm} 35 | 36 | RSPQ_Loop: 37 | j RSPQ_Loop 38 | nop 39 | .close 40 | `; 41 | 42 | const files = await assemble(asmSrc); 43 | return await createRSPEmu(files.imem, files.dmem); 44 | } 45 | 46 | /** 47 | * Creates an RSP emulation instance 48 | * @param {Uint8Array} imem 49 | * @param {Uint8Array} dmem 50 | * @return {Promise} 51 | */ 52 | export async function createRSPEmu(imem, dmem) 53 | { 54 | const rsp = await createRSP(); 55 | 56 | const imemView = new DataView(imem.buffer); 57 | for(let i=0; i 6 | { 7 | test('Unsigned', async () => 8 | { 9 | const {asm, warn} = await transpileSource(` state { u32 TEST_CONST; } 10 | function test() 11 | { 12 | u32<$t0> c = TEST_CONST; 13 | LOAD_A: c = 0; 14 | LOAD_B: c = 0xFF; 15 | LOAD_C: c = 0xFFFF; 16 | LOAD_D: c = 0x7FFF; 17 | LOAD_E: c = 0x8000; 18 | LOAD_F: c = 0xFF120000; 19 | LOAD_G: c = 0xFFFF7FFF; 20 | LOAD_H: c = 0xFFFF8000; 21 | LOAD_I: c = 0xFFFFF; 22 | LOAD_J: c = 0xFFFFFFFF; 23 | }`, CONF); 24 | 25 | expect(warn).toBe(""); 26 | expect(asm).toBe(`test: 27 | ori $t0, $zero, %lo(TEST_CONST) 28 | LOAD_A: 29 | or $t0, $zero, $zero 30 | LOAD_B: 31 | addiu $t0, $zero, 255 32 | LOAD_C: 33 | ori $t0, $zero, 0xFFFF 34 | LOAD_D: 35 | addiu $t0, $zero, 32767 36 | LOAD_E: 37 | ori $t0, $zero, 0x8000 38 | LOAD_F: 39 | lui $t0, 0xFF12 40 | LOAD_G: 41 | lui $t0, 0xFFFF 42 | ori $t0, $t0, 0x7FFF 43 | LOAD_H: 44 | addiu $t0, $zero, -32768 45 | LOAD_I: 46 | lui $t0, 0x0F 47 | ori $t0, $t0, 0xFFFF 48 | LOAD_J: 49 | addiu $t0, $zero, -1 50 | jr $ra 51 | nop`); 52 | }); 53 | 54 | test('Signed', async () => 55 | { 56 | const {asm, warn} = await transpileSource(` state { u32 TEST_CONST; } 57 | function test() 58 | { 59 | s32<$t0> c = TEST_CONST; 60 | LOAD_A: c = 0xFF; 61 | LOAD_B: c = 0xFFFF; 62 | LOAD_C: c = 0xFFFFF; 63 | LOAD_D: c = -255; 64 | LOAD_E: c = -65535; 65 | LOAD_F: c = -1048575; 66 | LOAD_G: c = -2147483648; 67 | LOAD_H: c = 2147483647; 68 | }`, CONF); 69 | 70 | expect(warn).toBe(""); 71 | expect(asm).toBe(`test: 72 | ori $t0, $zero, %lo(TEST_CONST) 73 | LOAD_A: 74 | addiu $t0, $zero, 255 75 | LOAD_B: 76 | ori $t0, $zero, 0xFFFF 77 | LOAD_C: 78 | lui $t0, 0x0F 79 | ori $t0, $t0, 0xFFFF 80 | LOAD_D: 81 | addiu $t0, $zero, -255 82 | LOAD_E: 83 | lui $t0, 0xFFFF 84 | ori $t0, $t0, 0x01 85 | LOAD_F: 86 | lui $t0, 0xFFF0 87 | ori $t0, $t0, 0x01 88 | LOAD_G: 89 | lui $t0, 0x8000 90 | LOAD_H: 91 | lui $t0, 0x7FFF 92 | ori $t0, $t0, 0xFFFF 93 | jr $ra 94 | nop`); 95 | }); 96 | 97 | }); -------------------------------------------------------------------------------- /src/tests/labels.test.js: -------------------------------------------------------------------------------- 1 | import {transpileSource} from "../lib/transpiler"; 2 | 3 | const CONF = {rspqWrapper: false}; 4 | 5 | describe('Labels', () => 6 | { 7 | test('Basic Labels', async () => { 8 | const {asm, warn} = await transpileSource(` 9 | function test_label() 10 | { 11 | label_a: 12 | label_b: label_c: 13 | }`, CONF); 14 | 15 | expect(warn).toBe(""); 16 | expect(asm).toBe(`test_label: 17 | label_a: 18 | label_b: 19 | label_c: 20 | jr $ra 21 | nop`); 22 | }); 23 | 24 | test('Labels with instr.', async () => { 25 | const {asm, warn} = await transpileSource(` 26 | function test_label() 27 | { 28 | u32<$t0> a; 29 | label_a: 30 | a += 1; 31 | goto label_b; 32 | label_b: 33 | a += 2; 34 | goto label_a; 35 | }`, CONF); 36 | 37 | expect(warn).toBe(""); 38 | expect(asm).toBe(`test_label: 39 | label_a: 40 | addiu $t0, $t0, 1 41 | j label_b 42 | nop 43 | label_b: 44 | addiu $t0, $t0, 2 45 | j label_a 46 | nop 47 | jr $ra 48 | nop`); 49 | }); 50 | }); -------------------------------------------------------------------------------- /src/tests/loop.test.js: -------------------------------------------------------------------------------- 1 | import {transpileSource} from "../lib/transpiler"; 2 | 3 | const CONF = {rspqWrapper: false}; 4 | 5 | describe('Loops', () => 6 | { 7 | test('Basic While-Loop', async () => { 8 | const {asm, warn} = await transpileSource(`function test() 9 | { 10 | u32<$t0> i=0; 11 | while(i<10) { 12 | i+=1; 13 | } 14 | }`, CONF); 15 | 16 | expect(warn).toBe(""); 17 | expect(asm).toBe(`test: 18 | or $t0, $zero, $zero 19 | LABEL_test_0001: 20 | sltiu $at, $t0, 10 21 | beq $at, $zero, LABEL_test_0002 22 | nop 23 | addiu $t0, $t0, 1 24 | j LABEL_test_0001 25 | nop 26 | LABEL_test_0002: 27 | jr $ra 28 | nop`); 29 | }); 30 | 31 | test('Nested While-Loop', async () => { 32 | const {asm, warn} = await transpileSource(`function test() 33 | { 34 | u32<$t0> i=0; 35 | u32<$t1> j=0; 36 | 37 | while(i<10) { 38 | while(j<20) { 39 | j+=1; 40 | } 41 | i+=1; 42 | } 43 | }`, CONF); 44 | 45 | expect(warn).toBe(""); 46 | expect(asm).toBe(`test: 47 | or $t0, $zero, $zero 48 | or $t1, $zero, $zero 49 | LABEL_test_0001: 50 | sltiu $at, $t0, 10 51 | beq $at, $zero, LABEL_test_0002 52 | nop 53 | LABEL_test_0003: 54 | sltiu $at, $t1, 20 55 | beq $at, $zero, LABEL_test_0004 56 | nop 57 | addiu $t1, $t1, 1 58 | j LABEL_test_0003 59 | nop 60 | LABEL_test_0004: 61 | addiu $t0, $t0, 1 62 | j LABEL_test_0001 63 | nop 64 | LABEL_test_0002: 65 | jr $ra 66 | nop`); 67 | }); 68 | 69 | test('While Loop - Break', async () => { 70 | const {asm, warn} = await transpileSource(`function test() 71 | { 72 | u32<$t0> i=0; 73 | while(i<10) { 74 | break; 75 | i+=1; 76 | } 77 | }`, CONF); 78 | 79 | expect(warn).toBe(""); 80 | expect(asm).toBe(`test: 81 | or $t0, $zero, $zero 82 | LABEL_test_0001: 83 | sltiu $at, $t0, 10 84 | beq $at, $zero, LABEL_test_0002 85 | nop 86 | j LABEL_test_0002 87 | nop 88 | addiu $t0, $t0, 1 89 | j LABEL_test_0001 90 | nop 91 | LABEL_test_0002: 92 | jr $ra 93 | nop`); 94 | }); 95 | 96 | test('While Loop - scoped Break', async () => { 97 | const {asm, warn} = await transpileSource(`function test() 98 | { 99 | u32<$t0> i=0; 100 | while(i<10) { 101 | if(!i)break; 102 | i+=1; 103 | } 104 | }`, CONF); 105 | 106 | expect(warn).toBe(""); 107 | expect(asm).toBe(`test: 108 | or $t0, $zero, $zero 109 | LABEL_test_0001: 110 | sltiu $at, $t0, 10 111 | beq $at, $zero, LABEL_test_0002 112 | nop 113 | bne $t0, $zero, LABEL_test_0003 114 | nop 115 | j LABEL_test_0002 116 | nop 117 | LABEL_test_0003: 118 | addiu $t0, $t0, 1 119 | j LABEL_test_0001 120 | nop 121 | LABEL_test_0002: 122 | jr $ra 123 | nop`); 124 | }); 125 | 126 | test('While Loop - Continue', async () => { 127 | const {asm, warn} = await transpileSource(`function test() 128 | { 129 | u32<$t0> i=0; 130 | while(i<10) { 131 | continue; 132 | i+=1; 133 | } 134 | }`, CONF); 135 | 136 | expect(warn).toBe(""); 137 | expect(asm).toBe(`test: 138 | or $t0, $zero, $zero 139 | LABEL_test_0001: 140 | sltiu $at, $t0, 10 141 | beq $at, $zero, LABEL_test_0002 142 | nop 143 | j LABEL_test_0001 144 | nop 145 | addiu $t0, $t0, 1 146 | j LABEL_test_0001 147 | nop 148 | LABEL_test_0002: 149 | jr $ra 150 | nop`); 151 | }); 152 | }); -------------------------------------------------------------------------------- /src/tests/macros.test.js: -------------------------------------------------------------------------------- 1 | import {transpileSource} from "../lib/transpiler"; 2 | 3 | const CONF = {rspqWrapper: false}; 4 | 5 | describe('Macros', () => 6 | { 7 | test('Basic replacement', async () => { 8 | const {asm, warn} = await transpileSource(` 9 | macro test(u32 add) { 10 | add += 42; 11 | } 12 | 13 | function test_macro() { 14 | u32<$t2> a; 15 | u32<$s3> b; 16 | test(a); 17 | 18 | if(a < 3) { 19 | test(a); 20 | } 21 | }`, CONF); 22 | 23 | expect(warn).toBe(""); 24 | expect(asm).toBe(`test_macro: 25 | addiu $t2, $t2, 42 26 | sltiu $at, $t2, 3 27 | beq $at, $zero, LABEL_test_macro_0001 28 | nop 29 | addiu $t2, $t2, 42 30 | LABEL_test_macro_0001: 31 | jr $ra 32 | nop`); 33 | }); 34 | 35 | test('Nested macro', async () => { 36 | const {asm, warn} = await transpileSource(` 37 | macro test_b(u32 argB) { 38 | argB += 42; 39 | } 40 | 41 | macro test_a(u32 argA) { 42 | test_b(argA); 43 | } 44 | 45 | function test_macro() { 46 | u32<$t2> a; 47 | test_a(a); 48 | }`, CONF); 49 | 50 | expect(warn).toBe(""); 51 | expect(asm).toBe(`test_macro: 52 | addiu $t2, $t2, 42 53 | jr $ra 54 | nop`); 55 | }); 56 | 57 | test('Scope local', async () => { 58 | const {asm, warn} = await transpileSource(` 59 | macro test_b(u32 argB) { 60 | argB += 42; 61 | } 62 | 63 | function test_macro() { 64 | u32<$t2> a; 65 | u32<$t3> argB; 66 | test_b(a); 67 | }`, CONF); 68 | 69 | expect(warn).toBe(""); 70 | expect(asm).toBe(`test_macro: 71 | addiu $t2, $t2, 42 72 | jr $ra 73 | nop`); 74 | }); 75 | 76 | test('Return Value', async () => { 77 | const {asm, warn} = await transpileSource(` 78 | macro test_a(u32 res, u32 argA, u32 argB) { 79 | res = argA + argB; 80 | } 81 | 82 | function test_macro() { 83 | u32<$a0> argA, argB; 84 | u32<$s0> a = test_a(argA, argB); 85 | }`, CONF); 86 | 87 | expect(warn).toBe(""); 88 | expect(asm).toBe(`test_macro: 89 | addu $s0, $a0, $a1 90 | jr $ra 91 | nop`); 92 | }); 93 | }); -------------------------------------------------------------------------------- /src/tests/optimizer/depScan/optDepScanCtrl.test.js: -------------------------------------------------------------------------------- 1 | import {asm, asmLabel} from "../../../lib/intsructions/asmWriter.js"; 2 | import {asmGetReorderIndices, asmInitDeps} from "../../../lib/optimizer/asmScanDeps.js"; 3 | 4 | function asmLinesToDeps(lines) 5 | { 6 | asmInitDeps({asm: lines}); 7 | return lines.map((line, i) => asmGetReorderIndices(lines, i).sort()); 8 | } 9 | 10 | describe('Optimizer - Dependency Scanner - Control', () => 11 | { 12 | test('Stop at Label', () => { 13 | const lines = [ 14 | /* 00 */ asm("or", ["$t0", "$zero", "$zero"]), 15 | /* 01 */ asm("or", ["$t1", "$zero", "$zero"]), 16 | /* 02 */ asmLabel("SOME_LABEL"), 17 | /* 03 */ asm("or", ["$t2", "$zero", "$zero"]), 18 | ]; 19 | expect(asmLinesToDeps(lines)).toEqual([ 20 | [0, 1], 21 | [0, 1], 22 | [2], 23 | [3], 24 | ]); 25 | }); 26 | 27 | test('Stop at Jump', () => { 28 | const lines = [ 29 | /* 00 */ asm("or", ["$t0", "$zero", "$zero"]), 30 | /* 01 */ asm("or", ["$t1", "$zero", "$zero"]), 31 | /* 02 */ asm("j", ["SOME_WHERE"]), 32 | /* 03 */ asm("or", ["$t2", "$zero", "$zero"]), // delay slot (filled) 33 | /* 04 */ asm("or", ["$t2", "$zero", "$zero"]), 34 | ]; 35 | expect(asmLinesToDeps(lines)).toEqual([ 36 | [0, 1], 37 | [0, 1], 38 | [2], // j 39 | [0, 1, 2, 3], // delay-slot, only move backwards 40 | [4], 41 | ]); 42 | }); 43 | }); -------------------------------------------------------------------------------- /src/tests/optimizer/depScan/optDepScanMem.test.js: -------------------------------------------------------------------------------- 1 | import {asm, asmLabel} from "../../../lib/intsructions/asmWriter.js"; 2 | import {asmGetReorderIndices, asmInitDeps} from "../../../lib/optimizer/asmScanDeps.js"; 3 | import state from "../../../lib/state.js"; 4 | 5 | function asmLinesToDeps(lines) 6 | { 7 | asmInitDeps({asm: lines}); 8 | return lines.map((line, i) => asmGetReorderIndices(lines, i).sort()); 9 | } 10 | 11 | describe('Optimizer - Dependency Scanner - Memory', () => 12 | { 13 | test('Read vs. Read', () => { 14 | const lines = [ 15 | /* 00 */ asm("lw", ["$t0", "0($s1)"]), 16 | /* 01 */ asm("or", ["$t1", "$zero", "$zero"]), 17 | /* 02 */ asm("lw", ["$t2", "0($s1)"]), 18 | /* 03 */ asm("or", ["$t3", "$zero", "$zero"]), 19 | ]; 20 | expect(asmLinesToDeps(lines)).toEqual([ 21 | [0, 1, 2, 3], 22 | [0, 1, 2, 3], 23 | [0, 1, 2, 3], 24 | [0, 1, 2, 3], 25 | ]); 26 | }); 27 | 28 | test('Read vs. Write', () => { 29 | const lines = [ 30 | /* 00 */ asm("lw", ["$t0", "0($s1)"]), 31 | /* 01 */ asm("or", ["$t1", "$zero", "$zero"]), 32 | /* 02 */ asm("sw", ["$t2", "0($s1)"]), 33 | /* 03 */ asm("or", ["$t3", "$zero", "$zero"]), 34 | ]; 35 | expect(asmLinesToDeps(lines)).toEqual([ 36 | [0, 1, 2, 3], 37 | [0, 1, 2, 3], 38 | [0, 1, 2, 3], 39 | [0, 1, 2, 3], 40 | ]); 41 | }); 42 | 43 | test('Read vs. Write (Barrier)', () => { 44 | const lines = [ 45 | /* 00 */ asm("lw", ["$t0", "0($s1)"]), 46 | /* 01 */ asm("or", ["$t1", "$zero", "$zero"]), 47 | /* 02 */ asm("sw", ["$t2", "0($s1)"]), 48 | /* 03 */ asm("or", ["$t3", "$zero", "$zero"]), 49 | ]; 50 | lines[0].annotations = [{name: "Barrier", value: "some barrier"}]; 51 | lines[2].annotations = [{name: "Barrier", value: "some barrier"}]; 52 | 53 | state.reset(); 54 | state.enterFunction("test", "command", 0); 55 | state.pushScope(); 56 | 57 | expect(asmLinesToDeps(lines)).toEqual([ 58 | [0, 1], 59 | [0, 1, 2, 3], 60 | [1, 2, 3], 61 | [0, 1, 2, 3], 62 | ]); 63 | }); 64 | 65 | test('Write vs. Write', () => { 66 | const lines = [ 67 | /* 00 */ asm("sw", ["$t0", "0($s2)"]), 68 | /* 01 */ asm("or", ["$t1", "$zero", "$zero"]), 69 | /* 02 */ asm("sw", ["$t2", "0($s1)"]), 70 | /* 03 */ asm("or", ["$t3", "$zero", "$zero"]), 71 | ]; 72 | expect(asmLinesToDeps(lines)).toEqual([ 73 | [0, 1, 2, 3], 74 | [0, 1, 2, 3], 75 | [0, 1, 2, 3], 76 | [0, 1, 2, 3], 77 | ]); 78 | }); 79 | 80 | }); -------------------------------------------------------------------------------- /src/tests/optimizer/depScan/optDepScanRegs.test.js: -------------------------------------------------------------------------------- 1 | import {asm} from "../../../lib/intsructions/asmWriter.js"; 2 | import {asmGetReorderIndices, asmInitDeps} from "../../../lib/optimizer/asmScanDeps.js"; 3 | 4 | function asmLinesToDeps(lines) 5 | { 6 | asmInitDeps({asm: lines}); 7 | return lines.map((line, i) => asmGetReorderIndices(lines, i).sort()); 8 | } 9 | 10 | describe('Optimizer - Dependency Scanner', () => 11 | { 12 | test('No Deps', () => { 13 | const lines = [ 14 | /* 00 */ asm("or", ["$t0", "$zero", "$zero"]), 15 | /* 01 */ asm("or", ["$t1", "$zero", "$zero"]), 16 | /* 02 */ asm("or", ["$t2", "$zero", "$zero"]), 17 | ]; 18 | expect(asmLinesToDeps(lines)).toEqual([ 19 | [0, 1, 2], 20 | [0, 1, 2], 21 | [0, 1, 2], 22 | ]); 23 | }); 24 | 25 | test('Basic Write Dep', () => { 26 | const lines = [ 27 | /* 00 */ asm("or", ["$t0", "$zero", "$zero"]), // writes to 2's input 28 | /* 01 */ asm("or", ["$t1", "$zero", "$zero"]), 29 | /* 02 */ asm("or", ["$t2", "$t0", "$zero"]), // needs 0's output 30 | /* 03 */ asm("or", ["$t3", "$zero", "$zero"]), 31 | ]; 32 | expect(asmLinesToDeps(lines)).toEqual([ 33 | [0, 1], 34 | [0, 1, 2, 3], 35 | [1, 2, 3], 36 | [0, 1, 2, 3], 37 | ]); 38 | }); 39 | 40 | test('Nested Write Dep', () => { 41 | const lines = [ 42 | /* 00 */ asm("or", ["$t0", "$zero", "$zero"]), // writes to 2's input 43 | /* 01 */ asm("or", ["$t1", "$zero", "$zero"]), 44 | /* 02 */ asm("or", ["$t2", "$t0", "$zero"]), // needs 0's output 45 | /* 03 */ asm("or", ["$t3", "$t2", "$zero"]), // needs 3's output 46 | /* 04 */ asm("or", ["$t4", "$zero", "$zero"]), 47 | ]; 48 | expect(asmLinesToDeps(lines)).toEqual([ 49 | [0, 1], 50 | [0, 1, 2, 3, 4], 51 | [1, 2], 52 | [3, 4], 53 | [0, 1, 2, 3, 4], 54 | ]); 55 | }); 56 | 57 | test('MTC2 (partial write)', () => { 58 | const lines = [ 59 | /* 00 */ asm("vxor", ["$v25", "$v00", "$v00.e0"]), 60 | /* 01 */ asm("addiu", ["$at", "$zero", 3]), 61 | /* 02 */ asm("mtc2", ["$at", "$v25.e6"]), 62 | ]; 63 | 64 | expect(asmLinesToDeps(lines)).toEqual([ 65 | [0, 1, 2], 66 | [0, 1], 67 | [2], 68 | ]); 69 | }); 70 | 71 | test('MTC2 (partial write, no return regs)', () => { 72 | const lines = [ 73 | /* 00 */ asm("vxor", ["$v25", "$v00", "$v00.e0"]), 74 | /* 01 */ asm("vxor", ["$v26", "$v00", "$v00"]), 75 | /* 02 */ asm("mtc2", ["$at", "$v25.e6"]), 76 | ]; 77 | 78 | expect(asmLinesToDeps(lines)).toEqual([ 79 | [0, 1, 2], 80 | [1, 2], // @TODO: allow 0-1 81 | [1, 2], 82 | ]); 83 | }); 84 | 85 | test('MTC2 (partial write, return regs)', () => { 86 | const lines = [ 87 | /* 00 */ asm("vxor", ["$v25", "$v00", "$v00.e0"]), 88 | /* 01 */ asm("vxor", ["$v26", "$v00", "$v00"]), 89 | /* 02 */ asm("mtc2", ["$at", "$v25.e6"]), 90 | ]; 91 | 92 | expect(asmLinesToDeps(lines)).toEqual([ 93 | [0, 1, 2], 94 | [1, 2], // @TODO: allow 0-1 95 | [1, 2], 96 | ]); 97 | }); 98 | 99 | test('Ignore Write when no read (simple)', () => { 100 | const lines = [ 101 | /* 00 */ asm("or", ["$t0", "$zero", "$zero"]), 102 | /* 01 */ asm("or", ["$t0", "$zero", "$zero"]), 103 | /* 02 */ asm("or", ["$t0", "$zero", "$zero"]), 104 | ]; 105 | expect(asmLinesToDeps(lines)).toEqual([ 106 | [0, 1, 2], 107 | [0, 1, 2], 108 | [2], 109 | ]); 110 | }); 111 | 112 | test('Ignore Write when no read (deps)', () => { 113 | const lines = [ 114 | /* 00 */ asm("or", ["$t0", "$zero", "$zero"]), 115 | /* 01 */ asm("or", ["$t0", "$zero", "$zero"]), 116 | /* 02 */ asm("or", ["$t1", "$t0", "$zero"]), // needs 1's output 117 | /* 03 */ asm("or", ["$t0", "$zero", "$zero"]), 118 | /* 04 */ asm("or", ["$t0", "$zero", "$zero"]), 119 | ]; 120 | expect(asmLinesToDeps(lines)).toEqual([ 121 | [0], 122 | [1], 123 | [2], 124 | [3, 4], 125 | [4], 126 | ]); 127 | }); 128 | 129 | test('Hidden Regs (simple)', () => { 130 | const lines = [ 131 | /* 00 */ asm("veq", ["$v11", "$v00", "$v00" ]), // sets VCC 132 | /* 01 */ asm("or", ["$t0", "$zero", "$zero"]), 133 | /* 02 */ asm("or", ["$t0", "$zero", "$zero"]), 134 | /* 03 */ asm("vmrg", ["$v01", "$v02", "$v03" ]), // reads VCC 135 | /* 04 */ asm("or", ["$t0", "$zero", "$zero"]), 136 | ]; 137 | expect(asmLinesToDeps(lines)).toEqual([ 138 | [0, 1, 2], 139 | [0, 1, 2, 3, 4], 140 | [0, 1, 2, 3, 4], 141 | [1, 2, 3, 4], 142 | [3, 4], 143 | ]); 144 | }); 145 | 146 | test('Regs Single-Lane', () => { 147 | const lines = [ 148 | /* 00 */ asm("vmov", ["$v11.e1", "$v05.e1"]), 149 | /* 01 */ asm("vmov", ["$v06.e1", "$v11.e2"]), // same ref, different lane as 0's target 150 | /* 02 */ asm("vmov", ["$v07.e1", "$v11.e1"]), // actual dep 151 | /* 03 */ asm("vmov", ["$v08.e1", "$v05.e1"]), 152 | ]; 153 | expect(asmLinesToDeps(lines)).toEqual([ 154 | [0, 1], 155 | [0, 1, 2, 3], 156 | [1, 2, 3], 157 | [3], 158 | ]); 159 | }); 160 | 161 | test('Offset Syntax', () => { 162 | const lines = [ 163 | /* 00 */ asm("or", ["$t0", "$zero", "$zero"]), 164 | /* 01 */ asm("or", ["$t1", "$zero", "$zero"]), 165 | /* 02 */ asm("lw", ["$t2", "0($t0)"]), 166 | /* 02 */ asm("or", ["$t3", "$zero", "$zero"]), 167 | ]; 168 | expect(asmLinesToDeps(lines)).toEqual([ 169 | [0, 1], 170 | [0, 1, 2, 3], 171 | [1, 2, 3], 172 | [0, 1, 2, 3], 173 | ]); 174 | }); 175 | 176 | test('Vector Example (mul + add)', () => { 177 | const lines = [ 178 | /* 00 */ asm("vmudl", ["$v27", "$v18", "$v26.v"]), 179 | /* 01 */ asm("vmadm", ["$v27", "$v17", "$v26.v"]), 180 | /* 02 */ asm("vmadn", ["$v18", "$v18", "$v25.v"]), 181 | /* 03 */ asm("vmadh", ["$v17", "$v17", "$v25.v"]), 182 | /* 04 */ asm("vaddc", ["$v18", "$v18", "$v24.v"]), 183 | /* 05 */ asm("vadd", ["$v17", "$v17", "$v23.v"]), 184 | ]; 185 | expect(asmLinesToDeps(lines)).toEqual([ 186 | [0], 187 | [1], 188 | [2], 189 | [3], 190 | [4], 191 | [5], 192 | ]); 193 | }); 194 | 195 | test('Vector Example (vabs)', () => { 196 | const lines = [ 197 | /* 00 */ asm("vmacf", ["$v27", "$v27", "$v27"]), 198 | /* 01 */ asm("vabs", ["$v01", "$v01", "$v01"]), 199 | /* 02 */ asm("vmacf", ["$v27", "$v27", "$v27"]), 200 | ]; 201 | expect(asmLinesToDeps(lines)).toEqual([ 202 | [0], 203 | [1], 204 | [2], 205 | ]); 206 | }); 207 | 208 | test('Vector Example ($VCE)', () => { 209 | const lines = [ 210 | /* 00 */ asm("vcr", ["$v01", "$v01", "$v01"]), 211 | /* 01 */ asm("ori", ["$t0", "$t0", "$t0"]), 212 | /* 02 */ asm("vcl", ["$v03", "$v03", "$v03"]), 213 | ]; 214 | expect(asmLinesToDeps(lines)).toEqual([ 215 | [0, 1 ], 216 | [0, 1, 2], 217 | [ 1, 2], 218 | ]); 219 | }); 220 | }); -------------------------------------------------------------------------------- /src/tests/optimizer/depScan/optRegScan.test.js: -------------------------------------------------------------------------------- 1 | import {asm} from "../../../lib/intsructions/asmWriter.js"; 2 | import {asmInitDep, REG_INDEX_MAP, REG_STALL_INDEX_MAP} from "../../../lib/optimizer/asmScanDeps.js"; 3 | 4 | const laneIds = ['_0', '_1', '_2', '_3', '_4', '_5', '_6', '_7']; 5 | function allLanes(reg) { 6 | return laneIds.map((id) => reg + id); 7 | } 8 | 9 | describe('Optimizer - Register Scanner', () => 10 | { 11 | const CASES = { 12 | "Logic": { 13 | asm: asm("or", ["$t0", "$a1", "$a0"]), 14 | src: ["$a1", "$a0"], 15 | tgt: ["$t0"], 16 | srcStall: ["$a1", "$a0"], 17 | tgtStall: ["$t0"], 18 | }, 19 | "Arith": { 20 | asm: asm("addiu", ["$t0", "$t1", 4]), 21 | src: ["$t1"], 22 | tgt: ["$t0"], 23 | srcStall: ["$t1"], 24 | tgtStall: ["$t0"], 25 | }, 26 | "Vec Store": { 27 | asm: asm("sdv", ["$v08", 0, 16, "$s6"]), 28 | src: [...allLanes("$v08"), "$s6"], 29 | tgt: [], 30 | srcStall: ["$v08", "$s6"], 31 | tgtStall: [], 32 | }, 33 | "Vec packed Store": { 34 | asm: asm("sfv", ["$v08", 0, "$s6"]), 35 | src: [...allLanes("$v08"), "$s6"], 36 | tgt: [], 37 | srcStall: ["$v08", "$s6"], 38 | tgtStall: [], 39 | }, 40 | "Lanes - Vec move": { 41 | asm: asm("vmov", ["$v07.e3", "$v05.e2"]), 42 | src: ["$v05_2"], 43 | tgt: ["$v07_3", "$acc"], 44 | srcStall: ["$v05"], 45 | tgtStall: ["$v07"], 46 | }, 47 | "Lanes - STV - base": { 48 | asm: asm("stv", ["$v16", 0, 0, "$t0"]), 49 | src: ["$v16_0", "$v17_1", "$v18_2", "$v19_3", "$v20_4", "$v21_5", "$v22_6", "$v23_7", "$t0"], 50 | tgt: [], 51 | srcStall: ["$v16", "$v17", "$v18", "$v19", "$v20", "$v21", "$v22", "$v23", "$t0"], 52 | tgtStall: [], 53 | }, 54 | "Lanes - STV - offset 2": { 55 | asm: asm("stv", ["$v08", 2, 0x10, "$t0"]), 56 | src: ["$v08_7", "$v09_0", "$v10_1", "$v11_2", "$v12_3", "$v13_4", "$v14_5", "$v15_6", "$t0"], 57 | tgt: [], 58 | srcStall: ["$v08", "$v09", "$v10", "$v11", "$v12", "$v13", "$v14", "$v15", "$t0"], 59 | tgtStall: [], 60 | }, 61 | "Lanes - STV - offset 8": { 62 | asm: asm("stv", ["$v08", 8, 0x20, "$t0"]), 63 | src: ["$v08_4", "$v09_5", "$v10_6", "$v11_7", "$v12_0", "$v13_1", "$v14_2", "$v15_3", "$t0"], 64 | tgt: [], 65 | srcStall: ["$v08", "$v09", "$v10", "$v11", "$v12", "$v13", "$v14", "$v15", "$t0"], 66 | tgtStall: [], 67 | }, 68 | "Lanes - LTV - base": { 69 | asm: asm("ltv", ["$v16", 0, 0, "$t0"]), 70 | src: ["$t0"], 71 | tgt: ["$v16_0", "$v17_1", "$v18_2", "$v19_3", "$v20_4", "$v21_5", "$v22_6", "$v23_7"], 72 | srcStall: ["$t0"], 73 | tgtStall: ["$v16", "$v17", "$v18", "$v19", "$v20", "$v21", "$v22", "$v23"], 74 | }, 75 | "Lanes - LTV - offset 2": { 76 | asm: asm("ltv", ["$v08", 2, 0x10, "$t0"]), 77 | src: ["$t0"], 78 | tgt: ["$v08_7", "$v09_0", "$v10_1", "$v11_2", "$v12_3", "$v13_4", "$v14_5", "$v15_6"], 79 | srcStall: ["$t0"], 80 | tgtStall: ["$v08", "$v09", "$v10", "$v11", "$v12", "$v13", "$v14", "$v15"], 81 | }, 82 | "Lanes - LTV - offset 8": { 83 | asm: asm("ltv", ["$v00", 8, 0x20, "$t0"]), 84 | src: ["$t0"], 85 | tgt: ["$v00_4", "$v01_5", "$v02_6", "$v03_7", "$v04_0", "$v05_1", "$v06_2", "$v07_3"], 86 | srcStall: ["$t0"], 87 | tgtStall: ["$v00", "$v01", "$v02", "$v03", "$v04", "$v05", "$v06", "$v07"], 88 | }, 89 | "ctc2 - VCC": { 90 | asm: asm("ctc2", ["$at", "$vcc"]), 91 | src: ["$at"], 92 | tgt: ["$vcc"], 93 | srcStall: ["$at"], 94 | tgtStall: [], 95 | } 96 | }; 97 | 98 | for(const [name, {asm, src, tgt, srcStall, tgtStall}] of Object.entries(CASES)) { 99 | asmInitDep(asm); 100 | test(`Source (Logic) - ${name}`, () => expect(asm.depsSourceIdx).toEqual(src.map(r => REG_INDEX_MAP[r]))); 101 | test(`Target (Logic) - ${name}`, () => expect(asm.depsTargetIdx).toEqual(tgt.map(r => REG_INDEX_MAP[r]))); 102 | test(`Source (Stalls) - ${name}`, () => expect(asm.depsStallSourceIdx).toEqual(srcStall.map(r => REG_STALL_INDEX_MAP[r]))); 103 | test(`Target (Stalls) - ${name}`, () => expect(asm.depsStallTargetIdx).toEqual(tgtStall.map(r => REG_STALL_INDEX_MAP[r]))); 104 | } 105 | }); -------------------------------------------------------------------------------- /src/tests/optimizer/e2e/optAssert.test.js: -------------------------------------------------------------------------------- 1 | import {transpileSource} from "../../../lib/transpiler"; 2 | 3 | describe('Optimizer E2E - Assertion', () => 4 | { 5 | test('Assert variations (unopt)', async () => { 6 | const {asm, warn} = await transpileSource(`function a() 7 | { 8 | u32 buff,test; 9 | TEST_A: 10 | if(buff > 4)assert(0xAB); 11 | 12 | TEST_B: 13 | if(buff < 4)assert(0xAB); 14 | 15 | TEST_C: 16 | if(buff == 4)assert(0xAB); 17 | 18 | TEST_D: 19 | if(buff != 4)assert(0xAB); 20 | 21 | TEST_E: 22 | if(buff == 0)assert(0xAB); 23 | 24 | TEST_F: 25 | if(buff != 0)assert(0xAB); 26 | 27 | TEST_G: 28 | if(buff != test)assert(0xAB); 29 | 30 | TEST_H: 31 | if(buff < test)assert(0xAB); 32 | 33 | TEST_I: 34 | assert(0xAB); 35 | }`, {rspqWrapper: false, optimize: false}); 36 | 37 | expect(warn).toBe(""); 38 | expect(asm).toBe(`a: 39 | TEST_A: 40 | sltiu $at, $t0, 5 41 | bne $at, $zero, LABEL_a_0001 42 | nop 43 | lui $at, 171 44 | j assertion_failed 45 | nop 46 | LABEL_a_0001: 47 | TEST_B: 48 | sltiu $at, $t0, 4 49 | beq $at, $zero, LABEL_a_0002 50 | nop 51 | lui $at, 171 52 | j assertion_failed 53 | nop 54 | LABEL_a_0002: 55 | TEST_C: 56 | addiu $at, $zero, 4 57 | bne $t0, $at, LABEL_a_0003 58 | nop 59 | lui $at, 171 60 | j assertion_failed 61 | nop 62 | LABEL_a_0003: 63 | TEST_D: 64 | addiu $at, $zero, 4 65 | beq $t0, $at, LABEL_a_0004 66 | nop 67 | lui $at, 171 68 | j assertion_failed 69 | nop 70 | LABEL_a_0004: 71 | TEST_E: 72 | bne $t0, $zero, LABEL_a_0005 73 | nop 74 | lui $at, 171 75 | j assertion_failed 76 | nop 77 | LABEL_a_0005: 78 | TEST_F: 79 | beq $t0, $zero, LABEL_a_0006 80 | nop 81 | lui $at, 171 82 | j assertion_failed 83 | nop 84 | LABEL_a_0006: 85 | TEST_G: 86 | beq $t0, $t1, LABEL_a_0007 87 | nop 88 | lui $at, 171 89 | j assertion_failed 90 | nop 91 | LABEL_a_0007: 92 | TEST_H: 93 | sltu $at, $t0, $t1 94 | beq $at, $zero, LABEL_a_0008 95 | nop 96 | lui $at, 171 97 | j assertion_failed 98 | nop 99 | LABEL_a_0008: 100 | TEST_I: 101 | lui $at, 171 102 | j assertion_failed 103 | nop 104 | jr $ra 105 | nop`); 106 | }); 107 | 108 | test('Assert variations (opt)', async () => { 109 | const {asm, warn} = await transpileSource(`function a() 110 | { 111 | u32 buff,test; 112 | TEST_A: 113 | if(buff > 4)assert(0xAB); 114 | 115 | TEST_B: 116 | if(buff < 4)assert(0xAB); 117 | 118 | TEST_C: 119 | if(buff == 4)assert(0xAB); 120 | 121 | TEST_D: 122 | if(buff != 4)assert(0xAB); 123 | 124 | TEST_E: 125 | if(buff == 0)assert(0xAB); 126 | 127 | TEST_F: 128 | if(buff != 0)assert(0xAB); 129 | 130 | TEST_G: 131 | if(buff != test)assert(0xAB); 132 | 133 | TEST_H: 134 | if(buff < test)assert(0xAB); 135 | 136 | TEST_I: 137 | assert(0xAB); 138 | }`, {rspqWrapper: false, optimize: true}); 139 | 140 | expect(warn).toBe(""); 141 | expect(asm).toBe(`a: 142 | TEST_A: 143 | sltiu $at, $t0, 5 144 | beq $at, $zero, assertion_failed 145 | lui $at, 171 146 | TEST_B: 147 | sltiu $at, $t0, 4 148 | bne $at, $zero, assertion_failed 149 | lui $at, 171 150 | TEST_C: 151 | addiu $at, $zero, 4 152 | beq $t0, $at, assertion_failed 153 | lui $at, 171 154 | TEST_D: 155 | addiu $at, $zero, 4 156 | bne $t0, $at, assertion_failed 157 | lui $at, 171 158 | TEST_E: 159 | beq $t0, $zero, assertion_failed 160 | lui $at, 171 161 | TEST_F: 162 | bne $t0, $zero, assertion_failed 163 | lui $at, 171 164 | TEST_G: 165 | bne $t0, $t1, assertion_failed 166 | lui $at, 171 167 | TEST_H: 168 | sltu $at, $t0, $t1 169 | bne $at, $zero, assertion_failed 170 | lui $at, 171 171 | TEST_I: 172 | j assertion_failed 173 | lui $at, 171`); 174 | }); 175 | }); -------------------------------------------------------------------------------- /src/tests/optimizer/e2e/optBranchJump.test.js: -------------------------------------------------------------------------------- 1 | import {transpileSource} from "../../../lib/transpiler"; 2 | 3 | const CONF = {rspqWrapper: false, optimize: true}; 4 | 5 | describe('Optimizer E2E - Branch-Jump', () => 6 | { 7 | test('Branch + Goto', async () => { 8 | const {asm, warn} = await transpileSource(`function test() 9 | { 10 | u32<$t0> a; 11 | LABEL_A: 12 | if(a != 0)goto LABEL_A; 13 | }`, CONF); 14 | 15 | expect(warn).toBe(""); 16 | expect(asm).toBe(`test: 17 | LABEL_A: 18 | bne $t0, $zero, LABEL_A 19 | nop 20 | jr $ra 21 | nop`); 22 | }); 23 | 24 | test('Branch + Goto (no opt)', async () => { 25 | const {asm, warn} = await transpileSource(`function test() 26 | { 27 | u32<$t0> a; 28 | LABEL_A: 29 | if(a != 0) { 30 | a += 1; 31 | goto LABEL_A; 32 | } 33 | }`, CONF); 34 | 35 | expect(warn).toBe(""); 36 | expect(asm).toBe(`test: 37 | LABEL_A: 38 | beq $t0, $zero, LABEL_test_0001 39 | nop 40 | j LABEL_A 41 | addiu $t0, $t0, 1 42 | LABEL_test_0001: 43 | jr $ra 44 | nop`); 45 | }); 46 | 47 | test('Loop - Used Label', async () => { 48 | // 'SOME_LABEL' here happens to be after a branch that will be optimized 49 | // however we can't remove it since something later on will reference it 50 | const {asm, warn} = await transpileSource(`function test() 51 | { 52 | u32<$t0> a; 53 | loop { 54 | if(a == 1)continue; 55 | SOME_LABEL: 56 | 57 | if(a == 0)goto SOME_LABEL; 58 | LOOP_END: 59 | } 60 | }`, CONF); 61 | 62 | expect(warn).toBe(""); 63 | expect(asm).toBe(`test: 64 | LABEL_test_0001: 65 | addiu $at, $zero, 1 66 | beq $t0, $at, LABEL_test_0001 67 | nop 68 | SOME_LABEL: 69 | bne $t0, $zero, LABEL_test_0001 70 | nop 71 | j SOME_LABEL 72 | nop 73 | LABEL_test_0002: 74 | jr $ra 75 | nop`); 76 | }); 77 | 78 | test('Loop - Unused Label', async () => { 79 | // Same as a above, but this time the label is actually unused 80 | const {asm, warn} = await transpileSource(`function test() 81 | { 82 | u32<$t0> a; 83 | loop { 84 | if(a == 1)continue; 85 | SOME_LABEL: 86 | 87 | if(a == 0)continue; 88 | LOOP_END: 89 | } 90 | }`, CONF); 91 | 92 | expect(warn).toBe(""); 93 | expect(asm).toBe(`test: 94 | LABEL_test_0001: 95 | addiu $at, $zero, 1 96 | beq $t0, $at, LABEL_test_0001 97 | nop 98 | bne $t0, $zero, LABEL_test_0001 99 | nop 100 | j LABEL_test_0001 101 | nop 102 | LABEL_test_0002: 103 | jr $ra 104 | nop`); 105 | }); 106 | }); -------------------------------------------------------------------------------- /src/tests/optimizer/e2e/optDeadCode.test.js: -------------------------------------------------------------------------------- 1 | import {transpileSource} from "../../../lib/transpiler"; 2 | 3 | const CONF = {rspqWrapper: false, optimize: true}; 4 | 5 | describe('Optimizer E2E - Dead Code', () => 6 | { 7 | test('Jump at end - safe', async () => { 8 | const {asm, warn} = await transpileSource(`function test() 9 | { 10 | goto TEST; 11 | }`, CONF); 12 | 13 | expect(warn).toBe(""); 14 | expect(asm).toBe(`test: 15 | j TEST 16 | nop`); 17 | }); 18 | 19 | test('Jump at end - unsafe with code', async () => { 20 | const {asm, warn} = await transpileSource(`function test() 21 | { 22 | goto TEST; 23 | u32 x = 2; 24 | x = 3; 25 | }`, CONF); 26 | 27 | expect(warn).toBe(""); 28 | expect(asm).toBe(`test: 29 | j TEST 30 | nop 31 | addiu $t0, $zero, 3 32 | jr $ra 33 | addiu $t0, $zero, 2`); 34 | }); 35 | 36 | test('Jump at end - unsafe', async () => { 37 | const {asm, warn} = await transpileSource(` 38 | function test2(); 39 | function test() 40 | { 41 | test2(); 42 | u32 x = 2; 43 | }`, CONF); 44 | 45 | expect(warn).toBe(""); 46 | expect(asm).toBe(`test: 47 | jal test2 48 | nop 49 | jr $ra 50 | addiu $t0, $zero, 2`); 51 | }); 52 | 53 | test('Jump in branch - safe jal', async () => { 54 | const {asm, warn} = await transpileSource(` 55 | function test2(); 56 | function test() 57 | { 58 | u32 x = 1; 59 | if(x) { 60 | test2(); 61 | } 62 | }`, CONF); 63 | 64 | expect(warn).toBe(""); 65 | expect(asm).toBe(`test: 66 | addiu $t0, $zero, 1 67 | bne $t0, $zero, test2 68 | ori $ra, $zero, LABEL_test_0001 69 | LABEL_test_0001: 70 | jr $ra 71 | nop`); 72 | }); 73 | }); -------------------------------------------------------------------------------- /src/tests/optimizer/e2e/optDelaySlot.test.js: -------------------------------------------------------------------------------- 1 | import {transpileSource} from "../../../lib/transpiler"; 2 | 3 | const CONF = {rspqWrapper: false, optimize: true}; 4 | 5 | describe('Optimizer E2E - Delay-Slots', () => 6 | { 7 | test('Fill - Basic', async () => { 8 | const {asm, warn} = await transpileSource(`function test(u32 dummy) 9 | { 10 | u32 a = 1; 11 | goto SOME_LABEL; 12 | }`, CONF); 13 | 14 | expect(warn).toBe(""); 15 | expect(asm).toBe(`test: 16 | j SOME_LABEL 17 | addiu $t0, $zero, 1`); 18 | }); 19 | 20 | test('Fill - Complex', async () => { 21 | const {asm, warn} = await transpileSource(`function test(u32 i) 22 | { 23 | u32 test = 0; 24 | while(i != 0) { 25 | if(i == 6) { 26 | test = 42; 27 | break; 28 | } 29 | i -= 1; 30 | } 31 | }`, CONF); 32 | 33 | expect(warn).toBe(""); 34 | expect(asm).toBe(`test: 35 | or $t0, $zero, $zero 36 | LABEL_test_0001: 37 | beq $a0, $zero, LABEL_test_0002 38 | nop 39 | addiu $at, $zero, 6 40 | bne $a0, $at, LABEL_test_0003 41 | nop 42 | j LABEL_test_0002 43 | addiu $t0, $zero, 42 44 | LABEL_test_0003: 45 | j LABEL_test_0001 46 | addiu $a0, $a0, 65535 47 | LABEL_test_0002: 48 | jr $ra 49 | nop`); 50 | }); 51 | }); -------------------------------------------------------------------------------- /src/tests/optimizer/e2e/optJumpDedupe.test.js: -------------------------------------------------------------------------------- 1 | import {transpileSource} from "../../../lib/transpiler"; 2 | 3 | const CONF = {rspqWrapper: false, optimize: true}; 4 | 5 | describe('Optimizer E2E - Jump Dedupe', () => 6 | { 7 | test('Nested-If - Used Label', async () => { 8 | const {asm, warn} = await transpileSource(`command<0> test() 9 | { 10 | u32 a = 1; 11 | if(a > 1) { 12 | if(a > 10) { 13 | a += 1; 14 | } 15 | } 16 | }`, CONF); 17 | 18 | expect(warn).toBe(""); 19 | expect(asm).toBe(`test: 20 | addiu $s7, $zero, 1 21 | sltiu $at, $s7, 2 22 | bne $at, $zero, RSPQ_Loop 23 | nop 24 | sltiu $at, $s7, 11 25 | bne $at, $zero, RSPQ_Loop 26 | nop 27 | addiu $s7, $s7, 1 28 | LABEL_test_0001: 29 | j RSPQ_Loop 30 | nop`); 31 | }); 32 | 33 | test('Nested-If - Unused Label', async () => { 34 | const {asm, warn} = await transpileSource(`command<0> test() 35 | { 36 | u32 a = 1; 37 | while(a < 2) { 38 | a -= 1; 39 | } 40 | }`, CONF); 41 | 42 | expect(warn).toBe(""); 43 | expect(asm).toBe(`test: 44 | addiu $s7, $zero, 1 45 | LABEL_test_0001: 46 | sltiu $at, $s7, 2 47 | beq $at, $zero, RSPQ_Loop 48 | nop 49 | j LABEL_test_0001 50 | addiu $s7, $s7, 65535`); 51 | }); 52 | }); -------------------------------------------------------------------------------- /src/tests/optimizer/e2e/optLabels.test.js: -------------------------------------------------------------------------------- 1 | import {transpileSource} from "../../../lib/transpiler"; 2 | 3 | const CONF = {rspqWrapper: false, optimize: true}; 4 | 5 | describe('Optimizer E2E - Labels', () => 6 | { 7 | test('De-dupe Labels', async () => { 8 | const {asm, warn} = await transpileSource(`function test(u32 dummy) 9 | { 10 | LABEL_A: 11 | LABEL_B: 12 | LABEL_C: 13 | goto LABEL_A; 14 | }`, CONF); 15 | 16 | expect(warn).toBe(""); 17 | expect(asm).toBe(`test: 18 | LABEL_C: 19 | j LABEL_C 20 | nop`); 21 | }); 22 | 23 | test('De-dupe Labels - keep single', async () => { 24 | const {asm, warn} = await transpileSource(`function test(u32 dummy) 25 | { 26 | LABEL_A: 27 | dummy += 1; 28 | LABEL_B: 29 | dummy += 2; 30 | LABEL_C: 31 | goto LABEL_A; 32 | }`, CONF); 33 | 34 | expect(warn).toBe(""); 35 | expect(asm).toBe(`test: 36 | LABEL_A: 37 | addiu $a0, $a0, 1 38 | LABEL_B: 39 | addiu $a0, $a0, 2 40 | LABEL_C: 41 | j LABEL_A 42 | nop`); 43 | }); 44 | }); -------------------------------------------------------------------------------- /src/tests/optimizer/e2e/optMergeSequence.test.js: -------------------------------------------------------------------------------- 1 | import {transpileSource} from "../../../lib/transpiler"; 2 | 3 | const CONF = {rspqWrapper: false, optimize: true}; 4 | 5 | describe('Optimizer E2E - Merge Sequence', () => 6 | { 7 | test('Multiply - Zero fractional', async () => { 8 | const {asm, warn} = await transpileSource(` 9 | state { vec16 SCREEN_SCALE_OFFSET; } 10 | function test(u32 dummy) 11 | { 12 | vec32 screenSize; 13 | screenSize:sint = load(SCREEN_SCALE_OFFSET); 14 | screenSize:sfract = 0; 15 | screenSize >>= 8; 16 | END: 17 | }`, CONF); 18 | 19 | expect(warn).toBe(""); 20 | expect(asm).toBe(`test: 21 | ori $at, $zero, %lo(SCREEN_SCALE_OFFSET) 22 | lqv $v01, 0, 0, $at 23 | vmudl $v02, $v00, $v31.e7 24 | vmadm $v01, $v01, $v31.e7 25 | vmadn $v02, $v00, $v00 26 | END: 27 | jr $ra 28 | nop`); 29 | }); 30 | 31 | test('Multiply - Non-Zero fractional (no opt)', async () => { 32 | const {asm, warn} = await transpileSource(` 33 | state { vec16 SCREEN_SCALE_OFFSET; } 34 | function test(u32 dummy) 35 | { 36 | vec32 screenSize; 37 | screenSize:sint = load(SCREEN_SCALE_OFFSET); 38 | screenSize:sfract = 1; 39 | screenSize >>= 8; 40 | END: 41 | }`, CONF); 42 | 43 | expect(warn).toBe(""); 44 | expect(asm).toBe(`test: 45 | ori $at, $zero, %lo(SCREEN_SCALE_OFFSET) 46 | lqv $v01, 0, 0, $at 47 | vxor $v02, $v00, $v30.e7 48 | vmudl $v02, $v02, $v31.e7 49 | vmadm $v01, $v01, $v31.e7 50 | vmadn $v02, $v00, $v00 51 | END: 52 | jr $ra 53 | nop`); 54 | }); 55 | 56 | }); -------------------------------------------------------------------------------- /src/tests/preproc/defineAsm.test.js: -------------------------------------------------------------------------------- 1 | import {transpileSource} from "../../lib/transpiler"; 2 | 3 | const CONF = {rspqWrapper: true}; 4 | 5 | describe('Define (ASM)', () => 6 | { 7 | // defines in RSPL should be put into the ASM output too 8 | test('Define in ASM', async () => { 9 | const {asm, warn} = await transpileSource(` 10 | include "rsp_queue.inc" 11 | include "rdpq_macros.h" 12 | 13 | #define SOME_DEF_A 1 14 | #define SOME_DEF_B 2 15 | 16 | state{} 17 | 18 | #define SOME_DEF_C 3 19 | 20 | command<0> test(u32 a) 21 | { 22 | } 23 | 24 | #define SOME_DEF_D 4 25 | `, CONF); 26 | 27 | expect(warn).toBe(""); 28 | expect(asm).toContain("#define SOME_DEF_A 1\n"); 29 | expect(asm).toContain("#define SOME_DEF_B 2\n"); 30 | expect(asm).toContain("#define SOME_DEF_C 3\n"); 31 | expect(asm).toContain("#define SOME_DEF_D 4\n"); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /src/tests/preproc/preproc.test.js: -------------------------------------------------------------------------------- 1 | import {preprocess} from "../../lib/preproc/preprocess"; 2 | const CONF = {rspqWrapper: false}; 3 | 4 | describe('Preproc - Base', () => 5 | { 6 | test('Define - Basic', () => { 7 | const src = ` 8 | #define TEST 42 9 | macro test() { 10 | u32 x = TEST; 11 | } 12 | `; 13 | const res = preprocess(src, CONF); 14 | expect(res.trim()).toBe(` 15 | macro test() { 16 | u32 x = 42; 17 | } 18 | `.trim()); 19 | }); 20 | 21 | test('Define - Multiple', () => { 22 | const src = ` 23 | #define TEST 42 24 | #define TEST_AB 43 25 | 26 | macro test() { 27 | u32 x = TEST; 28 | u32 y = TEST_AB; 29 | } 30 | `; 31 | const res = preprocess(src, CONF); 32 | expect(res.trim()).toBe(` 33 | macro test() { 34 | u32 x = 42; 35 | u32 y = 43; 36 | } 37 | `.trim()); 38 | }); 39 | 40 | test('Define - Deps', () => { 41 | const src = ` 42 | #define TEST 42 43 | #define TEST_AB TEST+1 44 | 45 | macro test() { 46 | u32 x = TEST; 47 | u32 y = TEST_AB; 48 | } 49 | `; 50 | const res = preprocess(src, CONF); 51 | expect(res.trim()).toBe(` 52 | macro test() { 53 | u32 x = 42; 54 | u32 y = 42+1; 55 | } 56 | `.trim()); 57 | }); 58 | 59 | test('Define - Partial', () => { 60 | const src = ` 61 | #define my 42 62 | 63 | macro my_function() { 64 | u32 x = my; 65 | } 66 | `; 67 | const res = preprocess(src, CONF); 68 | expect(res.trim()).toBe(` 69 | macro my_function() { 70 | u32 x = 42; 71 | } 72 | `.trim()); 73 | }); 74 | 75 | test('Define - Undef', () => { 76 | const src = ` 77 | #define TEST 42 78 | macro test() { 79 | u32 x = TEST; 80 | } 81 | #undef TEST 82 | `; 83 | const res = preprocess(src, CONF); 84 | expect(res.trim()).toBe(` 85 | macro test() { 86 | u32 x = 42; 87 | } 88 | `.trim()); 89 | }); 90 | 91 | test('Define - Undef Before usage', () => { 92 | const src = ` 93 | #define TEST 42 94 | #undef TEST 95 | 96 | macro test() { 97 | u32 x = TEST; 98 | } 99 | `; 100 | const res = preprocess(src, CONF); 101 | expect(res.trim()).toBe(` 102 | macro test() { 103 | u32 x = TEST; 104 | } 105 | `.trim()); 106 | }); 107 | 108 | test('Define - Empty', () => { 109 | const src = ` 110 | #define 111 | macro test() { 112 | u32 x = TEST; 113 | } 114 | `; 115 | expect(() => preprocess(src, CONF)) 116 | .toThrowError("Line 2: Invalid #define statement!"); 117 | }); 118 | 119 | test('Ifdef - Basic', () => { 120 | const src = ` 121 | #define TEST 42 122 | 123 | #ifdef TEST2 124 | macro test2() {} 125 | #endif 126 | 127 | #ifdef TEST 128 | macro test() {} 129 | #endif 130 | `; 131 | const res = preprocess(src, CONF); 132 | expect(res.trim()).toBe(` 133 | macro test() {} 134 | `.trim()); 135 | }); 136 | 137 | test('Ifdef - Else', () => { 138 | const src = ` 139 | #define TEST 42 140 | 141 | #ifdef TEST2 142 | macro test2() {} 143 | #else 144 | macro test() {} 145 | #endif 146 | `; 147 | const res = preprocess(src, CONF); 148 | expect(res.trim()).toBe(` 149 | macro test() {} 150 | `.trim()); 151 | }); 152 | 153 | test('Ifdef - define (true)', () => { 154 | const src = ` 155 | #define TEST 42 156 | 157 | #ifdef TEST 158 | #define VAL 1 159 | #else 160 | #define VAL 2 161 | #endif 162 | VAL 163 | `; 164 | const res = preprocess(src, CONF); 165 | expect(res.trim()).toBe(` 166 | 1 167 | `.trim()); 168 | }); 169 | 170 | test('Ifdef - define (false)', () => { 171 | const src = ` 172 | #define TEST 42 173 | 174 | #ifdef TEST_OTHER 175 | #define VAL 1 176 | #else 177 | #define VAL 2 178 | #endif 179 | VAL 180 | `; 181 | const res = preprocess(src, CONF); 182 | expect(res.trim()).toBe(` 183 | 2 184 | `.trim()); 185 | }); 186 | 187 | test('Ifdef - nested', () => { 188 | const src = ` 189 | #ifdef TEST 190 | #ifdef TEST2 191 | #endif 192 | #endif 193 | 194 | `; 195 | expect(() => preprocess(src, CONF)) 196 | .toThrowError("Line 3: Nested #ifdef statements are not allowed!"); 197 | }); 198 | }); -------------------------------------------------------------------------------- /src/tests/scalarOps.test.js: -------------------------------------------------------------------------------- 1 | import {transpileSource} from "../lib/transpiler"; 2 | 3 | const CONF = {rspqWrapper: false}; 4 | 5 | describe('Scalar - Ops', () => 6 | { 7 | test('32-Bit Arithmetic', async () => { 8 | const {asm, warn} = await transpileSource(`state { u32 TEST_CONST; } 9 | function test_scalar_ops() 10 | { 11 | u32<$t0> a, b, c; 12 | s32<$t3> sa, sb, sc; 13 | 14 | ADD: 15 | c = a + b; sc = sa + sb; 16 | c = a + 1; sc = sa + 1; 17 | c = a + TEST_CONST; sc = sa + TEST_CONST; 18 | 19 | SUB: 20 | c = a - b; sc = sa - sb; 21 | c = a - 1; sc = sa - 1; 22 | //c = a - TEST_CONST; sc = sa - TEST_CONST; Invalid 23 | 24 | MUL: 25 | c = a * 4; 26 | 27 | DIV: 28 | c = a / 8; 29 | }`, CONF); 30 | 31 | expect(warn).toBe(""); 32 | expect(asm).toBe( 33 | `test_scalar_ops: 34 | ADD: 35 | addu $t2, $t0, $t1 36 | addu $t5, $t3, $t4 37 | addiu $t2, $t0, 1 38 | addiu $t5, $t3, 1 39 | addiu $t2, $t0, %lo(TEST_CONST) 40 | addiu $t5, $t3, %lo(TEST_CONST) 41 | SUB: 42 | subu $t2, $t0, $t1 43 | subu $t5, $t3, $t4 44 | addiu $t2, $t0, 65535 45 | addiu $t5, $t3, 65535 46 | MUL: 47 | sll $t2, $t0, 2 48 | DIV: 49 | srl $t2, $t0, 3 50 | jr $ra 51 | nop`); 52 | }); 53 | 54 | test('32-Bit - Logic', async () => { 55 | const {asm, warn} = await transpileSource(`state { u32 TEST_CONST; } 56 | function test_scalar_ops() 57 | { 58 | u32<$t0> a, b, c; 59 | s32<$t3> sa, sb, sc; 60 | 61 | AND: 62 | c = a & b; 63 | c = a & 1; 64 | c = a & TEST_CONST; 65 | 66 | OR: 67 | c = a | b; 68 | c = a | 2; 69 | c = a | TEST_CONST; 70 | 71 | XOR: 72 | c = a ^ b; 73 | c = a ^ 2; 74 | c = a ^ TEST_CONST; 75 | 76 | NOT: 77 | c = ~b; 78 | 79 | NOR: 80 | c = a ~| b; 81 | 82 | SHIFT_LEFT: 83 | c = a << b; 84 | c = a << 2; 85 | //c = a << TEST_CONST; Invalid 86 | 87 | SHIFT_RIGHT: 88 | c = a >> b; 89 | c = a >> 2; 90 | sc = sa >> sb; 91 | sc = sa >> 2; 92 | sc = sa >>> 2; 93 | 94 | //c = a >> TEST_CONST; Invalid 95 | }`, CONF); 96 | 97 | expect(warn).toBe(""); 98 | expect(asm).toBe( 99 | `test_scalar_ops: 100 | AND: 101 | and $t2, $t0, $t1 102 | andi $t2, $t0, 1 103 | andi $t2, $t0, %lo(TEST_CONST) 104 | OR: 105 | or $t2, $t0, $t1 106 | ori $t2, $t0, 2 107 | ori $t2, $t0, %lo(TEST_CONST) 108 | XOR: 109 | xor $t2, $t0, $t1 110 | xori $t2, $t0, 2 111 | xori $t2, $t0, %lo(TEST_CONST) 112 | NOT: 113 | nor $t2, $zero, $t1 114 | NOR: 115 | nor $t2, $t0, $t1 116 | SHIFT_LEFT: 117 | sllv $t2, $t0, $t1 118 | sll $t2, $t0, 2 119 | SHIFT_RIGHT: 120 | srlv $t2, $t0, $t1 121 | srl $t2, $t0, 2 122 | srav $t5, $t3, $t4 123 | sra $t5, $t3, 2 124 | srl $t5, $t3, 2 125 | jr $ra 126 | nop`); 127 | }); 128 | 129 | test('Multiplication (2^x)', async () => { 130 | const src = `function test() { 131 | u32<$t0> a, b; 132 | a = b * 4; 133 | }`; 134 | 135 | const {asm, warn} = await transpileSource(src, CONF); 136 | expect(warn).toBe(""); 137 | expect(asm).toBe(`test: 138 | sll $t0, $t1, 2 139 | jr $ra 140 | nop`); 141 | }); 142 | 143 | test('Division (2^x)', async () => { 144 | const src = `function test() { 145 | u32<$t0> a, b; 146 | a = b / 8; 147 | }`; 148 | 149 | const {asm, warn} = await transpileSource(src, CONF); 150 | expect(warn).toBe(""); 151 | expect(asm).toBe(`test: 152 | srl $t0, $t1, 3 153 | jr $ra 154 | nop`); 155 | }); 156 | 157 | test('Assign - scalar', async () => { 158 | const {asm, warn} = await transpileSource(`function test() { 159 | u32<$t0> a; 160 | u32<$t1> b = a; 161 | }`, CONF); 162 | 163 | expect(warn).toBe(""); 164 | expect(asm).toBe(`test: 165 | or $t1, $zero, $t0 166 | jr $ra 167 | nop`); 168 | }); 169 | 170 | test('Assign - Vector (ufract)', async () => { 171 | const {asm, warn} = await transpileSource(`function test() { 172 | vec32 v0; 173 | vec16 v1; 174 | u32 a = v0:ufract.y; 175 | u32 b = v1:ufract.y; 176 | }`, CONF); 177 | 178 | expect(warn).toBe(""); 179 | expect(asm).toBe(`test: 180 | mfc2 $t0, $v02.e1 181 | mfc2 $t1, $v03.e1 182 | jr $ra 183 | nop`); 184 | }); 185 | 186 | test('Assign - Vector (sint)', async () => { 187 | const {asm, warn} = await transpileSource(`function test() { 188 | vec32 v0; 189 | vec16 v1; 190 | u32 a = v0:sint.y; 191 | u32 b = v1:sint.y; 192 | }`, CONF); 193 | 194 | expect(warn).toBe(""); 195 | expect(asm).toBe(`test: 196 | mfc2 $t0, $v01.e1 197 | mfc2 $t1, $v03.e1 198 | jr $ra 199 | nop`); 200 | }); 201 | 202 | test('Invalid (multiplication)', async () => { 203 | const src = `function test() { 204 | u32<$t0> a, b; 205 | a = a * b; 206 | }`; 207 | 208 | await expect(() => transpileSource(src, CONF)) 209 | .rejects.toThrowError(/line 3: Scalar-Multiplication only allowed with a power-of-two /); 210 | }); 211 | 212 | test('Invalid (division)', async () => { 213 | const src = `function test() { 214 | u32<$t0> a, b; 215 | a = a / b; 216 | }`; 217 | 218 | await expect(() => transpileSource(src, CONF)) 219 | .rejects.toThrowError(/line 3: Scalar-Division only allowed with a power-of-two /); 220 | }); 221 | 222 | test('Invalid (sub with label)', async () => { 223 | const src = `state { u32 TEST_CONST; } 224 | function test() { 225 | u32<$t0> a; 226 | a = a - TEST_CONST; 227 | }`; 228 | 229 | await expect(() => transpileSource(src, CONF)) 230 | .rejects.toThrowError(/line 4: Subtraction cannot use labels!/); 231 | }); 232 | }); -------------------------------------------------------------------------------- /src/tests/scope.test.js: -------------------------------------------------------------------------------- 1 | import {transpileSource} from "../lib/transpiler"; 2 | 3 | const CONF = {rspqWrapper: false}; 4 | 5 | describe('Scope', () => 6 | { 7 | test('Var Declaration', async () => { 8 | const {asm, warn} = await transpileSource(`function test_scope() 9 | { 10 | u32<$t0> a; 11 | { 12 | u32<$t1> b; 13 | b += 2; 14 | } // 'b' is no longer defined now 15 | a += 2; 16 | }`, CONF); 17 | 18 | expect(warn).toBe(""); 19 | expect(asm).toBe(`test_scope: 20 | addiu $t1, $t1, 2 21 | addiu $t0, $t0, 2 22 | jr $ra 23 | nop`); 24 | }); 25 | 26 | test('Var Un-Declaration', async () => { 27 | const src = `function test_scope() 28 | { 29 | u32<$t0> a; 30 | a += 2; 31 | undef a; 32 | a = 2; 33 | }`; 34 | await expect(() => transpileSource(src, CONF)) 35 | .rejects.toThrowError(/line 6: result Variable a not known!/); 36 | }); 37 | 38 | test('Var Decl. invalid', async () => 39 | { 40 | const src = `function test_scope() 41 | { 42 | u32<$t0> a; 43 | { 44 | u32<$t1> b; 45 | b += 2; 46 | } 47 | b += 2; 48 | }`; 49 | await expect(() => transpileSource(src, CONF)) 50 | .rejects.toThrowError(/line 8: result Variable b not known!/); 51 | }); 52 | }); -------------------------------------------------------------------------------- /src/tests/state.test.js: -------------------------------------------------------------------------------- 1 | import {transpileSource} from "../lib/transpiler"; 2 | 3 | const CONF = {rspqWrapper: true}; 4 | 5 | const getDataSection = asm => { 6 | const idxData = asm.indexOf(".data"); 7 | const idxText = asm.indexOf(".text"); 8 | return asm.substring(idxData, idxText); 9 | } 10 | 11 | describe('State', () => 12 | { 13 | test('Empty State', async () => { 14 | const {asm, warn} = await transpileSource(` 15 | state {} 16 | `, CONF); 17 | 18 | expect(warn).toBe(""); 19 | expect(getDataSection(asm)).toBe(`.data 20 | RSPQ_BeginOverlayHeader 21 | RSPQ_EndOverlayHeader 22 | 23 | RSPQ_EmptySavedState 24 | 25 | `); 26 | }); 27 | 28 | test('Types', async () => { 29 | const {asm, warn} = await transpileSource(` 30 | state { 31 | u8 a; 32 | u16 b; 33 | u32 c; 34 | vec16 d; 35 | vec32 e; 36 | } 37 | `, CONF); 38 | 39 | expect(warn).toBe(""); 40 | expect(getDataSection(asm)).toBe(`.data 41 | RSPQ_BeginOverlayHeader 42 | RSPQ_EndOverlayHeader 43 | 44 | RSPQ_BeginSavedState 45 | STATE_MEM_START: 46 | a: .ds.b 1 47 | .align 1 48 | b: .ds.b 2 49 | .align 2 50 | c: .ds.b 4 51 | .align 4 52 | d: .ds.b 16 53 | .align 4 54 | e: .ds.b 32 55 | STATE_MEM_END: 56 | RSPQ_EndSavedState 57 | 58 | `); 59 | }); 60 | 61 | test('Arrays', async () => { 62 | const {asm, warn} = await transpileSource(` 63 | state { 64 | u32 a0[1]; 65 | u32 a1[4]; 66 | u32 a2[2][4]; 67 | vec32 b0[1]; 68 | vec32 b1[2]; 69 | vec32 b2[4][2]; 70 | } 71 | `, CONF); 72 | 73 | expect(warn).toBe(""); 74 | expect(getDataSection(asm)).toBe(`.data 75 | RSPQ_BeginOverlayHeader 76 | RSPQ_EndOverlayHeader 77 | 78 | RSPQ_BeginSavedState 79 | STATE_MEM_START: 80 | .align 2 81 | a0: .ds.b 4 82 | .align 2 83 | a1: .ds.b 16 84 | .align 2 85 | a2: .ds.b 32 86 | .align 4 87 | b0: .ds.b 32 88 | .align 4 89 | b1: .ds.b 64 90 | .align 4 91 | b2: .ds.b 256 92 | STATE_MEM_END: 93 | RSPQ_EndSavedState 94 | 95 | `); 96 | }); 97 | 98 | test('Extern', async () => { 99 | const {asm, warn} = await transpileSource(` 100 | state { 101 | u32 a; 102 | extern u32 b; 103 | u32 c; 104 | } 105 | `, CONF); 106 | 107 | expect(warn).toBe(""); 108 | expect(getDataSection(asm)).toBe(`.data 109 | RSPQ_BeginOverlayHeader 110 | RSPQ_EndOverlayHeader 111 | 112 | RSPQ_BeginSavedState 113 | STATE_MEM_START: 114 | .align 2 115 | a: .ds.b 4 116 | .align 2 117 | c: .ds.b 4 118 | STATE_MEM_END: 119 | RSPQ_EndSavedState 120 | 121 | `); 122 | }); 123 | 124 | test('Align', async () => { 125 | const {asm, warn} = await transpileSource(` 126 | state { 127 | u16 a; 128 | alignas(8) u16 b; 129 | alignas(4) u8 c; 130 | } 131 | `, CONF); 132 | 133 | expect(warn).toBe(""); 134 | expect(getDataSection(asm)).toBe(`.data 135 | RSPQ_BeginOverlayHeader 136 | RSPQ_EndOverlayHeader 137 | 138 | RSPQ_BeginSavedState 139 | STATE_MEM_START: 140 | .align 1 141 | a: .ds.b 2 142 | .align 3 143 | b: .ds.b 2 144 | .align 2 145 | c: .ds.b 1 146 | STATE_MEM_END: 147 | RSPQ_EndSavedState 148 | 149 | `); 150 | }); 151 | 152 | test('Align lower', async () => { 153 | const {asm, warn} = await transpileSource(` 154 | state { 155 | vec16 VEC_A; 156 | alignas(8) vec16 VEC_A; 157 | } 158 | `, CONF); 159 | 160 | expect(warn).toBe(""); 161 | expect(getDataSection(asm)).toBe(`.data 162 | RSPQ_BeginOverlayHeader 163 | RSPQ_EndOverlayHeader 164 | 165 | RSPQ_BeginSavedState 166 | STATE_MEM_START: 167 | .align 4 168 | VEC_A: .ds.b 16 169 | .align 3 170 | VEC_A: .ds.b 16 171 | STATE_MEM_END: 172 | RSPQ_EndSavedState 173 | 174 | `); 175 | }); 176 | 177 | test('Data State', async () => { 178 | const {asm, warn} = await transpileSource(` 179 | data { 180 | u32 BBB; 181 | u32 CCC; 182 | } 183 | `, CONF); 184 | 185 | expect(warn).toBe(""); 186 | expect(getDataSection(asm)).toBe(`.data 187 | RSPQ_BeginOverlayHeader 188 | RSPQ_EndOverlayHeader 189 | 190 | RSPQ_EmptySavedState 191 | 192 | .align 2 193 | BBB: .ds.b 4 194 | .align 2 195 | CCC: .ds.b 4 196 | 197 | `); 198 | }); 199 | 200 | test('BSS Only', async () => { 201 | const {asm, warn} = await transpileSource(` 202 | bss { 203 | u32 DDD; 204 | } 205 | `, CONF); 206 | 207 | expect(warn).toBe(""); 208 | expect(getDataSection(asm)).toBe(`.data 209 | RSPQ_BeginOverlayHeader 210 | RSPQ_EndOverlayHeader 211 | 212 | RSPQ_EmptySavedState 213 | 214 | .bss 215 | TEMP_STATE_MEM_START: 216 | .align 2 217 | DDD: .ds.b 4 218 | TEMP_STATE_MEM_END: 219 | 220 | `); 221 | }); 222 | 223 | test('Data + State', async () => { 224 | const {asm, warn} = await transpileSource(` 225 | state { 226 | u32 AAA; 227 | } 228 | data { 229 | u32 BBB; 230 | u32 CCC; 231 | } 232 | `, CONF); 233 | 234 | expect(warn).toBe(""); 235 | expect(getDataSection(asm)).toBe(`.data 236 | RSPQ_BeginOverlayHeader 237 | RSPQ_EndOverlayHeader 238 | 239 | RSPQ_BeginSavedState 240 | STATE_MEM_START: 241 | .align 2 242 | AAA: .ds.b 4 243 | STATE_MEM_END: 244 | RSPQ_EndSavedState 245 | 246 | .align 2 247 | BBB: .ds.b 4 248 | .align 2 249 | CCC: .ds.b 4 250 | 251 | `); 252 | }); 253 | 254 | test('Data + State + BSS', async () => { 255 | const {asm, warn} = await transpileSource(` 256 | state { 257 | u32 AAA; 258 | } 259 | data { 260 | u32 BBB; 261 | u32 CCC; 262 | } 263 | bss { 264 | u32 DDD; 265 | } 266 | `, CONF); 267 | 268 | expect(warn).toBe(""); 269 | expect(getDataSection(asm)).toBe(`.data 270 | RSPQ_BeginOverlayHeader 271 | RSPQ_EndOverlayHeader 272 | 273 | RSPQ_BeginSavedState 274 | STATE_MEM_START: 275 | .align 2 276 | AAA: .ds.b 4 277 | STATE_MEM_END: 278 | RSPQ_EndSavedState 279 | 280 | .align 2 281 | BBB: .ds.b 4 282 | .align 2 283 | CCC: .ds.b 4 284 | 285 | .bss 286 | TEMP_STATE_MEM_START: 287 | .align 2 288 | DDD: .ds.b 4 289 | TEMP_STATE_MEM_END: 290 | 291 | `); 292 | }); 293 | }); -------------------------------------------------------------------------------- /src/tests/swizzle.test.js: -------------------------------------------------------------------------------- 1 | import {transpileSource} from "../lib/transpiler"; 2 | 3 | const CONF = {rspqWrapper: false}; 4 | 5 | describe('Syntax - Swizzle', () => 6 | { 7 | test('Assign single (vec32 <- vec32)', async () => { 8 | const {asm, warn} = await transpileSource(`function test() { 9 | vec32<$v01> a, b; 10 | a.x = b.X; 11 | }`, CONF); 12 | 13 | expect(warn).toBe(""); 14 | expect(asm).toBe(`test: 15 | vmov $v01.e0, $v03.e4 16 | vmov $v02.e0, $v04.e4 17 | jr $ra 18 | nop`); 19 | }); 20 | 21 | test('Assign single (vec32 <- vec32, cast)', async () => { 22 | const {asm, warn} = await transpileSource(`function test() { 23 | vec32<$v01> a, b; 24 | SINT: 25 | a.x = b:sint.X; // all <- sint 26 | a:sint.x = b:sint.X; // sint <- sint 27 | a:ufract.x = b:sint.X; // ufract <- sint 28 | 29 | UFRACT: 30 | a.x = b:ufract.X; // all <- ufract 31 | a:sint.x = b:ufract.X; // sint <- ufract 32 | a:ufract.x = b:ufract.X; // ufract <- ufract 33 | }`, CONF); 34 | 35 | expect(warn).toBe(""); 36 | expect(asm).toBe(`test: 37 | SINT: 38 | vmov $v01.e0, $v03.e4 39 | vmov $v02.e0, $v00.e4 40 | vmov $v01.e0, $v03.e4 41 | vmov $v02.e0, $v03.e4 42 | UFRACT: 43 | vmov $v01.e0, $v00.e4 44 | vmov $v02.e0, $v04.e4 45 | vmov $v01.e0, $v04.e4 46 | vmov $v02.e0, $v04.e4 47 | jr $ra 48 | nop`); 49 | }); 50 | 51 | test('Assign single (vec16 <- vec16)', async () => { 52 | const {asm, warn} = await transpileSource(`function test() { 53 | vec16<$v01> a, b; 54 | a.x = b.X; 55 | }`, CONF); 56 | 57 | expect(warn).toBe(""); 58 | expect(asm).toBe(`test: 59 | vmov $v01.e0, $v02.e4 60 | jr $ra 61 | nop`); 62 | }); 63 | 64 | test('Assign single (vec32 <- vec16)', async () => { 65 | const {asm, warn} = await transpileSource(`function test() { 66 | vec32<$v01> a; 67 | vec16<$v03> b; 68 | a.x = b.X; 69 | }`, CONF); 70 | 71 | expect(warn).toBe(""); 72 | expect(asm).toBe(`test: 73 | vmov $v01.e0, $v03.e4 74 | vmov $v02.e0, $v00.e4 75 | jr $ra 76 | nop`); 77 | }); 78 | 79 | test('Assign single (vec16 <- vec32)', async () => { 80 | const {asm, warn} = await transpileSource(`function test() { 81 | vec16<$v01> a; 82 | vec32<$v02> b; 83 | a.x = b.X; 84 | }`, CONF); 85 | 86 | expect(warn).toBe(""); 87 | expect(asm).toBe(`test: 88 | vmov $v01.e0, $v02.e4 89 | jr $ra 90 | nop`); 91 | }); 92 | 93 | test('Invalid on Scalar (calc)', async () => { 94 | const src = `function test() { 95 | u32<$t0> a; 96 | a += a.x; 97 | }`; 98 | 99 | await expect(() => transpileSource(src, CONF)) 100 | .rejects.toThrowError(/line 3: Swizzling not allowed for scalar operations!/); 101 | }); 102 | 103 | test('Invalid on Scalar (assign)', async () => { 104 | const src = `function test() { 105 | u32<$t0> a; 106 | a = a.x; 107 | }`; 108 | 109 | await expect(() => transpileSource(src, CONF)) 110 | .rejects.toThrowError(/line 3: Swizzling not allowed for scalar operations!/); 111 | }); 112 | 113 | test('Alias (integer index)', async () => { 114 | const {asm, warn} = await transpileSource(`function test() { 115 | vec16<$v01> a; 116 | a.x = a.0; 117 | a.1 = a.z; 118 | 119 | a += a.xxzzXXZZ; 120 | a += a.00224466; 121 | 122 | a += a.wwwwWWWW; 123 | a += a.33337777; 124 | }`, CONF); 125 | 126 | expect(warn).toBe(""); 127 | expect(asm).toBe(`test: 128 | vmov $v01.e0, $v01.e0 129 | vmov $v01.e1, $v01.e2 130 | vaddc $v01, $v01, $v01.q0 131 | vaddc $v01, $v01, $v01.q0 132 | vaddc $v01, $v01, $v01.h3 133 | vaddc $v01, $v01, $v01.h3 134 | jr $ra 135 | nop`); 136 | }); 137 | }); -------------------------------------------------------------------------------- /src/tests/syntaxNumbers.test.js: -------------------------------------------------------------------------------- 1 | import {transpileSource} from "../lib/transpiler"; 2 | 3 | const CONF = {rspqWrapper: false}; 4 | 5 | describe('Syntax - Numbers', () => 6 | { 7 | test('Scalar - Assignment', async () => { 8 | const {asm, warn} = await transpileSource(`function test() 9 | { 10 | u32<$t0> a; 11 | a = 1234; 12 | a = 0x1234; 13 | a = 0b1010; 14 | }`, CONF); 15 | 16 | expect(asm).toBe(`test: 17 | addiu $t0, $zero, 1234 18 | addiu $t0, $zero, 4660 19 | addiu $t0, $zero, 10 20 | jr $ra 21 | nop`); 22 | }); 23 | 24 | test('Scalar - Calc', async () => { 25 | const {asm, warn} = await transpileSource(`function test() 26 | { 27 | u32<$t0> a; 28 | a = a + 1234; 29 | a = a + 0x1234; 30 | a = a + 0b1010; 31 | }`, CONF); 32 | 33 | expect(warn).toBe(""); 34 | expect(asm).toBe(`test: 35 | addiu $t0, $t0, 1234 36 | addiu $t0, $t0, 4660 37 | addiu $t0, $t0, 10 38 | jr $ra 39 | nop`); 40 | }); 41 | }); -------------------------------------------------------------------------------- /src/tests/syntaxVar.test.js: -------------------------------------------------------------------------------- 1 | import {transpileSource} from "../lib/transpiler"; 2 | 3 | const CONF = {rspqWrapper: false}; 4 | 5 | describe('Syntax - Vars', () => 6 | { 7 | test('Declare - Invalid (type scalar)', async () => { 8 | const src = `function test() { 9 | u32<$v03> a; 10 | }`; 11 | 12 | await expect(async () => transpileSource(src, CONF)) 13 | .rejects.toThrowError(/line 2: Cannot use vector register for scalar variable!/); 14 | }); 15 | 16 | test('Declare - Invalid (vector scalar)', async () => { 17 | const src = `function test() { 18 | vec16<$t0> a; 19 | }`; 20 | 21 | await expect(() => transpileSource(src, CONF)) 22 | .rejects.toThrowError(/line 2: Cannot use scalar register for vector variable!/); 23 | }); 24 | 25 | test('Declare - Invalid (swizzle)', async () => { 26 | const src = `function test() { 27 | vec16<$v03> a.x; 28 | }`; 29 | 30 | await expect(() => transpileSource(src, CONF)) 31 | .rejects.toThrowError(/Syntax error at line 2/); 32 | }); 33 | 34 | test('Declare - Invalid (cast)', async () => { 35 | const src = `function test() { 36 | vec16<$v03> a:sint; 37 | }`; 38 | 39 | await expect(() => transpileSource(src, CONF)) 40 | .rejects.toThrowError(/line 2: Variable name cannot contain a cast \(':'\)!/); 41 | }); 42 | }); -------------------------------------------------------------------------------- /src/web/fonts/FiraCode-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HailToDodongo/rspl/2b8716dcb9498c87089b076fad18513c38c02495/src/web/fonts/FiraCode-Bold.ttf -------------------------------------------------------------------------------- /src/web/fonts/FiraCode-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HailToDodongo/rspl/2b8716dcb9498c87089b076fad18513c38c02495/src/web/fonts/FiraCode-Regular.ttf -------------------------------------------------------------------------------- /src/web/fonts/Roboto-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HailToDodongo/rspl/2b8716dcb9498c87089b076fad18513c38c02495/src/web/fonts/Roboto-Regular.ttf -------------------------------------------------------------------------------- /src/web/img/copy.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /src/web/img/save.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | RSPL Transpiler 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
22 |
23 |
24 |
25 | 26 | 27 |
28 |
30 |
31 |
32 | 33 |
34 |

35 |             
36 | 37 | 38 | 39 |
40 |
41 |
42 |
43 | 44 |
45 |
46 | 47 | 51 | 52 | -------------------------------------------------------------------------------- /src/web/js/editor.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @copyright 2023 - Max Bebök 3 | * @license Apache-2.0 4 | */ 5 | 6 | // Ace-Editor 7 | import {edit as aceEdit} from "ace-builds"; 8 | import rsplHighlightRules from './editorMode/rspl_highlight_rules'; 9 | import modeGLSL from 'ace-builds/src-min-noconflict/mode-glsl'; 10 | 11 | import "ace-builds/src-min-noconflict/ext-searchbox.js"; 12 | import "ace-builds/src-min-noconflict/theme-tomorrow_night_eighties"; 13 | import "ace-builds/src-min-noconflict/mode-glsl"; 14 | 15 | // Highlight.JS 16 | import hljs from 'highlight.js/lib/core'; 17 | import mipsasm from 'highlight.js/lib/languages/mipsasm'; 18 | import json from 'highlight.js/lib/languages/json'; 19 | 20 | let highlightLines = []; 21 | let highlightLinesDeps = []; 22 | let highlightLineOptDeps = []; 23 | 24 | hljs.registerLanguage('mipsasm', mipsasm); 25 | hljs.registerLanguage('json', json); 26 | hljs.highlightAll(); 27 | 28 | /** 29 | * @param {number} i 30 | * @return {string} 31 | */ 32 | function getRandColor(i, color = 30, brightness = 50) { 33 | return "hsl(" + ((i*152+200) % 360) + ","+color+"%,"+brightness+"%)"; 34 | } 35 | 36 | function getLineHeight(lineCount) { 37 | return 12 + (15 * lineCount); 38 | } 39 | 40 | export function clearHighlightCache() 41 | { 42 | highlightLines = []; 43 | highlightLinesDeps = []; 44 | highlightLineOptDeps = []; 45 | } 46 | 47 | /** 48 | * Create a new RSPL editor 49 | * @param {string} id HTML id 50 | * @param {string} source 51 | * @return {Ace.Editor} 52 | */ 53 | export function createEditor(id, source, line = 0) 54 | { 55 | const editor = aceEdit(id); 56 | const mode = new modeGLSL.Mode(); 57 | mode.HighlightRules = rsplHighlightRules; 58 | 59 | editor.setTheme("ace/theme/tomorrow_night_eighties"); 60 | editor.session.setMode(mode); 61 | editor.session.setOptions({ 62 | tabSize: 2, 63 | useSoftTabs: true, 64 | newLineMode: 'unix', 65 | //enableBasicAutocompletion: true 66 | }); 67 | editor.setValue(source); 68 | editor.clearSelection(); 69 | 70 | editor.gotoLine(line, 0, false); 71 | setTimeout(() => editor.scrollToLine(line, false, false, () => {}), 10); 72 | 73 | return editor; 74 | } 75 | 76 | export function codeHighlightElem(elem, newText = undefined) 77 | { 78 | elem.textContent = newText; 79 | if(elem.dataset.highlighted) { 80 | delete elem.dataset.highlighted; 81 | } 82 | 83 | console.time("highlight"); 84 | hljs.highlightElement(elem); 85 | console.timeEnd("highlight"); 86 | codeHighlightLines(elem); 87 | } 88 | 89 | /** 90 | * 91 | * @param {HTMLElement} elem 92 | * @param {number} hMin 93 | * @param {number} hMax 94 | */ 95 | function scrollTo(elem, hMin, hMax) 96 | { 97 | // Scroll to midpoint of all highlighted lines 98 | const elemHeight = elem.parentElement.clientHeight; 99 | let newScroll = (hMin + hMax) / 2; 100 | newScroll = Math.max(0, newScroll - (elemHeight / 2)); 101 | if(isNaN(newScroll))return; 102 | 103 | const oldScroll = elem.parentElement.scrollTop; 104 | if(Math.abs(oldScroll - newScroll) > 200) { 105 | elem.parentElement.scrollTo({top: newScroll, behavior: 'smooth'}); 106 | } else { 107 | elem.parentElement.scrollTop = newScroll; 108 | } 109 | } 110 | 111 | /** 112 | * @param {HTMLElement} elem 113 | * @param {number[]|undefined} lines 114 | * @param {Record|undefined} linesDeps 115 | */ 116 | export function codeHighlightLines(elem, lines = undefined, linesDeps = undefined) 117 | { 118 | if(lines)highlightLines = lines; 119 | if(linesDeps)highlightLinesDeps = linesDeps; 120 | let hMin = Infinity, hMax = -Infinity; 121 | 122 | // Create overlays and insert into DOM 123 | const ovl = elem.parentElement.querySelector(".asmOverlay"); 124 | const newElements = []; 125 | 126 | const addLine = (height) => { 127 | const lineElem = document.createElement("span"); 128 | lineElem.style.top = height + "px"; 129 | newElements.push(lineElem); 130 | return lineElem; 131 | }; 132 | 133 | let posL = 32; 134 | let width = 8; 135 | let i=0; 136 | for(const line of highlightLines) { 137 | const lineHeight = getLineHeight(line); 138 | const elemMain = addLine(lineHeight); 139 | hMin = Math.min(hMin, lineHeight); 140 | hMax = Math.max(hMax, lineHeight); 141 | elemMain.style.backgroundColor = getRandColor(i, 40, 28); 142 | elemMain.innerText = line+""; 143 | const deps = (highlightLinesDeps[line] || []); 144 | 145 | if(deps.length > 0 && deps[0] !== deps[1]) 146 | { 147 | const heightStart = getLineHeight(deps[0]); 148 | const heightEnd = getLineHeight(deps[1])+10; 149 | const elem = addLine(heightStart); 150 | let relHeight = (heightEnd - heightStart); 151 | 152 | elem.classList.add("dep"); 153 | elem.style.left = (posL - i*width) + "px"; 154 | elem.style.width = (i*width+4) + "px"; 155 | elem.style.borderColor = getRandColor(i, 50); 156 | elem.style.height = relHeight + "px"; 157 | elem.style.zIndex = 1000 - relHeight; 158 | } 159 | ++i; 160 | } 161 | 162 | ovl.replaceChildren(...newElements); 163 | 164 | // Scroll to midpoint of all highlighted lines 165 | scrollTo(elem, hMin, hMax); 166 | } 167 | 168 | /** 169 | * @param {HTMLElement} elem 170 | * @param {number[]|undefined} lines 171 | * @param {Record|undefined} linesDeps 172 | */ 173 | export function codeHighlightOptLines(elem, lines = undefined, linesOptMap = undefined) 174 | { 175 | if(lines)highlightLines = lines; 176 | if(linesOptMap)highlightLineOptDeps = linesOptMap; 177 | let hMin = Infinity, hMax = -Infinity; 178 | 179 | const ovl = elem.parentElement.querySelector(".asmOverlay"); 180 | const newElements = []; 181 | 182 | const addLine = (height) => { 183 | const lineElem = document.createElement("span"); 184 | hMin = Math.min(hMin, height); 185 | hMax = Math.max(hMax, height); 186 | lineElem.style.top = height + "px"; 187 | lineElem.classList.add("opt"); 188 | newElements.push(lineElem); 189 | return lineElem; 190 | }; 191 | 192 | let i=0; 193 | for(const line of highlightLines) { 194 | const optLine = highlightLineOptDeps[line]; 195 | const lineHeight = getLineHeight(optLine); 196 | const elemMain = addLine(lineHeight); 197 | elemMain.style.backgroundColor = getRandColor(i, 40, 28); 198 | elemMain.innerText = optLine+""; 199 | ++i; 200 | } 201 | ovl.replaceChildren(...newElements); 202 | scrollTo(elem, hMin, hMax); 203 | } 204 | 205 | /** 206 | * @param {HTMLElement} elem 207 | * @param {Record} cycleMap 208 | */ 209 | export function codeUpdateCycles(elem, cycleMap, stallMap) 210 | { 211 | const ovl = elem.parentElement.querySelector(".cycleOverlay"); 212 | const elems = []; 213 | let lastCycle = -1; 214 | for(const [line, cycle] of Object.entries(cycleMap)) { 215 | const stall = stallMap[line] || 0; 216 | const span = document.createElement("span"); 217 | span.style.top = getLineHeight(line) + "px"; 218 | span.innerText = cycle === lastCycle ? "^" : cycle+""; 219 | if(stall > 0) { 220 | span.classList.add("stall-" + stall); 221 | } 222 | elems.push(span); 223 | lastCycle = cycle; 224 | } 225 | ovl.replaceChildren(...elems); 226 | } -------------------------------------------------------------------------------- /src/web/js/editorMode/rspl_highlight_rules.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @TODO: 3 | * This is extremely hacky, use the proper method of defining custom syntax 4 | * even though it sucks. 5 | * 6 | * Ref: 7 | * https://github.com/ajaxorg/ace/blob/master/src/mode/glsl_highlight_rules.js 8 | */ 9 | 10 | import ace from "ace-builds"; 11 | import cpp from 'ace-builds/src-min-noconflict/mode-c_cpp'; 12 | const cppMode = new cpp.Mode(); 13 | 14 | var oop = { 15 | inherits: function(ctor, superCtor) { 16 | if(!superCtor)return; 17 | ctor.super_ = superCtor; 18 | ctor.prototype = Object.create(superCtor.prototype, { 19 | constructor: { 20 | value: ctor, 21 | enumerable: false, 22 | writable: true, 23 | configurable: true 24 | } 25 | }); 26 | } 27 | }; 28 | 29 | const rsplHighlightRules = function() { 30 | const keywords = 31 | [ 32 | "state", "temp_state", "function", "command", "macro", 33 | "if", "else", "for", "goto", "break", "continue", 34 | "include", "extern", "while", 35 | "const", "undef", "exit", "loop", 36 | 37 | "s8","u8","s16","u16","s32","u32", 38 | "vec16","vec32", 39 | 40 | "xyzwxyzw", "xyzw", "XYZW", 41 | "xy", "zw", "XY", "ZW", 42 | "xxzzXXZZ", "yywwYYWW", 43 | "xxxxXXXX", "yyyyYYYY", "zzzzZZZZ", "wwwwWWWW", 44 | "xxxxxxxx", "yyyyyyyy", "zzzzzzzz", "wwwwwwww", 45 | "XXXXXXXX", "YYYYYYYY", "ZZZZZZZZ", "WWWWWWWW", 46 | "x", "y", "z", "w", "X", "Y", "Z", "W", 47 | 48 | ].join("|"); 49 | 50 | const buildinConstants = [ 51 | "ZERO", "VZERO", "Relative", "Barrier", "Align", "NoReturn" 52 | ].join("|") 53 | 54 | const variables = [ 55 | "$v00" , "$v01" , "$v02" , "$v03" , "$v04" , "$v05" , "$v06" , "$v07" , 56 | "$v08" , "$v09" , "$v10" , "$v11" , "$v12" , "$v13" , "$v14" , "$v15" , 57 | "$v16" , "$v17" , "$v18" , "$v19" , "$v20" , "$v21" , "$v22" , "$v23" , 58 | "$v24" , "$v25" , "$v26" , "$v27" , "$v28" , "$v29" , "$v30" , "$v31" , 59 | "$at" , "$zero" , "$v0" , "$v1" , "$a0" , "$a1" , "$a2" , "$a3" , 60 | "$t0" , "$t1" , "$t2" , "$t3" , "$t4" , "$t5" , "$t6" , "$t7" , "$t8" , "$t9" , 61 | "$s0" , "$s1" , "$s2" , "$s3" , "$s4" , "$s5" , "$s6" , "$s7" , 62 | "$k0" , "$k1" , "$gp" , "$sp" , "$fp" , "$ra", 63 | ].join("|"); 64 | 65 | const functions = [ 66 | "load", "store", "asm", "printf", 67 | "dma_in", "dma_out", "dma_size", "dma_in_async", "dma_out_async", "dma_await", 68 | "invert_half", "invert_half_sqrt", "sint", "uint", "ufract", "sfract", "swap", "invert", 69 | "load_vec_u8", "load_vec_s8", "store_vec_u8", "store_vec_s8", "select", "abs", "clip", 70 | "get_cmd_address", "get_vcc", "max", "min", "load_arg", 71 | "alignas", 72 | ].join("|"); 73 | 74 | var keywordMapper = this.createKeywordMapper({ 75 | "support.function": functions, 76 | "variable.language": variables, 77 | "keyword": keywords, 78 | "constant.language": buildinConstants, 79 | "constant.language.boolean": "true|false" 80 | }, "identifier"); 81 | 82 | this.$rules = new cppMode.HighlightRules().$rules; 83 | this.$rules.start.forEach(function(rule) { 84 | if (typeof rule.token == "function") 85 | rule.token = keywordMapper; 86 | }); 87 | }; 88 | console.log(cpp); 89 | 90 | oop.inherits(rsplHighlightRules, cppMode.HighlightRules); 91 | 92 | export default rsplHighlightRules; -------------------------------------------------------------------------------- /src/web/js/logger.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @copyright 2023 - Max Bebök 3 | * @license Apache-2.0 4 | */ 5 | 6 | export const Log = 7 | { 8 | set(text) { 9 | logOutput.innerHTML = text + "\n"; 10 | }, 11 | 12 | append(text) { 13 | logOutput.innerHTML += text + "\n"; 14 | }, 15 | 16 | setErrorState(hasError, hasWarn) { 17 | logOutput.className = hasError ? "error" : (hasWarn ? " warn" : ""); 18 | } 19 | }; -------------------------------------------------------------------------------- /src/web/js/main.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @copyright 2023 - Max Bebök 3 | * @license Apache-2.0 4 | */ 5 | 6 | import {transpile, transpileSource} from "../../lib/transpiler"; 7 | import {debounce} from "./utils.js"; 8 | import { 9 | clearHighlightCache, 10 | codeHighlightElem, 11 | codeHighlightLines, 12 | codeHighlightOptLines, codeUpdateCycles, createEditor 13 | } from "./editor.js"; 14 | import {loadLastLine, loadSource, saveLastLine, saveSource, saveToDevice} from "./storage.js"; 15 | import {Log} from "./logger.js"; 16 | 17 | /** @type {ASMOutputDebug} */ 18 | let currentDebug = {lineMap: {}, lineDepMap: {}, lineOptMap: {}, lineCycleMap: {}}; 19 | let lastLine = loadLastLine(); 20 | 21 | const editor = createEditor("inputRSPL", loadSource(), lastLine); 22 | 23 | /** @type {RSPLConfig} */ 24 | let config = { 25 | optimize: true, 26 | rspqWrapper: true, 27 | reorder: false, 28 | patchFunctions: [], 29 | }; 30 | 31 | function getEditorLine() { 32 | return editor.getCursorPosition().row + 1; 33 | } 34 | 35 | function highlightASM(line) 36 | { 37 | if(lastLine === line)return; 38 | lastLine = line; 39 | 40 | const lines = currentDebug.lineMap[line]; 41 | if(!lines || !lines.length) return; 42 | 43 | codeHighlightLines(outputASM, lines, currentDebug.lineDepMap); 44 | if(Object.keys(currentDebug.lineOptMap).length > 0) { 45 | codeHighlightOptLines(outputASMOpt, lines, currentDebug.lineOptMap); 46 | } else { 47 | codeHighlightLines(outputASMOpt, lines, currentDebug.lineDepMap); 48 | } 49 | } 50 | 51 | async function update(reset = false) 52 | { 53 | try { 54 | console.clear(); 55 | if(reset) { 56 | clearHighlightCache(); 57 | lastLine = 0; 58 | } 59 | 60 | const source = editor.getValue(); 61 | saveSource(source); 62 | 63 | const liveUpdateCb = (data) => { 64 | const {asm, asmUnoptimized, warn, info, debug} = data; 65 | updateAsmUI(asm, asmUnoptimized, warn, info, debug); 66 | }; 67 | 68 | console.time("transpile"); 69 | const {asm, asmUnoptimized, warn, info, debug} = await transpileSource(source, config, liveUpdateCb); 70 | console.timeEnd("transpile"); 71 | await updateAsmUI(asm, asmUnoptimized, warn, info, debug); 72 | 73 | } catch(e) { 74 | Log.set(e.message); 75 | if(!e.message.includes("Syntax error")) { 76 | console.error(e); 77 | } 78 | Log.setErrorState(true, false); 79 | } 80 | } 81 | 82 | async function updateAsmUI(asm, asmUnoptimized, warn, info, debug) 83 | { 84 | currentDebug = debug; 85 | 86 | Log.set(info); 87 | Log.append("Transpiled successfully!"); 88 | 89 | outputASM.parentElement.parentElement.hidden = !config.optimize; 90 | if(config.optimize) { 91 | codeHighlightElem(outputASM, asmUnoptimized); 92 | } 93 | codeHighlightElem(outputASMOpt, asm); 94 | codeUpdateCycles(outputASMOpt, debug.lineCycleMap, debug.lineStallMap); 95 | 96 | await saveToDevice("asm", asm, true); 97 | 98 | highlightASM(getEditorLine()); 99 | 100 | Log.setErrorState(false, warn !== ""); 101 | } 102 | 103 | buttonCopyASM.onclick = async () => { 104 | try { 105 | await navigator.clipboard.writeText(outputASMOpt.textContent); 106 | Log.append("Copied to clipboard!"); 107 | } catch (err) { 108 | console.error('Failed to copy: ', err); 109 | } 110 | }; 111 | 112 | buttonSaveASM.onclick = async () => { 113 | await saveToDevice("asm", outputASMOpt.textContent); 114 | }; 115 | 116 | optionOptimize.onchange = async () => { 117 | config.optimize = optionOptimize.checked; 118 | await update(true); 119 | }; 120 | 121 | optionWrapper.onchange = async () => { 122 | config.rspqWrapper = optionWrapper.checked; 123 | await update(true); 124 | }; 125 | 126 | optionReorder.onchange = async () => { 127 | config.reorder = optionReorder.checked; 128 | await update(true); 129 | }; 130 | 131 | update().catch(console.error); 132 | 133 | editor.getSession().on('change', debounce(update, 150)); 134 | editor.getSession().selection.on('changeCursor', () => { 135 | const line = getEditorLine(); 136 | highlightASM(line); 137 | saveLastLine(line); 138 | }); 139 | -------------------------------------------------------------------------------- /src/web/js/storage.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @copyright 2023 - Max Bebök 3 | * @license Apache-2.0 4 | */ 5 | import {EXAMPLE_CODE} from "./exampleCode.js"; 6 | 7 | const STORAGE_KEY = "lastCode05"; 8 | const FILE_HANDLES = {}; 9 | 10 | export function loadSource() { 11 | let oldSource = localStorage.getItem(STORAGE_KEY) || ""; 12 | return (oldSource === "") ? EXAMPLE_CODE : oldSource; 13 | } 14 | 15 | export function saveSource(source) { 16 | localStorage.setItem(STORAGE_KEY, source); 17 | } 18 | 19 | export function saveLastLine(line) { 20 | localStorage.setItem(STORAGE_KEY + "_line", line); 21 | } 22 | 23 | export function loadLastLine() { 24 | return localStorage.getItem(STORAGE_KEY + "_line") || 0; 25 | } 26 | 27 | export async function saveToDevice(id, data, skipDialog = false) 28 | { 29 | if(!FILE_HANDLES[id]) { 30 | if(skipDialog)return; 31 | 32 | const options = { 33 | types: [{ 34 | description: 'MIPS ASM', 35 | accept: {'application/asm': ['.S', '.s'],}, 36 | }], 37 | }; 38 | FILE_HANDLES[id] = await window.showSaveFilePicker(options); 39 | } 40 | 41 | if(data) { 42 | const writable = await FILE_HANDLES[id].createWritable(); 43 | await writable.write(data); 44 | await writable.close(); 45 | } 46 | } -------------------------------------------------------------------------------- /src/web/js/utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @copyright 2023 - Max Bebök 3 | * @license Apache-2.0 4 | */ 5 | 6 | export function debounce(func, timeout){ 7 | let timer; 8 | return (...args) => { 9 | clearTimeout(timer); 10 | timer = setTimeout(() => { func.apply(this, args); }, timeout); 11 | }; 12 | } --------------------------------------------------------------------------------