├── .github ├── dependabot.yml └── workflows │ ├── build.yaml │ └── deploy-playground.yaml ├── .gitignore ├── .prettierrc ├── LICENSE ├── README.md ├── docs ├── Microsoft QuickBASIC BASIC: Language Reference.txt └── assets │ ├── hello.gif │ ├── nibbles.gif │ └── playground.jpg ├── monaco-qb ├── .gitignore ├── .prettierignore ├── README.md ├── babel.config.js ├── jest.config.js ├── package.json ├── src │ ├── demo │ │ ├── index.html │ │ ├── index.ts │ │ └── styles.css │ ├── index.ts │ ├── qb.ts │ └── tests │ │ └── qb.test.ts ├── tsconfig.json └── webpack.config.js ├── package-lock.json ├── package.json ├── playground ├── .env ├── .gitignore ├── .prettierignore ├── examples │ ├── .gitignore │ ├── cal.bas │ ├── check.bas │ ├── guess.bas │ ├── nibbles.bas │ └── strtonum.bas ├── package.json ├── public │ ├── favicon.ico │ ├── icon.svg │ ├── index.html │ ├── logo192.png │ ├── logo512.png │ ├── manifest.json │ └── robots.txt ├── src │ ├── app-header.tsx │ ├── app-splash-screen.tsx │ ├── app.css │ ├── app.tsx │ ├── compile-result-dialog.tsx │ ├── config-manager.ts │ ├── deps.d.ts │ ├── editor-controller.tsx │ ├── editor-pane.tsx │ ├── examples.json │ ├── examples.ts │ ├── external-link.tsx │ ├── help-dialog.tsx │ ├── index.tsx │ ├── messages-pane.tsx │ ├── monaco-editor-interface.tsx │ ├── open-dialog.tsx │ ├── output-screen-pane.tsx │ ├── pane-header.tsx │ ├── qbjc-manager.ts │ ├── react-app-env.d.ts │ ├── run-fab.tsx │ ├── segment.js │ ├── settings-dialog.tsx │ ├── setupTests.ts │ ├── split.css │ └── tools │ │ ├── copy-monaco-assets.sh │ │ ├── generate-examples-bundle-json.js │ │ └── generate-monaco-themes-bundle-json.js └── tsconfig.json ├── qbjc.code-workspace └── qbjc ├── .gitignore ├── .npmignore ├── .prettierignore ├── browser └── index.ts ├── jest.config.js ├── node └── index.ts ├── package.json ├── src ├── codegen │ └── codegen.ts ├── compile.ts ├── deps.d.ts ├── index.ts ├── lib │ ├── ast.ts │ ├── data-item.ts │ ├── error-with-loc.ts │ ├── round-half-to-even.ts │ ├── symbol-table.ts │ └── types.ts ├── parser │ ├── grammar.ne │ ├── grammar.ts │ ├── lexer.ts │ └── parser.ts ├── qbjc.ts ├── runtime │ ├── ansi-terminal-key-code-map.ts │ ├── ansi-terminal-platform.ts │ ├── browser-platform.ts │ ├── builtins.ts │ ├── compiled-code.ts │ ├── executor.ts │ ├── init-value.ts │ ├── node-platform.ts │ ├── node-runtime-bundle-bootstrap.ts │ ├── qb-array.ts │ ├── qb-udt.ts │ └── runtime.ts ├── semantic-analysis │ └── semantic-analysis.ts ├── tests │ ├── compile-and-run.test.ts │ ├── package-test.sh │ ├── qb-array.test.ts │ ├── qbjc.test.ts │ └── testdata │ │ ├── compile-and-run │ │ ├── .gitignore │ │ ├── array.bas │ │ ├── builtins.bas │ │ ├── case.bas │ │ ├── data.bas │ │ ├── def-fn.bas │ │ ├── deftype.bas │ │ ├── for-loop.bas │ │ ├── function.bas │ │ ├── gosub.bas │ │ ├── hello-world.bas │ │ ├── if-stmt.bas │ │ ├── input.bas │ │ ├── loop.bas │ │ ├── pascals-triangle.bas │ │ ├── print-numbers.bas │ │ ├── print.bas │ │ ├── rounding.bas │ │ ├── select-stmt.bas │ │ ├── shell-sort.bas │ │ ├── strtonum.bas │ │ ├── sub.bas │ │ ├── token.bas │ │ ├── udt.bas │ │ └── var-decl-init.bas │ │ └── qbjc │ │ ├── .gitignore │ │ └── hello-world.bas └── tools │ ├── build-runtime-bundle.ts │ └── extract-stmt-fn-ref.ts ├── tsconfig.base.json ├── tsconfig.json └── tsconfig.platforms.json /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: 'npm' 4 | directory: '/' 5 | schedule: 6 | interval: 'weekly' 7 | -------------------------------------------------------------------------------- /.github/workflows/build.yaml: -------------------------------------------------------------------------------- 1 | name: build 2 | on: [push, pull_request] 3 | jobs: 4 | build: 5 | runs-on: ubuntu-latest 6 | strategy: 7 | matrix: 8 | node-version: [20, 22] 9 | steps: 10 | - uses: actions/checkout@v4 11 | - uses: actions/setup-node@v4 12 | with: 13 | node-version: ${{ matrix.node-version }} 14 | - run: npm install --legacy-peer-deps 15 | - run: npm run --workspaces lint 16 | - run: npm run --workspaces build 17 | - run: npm run --workspace=qbjc test 18 | - run: npm run --workspace=qbjc packageTest 19 | - run: npm run --workspace=monaco-qb test 20 | - run: npm run --workspace=monaco-qb build:demo 21 | -------------------------------------------------------------------------------- /.github/workflows/deploy-playground.yaml: -------------------------------------------------------------------------------- 1 | name: deploy qbjc playground 2 | on: 3 | push: 4 | branches: [master] 5 | jobs: 6 | build-and-deploy-playground: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v4 10 | - uses: actions/setup-node@v4 11 | with: 12 | node-version: 22 13 | - run: npm install --legacy-peer-deps 14 | - env: 15 | REACT_APP_SEGMENT_WRITE_KEY: ${{ secrets.SEGMENT_WRITE_KEY }} 16 | run: npm run --workspaces build 17 | - uses: peaceiris/actions-gh-pages@v4 18 | with: 19 | github_token: ${{ secrets.GITHUB_TOKEN }} 20 | publish_dir: ./playground/build 21 | cname: qbjc.dev 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | 3 | .DS_Store 4 | 5 | npm-debug.log* 6 | yarn-debug.log* 7 | yarn-error.log* 8 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "es5", 4 | "bracketSpacing": false, 5 | "arrowParens": "always" 6 | } 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![NPM Version][npm-version-image]][npm-url] 2 | [![Build Status][build-status-image]][github-url] 3 | 4 | # qbjc 5 | 6 | **qbjc** is a QBasic to JavaScript compiler. It can compile a QBasic / 7 | QuickBASIC program to: 8 | 9 | - A standalone executable Node.js script, with zero external dependencies; or 10 | - An ES6 module that can be imported and executed in both Node.js and browser environments. 11 | 12 | Try it out in the browser: [👉 **qbjc.dev** 👈](https://qbjc.dev) 13 | 14 | ## But why? 15 | 16 | QBasic was my first introduction to programming as a kid back in the 90s. 17 | Despite its many limitations, it really inspired my passion for building things 18 | with technology that continues to this day. The balance of simplicity and power 19 | made programming feel both approachable and incredibly fun [1]. 20 | 21 | I wanted to see if I could recreate a little bit of that magic for the modern 22 | age, something easy to get started with in today's web-centric world. So this 23 | project was born - a QBasic compiler + runtime + basic web IDE that compiles 24 | your QBasic code to JavaScript! 25 | 26 | [1] See also: [30 years later, QBasic is still the 27 | best](http://www.nicolasbize.com/blog/30-years-later-qbasic-is-still-the-best/) 28 | 29 | ## Usage 30 | 31 | ### qbjc playground 32 | 33 | The qbjc playground 34 | ([👉 **qbjc.dev** 👈](https://qbjc.dev)) 35 | allows you to edit and run QBasic / QuickBASIC programs directly in the browser, 36 | no installation required. 37 | 38 | ![qbjc playground screenshot](./docs/assets/playground.jpg) 39 | 40 | ### Command line usage 41 | 42 | ```bash 43 | # Install qbjc from NPM 44 | npm install -g qbjc 45 | 46 | # Compile hello.bas and write output to hello.bas.js 47 | qbjc hello.bas 48 | 49 | # Run the compiled program 50 | ./hello.bas.js 51 | 52 | # ...or run hello.bas directly: 53 | qbjc --run hello.bas 54 | 55 | # See all command line options 56 | qbjc --help 57 | ``` 58 | 59 | ![Compiling and running a simple program](./docs/assets/hello.gif) 60 | 61 | ### API usage 62 | 63 | Compiling a QBasic program: 64 | 65 | ```TypeScript 66 | import {compile} from 'qbjc'; 67 | 68 | ... 69 | const { 70 | // Compiled JavaScript code. 71 | code, 72 | // Sourcemap for the compiled JavaScript code. 73 | map, 74 | // Abstract syntax tree representing the compiled code. 75 | astModule, 76 | } = await compile({ 77 | // QBasic source code. 78 | source: 'PRINT "HELLO WORLD"', 79 | // Optional - Source file name (for debugging information). 80 | sourceFileName: 'hello.bas', 81 | // Optional - Whether to bundle with Node.js runtime code in order 82 | // to produce a standalone script. 83 | enableBundling: false, 84 | // Optional - Whether to minify the output. 85 | enableMinify: false, 86 | }); 87 | ``` 88 | 89 | Executing the compiled code: 90 | 91 | - In browsers (using [xterm.js](https://xtermjs.org/)): 92 | 93 | ```TypeScript 94 | import {Terminal} from 'xterm'; 95 | import {BrowserExecutor} from 'qbjc/browser'; 96 | 97 | // Set up xterm.js Terminal instance 98 | const terminal = new Terminal(...); 99 | 100 | await new BrowserExecutor(terminal).executeModule(code); 101 | ``` 102 | 103 | - In Node.js: 104 | 105 | ```TypeScript 106 | import {NodeExecutor} from 'qbjc/node'; 107 | 108 | await new NodeExecutor().executeModule(code); 109 | ``` 110 | 111 | - In Node.js with bundling enabled (i.e. compiled with `enableBundling: true`): 112 | ```TypeScript 113 | import {run} from './hello.bas.js'; 114 | await run(); 115 | ``` 116 | 117 | ## Compatibility 118 | 119 | ### What works: 120 | 121 | - Core language features 122 | 123 | - Control flow structures - loops, conditionals, `GOTO`, `GOSUB` etc. 124 | - Data types - primitive types, arrays and user-defined types (a.k.a. records) 125 | - Expressions - arithmetic, string, comparison, boolean 126 | - `SUB`s and `FUNCTION`s 127 | - `DATA` constants 128 | - Many built-in commands and functions like `VAL`, `STR$`, `INSTR`, `MID$` 129 | 130 | - Text mode 131 | 132 | - Basic text mode I/O - `PRINT`, `INPUT`, `INKEY$`, `INPUT$` etc. 133 | - Text mode screen manipulation - `COLOR`, `LOCATE` etc. 134 | - Note that the current implementation depends on a VT100-compatible terminal emulator. 135 | - On Windows, this means using WSL or something like PuTTY. 136 | - In the browser, the implementation uses [xterm.js](https://xtermjs.org/). 137 | 138 | - It's just enough to run the original [`NIBBLES.BAS` game](./playground/examples/nibbles.bas) that shipped with QBasic: 139 | ![Compiling and running NIBBLES.BAS](./docs/assets/nibbles.gif) 140 | 141 | ### What doesn't work (yet): 142 | 143 | - Graphics and audio 144 | - Events - `ON ERROR`, `ON TIMER` etc. 145 | - OS APIs like file I/O, `CALL INTERRUPT` etc. 146 | - Direct memory access - `PEEK`, `POKE` etc. 147 | - Less common syntax, inputs or options 148 | - ...and more - contributions are welcome! 149 | 150 | For detailed compatibility information on individual commands and functions, see 151 | [👉 Implementation Status](https://airtable.com/shrITVmjepv00kwpT). 152 | 153 | ## About 154 | 155 | qbjc is distributed under the Apache License v2. 156 | 157 | [npm-url]: https://npmjs.org/package/qbjc 158 | [npm-version-image]: https://badgen.net/npm/v/qbjc 159 | [github-url]: https://github.com/jichu4n/qbjc 160 | [build-status-image]: https://github.com/jichu4n/qbjc/actions/workflows/build.yaml/badge.svg 161 | -------------------------------------------------------------------------------- /docs/assets/hello.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jichu4n/qbjc/155006b930547a89fa6dba224cae98035c5b2625/docs/assets/hello.gif -------------------------------------------------------------------------------- /docs/assets/nibbles.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jichu4n/qbjc/155006b930547a89fa6dba224cae98035c5b2625/docs/assets/nibbles.gif -------------------------------------------------------------------------------- /docs/assets/playground.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jichu4n/qbjc/155006b930547a89fa6dba224cae98035c5b2625/docs/assets/playground.jpg -------------------------------------------------------------------------------- /monaco-qb/.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | 3 | /dist 4 | 5 | .DS_Store 6 | 7 | npm-debug.log* 8 | yarn-debug.log* 9 | yarn-error.log* 10 | -------------------------------------------------------------------------------- /monaco-qb/.prettierignore: -------------------------------------------------------------------------------- 1 | /dist/ 2 | 3 | # Generated files. 4 | /node.js 5 | /node.d.ts 6 | /browser.js 7 | /browser.d.ts 8 | 9 | # Generated parser. 10 | /src/parser/grammar.ts 11 | 12 | # Intermediate debugging output generated during testing. 13 | *.bas.js 14 | *.ast.json 15 | 16 | -------------------------------------------------------------------------------- /monaco-qb/README.md: -------------------------------------------------------------------------------- 1 | # monaco-qb 2 | 3 | QBasic / QuickBASIC syntax highlighting for the Monaco editor. 4 | 5 | ## Usage 6 | 7 | Install from NPM: 8 | 9 | ``` 10 | npm install monaco-qb 11 | ``` 12 | 13 | To enable QBasic / QuickBASIC mode in Monaco, simply import the `monaco-qb` 14 | package and specify `qb` as the language when creating an editor instance: 15 | 16 | ```ts 17 | import * as monaco from 'monaco-editor'; 18 | import 'monaco-qb'; 19 | 20 | const editor = monaco.editor.create(document.getElementById('editor')!, { 21 | value: '', 22 | language: 'qb', 23 | // ... 24 | }); 25 | ``` 26 | 27 | Alternatively, you can import and set up monaco-qb lazily: 28 | 29 | ```ts 30 | import {setupLanguage} from 'monaco-qb/qb'; 31 | // This will register the 'qb' language with Monaco. 32 | setupLanguage(); 33 | ``` 34 | 35 | ## Examples 36 | 37 | For a basic example of monaco-qb usage, see 38 | [`src/demo`](https://github.com/jichu4n/qbjc/blob/master/monaco-qb/src/demo/). 39 | 40 | For a live example, please check out the qbjc playground 41 | ([👉 **qbjc.dev** 👈](https://qbjc.dev)) 42 | which allows you to edit and run QBasic / QuickBASIC programs directly in the 43 | browser. 44 | -------------------------------------------------------------------------------- /monaco-qb/babel.config.js: -------------------------------------------------------------------------------- 1 | // See https://stackoverflow.com/a/54656593 2 | module.exports = { 3 | presets: ['@babel/preset-env'], 4 | }; 5 | -------------------------------------------------------------------------------- /monaco-qb/jest.config.js: -------------------------------------------------------------------------------- 1 | // See https://gitlab.com/gitlab-org/gitlab/-/issues/119194 2 | module.exports = { 3 | roots: ['/dist'], 4 | transformIgnorePatterns: ['node_modules/(?!monaco-editor/)'], 5 | moduleNameMapper: { 6 | '^monaco-editor$': 'monaco-editor/esm/vs/editor/editor.main.js', 7 | '\\.(css|less)$': 'identity-obj-proxy', 8 | }, 9 | testEnvironment: 'jsdom', 10 | }; 11 | -------------------------------------------------------------------------------- /monaco-qb/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "monaco-qb", 3 | "version": "0.0.1", 4 | "description": "QBasic / QuickBASIC syntax highlighting for Monaco editor", 5 | "repository": { 6 | "type": "git", 7 | "url": "git+https://github.com/jichu4n/qbjc.git" 8 | }, 9 | "author": "Chuan Ji ", 10 | "license": "Apache-2.0", 11 | "bugs": { 12 | "url": "https://github.com/jichu4n/qbjc/issues" 13 | }, 14 | "homepage": "https://github.com/jichu4n/qbjc#readme", 15 | "main": "./dist/index.js", 16 | "types": "./dist/index.d.ts", 17 | "scripts": { 18 | "build": "tsc", 19 | "build:demo": "webpack", 20 | "start:demo": "webpack serve", 21 | "lint": "prettier --check .", 22 | "test": "jest", 23 | "prepack": "npm run lint && npm run build && npm test && rm -rf ./dist/tests ./dist/demo" 24 | }, 25 | "devDependencies": { 26 | "@types/jest": "^29.5.13", 27 | "copy-webpack-plugin": "^11.0.0", 28 | "html-webpack-plugin": "^5.5.0", 29 | "identity-obj-proxy": "^3.0.0", 30 | "jest": "^29.7.0", 31 | "jest-environment-jsdom": "^29.7.0", 32 | "monaco-editor": "^0.34.1", 33 | "monaco-themes": "^0.4.2", 34 | "prettier": "^3.5.3", 35 | "ts-loader": "^9.4.1", 36 | "typescript": "^4.8.4", 37 | "webpack": "^5.94.0", 38 | "webpack-cli": "^6.0.1", 39 | "webpack-dev-server": "^4.11.1" 40 | }, 41 | "peerDependencies": { 42 | "monaco-editor": ">=0.29.0" 43 | }, 44 | "files": [ 45 | "dist" 46 | ] 47 | } 48 | -------------------------------------------------------------------------------- /monaco-qb/src/demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | monaco-qb demo 6 | 7 | 8 |
9 |
10 | 14 |
15 | 16 | 17 | -------------------------------------------------------------------------------- /monaco-qb/src/demo/index.ts: -------------------------------------------------------------------------------- 1 | import * as monaco from 'monaco-editor'; 2 | import '../index'; 3 | import './styles.css'; 4 | 5 | enum LocalStorageKeys { 6 | CONTENT = 'content', 7 | THEME = 'theme', 8 | } 9 | 10 | async function loadThemes() { 11 | const themeList = await (await fetch('/themes/themelist.json')).json(); 12 | const themeData = Object.fromEntries( 13 | await Promise.all( 14 | Object.entries(themeList).map(async ([themeKey, themeName]) => [ 15 | themeKey, 16 | await (await fetch(`/themes/${themeName}.json`)).json(), 17 | ]) 18 | ) 19 | ) as {[key: string]: monaco.editor.IStandaloneThemeData}; 20 | 21 | const themeSelect = document.getElementById( 22 | 'themeSelect' 23 | ) as HTMLSelectElement; 24 | for (const [themeKey, themeJson] of Object.entries(themeData)) { 25 | monaco.editor.defineTheme(themeKey, themeJson); 26 | const themeOption = document.createElement('option'); 27 | themeOption.value = themeKey; 28 | themeOption.text = themeList[themeKey]; 29 | themeSelect.add(themeOption); 30 | } 31 | const initialTheme = 32 | localStorage.getItem(LocalStorageKeys.THEME) || 'vs-dark'; 33 | monaco.editor.setTheme(initialTheme); 34 | themeSelect.value = initialTheme; 35 | themeSelect.style.visibility = 'visible'; 36 | 37 | themeSelect.addEventListener('change', () => { 38 | const themeKey = themeSelect.value; 39 | monaco.editor.setTheme(themeKey); 40 | localStorage.setItem(LocalStorageKeys.THEME, themeKey); 41 | }); 42 | } 43 | 44 | async function loadInitialContent() { 45 | let initialContent = localStorage.getItem(LocalStorageKeys.CONTENT); 46 | if (!initialContent) { 47 | initialContent = await (await fetch('/examples/nibbles.bas')).text(); 48 | } 49 | return initialContent; 50 | } 51 | 52 | async function setupEditor() { 53 | // @ts-ignore 54 | window.MonacoEnvironment = window.MonacoEnvironment || { 55 | getWorkerUrl() { 56 | return './monaco/vs/base/worker/workerMain.js'; 57 | }, 58 | }; 59 | 60 | const editor = monaco.editor.create(document.getElementById('editor')!, { 61 | value: await loadInitialContent(), 62 | language: 'qb', 63 | minimap: {enabled: false}, 64 | scrollBeyondLastLine: false, 65 | }); 66 | const editorModel = editor.getModel()!; 67 | 68 | editorModel.onDidChangeContent(() => { 69 | localStorage.setItem(LocalStorageKeys.CONTENT, editorModel.getValue()); 70 | }); 71 | } 72 | 73 | (async () => { 74 | await loadThemes(); 75 | await setupEditor(); 76 | })(); 77 | -------------------------------------------------------------------------------- /monaco-qb/src/demo/styles.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | padding: 0; 4 | margin: 0; 5 | background-color: black; 6 | } 7 | 8 | #editor { 9 | position: fixed; 10 | left: 0; 11 | top: 0; 12 | height: 100%; 13 | width: 100%; 14 | } 15 | 16 | #toolbar { 17 | position: fixed; 18 | right: 20px; 19 | opacity: 0.5; 20 | } 21 | 22 | #toolbar:hover { 23 | opacity: 1; 24 | } 25 | -------------------------------------------------------------------------------- /monaco-qb/src/index.ts: -------------------------------------------------------------------------------- 1 | import {setupLanguage} from './qb'; 2 | 3 | export * from './qb'; 4 | 5 | setupLanguage(); 6 | -------------------------------------------------------------------------------- /monaco-qb/src/qb.ts: -------------------------------------------------------------------------------- 1 | import {languages} from 'monaco-editor'; 2 | 3 | export const languageId = 'qb'; 4 | 5 | // All QBasic keywords and built-in functions. 6 | export const Keywords = [ 7 | 'ABS', 8 | 'ABSOLUTE', 9 | 'ACCESS', 10 | 'ALIAS', 11 | 'AND', 12 | 'AS', 13 | 'ASC', 14 | 'ATN', 15 | 'BASE', 16 | 'BEEP', 17 | 'BINARY', 18 | 'BLOAD', 19 | 'BSAVE', 20 | 'CALL', 21 | 'CALLS', 22 | 'CASE', 23 | 'CDBL', 24 | 'CDECL', 25 | 'CHAIN', 26 | 'CHDIR', 27 | 'CHR$', 28 | 'CINT', 29 | 'CIRCLE', 30 | 'CLEAR', 31 | 'CLNG', 32 | 'CLOSE', 33 | 'CLS', 34 | 'COLOR', 35 | 'COM', 36 | 'COMMAND$', 37 | 'COMMON', 38 | 'CONST', 39 | 'COS', 40 | 'CSNG', 41 | 'CSRLIN', 42 | 'CVD', 43 | 'CVDMBF', 44 | 'CVI', 45 | 'CVL', 46 | 'CVS', 47 | 'CVSMBF', 48 | 'DATA', 49 | 'DATE$', 50 | 'DECLARE', 51 | 'DEF', 52 | 'DEFDBL', 53 | 'DEFINT', 54 | 'DEFLNG', 55 | 'DEFSNG', 56 | 'DEFSTR', 57 | 'DIM', 58 | 'DO', 59 | 'DOUBLE', 60 | 'DRAW', 61 | 'ELSE', 62 | 'ELSEIF', 63 | 'END', 64 | 'ENVIRON$', 65 | 'EOF', 66 | 'ERASE', 67 | 'ERDEV', 68 | 'ERDEV$', 69 | 'ERL', 70 | 'ERR', 71 | 'ERROR', 72 | 'EXIT', 73 | 'EXP', 74 | 'FIELD', 75 | 'FILEATTR', 76 | 'FILES', 77 | 'FIX', 78 | 'FN', 79 | 'FOR', 80 | 'FRE', 81 | 'FREEFILE', 82 | 'FUNCTION', 83 | 'GET', 84 | 'GOSUB', 85 | 'GOTO', 86 | 'HEX$', 87 | 'IF', 88 | 'INKEY$', 89 | 'INP', 90 | 'INPUT', 91 | 'INPUT$', 92 | 'INSTR', 93 | 'INT', 94 | 'INT86OLD', 95 | 'INT86XOLD', 96 | 'INTEGER', 97 | 'INTERRUPT', 98 | 'INTERRUPTX', 99 | 'IOCTL', 100 | 'IOCTL$', 101 | 'IS', 102 | 'KEY', 103 | 'KILL', 104 | 'LBOUND', 105 | 'LCASE$', 106 | 'LEFT$', 107 | 'LEN', 108 | 'LET', 109 | 'LINE', 110 | 'LOC', 111 | 'LOCATE', 112 | 'LOCK', 113 | 'LOF', 114 | 'LOG', 115 | 'LONG', 116 | 'LOOP', 117 | 'LPOS', 118 | 'LPRINT', 119 | 'LSET', 120 | 'LTRIM$', 121 | 'MID$', 122 | 'MKD$', 123 | 'MKDIR', 124 | 'MKDMBF$', 125 | 'MKI$', 126 | 'MKL$', 127 | 'MKS$', 128 | 'MKSMBF$', 129 | 'MOD', 130 | 'NAME', 131 | 'NEXT', 132 | 'NOT', 133 | 'OCT$', 134 | 'OFF', 135 | 'ON', 136 | 'OPEN', 137 | 'OPTION', 138 | 'OR', 139 | 'OUT', 140 | 'OUTPUT', 141 | 'PAINT', 142 | 'PALETTE', 143 | 'PCOPY', 144 | 'PEEK', 145 | 'PEN', 146 | 'PLAY', 147 | 'PMAP', 148 | 'POINT', 149 | 'POKE', 150 | 'POS', 151 | 'PRESET', 152 | 'PRINT', 153 | 'PSET', 154 | 'PUT', 155 | 'RANDOM', 156 | 'RANDOMIZE', 157 | 'READ', 158 | 'REDIM', 159 | 'RESET', 160 | 'RESTORE', 161 | 'RESUME', 162 | 'RETURN', 163 | 'RIGHT$', 164 | 'RMDIR', 165 | 'RND', 166 | 'RSET', 167 | 'RTRIM$', 168 | 'RUN', 169 | 'SADD', 170 | 'SCREEN', 171 | 'SEEK', 172 | 'SEG', 173 | 'SELECT', 174 | 'SETMEM', 175 | 'SGN', 176 | 'SHARED', 177 | 'SHELL', 178 | 'SIN', 179 | 'SINGLE', 180 | 'SLEEP', 181 | 'SOUND', 182 | 'SPACE$', 183 | 'SPC', 184 | 'SQR', 185 | 'STATIC', 186 | 'STEP', 187 | 'STICK', 188 | 'STOP', 189 | 'STR$', 190 | 'STRIG', 191 | 'STRING', 192 | 'STRING$', 193 | 'SUB', 194 | 'SWAP', 195 | 'SYSTEM', 196 | 'TAB', 197 | 'TAN', 198 | 'THEN', 199 | 'TIME$', 200 | 'TIMER', 201 | 'TO', 202 | 'TROFF', 203 | 'TRON', 204 | 'TYPE', 205 | 'UBOUND', 206 | 'UCASE$', 207 | 'UEVENT', 208 | 'UNLOCK', 209 | 'UNTIL', 210 | 'USING', 211 | 'VAL', 212 | 'VARPTR', 213 | 'VARPTR$', 214 | 'VARSEG', 215 | 'VIEW', 216 | 'WAIT', 217 | 'WEND', 218 | 'WHILE', 219 | 'WIDTH', 220 | 'WINDOW', 221 | 'WRITE', 222 | ]; 223 | 224 | export const languageExtensionPoint: languages.ILanguageExtensionPoint = { 225 | id: languageId, 226 | extensions: ['.bas'], 227 | aliases: ['QBasic', 'QuickBASIC', 'qb'], 228 | }; 229 | 230 | export const languageConfiguration: languages.LanguageConfiguration = { 231 | comments: { 232 | lineComment: "'", 233 | }, 234 | brackets: [ 235 | ['[', ']'], 236 | ['(', ')'], 237 | ['def', 'end def'], 238 | ['function', 'end function'], 239 | ['if', 'end if'], 240 | ['select', 'end select'], 241 | ['sub', 'end sub'], 242 | ['type', 'end type'], 243 | ], 244 | autoClosingPairs: [ 245 | {open: '[', close: ']', notIn: ['string', 'comment']}, 246 | {open: '(', close: ')', notIn: ['string', 'comment']}, 247 | {open: '"', close: '"', notIn: ['string', 'comment']}, 248 | ], 249 | }; 250 | 251 | export const language: languages.IMonarchLanguage = { 252 | ignoreCase: true, 253 | brackets: [ 254 | {token: 'delimiter.array', open: '[', close: ']'}, 255 | {token: 'delimiter.parenthesis', open: '(', close: ')'}, 256 | 257 | {token: 'keyword.tag-def', open: 'def', close: 'end def'}, 258 | {token: 'keyword.tag-function', open: 'function', close: 'end function'}, 259 | {token: 'keyword.tag-if', open: 'if', close: 'end if'}, 260 | {token: 'keyword.tag-select', open: 'select', close: 'end select'}, 261 | {token: 'keyword.tag-sub', open: 'sub', close: 'end sub'}, 262 | {token: 'keyword.tag-type', open: 'type', close: 'end type'}, 263 | 264 | {token: 'keyword.tag-do', open: 'do', close: 'loop'}, 265 | {token: 'keyword.tag-for', open: 'for', close: 'next'}, 266 | ], 267 | keywords: Object.values(Keywords), 268 | tagwords: [ 269 | 'def', 270 | 'function', 271 | 'if', 272 | 'select', 273 | 'sub', 274 | 'do', 275 | 'loop', 276 | 'for', 277 | 'next', 278 | ], 279 | tokenizer: { 280 | root: [ 281 | {include: '@whitespace'}, 282 | 283 | // End tags 284 | [/next(?!\w)/, {token: 'keyword.tag-for'}], 285 | [/loop(?!\w)/, {token: 'keyword.tag-do'}], 286 | [ 287 | /end\s+(?!for|do)(def|function|if|select|sub|type)/, 288 | {token: 'keyword.tag-$1'}, 289 | ], 290 | 291 | // Identifiers, tagwords, and keywords 292 | [ 293 | /[a-zA-Z_][a-zA-Z0-9_]*[\$%#&!]?/, 294 | { 295 | cases: { 296 | '@tagwords': {token: 'keyword.tag-$0'}, 297 | '@keywords': {token: 'keyword.$0'}, 298 | '@default': 'identifier', 299 | }, 300 | }, 301 | ], 302 | 303 | // Numeric constants 304 | [/\d+[de]([-+]?\d+)?[#!]?/, 'number.float'], 305 | [/\d*\.\d+([de][-+]?\d+)?[#!]?/, 'number.float'], 306 | [/\d+[#!]/, 'number.float'], 307 | [/&h[0-9a-f]+&?/, 'number.hex'], 308 | [/&o?[0-7]+&?/, 'number.octal'], 309 | [/\d+&?/, 'number'], 310 | 311 | // Symbols 312 | [/[()\[\]]/, '@brackets'], 313 | [/[=> { 334 | languages.setLanguageConfiguration(languageId, languageConfiguration); 335 | languages.setMonarchTokensProvider(languageId, language); 336 | }); 337 | } 338 | -------------------------------------------------------------------------------- /monaco-qb/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "strict": true, 6 | "esModuleInterop": true, 7 | "declaration": true, 8 | "outDir": "dist", 9 | "baseUrl": ".", 10 | "resolveJsonModule": true 11 | }, 12 | "include": ["src/**/*"] 13 | } 14 | -------------------------------------------------------------------------------- /monaco-qb/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const CopyWebpackPlugin = require('copy-webpack-plugin'); 3 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 4 | 5 | const monacoEditorPath = path.join( 6 | path.dirname(require.resolve('monaco-editor/esm/vs/editor/editor.main.js')), 7 | '..', 8 | '..', 9 | '..' 10 | ); 11 | 12 | module.exports = { 13 | name: 'demo', 14 | mode: 'development', 15 | devtool: 'inline-source-map', 16 | devServer: { 17 | static: './dist', 18 | }, 19 | module: { 20 | rules: [ 21 | { 22 | test: /\.tsx?$/, 23 | use: 'ts-loader', 24 | exclude: /node_modules/, 25 | }, 26 | { 27 | test: /\.css$/, 28 | use: ['style-loader', 'css-loader'], 29 | }, 30 | ], 31 | }, 32 | resolve: { 33 | extensions: ['.tsx', '.ts', '.js'], 34 | }, 35 | output: { 36 | filename: 'bundle.js', 37 | path: path.resolve(__dirname, 'dist', 'demo'), 38 | clean: true, 39 | }, 40 | entry: './src/demo/index.ts', 41 | plugins: [ 42 | new HtmlWebpackPlugin({ 43 | template: './src/demo/index.html', 44 | }), 45 | new CopyWebpackPlugin({ 46 | patterns: [ 47 | {from: '../playground/examples/*.bas', to: 'examples/[name][ext]'}, 48 | { 49 | from: path.join( 50 | path.dirname(require.resolve('monaco-themes/package.json')), 51 | 'themes' 52 | ), 53 | to: 'themes', 54 | }, 55 | { 56 | from: path.join(monacoEditorPath, 'min', 'vs', 'base', 'worker'), 57 | to: path.join('monaco', 'vs', 'base', 'worker'), 58 | }, 59 | { 60 | from: path.join( 61 | monacoEditorPath, 62 | 'min', 63 | 'vs', 64 | 'base', 65 | 'common', 66 | 'worker' 67 | ), 68 | to: path.join('monaco', 'vs', 'base', 'common', 'worker'), 69 | }, 70 | ], 71 | }), 72 | ], 73 | }; 74 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "qbjc-monorepo", 3 | "version": "0.1.0", 4 | "private": true, 5 | "workspaces": [ 6 | "./qbjc", 7 | "./monaco-qb", 8 | "./playground" 9 | ], 10 | "overrides": { 11 | "ansi-escapes": { 12 | "type-fest": "^3.0.0" 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /playground/.env: -------------------------------------------------------------------------------- 1 | SKIP_PREFLIGHT_CHECK=true 2 | 3 | -------------------------------------------------------------------------------- /playground/.gitignore: -------------------------------------------------------------------------------- 1 | # copied from node_modules during build 2 | /public/monaco 3 | 4 | # genenerated during build 5 | /src/examples-bundle.json 6 | /src/monaco-themes-bundle.json 7 | 8 | # dependencies 9 | /node_modules 10 | /.pnp 11 | .pnp.js 12 | 13 | # testing 14 | /coverage 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | .env.local 22 | .env.development.local 23 | .env.test.local 24 | .env.production.local 25 | .eslintcache 26 | 27 | npm-debug.log* 28 | yarn-debug.log* 29 | yarn-error.log* 30 | -------------------------------------------------------------------------------- /playground/.prettierignore: -------------------------------------------------------------------------------- 1 | /build/ 2 | -------------------------------------------------------------------------------- /playground/examples/.gitignore: -------------------------------------------------------------------------------- 1 | # Intermediate debugging output generated during testing. 2 | *.ast.json 3 | *.bas.js 4 | *.bas.js.map 5 | -------------------------------------------------------------------------------- /playground/examples/cal.bas: -------------------------------------------------------------------------------- 1 | ' Example from "Microsoft QuickBASIC: Programming in BASIC" 2 | ' Source: https://www.pcjs.org/documents/books/mspl13/basic/qbprog/ 3 | 4 | DEFINT A-Z ' Default variable type is integer 5 | 6 | ' Define a data type for the names of the months and the 7 | ' number of days in each: 8 | TYPE MonthType 9 | Number AS INTEGER ' Number of days in the month 10 | MName AS STRING * 9 ' Name of the month 11 | END TYPE 12 | 13 | ' Declare procedures used: 14 | DECLARE FUNCTION IsLeapYear% (N%) 15 | DECLARE FUNCTION GetInput% (Prompt$, Row%, LowVal%, HighVal%) 16 | 17 | DECLARE SUB PrintCalendar (Year%, Month%) 18 | DECLARE SUB ComputeMonth (Year%, Month%, StartDay%, TotalDays%) 19 | 20 | DIM MonthData(1 TO 12) AS MonthType 21 | 22 | ' Initialize month definitions from DATA statements below: 23 | FOR I = 1 TO 12 24 | READ MonthData(I).MName, MonthData(I).Number 25 | NEXT 26 | 27 | ' Main loop, repeat for as many months as desired: 28 | DO 29 | 30 | CLS 31 | 32 | ' Get year and month as input: 33 | Year = GetInput("Year (1899 to 2099): ", 1, 1899, 2099) 34 | Month = GetInput("Month (1 to 12): ", 2, 1, 12) 35 | 36 | ' Print the calendar: 37 | PrintCalendar Year, Month 38 | 39 | ' Another Date? 40 | LOCATE 13, 1 ' Locate in 13th row, 1st column 41 | PRINT "New Date? "; ' Keep cursor on same line 42 | LOCATE , , 1, 0, 13 ' Turn cursor on and make it one 43 | ' character high 44 | Resp$ = INPUT$(1) ' Wait for a key press 45 | PRINT Resp$ ' Print the key pressed 46 | 47 | LOOP WHILE UCASE$(Resp$) = "Y" 48 | END 49 | 50 | ' Data for the months of a year: 51 | DATA January, 31, February, 28, March, 31 52 | DATA April, 30, May, 31, June, 30, July, 31, August, 31 53 | DATA September, 30, October, 31, November, 30, December, 31 54 | ' 55 | ' ====================== COMPUTEMONTH ======================== 56 | ' Computes the first day and the total days in a month. 57 | ' ============================================================ 58 | ' 59 | SUB ComputeMonth (Year, Month, StartDay, TotalDays) STATIC 60 | SHARED MonthData() AS MonthType 61 | CONST LEAP = 366 MOD 7 62 | CONST NORMAL = 365 MOD 7 63 | 64 | ' Calculate total number of days (NumDays) since 1/1/1899. 65 | 66 | ' Start with whole years: 67 | NumDays = 0 68 | FOR I = 1899 TO Year - 1 69 | IF IsLeapYear(I) THEN ' If year is leap, add 70 | NumDays = NumDays + LEAP ' 366 MOD 7. 71 | ELSE ' If normal year, add 72 | NumDays = NumDays + NORMAL ' 365 MOD 7. 73 | END IF 74 | NEXT 75 | 76 | ' Next, add in days from whole months: 77 | FOR I = 1 TO Month - 1 78 | NumDays = NumDays + MonthData(I).Number 79 | NEXT 80 | 81 | ' Set the number of days in the requested month: 82 | TotalDays = MonthData(Month).Number 83 | 84 | ' Compensate if requested year is a leap year: 85 | IF IsLeapYear(Year) THEN 86 | 87 | ' If after February, add one to total days: 88 | IF Month > 2 THEN 89 | NumDays = NumDays + 1 90 | 91 | ' If February, add one to the month's days: 92 | ELSEIF Month = 2 THEN 93 | TotalDays = TotalDays + 1 94 | 95 | END IF 96 | END IF 97 | 98 | ' 1/1/1899 was a Sunday, so calculating "NumDays MOD 7" 99 | ' gives the day of week (Sunday = 0, Monday = 1, Tuesday = 2, 100 | ' and so on) for the first day of the input month: 101 | StartDay = NumDays MOD 7 102 | END SUB 103 | ' 104 | ' ======================== GETINPUT ========================== 105 | ' Prompts for input, then tests for a valid range. 106 | ' ============================================================ 107 | ' 108 | FUNCTION GetInput (Prompt$, Row, LowVal, HighVal) STATIC 109 | 110 | ' Locate prompt at specified row, turn cursor on and 111 | ' make it one character high: 112 | LOCATE Row, 1, 1, 0, 13 113 | PRINT Prompt$; 114 | 115 | ' Save column position: 116 | Column = POS(0) 117 | 118 | ' Input value until it's within range: 119 | DO 120 | LOCATE Row, Column ' Locate cursor at end of prompt 121 | PRINT SPACE$(10) ' Erase anything already there 122 | LOCATE Row, Column ' Relocate cursor at end of prompt 123 | INPUT "", Value ' Input value with no prompt 124 | LOOP WHILE (Value < LowVal OR Value > HighVal) 125 | 126 | ' Return valid input as value of function: 127 | GetInput = Value 128 | 129 | END FUNCTION 130 | ' 131 | ' ====================== ISLEAPYEAR ========================== 132 | ' Determines if a year is a leap year or not. 133 | ' ============================================================ 134 | ' 135 | FUNCTION IsLeapYear (N) STATIC 136 | 137 | ' If the year is evenly divisible by 4 and not divisible 138 | ' by 100, or if the year is evenly divisible by 400, then 139 | ' it's a leap year: 140 | IsLeapYear = (N MOD 4 = 0 AND N MOD 100 <> 0) OR (N MOD 400 = 0) 141 | END FUNCTION 142 | ' 143 | ' ===================== PRINTCALENDAR ======================== 144 | ' Prints a formatted calendar given the year and month. 145 | ' ============================================================ 146 | ' 147 | SUB PrintCalendar (Year, Month) STATIC 148 | SHARED MonthData() AS MonthType 149 | 150 | ' Compute starting day (Su M Tu ...) and total days 151 | ' for the month: 152 | ComputeMonth Year, Month, StartDay, TotalDays 153 | CLS 154 | Header$ = RTRIM$(MonthData(Month).MName) + "," + STR$(Year) 155 | 156 | ' Calculates location for centering month and year: 157 | LeftMargin = (35 - LEN(Header$)) \ 2 158 | 159 | ' Print header: 160 | PRINT TAB(LeftMargin); Header$ 161 | PRINT 162 | PRINT "Su M Tu W Th F Sa" 163 | PRINT 164 | 165 | ' Recalculate and print tab to the first day 166 | ' of the month (Su M Tu ...): 167 | LeftMargin = 5 * StartDay + 1 168 | PRINT TAB(LeftMargin); 169 | 170 | ' Print out the days of the month: 171 | FOR I = 1 TO TotalDays 172 | PRINT USING "## "; I; 173 | 174 | ' Advance to the next line when the cursor 175 | ' is past column 32: 176 | IF POS(0) > 32 THEN PRINT 177 | NEXT 178 | 179 | END SUB 180 | -------------------------------------------------------------------------------- /playground/examples/check.bas: -------------------------------------------------------------------------------- 1 | ' Example from "Microsoft QuickBASIC: Programming in BASIC" 2 | ' Source: https://www.pcjs.org/documents/books/mspl13/basic/qbprog/ 3 | 4 | DIM Amount(1 TO 100) 5 | CONST FALSE = 0, TRUE = NOT FALSE 6 | 7 | CLS 8 | ' Get account's starting balance: 9 | INPUT "Type starting balance, then press : ", Balance 10 | 11 | ' Get transactions. Continue accepting input 12 | ' until the input is zero for a transaction, 13 | ' or until 100 transactions have been entered: 14 | FOR TransacNum% = 1 TO 100 15 | PRINT TransacNum%; 16 | PRINT ") Enter transaction amount (0 to end): "; 17 | INPUT "", Amount(TransacNum%) 18 | IF Amount(TransacNum%) = 0 THEN 19 | TransacNum% = TransacNum% - 1 20 | EXIT FOR 21 | END IF 22 | NEXT 23 | 24 | ' Sort transactions in ascending order, 25 | ' using a "bubble sort": 26 | Limit% = TransacNum% 27 | DO 28 | Swaps% = FALSE 29 | FOR I% = 1 TO (Limit% - 1) 30 | 31 | ' If two adjacent elements are out of order, 32 | ' switch those elements: 33 | IF Amount(I%) < Amount(I% + 1) THEN 34 | SWAP Amount(I%), Amount(I% + 1) 35 | Swaps% = I% 36 | END IF 37 | NEXT I% 38 | 39 | ' Sort on next pass only to where last switch was made: 40 | Limit% = Swaps% 41 | 42 | ' Sort until no elements are exchanged: 43 | LOOP WHILE Swaps% 44 | 45 | ' Print the sorted transaction array. If a transaction 46 | ' is greater than zero, print it as a "CREDIT"; if a 47 | ' transaction is less than zero, print it as a "DEBIT": 48 | FOR I% = 1 TO TransacNum% 49 | IF Amount(I%) > 0 THEN 50 | PRINT USING "CREDIT: $$#####.##"; Amount(I%) 51 | ELSEIF Amount(I%) < 0 THEN 52 | PRINT USING "DEBIT: $$#####.##"; Amount(I%) 53 | END IF 54 | 55 | ' Update balance: 56 | Balance = Balance + Amount(I%) 57 | NEXT I% 58 | 59 | ' Print the final balance: 60 | PRINT 61 | PRINT "--------------------------" 62 | PRINT USING "Final Balance: $$######.##"; Balance 63 | END 64 | -------------------------------------------------------------------------------- /playground/examples/guess.bas: -------------------------------------------------------------------------------- 1 | ' Welcome to qbjc playground! 2 | ' 3 | ' qbjc is a QBasic to JavaScript compiler. The qbjc playground 4 | ' lets you edit and run QBasic / QuickBASIC programs directly in 5 | ' the browser. 6 | ' 7 | ' To get started: 8 | ' 9 | ' - Press the blue "PLAY" button below to run this program. The 10 | ' result will be displayed in the "OUTPUT" window on the right. 11 | ' 12 | ' - Feel free to modify and play around with this program. You 13 | ' can also check out some other example programs using the 14 | ' "Open" button at the top. 15 | ' 16 | 17 | CLS 18 | PRINT "Hi! Welcome to qbjc playground!" 19 | PRINT 20 | 21 | INPUT "What's your name? ", name$ 22 | PRINT 23 | PRINT "Hello, "; name$; "! Let's play a game! " 24 | 25 | DO 26 | PRINT 27 | PlayNumberGuessingGame 28 | PRINT 29 | playAgain$ = InputYN$("Play again?") 30 | PRINT 31 | LOOP WHILE playAgain$ = "Y" 32 | 33 | PRINT 34 | PRINT "Thanks for playing "; name$; "! Have a great day!" 35 | PRINT 36 | 37 | 38 | SUB PlayNumberGuessingGame 39 | PRINT "I'll think of a number between 1 and 10, "; 40 | PRINT "and you have 3 tries to guess it!" 41 | answer = INT(RND * 10) + 1 42 | remainingTries = 3 43 | DO 44 | PRINT 45 | INPUT "Enter your guess: ", guess 46 | IF answer = guess THEN 47 | PRINT "Yay! You guessed the answer!" 48 | EXIT DO 49 | ELSE 50 | remainingTries = remainingTries - 1 51 | IF remainingTries <= 0 THEN 52 | PRINT "Sorry, you lost! The answer was"; answer 53 | EXIT DO 54 | ELSEIF answer < guess THEN 55 | PRINT "The answer is smaller than"; guess 56 | ELSE 57 | PRINT "The answer is larger than"; guess 58 | END IF 59 | END IF 60 | LOOP 61 | END SUB 62 | 63 | 64 | FUNCTION InputYN$(prompt$) 65 | PRINT prompt$; " (Y/N)"; 66 | DO 67 | key$ = UCASE$(INKEY$) 68 | LOOP UNTIL key$ = "Y" OR key$ = "N" 69 | InputYN$ = key$ 70 | END FUNCTION 71 | -------------------------------------------------------------------------------- /playground/examples/strtonum.bas: -------------------------------------------------------------------------------- 1 | ' Example from "Microsoft QuickBASIC: Programming in BASIC" 2 | ' Source: https://www.pcjs.org/documents/books/mspl13/basic/qbprog/ 3 | 4 | DECLARE FUNCTION Filter$ (Txt$, FilterString$) 5 | 6 | ' Input a line: 7 | LINE INPUT "Enter a number with commas: ", A$ 8 | 9 | ' Look for only valid numeric characters (0123456789.-) in the 10 | ' input string: 11 | CleanNum$ = Filter$(A$, "0123456789.-") 12 | 13 | ' Convert the string to a number: 14 | PRINT "The number's value = "; VAL(CleanNum$) 15 | END 16 | ' 17 | ' ========================== FILTER ========================== 18 | ' Takes unwanted characters out of a string by 19 | ' comparing them with a filter string containing 20 | ' only acceptable numeric characters 21 | ' ============================================================ 22 | ' 23 | FUNCTION Filter$ (Txt$, FilterString$) STATIC 24 | Temp$ = "" 25 | TxtLength = LEN(Txt$) 26 | 27 | FOR I = 1 TO TxtLength ' Isolate each character in 28 | C$ = MID$(Txt$, I, 1) ' the string. 29 | 30 | ' If the character is in the filter string, save it: 31 | IF INSTR(FilterString$, C$) <> 0 THEN 32 | Temp$ = Temp$ + C$ 33 | END IF 34 | NEXT I 35 | 36 | Filter$ = Temp$ 37 | END FUNCTION 38 | -------------------------------------------------------------------------------- /playground/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "qbjc-playground", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@emotion/react": "^11.13.3", 7 | "@emotion/styled": "^11.10.4", 8 | "@fontsource/cascadia-mono": "^4.2.1", 9 | "@fontsource/roboto": "^5.1.0", 10 | "@mui/icons-material": "^5.10.9", 11 | "@mui/material": "^5.10.10", 12 | "@mui/styles": "^5.10.10", 13 | "@testing-library/jest-dom": "^6.5.0", 14 | "@testing-library/react": "^13.4.0", 15 | "@testing-library/user-event": "^14.4.3", 16 | "@types/file-saver": "^2.0.7", 17 | "@types/lodash": "^4.17.7", 18 | "@types/node": "^22.6.0", 19 | "@types/react": "^18.0.21", 20 | "@types/react-dom": "^18.3.0", 21 | "@types/react-helmet": "^6.1.5", 22 | "@types/segment-analytics": "^0.0.38", 23 | "buffer": "^6.0.3", 24 | "file-saver": "^2.0.5", 25 | "lodash": "^4.17.21", 26 | "mdi-material-ui": "^7.5.0", 27 | "mobx": "^6.6.2", 28 | "mobx-react": "^9.2.0", 29 | "monaco-editor": "^0.34.1", 30 | "monaco-qb": "^0.0.1", 31 | "monaco-themes": "^0.4.2", 32 | "prettier": "^3.5.3", 33 | "process": "^0.11.10", 34 | "react": "^18.2.0", 35 | "react-dom": "^18.3.1", 36 | "react-helmet": "^6.1.0", 37 | "react-mui-dropzone": "^4.0.6", 38 | "react-scripts": "^5.0.1", 39 | "react-split": "^2.0.14", 40 | "typescript": "^4.8.4", 41 | "web-vitals": "^1.1.2", 42 | "xterm": "^4.19.0", 43 | "xterm-addon-fit": "^0.5.0", 44 | "xterm-webfont": "^2.0.0" 45 | }, 46 | "scripts": { 47 | "prebuild:copyMonacoAssets": "./src/tools/copy-monaco-assets.sh", 48 | "prebuild:generateExamplesBundleJson": "node ./src/tools/generate-examples-bundle-json.js", 49 | "prebuild:generateMonacoThemesBundleJson": "node ./src/tools/generate-monaco-themes-bundle-json.js", 50 | "prebuild": "npm run prebuild:copyMonacoAssets && npm run prebuild:generateMonacoThemesBundleJson && npm run prebuild:generateExamplesBundleJson", 51 | "start": "npm run prebuild && react-scripts start", 52 | "build": "npm run prebuild && react-scripts build", 53 | "test": "npm run prebuild && react-scripts test", 54 | "eject": "react-scripts eject", 55 | "lint": "prettier --check ." 56 | }, 57 | "eslintConfig": { 58 | "extends": [ 59 | "react-app", 60 | "react-app/jest" 61 | ] 62 | }, 63 | "browserslist": { 64 | "production": [ 65 | ">0.2%", 66 | "not dead", 67 | "not op_mini all" 68 | ], 69 | "development": [ 70 | "last 1 chrome version", 71 | "last 1 firefox version", 72 | "last 1 safari version" 73 | ] 74 | }, 75 | "homepage": "./" 76 | } 77 | -------------------------------------------------------------------------------- /playground/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jichu4n/qbjc/155006b930547a89fa6dba224cae98035c5b2625/playground/public/favicon.ico -------------------------------------------------------------------------------- /playground/public/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 20 | 22 | 42 | 44 | 45 | 47 | image/svg+xml 48 | 50 | 51 | 52 | 53 | 54 | 58 | 66 | 67 | 71 | QB 82 | 83 | 84 | -------------------------------------------------------------------------------- /playground/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | qbjc 12 | 13 | 14 | 15 |
16 | 17 | 18 | -------------------------------------------------------------------------------- /playground/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jichu4n/qbjc/155006b930547a89fa6dba224cae98035c5b2625/playground/public/logo192.png -------------------------------------------------------------------------------- /playground/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jichu4n/qbjc/155006b930547a89fa6dba224cae98035c5b2625/playground/public/logo512.png -------------------------------------------------------------------------------- /playground/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "qbjc", 3 | "name": "qbjc", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#2222aa", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /playground/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /playground/src/app-header.tsx: -------------------------------------------------------------------------------- 1 | import AppBar from '@mui/material/AppBar'; 2 | import IconButton from '@mui/material/IconButton'; 3 | import {useTheme} from '@mui/material/styles'; 4 | import Toolbar from '@mui/material/Toolbar'; 5 | import Tooltip from '@mui/material/Tooltip'; 6 | import Typography from '@mui/material/Typography'; 7 | import FolderIcon from '@mui/icons-material/Folder'; 8 | import GitHubIcon from '@mui/icons-material/GitHub'; 9 | import HelpIcon from '@mui/icons-material/Help'; 10 | import LaunchIcon from '@mui/icons-material/Launch'; 11 | import SaveIcon from '@mui/icons-material/Save'; 12 | import SettingsIcon from '@mui/icons-material/Settings'; 13 | import {saveAs} from 'file-saver'; 14 | import React, {useCallback, useState} from 'react'; 15 | import EditorController from './editor-controller'; 16 | import HelpDialog from './help-dialog'; 17 | import OpenDialog from './open-dialog'; 18 | import SettingsDialog from './settings-dialog'; 19 | 20 | function AppHeader({ 21 | isReady, 22 | editorController, 23 | sourceFileName, 24 | onChangeSourceFileName, 25 | }: { 26 | isReady: boolean; 27 | editorController: EditorController | null; 28 | sourceFileName: string; 29 | onChangeSourceFileName: (sourceFileName: string) => void; 30 | }) { 31 | const onSaveClick = useCallback(() => { 32 | if (!isReady || !editorController) { 33 | return; 34 | } 35 | const blob = new Blob([editorController.getText()], { 36 | type: 'text/plain;charset=utf-8', 37 | }); 38 | saveAs(blob, sourceFileName); 39 | }, [isReady, editorController, sourceFileName]); 40 | 41 | const theme = useTheme(); 42 | 43 | const [isOpenDialogOpen, setIsOpenDialogOpen] = useState(false); 44 | const showOpenDialog = useCallback(() => setIsOpenDialogOpen(true), []); 45 | const hideOpenDialog = useCallback(() => setIsOpenDialogOpen(false), []); 46 | const [isSettingsDialogOpen, setIsSettingsDialogOpen] = useState(false); 47 | const showSettingsDialog = useCallback( 48 | () => setIsSettingsDialogOpen(true), 49 | [] 50 | ); 51 | const hideSettingsDialog = useCallback( 52 | () => setIsSettingsDialogOpen(false), 53 | [] 54 | ); 55 | const [isHelpDialogOpen, setIsHelpDialogOpen] = useState(false); 56 | const showHelpDialog = useCallback(() => setIsHelpDialogOpen(true), []); 57 | const hideHelpDialog = useCallback(() => setIsHelpDialogOpen(false), []); 58 | 59 | return ( 60 | <> 61 | 62 | 67 | 68 | qbjc 69 | 70 | {isReady && ( 71 | <> 72 | 73 | 79 | 80 | 81 | 82 | 83 | 89 | 90 | 91 | 92 | 93 | 99 | 100 | 101 | 102 | 103 | )} 104 | 105 | 111 | 112 | 113 | 114 | 123 | View on GitHub 124 | 130 | 131 | } 132 | > 133 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 152 | 156 | 157 | 158 | ); 159 | } 160 | 161 | export default AppHeader; 162 | -------------------------------------------------------------------------------- /playground/src/app-splash-screen.tsx: -------------------------------------------------------------------------------- 1 | import CircularProgress from '@mui/material/CircularProgress'; 2 | import {useTheme} from '@mui/material/styles'; 3 | import React, {useEffect, useState} from 'react'; 4 | 5 | function AppSplashScreen({isReady}: {isReady: boolean}) { 6 | enum Status { 7 | VISIBLE = 'visible', 8 | TRANSITIONING = 'transitioning', 9 | HIDDEN = 'hidden', 10 | } 11 | const TRANSITION_DELAY_MS = 600; 12 | const [status, setStatus] = useState(Status.VISIBLE); 13 | const theme = useTheme(); 14 | 15 | useEffect(() => { 16 | if (isReady && status === Status.VISIBLE) { 17 | setStatus(Status.TRANSITIONING); 18 | setTimeout(() => setStatus(Status.HIDDEN), TRANSITION_DELAY_MS); 19 | } 20 | }, [isReady, status, Status]); 21 | 22 | return ( 23 |
53 | 54 |
55 | ); 56 | } 57 | 58 | export default AppSplashScreen; 59 | -------------------------------------------------------------------------------- /playground/src/app.css: -------------------------------------------------------------------------------- 1 | html, 2 | body, 3 | #root { 4 | margin: 0; 5 | padding: 0; 6 | height: 100%; 7 | width: 100%; 8 | overflow: hidden; 9 | } 10 | 11 | *::-webkit-scrollbar { 12 | width: 10px; 13 | height: 10px; 14 | } 15 | 16 | *::-webkit-scrollbar-track { 17 | background: transparent; 18 | } 19 | 20 | *::-webkit-scrollbar-thumb { 21 | background-color: #757575; 22 | opacity: 0.2; 23 | border-radius: 20px; 24 | } 25 | 26 | *::-webkit-scrollbar-thumb:hover { 27 | background-color: #bdbdbd; 28 | } 29 | -------------------------------------------------------------------------------- /playground/src/app.tsx: -------------------------------------------------------------------------------- 1 | import '@fontsource/cascadia-mono'; 2 | import '@fontsource/roboto/300.css'; 3 | import '@fontsource/roboto/400.css'; 4 | import '@fontsource/roboto/500.css'; 5 | import '@fontsource/roboto/700.css'; 6 | import {blue, red} from '@mui/material/colors'; 7 | import CssBaseline from '@mui/material/CssBaseline'; 8 | import { 9 | createTheme, 10 | StyledEngineProvider, 11 | ThemeProvider, 12 | } from '@mui/material/styles'; 13 | import {observer} from 'mobx-react'; 14 | import {CompileResult} from 'qbjc'; 15 | import {useCallback, useRef, useState} from 'react'; 16 | import {Helmet} from 'react-helmet'; 17 | import Split from 'react-split'; 18 | import AppHeader from './app-header'; 19 | import AppSplashScreen from './app-splash-screen'; 20 | import './app.css'; 21 | import CompileResultDialog from './compile-result-dialog'; 22 | import EditorPane from './editor-pane'; 23 | import MessagesPane from './messages-pane'; 24 | import OutputScreenPane from './output-screen-pane'; 25 | import QbjcManager from './qbjc-manager'; 26 | import RunFab from './run-fab'; 27 | import './split.css'; 28 | 29 | const darkTheme = createTheme({ 30 | palette: { 31 | mode: 'dark', 32 | primary: { 33 | light: blue[300], 34 | main: blue[400], 35 | dark: blue[700], 36 | contrastText: 'white', 37 | }, 38 | secondary: { 39 | light: red[300], 40 | main: red[400], 41 | dark: red[700], 42 | }, 43 | }, 44 | }); 45 | 46 | const SPLIT_GUTTER_SIZE = 6; 47 | 48 | const App = observer(() => { 49 | const qbjcManagerRef = useRef(new QbjcManager()); 50 | const qbjcManager = qbjcManagerRef.current; 51 | 52 | const [dimensions, setDimensions] = useState<{ 53 | horizontalSplit?: Array; 54 | rightVerticalSplit?: Array; 55 | } | null>({}); 56 | 57 | const [isCompileResultDialogOpen, setIsCompileResultDialogOpen] = 58 | useState(false); 59 | const [displayedCompileResult, setDisplayedCompileResult] = 60 | useState(null); 61 | const showCompileResultDialog = useCallback( 62 | (compileResult: CompileResult) => { 63 | setDisplayedCompileResult(compileResult); 64 | setIsCompileResultDialogOpen(true); 65 | }, 66 | [] 67 | ); 68 | const hideCompileResultDialog = useCallback(() => { 69 | setIsCompileResultDialogOpen(false); 70 | setDisplayedCompileResult(null); 71 | }, []); 72 | 73 | const onChangeSourceFileName = useCallback( 74 | (sourceFileName: string) => 75 | qbjcManager.updateSourceFileName(sourceFileName), 76 | [qbjcManager] 77 | ); 78 | 79 | return ( 80 | 81 | 82 | 83 | 84 | 85 | qbjc{qbjcManager.isRunning ? ' - Running...' : ''} 86 | 87 | 88 |
97 | 103 | 104 | ) => { 108 | setDimensions({ 109 | ...dimensions, 110 | horizontalSplit: sizes, 111 | }); 112 | }} 113 | style={{ 114 | display: 'flex', 115 | flexDirection: 'row', 116 | flexGrow: 1, 117 | marginTop: 5, 118 | }} 119 | gutterSize={SPLIT_GUTTER_SIZE} 120 | > 121 |
122 | 126 | qbjcManager.init({editorController}) 127 | } 128 | style={{ 129 | width: '100%', 130 | height: '100%', 131 | // @ts-ignore 132 | overflow: 'overlay', 133 | }} 134 | /> 135 | 136 |
137 | 138 | ) => { 144 | setDimensions({ 145 | ...dimensions, 146 | rightVerticalSplit: sizes, 147 | }); 148 | }} 149 | style={{ 150 | display: 'flex', 151 | flexDirection: 'column', 152 | }} 153 | gutterSize={SPLIT_GUTTER_SIZE} 154 | > 155 | qbjcManager.init({terminal})} 157 | dimensions={dimensions} 158 | isRunning={qbjcManager.isRunning} 159 | /> 160 | qbjcManager.goToMessageLocInEditor(loc), 164 | [qbjcManager] 165 | )} 166 | onShowCompileResultClick={showCompileResultDialog} 167 | /> 168 | 169 |
170 |
171 | 172 | 177 |
178 |
179 | ); 180 | }); 181 | 182 | export default App; 183 | -------------------------------------------------------------------------------- /playground/src/config-manager.ts: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import { 3 | action, 4 | computed, 5 | makeObservable, 6 | observable, 7 | reaction, 8 | toJS, 9 | } from 'mobx'; 10 | import {MONACO_THEMES} from './monaco-editor-interface'; 11 | 12 | export interface SourceFile { 13 | name: string; 14 | content: string; 15 | } 16 | 17 | export enum ConfigKey { 18 | SOURCE_FILES = 'v1/sourceFiles', 19 | CURRENT_SOURCE_FILE_NAME = 'v1/currentSourceFileName', 20 | EDITOR_FONT_FAMILY = 'v1/editorFontFamily', 21 | EDITOR_FONT_SIZE = 'v1/editorFontSize', 22 | EDITOR_THEME = 'v1/editorTheme', 23 | EDITOR_KEYBINDINGS = 'v1/editorKeybindings', 24 | SCREEN_FONT_FAMILY = 'v1/outputScreenFontFamily', 25 | SCREEN_FONT_SIZE = 'v1/outputScreenFontSize', 26 | SCREEN_LETTER_SPACING = 'v1/outputScreenLetterSpacing', 27 | SCREEN_LINE_HEIGHT = 'v1/outputScreenLineHeight', 28 | EXECUTION_DELAY = 'v1/executionDelayUs', 29 | } 30 | 31 | export const DEFAULT_SOURCE_FILE_NAME = 'source.bas'; 32 | 33 | const DEFAULT_CONFIG = { 34 | [ConfigKey.SOURCE_FILES]: [{name: DEFAULT_SOURCE_FILE_NAME, content: ''}], 35 | [ConfigKey.CURRENT_SOURCE_FILE_NAME]: DEFAULT_SOURCE_FILE_NAME, 36 | [ConfigKey.EDITOR_FONT_FAMILY]: 'Cascadia Mono', 37 | [ConfigKey.EDITOR_FONT_SIZE]: 14, 38 | [ConfigKey.EDITOR_THEME]: 'nord', 39 | [ConfigKey.EDITOR_KEYBINDINGS]: '', 40 | [ConfigKey.SCREEN_FONT_FAMILY]: 'Cascadia Mono', 41 | [ConfigKey.SCREEN_FONT_SIZE]: 14, 42 | [ConfigKey.SCREEN_LETTER_SPACING]: 0, 43 | [ConfigKey.SCREEN_LINE_HEIGHT]: 1.0, 44 | [ConfigKey.EXECUTION_DELAY]: 0, 45 | }; 46 | 47 | export const EDITOR_THEMES = _.sortBy( 48 | [ 49 | {label: 'Visual Studio', value: 'vs'}, 50 | {label: 'Visual Studio Dark', value: 'vs-dark'}, 51 | ...Object.entries(MONACO_THEMES).map(([themeKey, themeNameAndData]) => ({ 52 | label: themeNameAndData.name, 53 | value: themeKey, 54 | })), 55 | ], 56 | ({label}) => label.toLowerCase() 57 | ); 58 | 59 | export const EDITOR_KEYBINDINGS = [ 60 | {label: 'Default', value: ''}, 61 | {label: 'Vim', value: 'vim'}, 62 | {label: 'Emacs', value: 'emacs'}, 63 | {label: 'Sublime Text', value: 'sublime'}, 64 | {label: 'Visual Studio Code', value: 'vscode'}, 65 | ]; 66 | 67 | class ConfigManager { 68 | config = DEFAULT_CONFIG; 69 | 70 | constructor() { 71 | makeObservable(this, { 72 | config: observable, 73 | setKey: action, 74 | load: action, 75 | sourceFiles: computed, 76 | currentSourceFile: computed, 77 | setSourceFileName: action, 78 | setSourceFileContent: action, 79 | }); 80 | 81 | this.load(); 82 | 83 | reaction( 84 | () => toJS(this.config), 85 | () => this.save() 86 | ); 87 | } 88 | 89 | getKey(key: ConfigKey): any { 90 | return this.config[key]; 91 | } 92 | 93 | setKey(key: ConfigKey, value: any) { 94 | (this.config as {[key: string]: any})[key] = value; 95 | } 96 | 97 | save() { 98 | for (const key of Object.values(ConfigKey)) { 99 | const defaultValue = JSON.stringify(DEFAULT_CONFIG[key]); 100 | const currentValue = JSON.stringify(this.config[key]); 101 | if (currentValue === defaultValue) { 102 | localStorage.removeItem(key); 103 | } else { 104 | localStorage.setItem(key, currentValue); 105 | } 106 | } 107 | } 108 | 109 | load() { 110 | if (typeof localStorage === 'undefined') { 111 | console.warn(`localStorage not available`); 112 | return; 113 | } 114 | for (const key of Object.values(ConfigKey)) { 115 | const rawValue = localStorage.getItem(key); 116 | if (rawValue) { 117 | let value: any; 118 | try { 119 | value = JSON.parse(rawValue); 120 | } catch (e) { 121 | console.warn(`Failed to parse value for key "${key}": "${rawValue}"`); 122 | continue; 123 | } 124 | this.setKey(key, value); 125 | } 126 | } 127 | 128 | // Ensure current source file is sane. 129 | if (!this.currentSourceFile) { 130 | this.sourceFiles.push({ 131 | name: this.getKey(ConfigKey.CURRENT_SOURCE_FILE_NAME), 132 | content: '', 133 | }); 134 | } 135 | 136 | console.log(`Loaded config:`, toJS(this.config)); 137 | } 138 | 139 | get sourceFiles(): Array { 140 | return this.getKey(ConfigKey.SOURCE_FILES); 141 | } 142 | 143 | get currentSourceFile(): SourceFile { 144 | return _.find(this.sourceFiles, [ 145 | 'name', 146 | this.getKey(ConfigKey.CURRENT_SOURCE_FILE_NAME), 147 | ])!; 148 | } 149 | 150 | setSourceFileName(sourceFile: SourceFile, newSourceFileName: string) { 151 | sourceFile.name = newSourceFileName; 152 | } 153 | 154 | setSourceFileContent(sourceFile: SourceFile, newContent: string) { 155 | sourceFile.content = newContent; 156 | } 157 | } 158 | 159 | const configManager = new ConfigManager(); 160 | export default configManager; 161 | -------------------------------------------------------------------------------- /playground/src/deps.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'xterm-webfont'; 2 | declare module 'react-split'; 3 | -------------------------------------------------------------------------------- /playground/src/editor-controller.tsx: -------------------------------------------------------------------------------- 1 | /** Abstracted interface for controlling the code editor. */ 2 | abstract class EditorController { 3 | /** Returns the current editor content. */ 4 | abstract getText(): string; 5 | /** Updates the editor content. */ 6 | abstract setText(value: string): void; 7 | /** Make the editor grab focus. */ 8 | abstract focus(): void; 9 | /** Moves the editor cursor to a location (zero-based). */ 10 | abstract setCursor(line: number, col: number): void; 11 | } 12 | 13 | export default EditorController; 14 | -------------------------------------------------------------------------------- /playground/src/examples.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "fileName": "guess.bas", 4 | "title": "GUESS.BAS", 5 | "description": "Simple number guessing game" 6 | }, 7 | { 8 | "fileName": "nibbles.bas", 9 | "title": "NIBBLES.BAS", 10 | "description": "Classic snake game" 11 | }, 12 | { 13 | "fileName": "cal.bas", 14 | "title": "CAL.BAS", 15 | "description": "Perpetual calendar program" 16 | }, 17 | { 18 | "fileName": "check.bas", 19 | "title": "CHECK.BAS", 20 | "description": "Checkbook balancing program" 21 | }, 22 | { 23 | "fileName": "strtonum.bas", 24 | "title": "STRTONUM.BAS", 25 | "description": "Converts a string to a number" 26 | } 27 | ] 28 | -------------------------------------------------------------------------------- /playground/src/examples.ts: -------------------------------------------------------------------------------- 1 | export interface ExampleSpec { 2 | fileName: string; 3 | title: string; 4 | description: string; 5 | content: string; 6 | } 7 | 8 | // To update / add examples, update examples.json manually. The build process 9 | // runs tools/generate-examples-bundle-json.js to update the file contents based on the 10 | // files in the examples directory. 11 | const EXAMPLE_SPECS: Array = require('./examples-bundle.json'); 12 | export default EXAMPLE_SPECS; 13 | 14 | export const DEFAULT_EXAMPLE = EXAMPLE_SPECS[0]; 15 | -------------------------------------------------------------------------------- /playground/src/external-link.tsx: -------------------------------------------------------------------------------- 1 | import Link from '@mui/material/Link'; 2 | import LaunchIcon from '@mui/icons-material/Launch'; 3 | import React, {ReactNode} from 'react'; 4 | 5 | function ExternalLink({href, children}: {href: string; children: ReactNode}) { 6 | return ( 7 | 8 | {children} 9 | 16 | 17 | ); 18 | } 19 | 20 | export default ExternalLink; 21 | -------------------------------------------------------------------------------- /playground/src/help-dialog.tsx: -------------------------------------------------------------------------------- 1 | import Button from '@mui/material/Button'; 2 | import Dialog from '@mui/material/Dialog'; 3 | import DialogActions from '@mui/material/DialogActions'; 4 | import DialogTitle from '@mui/material/DialogTitle'; 5 | import Link from '@mui/material/Link'; 6 | import {useTheme} from '@mui/material/styles'; 7 | import Tab from '@mui/material/Tab'; 8 | import Tabs from '@mui/material/Tabs'; 9 | import Typography from '@mui/material/Typography'; 10 | import useMediaQuery from '@mui/material/useMediaQuery'; 11 | import InfoIcon from '@mui/icons-material/Info'; 12 | import BookshelfIcon from 'mdi-material-ui/Bookshelf'; 13 | import {observer} from 'mobx-react'; 14 | import React, {useState} from 'react'; 15 | import ExternalLink from './external-link'; 16 | 17 | function ResourcesTab() { 18 | return ( 19 |
20 | QBasic / QuickBASIC 21 | 22 |

23 | New to QBasic / QuickBASIC or need a refresher? Here are some 24 | resources to help get you started: 25 |

    26 |
  • 27 | 28 | QBasic Wikibook (wikibooks.org) 29 | 30 |
  • 31 |
  • 32 | 33 | QBasic for Beginners (qbasic.net) 34 | 35 |
  • 36 |
  • 37 | 38 | Reference documentation from Microsoft (pcjs.org) 39 | 40 |
  • 41 |
42 |

43 |
44 | 45 | Compatibility 46 | 47 |

48 | qbjc supports a subset of QBasic / QuickBASIC functionality. See{' '} 49 | 50 | compatibility guide 51 | {' '} 52 | for more information. 53 |

54 |

55 | Is there something you'd like to see supported in qbjc? Please file an 56 | issue or send a pull request at{' '} 57 | 58 | github.com/jichu4n/qbjc 59 | 60 | ! 61 |

62 |
63 |
64 | ); 65 | } 66 | 67 | function AboutTab() { 68 | return ( 69 |
70 | What is qbjc? 71 | 72 |

73 | qbjc is a QBasic to JavaScript compiler. 74 |

75 |

76 | The qbjc playground at qbjc.dev{' '} 77 | lets you edit and run QBasic / QuickBASIC programs directly in the 78 | browser. 79 |

80 |

81 | Visit the project homepage on GitHub to find out more:{' '} 82 | 83 | github.com/jichu4n/qbjc 84 | 85 |

86 |
87 | 88 | Who can see my source code? 89 | 90 |

91 | The source code you enter into the qbjc playground at{' '} 92 | qbjc.dev is entirely private. It 93 | is compiled and executed locally in your browser, and your source code 94 | is never uploaded to qbjc.dev. 95 |

96 |
97 | 98 | 99 | Is qbjc built / supported by Microsoft? 100 | 101 | 102 |

No, qbjc is NOT affliated with Microsoft in any way.

103 |

104 | qbjc is open source software distributed under the Apache 2.0 License. 105 | Please feel free to fork and contribute at{' '} 106 | 107 | github.com/jichu4n/qbjc 108 | 109 | ! 110 |

111 |

112 | qbjc is developed by Chuan Ji. Find me on: 113 |

    114 |
  • 115 | GitHub:{' '} 116 | 117 | github.com/jichu4n 118 | 119 |
  • 120 |
  • 121 | Personal website:{' '} 122 | 123 | jichu4n.com 124 | 125 |
  • 126 |
127 |

128 |
129 |
130 | ); 131 | } 132 | 133 | const HelpDialog = observer( 134 | ({isOpen, onClose}: {isOpen: boolean; onClose: () => void}) => { 135 | const theme = useTheme(); 136 | const isFullScreen = useMediaQuery(theme.breakpoints.down('md')); 137 | 138 | const [activeTab, setActiveTab] = useState<'resources' | 'about'>( 139 | 'resources' 140 | ); 141 | 142 | return ( 143 | <> 144 | 152 | Help 153 |
158 | setActiveTab(activeTab)} 163 | style={{ 164 | borderRight: `1px solid ${theme.palette.divider}`, 165 | }} 166 | indicatorColor="primary" 167 | textColor="primary" 168 | > 169 | } 172 | value={'resources'} 173 | /> 174 | } value={'about'} /> 175 | 176 |
186 | {activeTab === 'resources' && } 187 | {activeTab === 'about' && } 188 |
189 |
190 | 191 | 194 | 195 |
196 | 197 | ); 198 | } 199 | ); 200 | 201 | export default HelpDialog; 202 | -------------------------------------------------------------------------------- /playground/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {createRoot} from 'react-dom/client'; 3 | import App from './app'; 4 | import initializeSegment from './segment'; 5 | 6 | const root = createRoot(document.getElementById('root')!); 7 | root.render( 8 | 9 | 10 | 11 | ); 12 | initializeSegment(); 13 | -------------------------------------------------------------------------------- /playground/src/messages-pane.tsx: -------------------------------------------------------------------------------- 1 | import Button from '@mui/material/Button'; 2 | import IconButton from '@mui/material/IconButton'; 3 | import List from '@mui/material/List'; 4 | import ListItem from '@mui/material/ListItem'; 5 | import ListItemIcon from '@mui/material/ListItemIcon'; 6 | import ListItemSecondaryAction from '@mui/material/ListItemSecondaryAction'; 7 | import ListItemText from '@mui/material/ListItemText'; 8 | import {Theme, useTheme} from '@mui/material/styles'; 9 | import Tooltip from '@mui/material/Tooltip'; 10 | import BlockIcon from '@mui/icons-material/Block'; 11 | import ErrorIcon from '@mui/icons-material/Error'; 12 | import PlayCircleIcon from '@mui/icons-material/PlayCircleFilled'; 13 | import CodeBracesBoxIcon from 'mdi-material-ui/CodeBracesBox'; 14 | import MessageTextIcon from 'mdi-material-ui/MessageText'; 15 | import {runInAction} from 'mobx'; 16 | import {observer} from 'mobx-react'; 17 | import {CompileResult, Loc} from 'qbjc'; 18 | import React, {useCallback, useEffect, useRef} from 'react'; 19 | import PaneHeader from './pane-header'; 20 | import {QbjcMessage, QbjcMessageType} from './qbjc-manager'; 21 | 22 | function getMessageDisplayProps(theme: Theme, messageType: QbjcMessageType) { 23 | const MESSAGE_DISPLAY_PROPS = { 24 | [QbjcMessageType.ERROR]: { 25 | iconClass: ErrorIcon, 26 | color: theme.palette.warning.dark, 27 | }, 28 | [QbjcMessageType.INFO]: { 29 | iconClass: null, 30 | color: theme.palette.text.secondary, 31 | }, 32 | [QbjcMessageType.EXECUTION]: { 33 | iconClass: PlayCircleIcon, 34 | color: theme.palette.text.secondary, 35 | }, 36 | }; 37 | return MESSAGE_DISPLAY_PROPS[messageType]; 38 | } 39 | 40 | const MessagesPane = observer( 41 | ({ 42 | messages, 43 | onLocClick, 44 | onShowCompileResultClick, 45 | style = {}, 46 | }: { 47 | messages: Array; 48 | onLocClick: (loc: Loc) => void; 49 | onShowCompileResultClick: (compileResult: CompileResult) => void; 50 | style?: React.CSSProperties; 51 | }) => { 52 | const theme = useTheme(); 53 | 54 | const scrollContainerRef = useRef(null); 55 | const isScrolledToBottomRef = useRef(true); 56 | const prevNumMessagesRef = useRef(0); 57 | 58 | useEffect(() => { 59 | const {current: scrollContainerEl} = scrollContainerRef; 60 | const {current: prevNumMessages} = prevNumMessagesRef; 61 | const {current: isScrolledToBottom} = isScrolledToBottomRef; 62 | const numMessages = messages.length; 63 | if (!scrollContainerEl || numMessages === prevNumMessages) { 64 | return; 65 | } 66 | if (isScrolledToBottom) { 67 | scrollContainerEl.scrollTop = scrollContainerEl.scrollHeight; 68 | } 69 | prevNumMessagesRef.current = numMessages; 70 | }); 71 | 72 | return ( 73 |
80 | 88 | } 89 | > 90 | 91 | 93 | runInAction(() => { 94 | messages.length = 0; 95 | }) 96 | } 97 | size="large" 98 | > 99 | 105 | 106 | 107 | 108 |
{ 120 | const {current: scrollContainerEl} = scrollContainerRef; 121 | if (!scrollContainerEl) { 122 | return; 123 | } 124 | isScrolledToBottomRef.current = 125 | scrollContainerEl.scrollHeight - 126 | scrollContainerEl.scrollTop - 127 | scrollContainerEl.clientHeight < 128 | 1; 129 | }, [])} 130 | > 131 | 132 | {messages.map(({loc, message, type, compileResult}, idx) => { 133 | const {iconClass: Icon, color} = getMessageDisplayProps( 134 | theme, 135 | type 136 | ); 137 | let iconElement: React.ReactNode = null; 138 | if (Icon) { 139 | iconElement = ( 140 | 144 | ); 145 | } 146 | return ( 147 | (loc ? onLocClick(loc) : null)} 153 | > 154 | {iconElement && {iconElement}} 155 | 169 | {compileResult && ( 170 | 171 | 172 | 199 | 200 | 201 | )} 202 | 203 | ); 204 | })} 205 | 206 |
207 |
208 | ); 209 | } 210 | ); 211 | 212 | export default MessagesPane; 213 | -------------------------------------------------------------------------------- /playground/src/monaco-editor-interface.tsx: -------------------------------------------------------------------------------- 1 | import * as monaco from 'monaco-editor'; 2 | import EditorController from './editor-controller'; 3 | 4 | // Register qb syntax with Monaco. 5 | import 'monaco-qb'; 6 | 7 | // Initialize Monaco editor worker. 8 | // @ts-ignore 9 | window.MonacoEnvironment = window.MonacoEnvironment || { 10 | getWorkerUrl() { 11 | // Since we'll only be using the editor to edit BASIC, we won't be using the 12 | // other workers (HTML / CSS / JS etc). 13 | return './monaco/vs/base/worker/workerMain.js'; 14 | }, 15 | }; 16 | 17 | export class MonacoEditorController extends EditorController { 18 | constructor(private readonly editor: monaco.editor.ICodeEditor) { 19 | super(); 20 | } 21 | 22 | getText() { 23 | return this.editor.getValue(); 24 | } 25 | 26 | setText(value: string) { 27 | this.editor.setValue(value); 28 | } 29 | 30 | focus() { 31 | this.editor.focus(); 32 | } 33 | 34 | setCursor(line: number, col: number) { 35 | const pos: monaco.IPosition = {lineNumber: line + 1, column: col + 1}; 36 | this.editor.setPosition(pos); 37 | this.editor.revealPositionInCenterIfOutsideViewport( 38 | pos, 39 | monaco.editor.ScrollType.Smooth 40 | ); 41 | } 42 | } 43 | 44 | export type MonacoThemeBundle = { 45 | [key: string]: { 46 | name: string; 47 | data: monaco.editor.IStandaloneThemeData; 48 | }; 49 | }; 50 | 51 | export const MONACO_THEMES: MonacoThemeBundle = require('./monaco-themes-bundle.json'); 52 | 53 | // Register themes with Monaco. 54 | for (const [themeKey, themeNameAndData] of Object.entries(MONACO_THEMES)) { 55 | monaco.editor.defineTheme(themeKey, themeNameAndData.data); 56 | } 57 | -------------------------------------------------------------------------------- /playground/src/open-dialog.tsx: -------------------------------------------------------------------------------- 1 | import Avatar from '@mui/material/Avatar'; 2 | import Button from '@mui/material/Button'; 3 | import Dialog from '@mui/material/Dialog'; 4 | import DialogActions from '@mui/material/DialogActions'; 5 | import DialogContent from '@mui/material/DialogContent'; 6 | import DialogContentText from '@mui/material/DialogContentText'; 7 | import DialogTitle from '@mui/material/DialogTitle'; 8 | import List from '@mui/material/List'; 9 | import ListItem from '@mui/material/ListItem'; 10 | import ListItemAvatar from '@mui/material/ListItemAvatar'; 11 | import ListItemText from '@mui/material/ListItemText'; 12 | import {useTheme} from '@mui/material/styles'; 13 | import Tab from '@mui/material/Tab'; 14 | import Tabs from '@mui/material/Tabs'; 15 | import useMediaQuery from '@mui/material/useMediaQuery'; 16 | import _ from 'lodash'; 17 | import {DropzoneArea} from 'react-mui-dropzone'; 18 | import BookshelfIcon from 'mdi-material-ui/Bookshelf'; 19 | import FileCodeIcon from 'mdi-material-ui/FileCode'; 20 | import LaptopIcon from 'mdi-material-ui/Laptop'; 21 | import {observer} from 'mobx-react'; 22 | import React, {useCallback, useRef, useState} from 'react'; 23 | import EditorController from './editor-controller'; 24 | import EXAMPLES from './examples'; 25 | 26 | interface SelectedFileSpec { 27 | title: string; 28 | content: string; 29 | } 30 | 31 | function ConfirmationDialog({ 32 | isOpen, 33 | onClose, 34 | title, 35 | content, 36 | onConfirm, 37 | }: { 38 | isOpen: boolean; 39 | onClose: () => void; 40 | title: string; 41 | content: string; 42 | onConfirm: () => void; 43 | }) { 44 | return ( 45 | 46 | {title} 47 | 48 | {content} 49 | 50 | 51 | 54 | 63 | 64 | 65 | ); 66 | } 67 | 68 | function ExamplesTab({ 69 | onSelect, 70 | }: { 71 | onSelect: (fileSpec: SelectedFileSpec) => void; 72 | }) { 73 | const onExampleClick = useCallback( 74 | (exampleIdx: number) => { 75 | onSelect(EXAMPLES[exampleIdx]); 76 | }, 77 | [onSelect] 78 | ); 79 | 80 | return ( 81 | }> 82 | {EXAMPLES.map(({fileName, title, description}, idx) => ( 83 | onExampleClick(idx)} 87 | > 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | ))} 96 | 97 | ); 98 | } 99 | 100 | function UploadTab({ 101 | onSelect, 102 | }: { 103 | onSelect: (fileSpec: SelectedFileSpec) => void; 104 | }) { 105 | const onChange = useCallback( 106 | async (selectedFiles: Array) => { 107 | if (selectedFiles.length > 0) { 108 | const selectedFile = selectedFiles[0]; 109 | onSelect({ 110 | title: selectedFile.name, 111 | content: await selectedFile.text(), 112 | }); 113 | } 114 | }, 115 | [onSelect] 116 | ); 117 | 118 | return ( 119 |
128 | 135 |
136 | ); 137 | } 138 | 139 | const OpenDialog = observer( 140 | ({ 141 | isOpen, 142 | onClose, 143 | editorController, 144 | onChangeSourceFileName, 145 | }: { 146 | isOpen: boolean; 147 | onClose: () => void; 148 | editorController: EditorController | null; 149 | onChangeSourceFileName: (sourceFileName: string) => void; 150 | }) => { 151 | const theme = useTheme(); 152 | const isFullScreen = useMediaQuery(theme.breakpoints.down('md')); 153 | 154 | const selectedFileSpecRef = useRef(null); 155 | 156 | const [ 157 | isOverwriteConfirmationDialogOpen, 158 | setIsOverwriteConfirmationDialogOpen, 159 | ] = useState(false); 160 | 161 | const openSelectedFile = useCallback(() => { 162 | const selectedFileSpec = selectedFileSpecRef.current; 163 | if (!editorController || !selectedFileSpec) { 164 | return; 165 | } 166 | editorController.setText(selectedFileSpec.content); 167 | onChangeSourceFileName(selectedFileSpec.title); 168 | selectedFileSpecRef.current = null; 169 | onClose(); 170 | }, [editorController, onClose, onChangeSourceFileName]); 171 | 172 | const onSelect = useCallback( 173 | (fileSpec: SelectedFileSpec) => { 174 | if (!editorController) { 175 | return; 176 | } 177 | const trimmedSource = editorController.getText().trim(); 178 | const shouldConfirm = 179 | !!trimmedSource && 180 | !_.some(EXAMPLES, ({content}) => trimmedSource === content.trim()); 181 | 182 | selectedFileSpecRef.current = fileSpec; 183 | if (shouldConfirm) { 184 | setIsOverwriteConfirmationDialogOpen(true); 185 | } else { 186 | openSelectedFile(); 187 | } 188 | }, 189 | [editorController, openSelectedFile] 190 | ); 191 | 192 | const [activeTab, setActiveTab] = useState<'examples' | 'upload'>( 193 | 'examples' 194 | ); 195 | 196 | return ( 197 | <> 198 | 206 | Open program 207 |
212 | setActiveTab(activeTab)} 217 | style={{ 218 | borderRight: `1px solid ${theme.palette.divider}`, 219 | }} 220 | indicatorColor="primary" 221 | textColor="primary" 222 | > 223 | } 226 | value={'examples'} 227 | /> 228 | } value={'upload'} /> 229 | 230 |
240 | {activeTab === 'examples' && } 241 | {activeTab === 'upload' && } 242 |
243 |
244 | 245 | 248 | 249 |
250 | 251 | setIsOverwriteConfirmationDialogOpen(false)} 254 | onConfirm={openSelectedFile} 255 | title={`Open ${selectedFileSpecRef.current?.title}`} 256 | content="This will overwrite the current editor contents. Are you sure?" 257 | /> 258 | 259 | ); 260 | } 261 | ); 262 | 263 | export default OpenDialog; 264 | -------------------------------------------------------------------------------- /playground/src/pane-header.tsx: -------------------------------------------------------------------------------- 1 | import Paper from '@mui/material/Paper'; 2 | import {useTheme} from '@mui/material/styles'; 3 | import Typography from '@mui/material/Typography'; 4 | import React, {ReactNode} from 'react'; 5 | 6 | function PaneHeader({ 7 | title, 8 | icon, 9 | children, 10 | }: { 11 | title: ReactNode; 12 | icon?: ReactNode; 13 | children?: ReactNode; 14 | }) { 15 | const theme = useTheme(); 16 | return ( 17 | 28 |
{icon}
29 | 34 | {title} 35 | 36 | {children} 37 |
38 | ); 39 | } 40 | 41 | export default PaneHeader; 42 | -------------------------------------------------------------------------------- /playground/src/qbjc-manager.ts: -------------------------------------------------------------------------------- 1 | import {action, computed, makeObservable, observable, runInAction} from 'mobx'; 2 | import {compile, CompileResult, Loc} from 'qbjc'; 3 | import {BrowserExecutor} from 'qbjc/browser'; 4 | import {Terminal} from 'xterm'; 5 | import configManager, { 6 | ConfigKey, 7 | DEFAULT_SOURCE_FILE_NAME, 8 | } from './config-manager'; 9 | import EditorController from './editor-controller'; 10 | 11 | export enum QbjcMessageType { 12 | /** General error message. */ 13 | ERROR = 'error', 14 | /** General informational message with no special formatting. */ 15 | INFO = 'info', 16 | /** Specialized message representing program execution. */ 17 | EXECUTION = 'execution', 18 | } 19 | export interface QbjcMessage { 20 | /** Parsed location in the source code. */ 21 | loc?: Loc; 22 | /** Message type. */ 23 | type: QbjcMessageType; 24 | /** Message text. */ 25 | message: string; 26 | /** For EXECUTION messages - the compiled code. */ 27 | compileResult?: CompileResult; 28 | } 29 | 30 | class QbjcManager { 31 | /** Whether we're currently compiling / running code. */ 32 | isRunning: boolean = false; 33 | 34 | /** Compiler errors and status messages. */ 35 | messages: Array = []; 36 | 37 | /** Editor controller instance. */ 38 | editorController: EditorController | null = null; 39 | 40 | /** Current file name. */ 41 | sourceFileName: string = configManager.getKey( 42 | ConfigKey.CURRENT_SOURCE_FILE_NAME 43 | ); 44 | 45 | /** xterm.js terminal. */ 46 | terminal: Terminal | null = null; 47 | 48 | /** Current executor. */ 49 | private executor: BrowserExecutor | null = null; 50 | 51 | /** Connects this QbjcManager to the provided editor and terminal. */ 52 | init({ 53 | editorController, 54 | terminal, 55 | }: { 56 | editorController?: EditorController; 57 | terminal?: Terminal; 58 | }) { 59 | if (editorController) { 60 | this.editorController = editorController; 61 | } 62 | if (terminal) { 63 | this.terminal = terminal; 64 | } 65 | } 66 | 67 | /** Whether the required components (editor and terminal) are connected. */ 68 | get isReady() { 69 | return !!this.editorController && !!this.terminal; 70 | } 71 | 72 | async run() { 73 | if (!this.editorController || !this.terminal || this.isRunning) { 74 | return; 75 | } 76 | const source = this.editorController.getText(); 77 | if (!source.trim()) { 78 | return; 79 | } 80 | 81 | this.isRunning = true; 82 | let compileResult: CompileResult; 83 | try { 84 | compileResult = await compile({ 85 | source, 86 | sourceFileName: this.sourceFileName, 87 | }); 88 | console.log(compileResult.code); 89 | } catch (e: any) { 90 | console.error(`Compile error: ${e.message ?? JSON.stringify(e)}`); 91 | runInAction(() => { 92 | this.isRunning = false; 93 | }); 94 | if (e.message) { 95 | this.pushMessage({ 96 | loc: e.loc, 97 | type: QbjcMessageType.ERROR, 98 | message: e.message, 99 | }); 100 | } 101 | return; 102 | } 103 | 104 | this.pushMessage({ 105 | type: QbjcMessageType.EXECUTION, 106 | message: 'Running...', 107 | compileResult: compileResult, 108 | }); 109 | const startTs = new Date(); 110 | this.terminal.focus(); 111 | this.executor = new BrowserExecutor(this.terminal, { 112 | stmtExecutionDelayUs: configManager.getKey(ConfigKey.EXECUTION_DELAY), 113 | }); 114 | try { 115 | await this.executor.executeModule(compileResult.code); 116 | } finally { 117 | const endTs = new Date(); 118 | runInAction(() => { 119 | this.isRunning = false; 120 | }); 121 | this.pushMessage({ 122 | type: QbjcMessageType.INFO, 123 | message: 124 | 'Exited in ' + 125 | `${((endTs.getTime() - startTs.getTime()) / 1000).toFixed(3)}s`, 126 | }); 127 | this.executor = null; 128 | this.editorController.focus(); 129 | } 130 | } 131 | 132 | stop() { 133 | if (!this.isRunning || !this.executor) { 134 | return; 135 | } 136 | this.executor.stopExecution(); 137 | } 138 | 139 | goToMessageLocInEditor(loc: Loc) { 140 | if (!this.editorController || !loc) { 141 | return; 142 | } 143 | const {line, col} = loc; 144 | this.editorController.setCursor(line - 1, col - 1); 145 | this.editorController.focus(); 146 | } 147 | 148 | pushMessage(message: QbjcMessage) { 149 | this.messages.push(message); 150 | } 151 | 152 | clearMessages() { 153 | this.messages.splice(0, this.messages.length); 154 | } 155 | 156 | updateSourceFileName(sourceFileName: string) { 157 | this.sourceFileName = sourceFileName.trim() || DEFAULT_SOURCE_FILE_NAME; 158 | configManager.setSourceFileName( 159 | configManager.currentSourceFile, 160 | this.sourceFileName 161 | ); 162 | configManager.setKey( 163 | ConfigKey.CURRENT_SOURCE_FILE_NAME, 164 | this.sourceFileName 165 | ); 166 | } 167 | 168 | constructor() { 169 | makeObservable(this, { 170 | isRunning: observable, 171 | messages: observable, 172 | editorController: observable.ref, 173 | sourceFileName: observable, 174 | terminal: observable.ref, 175 | isReady: computed, 176 | init: action, 177 | run: action, 178 | pushMessage: action, 179 | clearMessages: action, 180 | updateSourceFileName: action, 181 | }); 182 | } 183 | } 184 | 185 | export default QbjcManager; 186 | -------------------------------------------------------------------------------- /playground/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /playground/src/run-fab.tsx: -------------------------------------------------------------------------------- 1 | import Fab from '@mui/material/Fab'; 2 | import Tooltip from '@mui/material/Tooltip'; 3 | import PlayArrowIcon from '@mui/icons-material/PlayArrow'; 4 | import StopIcon from '@mui/icons-material/Stop'; 5 | import {observer} from 'mobx-react'; 6 | import React, {useCallback} from 'react'; 7 | import QbjcManager from './qbjc-manager'; 8 | 9 | const RunFab = observer(({qbjcManager}: {qbjcManager: QbjcManager}) => { 10 | const isRunning = qbjcManager.isRunning; 11 | return ( 12 |
19 |
20 | 21 | { 23 | if (isRunning) { 24 | qbjcManager.stop(); 25 | } else { 26 | await qbjcManager.run(); 27 | } 28 | }, [qbjcManager, isRunning])} 29 | color={isRunning ? 'secondary' : 'primary'} 30 | style={{ 31 | zIndex: 10, 32 | }} 33 | > 34 | {isRunning ? : } 35 | 36 | 37 | {/* 38 | isRunning && ( 39 | 43 | ) 44 | */} 45 |
46 |
47 | ); 48 | }); 49 | 50 | export default RunFab; 51 | -------------------------------------------------------------------------------- /playground/src/segment.js: -------------------------------------------------------------------------------- 1 | /** Initialize Segment if enabled. */ 2 | export default function initializeSegment() { 3 | const segmentWriteKey = process.env.REACT_APP_SEGMENT_WRITE_KEY; 4 | if (!segmentWriteKey) { 5 | return; 6 | } 7 | 8 | var analytics = (window.analytics = window.analytics || []); 9 | if (!analytics.initialize) 10 | if (analytics.invoked) 11 | window.console && 12 | console.error && 13 | console.error('Segment snippet included twice.'); 14 | else { 15 | analytics.invoked = !0; 16 | analytics.methods = [ 17 | 'trackSubmit', 18 | 'trackClick', 19 | 'trackLink', 20 | 'trackForm', 21 | 'pageview', 22 | 'identify', 23 | 'reset', 24 | 'group', 25 | 'track', 26 | 'ready', 27 | 'alias', 28 | 'debug', 29 | 'page', 30 | 'once', 31 | 'off', 32 | 'on', 33 | 'addSourceMiddleware', 34 | 'addIntegrationMiddleware', 35 | 'setAnonymousId', 36 | 'addDestinationMiddleware', 37 | ]; 38 | analytics.factory = function (e) { 39 | return function () { 40 | var t = Array.prototype.slice.call(arguments); 41 | t.unshift(e); 42 | analytics.push(t); 43 | return analytics; 44 | }; 45 | }; 46 | for (var e = 0; e < analytics.methods.length; e++) { 47 | var key = analytics.methods[e]; 48 | analytics[key] = analytics.factory(key); 49 | } 50 | analytics.load = function (key, e) { 51 | var t = document.createElement('script'); 52 | t.type = 'text/javascript'; 53 | t.async = !0; 54 | t.src = 55 | 'https://cdn.segment.com/analytics.js/v1/' + 56 | key + 57 | '/analytics.min.js'; 58 | var n = document.getElementsByTagName('script')[0]; 59 | n.parentNode.insertBefore(t, n); 60 | analytics._loadOptions = e; 61 | }; 62 | analytics._writeKey = segmentWriteKey; 63 | analytics.SNIPPET_VERSION = '4.15.3'; 64 | analytics.load(segmentWriteKey); 65 | analytics.page(); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /playground/src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /playground/src/split.css: -------------------------------------------------------------------------------- 1 | /** Styles for react-split. */ 2 | 3 | :root { 4 | --gutter-border-color: #2c2c2c; 5 | } 6 | 7 | .gutter { 8 | background-color: #303030; 9 | } 10 | 11 | .gutter:hover { 12 | background-color: #616161; 13 | } 14 | 15 | .gutter.gutter-horizontal { 16 | border-left: 1px solid var(--gutter-border-color); 17 | border-right: 1px solid var(--gutter-border-color); 18 | cursor: col-resize; 19 | } 20 | .gutter.gutter.gutter-horizontal:hover { 21 | border: none; 22 | } 23 | 24 | .gutter.gutter-vertical { 25 | border-top: 1px solid var(--gutter-border-color); 26 | border-bottom: 1px solid var(--gutter-border-color); 27 | cursor: row-resize; 28 | } 29 | .gutter.gutter-vertical:hover { 30 | border: none; 31 | } 32 | -------------------------------------------------------------------------------- /playground/src/tools/copy-monaco-assets.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Script to copy static assets related to Monaco editor during build. 4 | 5 | function find_monaco_path() { 6 | editor_main_js_path=$(node -e 'console.log(require.resolve("monaco-editor/esm/vs/editor/editor.main.js"));') 7 | if [ $? -ne 0 ]; then 8 | echo 'Could not find monaco-editor in node_modules' 9 | exit 1 10 | fi 11 | echo "${editor_main_js_path}" | sed 's|/esm/vs/editor/editor.main.js$||' 12 | } 13 | 14 | cd "$(dirname "$0")/../../" 15 | 16 | monaco_path=$(find_monaco_path) 17 | echo "Copying assets from ${monaco_path}" 18 | 19 | (set -x; \ 20 | mkdir -p ./public/monaco/vs/base{,/common}/worker && \ 21 | cp "${monaco_path}"/min/vs/base/worker/workerMain.js ./public/monaco/vs/base/worker/ && \ 22 | cp "${monaco_path}"/min/vs/base/common/worker/simpleWorker.nls.js ./public/monaco/vs/base/common/worker/) 23 | -------------------------------------------------------------------------------- /playground/src/tools/generate-examples-bundle-json.js: -------------------------------------------------------------------------------- 1 | /** @file Script to generate example data bundle (example-bundle.json). */ 2 | const fs = require('fs-extra'); 3 | const path = require('path'); 4 | 5 | if (require.main === module) { 6 | (async () => { 7 | const rootDirPath = path.join(__dirname, '..', '..'); 8 | const examplesJsonFilePath = path.join(rootDirPath, 'src', 'examples.json'); 9 | const outputExamplesBundleFilePath = path.join( 10 | rootDirPath, 11 | 'src', 12 | 'examples-bundle.json' 13 | ); 14 | console.log(`> ${examplesJsonFilePath}`); 15 | const exampleMetaList = await fs.readJson(examplesJsonFilePath); 16 | const exampleList = await Promise.all( 17 | exampleMetaList.map(async (exampleMeta) => { 18 | const exampleFilePath = path.join( 19 | rootDirPath, 20 | 'examples', 21 | exampleMeta.fileName 22 | ); 23 | console.log(`> ${exampleFilePath}`); 24 | return { 25 | ...exampleMeta, 26 | content: await fs.readFile(exampleFilePath, 'utf-8'), 27 | }; 28 | }) 29 | ); 30 | console.log(`=> ${outputExamplesBundleFilePath}`); 31 | await fs.writeJson(outputExamplesBundleFilePath, exampleList); 32 | })(); 33 | } 34 | -------------------------------------------------------------------------------- /playground/src/tools/generate-monaco-themes-bundle-json.js: -------------------------------------------------------------------------------- 1 | /** @file Script to generate Monaco themes bundle (monaco-themes-bundle.json). */ 2 | const fs = require('fs-extra'); 3 | const path = require('path'); 4 | 5 | if (require.main === module) { 6 | (async () => { 7 | const themeListJsonPath = require.resolve( 8 | 'monaco-themes/themes/themelist.json' 9 | ); 10 | const themeDirPath = path.dirname(themeListJsonPath); 11 | const rootDirPath = path.join(__dirname, '..', '..'); 12 | const outputThemesBundleFilePath = path.join( 13 | rootDirPath, 14 | 'src', 15 | 'monaco-themes-bundle.json' 16 | ); 17 | 18 | console.log(`> ${themeListJsonPath}`); 19 | const themeList = await fs.readJson(themeListJsonPath); 20 | const themeBundle = {}; 21 | for (const [themeKey, themeName] of Object.entries(themeList)) { 22 | const themeFilePath = path.join(themeDirPath, `${themeName}.json`); 23 | console.log(`> ${themeFilePath}`); 24 | themeBundle[themeKey] = { 25 | name: themeName, 26 | data: await fs.readJson(themeFilePath), 27 | }; 28 | } 29 | 30 | console.log(`=> ${outputThemesBundleFilePath}`); 31 | await fs.writeJson(outputThemesBundleFilePath, themeBundle); 32 | })(); 33 | } 34 | -------------------------------------------------------------------------------- /playground/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "esModuleInterop": true, 8 | "allowSyntheticDefaultImports": true, 9 | "strict": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "noFallthroughCasesInSwitch": true, 12 | "module": "esnext", 13 | "moduleResolution": "node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx" 18 | }, 19 | "include": ["src"] 20 | } 21 | -------------------------------------------------------------------------------- /qbjc.code-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | { 4 | "path": "." 5 | } 6 | ], 7 | "settings": {} 8 | } 9 | -------------------------------------------------------------------------------- /qbjc/.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | 3 | /dist 4 | /browser/*.js 5 | /browser/*.d.ts 6 | /node/*.js 7 | /node/*.d.ts 8 | 9 | /README.md 10 | 11 | .DS_Store 12 | 13 | npm-debug.log* 14 | yarn-debug.log* 15 | yarn-error.log* 16 | 17 | *.tgz 18 | -------------------------------------------------------------------------------- /qbjc/.npmignore: -------------------------------------------------------------------------------- 1 | # Intentionally empty to prevent npm from using .gitignore 2 | -------------------------------------------------------------------------------- /qbjc/.prettierignore: -------------------------------------------------------------------------------- 1 | /dist/ 2 | 3 | # Generated files. 4 | /node.js 5 | /node.d.ts 6 | /browser.js 7 | /browser.d.ts 8 | 9 | # Generated parser. 10 | /src/parser/grammar.ts 11 | 12 | # Intermediate debugging output generated during testing. 13 | *.bas.js 14 | *.ast.json 15 | 16 | -------------------------------------------------------------------------------- /qbjc/browser/index.ts: -------------------------------------------------------------------------------- 1 | export * from '../dist/runtime/browser-platform'; 2 | -------------------------------------------------------------------------------- /qbjc/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | roots: ['/dist'], 3 | }; 4 | -------------------------------------------------------------------------------- /qbjc/node/index.ts: -------------------------------------------------------------------------------- 1 | export * from '../dist/runtime/node-platform'; 2 | -------------------------------------------------------------------------------- /qbjc/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "qbjc", 3 | "version": "0.1.2", 4 | "description": "QBasic to JavaScript compiler", 5 | "repository": { 6 | "type": "git", 7 | "url": "git+https://github.com/jichu4n/qbjc.git" 8 | }, 9 | "author": "Chuan Ji ", 10 | "license": "Apache-2.0", 11 | "bugs": { 12 | "url": "https://github.com/jichu4n/qbjc/issues" 13 | }, 14 | "homepage": "https://github.com/jichu4n/qbjc#readme", 15 | "main": "./dist/index.js", 16 | "types": "./dist/index.d.ts", 17 | "bin": { 18 | "qbjc": "./dist/qbjc.js" 19 | }, 20 | "scripts": { 21 | "build:grammar": "cd ./src/parser && nearleyc grammar.ne -o grammar.ts", 22 | "build:nodeRuntimeBundle": "node ./dist/tools/build-runtime-bundle.js ./dist/runtime/node-runtime-bundle-bootstrap.js ./dist/runtime/node-runtime-bundle.js", 23 | "build:tsc": "tsc && tsc -p tsconfig.platforms.json && chmod +x ./dist/qbjc.js", 24 | "build": "npm run build:grammar && npm run build:tsc && npm run build:nodeRuntimeBundle", 25 | "packageTest": "npm run build && ./src/tests/package-test.sh", 26 | "lint": "prettier --check .", 27 | "test": "jest", 28 | "prepack": "cp ../README.md ./ && rm -rf ./dist/{tests,tools}", 29 | "postpack": "rm ./README.md", 30 | "prepublishOnly": "npm run packageTest" 31 | }, 32 | "devDependencies": { 33 | "@types/ansi-styles": "^3.2.1", 34 | "@types/fs-extra": "^9.0.13", 35 | "@types/jest": "^29.5.13", 36 | "@types/lodash": "^4.17.7", 37 | "@types/moo": "^0.5.5", 38 | "@types/nearley": "^2.11.2", 39 | "@types/node": "^22.6.0", 40 | "@types/require-from-string": "^1.2.3", 41 | "airtable": "^0.12.2", 42 | "jest": "^29.7.0", 43 | "node-ansiparser": "^2.2.0", 44 | "node-ansiterminal": "^0.2.1-beta", 45 | "prettier": "^3.5.3", 46 | "strip-ansi": "^6.0.1", 47 | "typescript": "^4.8.4", 48 | "xterm": "^4.19.0" 49 | }, 50 | "dependencies": { 51 | "@vercel/ncc": "^0.38.3", 52 | "ansi-escapes": "^4.3.2", 53 | "ansi-styles": "^5.2.0", 54 | "commander": "^13.0.0", 55 | "fs-extra": "^10.1.0", 56 | "iconv-lite": "^0.6.3", 57 | "lodash": "^4.17.21", 58 | "moo": "^0.5.2", 59 | "nearley": "^2.20.1", 60 | "require-from-string": "^2.0.2", 61 | "source-map": "^0.8.0-beta.0", 62 | "terser": "^5.15.1" 63 | }, 64 | "files": [ 65 | "dist", 66 | "browser/*.js", 67 | "browser/*.d.ts", 68 | "node/*.js", 69 | "node/*.d.ts" 70 | ] 71 | } 72 | -------------------------------------------------------------------------------- /qbjc/src/compile.ts: -------------------------------------------------------------------------------- 1 | import {minify} from 'terser'; 2 | import codegen from './codegen/codegen'; 3 | import {Module} from './lib/ast'; 4 | import parse from './parser/parser'; 5 | import runSemanticAnalysis from './semantic-analysis/semantic-analysis'; 6 | 7 | export interface CompileArgs { 8 | /** QBasic source code. */ 9 | source: string; 10 | /** Source file name for populating debugging information in the compiled program. */ 11 | sourceFileName?: string; 12 | /** Whether to bundle output with runtime code to produce a standalone program. */ 13 | enableBundling?: boolean; 14 | /** Whether to minify the output. */ 15 | enableMinify?: boolean; 16 | } 17 | 18 | /** Result of a successful compilation. */ 19 | export interface CompileResult { 20 | /** Original QBasic source code. */ 21 | source: string; 22 | /** Original source file name. */ 23 | sourceFileName: string; 24 | /** Compiled JavaScript code. */ 25 | code: string; 26 | /** Sourcemap for the compiled JavaScript code. */ 27 | map: string; 28 | /** Abstract syntax tree representing the compiled code. */ 29 | astModule: Module; 30 | } 31 | 32 | const DEFAULT_SOURCE_FILE_NAME = 'source.bas'; 33 | 34 | /** Compiles a QBasic program. 35 | * 36 | * This is the main entrypoint to qbjc's compiler. 37 | */ 38 | async function compile({ 39 | source, 40 | sourceFileName = DEFAULT_SOURCE_FILE_NAME, 41 | enableBundling, 42 | enableMinify, 43 | }: CompileArgs): Promise { 44 | // 1. Parse input into AST. 45 | const astModule = parse(source, {sourceFileName}); 46 | if (!astModule) { 47 | throw new Error(`Invalid parse tree`); 48 | } 49 | 50 | // 2. Semantic analysis. 51 | runSemanticAnalysis(astModule, {sourceFileName}); 52 | 53 | // 3. Code generation. 54 | let {code, map: sourceMap} = codegen(astModule, { 55 | sourceFileName, 56 | enableBundling, 57 | }); 58 | let sourceMapContent = sourceMap.toString(); 59 | 60 | // 4. Minification. 61 | if (enableMinify) { 62 | const {code: minifiedCode, map: minifiedSourceMap} = await minify(code, { 63 | sourceMap: { 64 | filename: sourceFileName, 65 | content: sourceMapContent, 66 | }, 67 | }); 68 | if (minifiedCode && minifiedSourceMap) { 69 | code = minifiedCode; 70 | sourceMapContent = minifiedSourceMap as string; 71 | } 72 | } 73 | 74 | return { 75 | source, 76 | sourceFileName, 77 | code, 78 | map: sourceMapContent, 79 | astModule, 80 | }; 81 | } 82 | 83 | export default compile; 84 | -------------------------------------------------------------------------------- /qbjc/src/deps.d.ts: -------------------------------------------------------------------------------- 1 | declare module '@vercel/ncc'; 2 | -------------------------------------------------------------------------------- /qbjc/src/index.ts: -------------------------------------------------------------------------------- 1 | export {CompileArgs, CompileResult, default as compile} from './compile'; 2 | 3 | export * from './lib/error-with-loc'; 4 | export * from './lib/types'; 5 | export { 6 | ExecutionOpts, 7 | ExecutionError, 8 | default as Executor, 9 | } from './runtime/executor'; 10 | export {RuntimePlatform} from './runtime/runtime'; 11 | export {default as QbArray} from './runtime/qb-array'; 12 | export {default as QbUdt} from './runtime/qb-udt'; 13 | export * from './runtime/compiled-code'; 14 | -------------------------------------------------------------------------------- /qbjc/src/lib/data-item.ts: -------------------------------------------------------------------------------- 1 | import {isString, isNumeric, DataTypeSpec, typeSpecName} from './types'; 2 | 3 | /** A constant defined by a DATA statement. 4 | * 5 | * The format is [[line, col], value]. 6 | * 7 | * We use this shortened form instead of JSON due to the highly repetitive nature of these items. 8 | */ 9 | export type DataItem = [[number, number], DataItemValue]; 10 | 11 | /** A sequence of DataItems. */ 12 | export type DataItems = Array; 13 | 14 | /** Actual value of a DataItem. */ 15 | export type DataItemValue = number | string | null; 16 | 17 | export function dataItemTypeName([_, value]: DataItem) { 18 | return value === null ? 'null' : typeof value; 19 | } 20 | 21 | /** Converts a data item to the expected type. */ 22 | export function getDataItem(item: DataItem, expectedTypeSpec: DataTypeSpec) { 23 | const [_, value] = item; 24 | if (isNumeric(expectedTypeSpec)) { 25 | switch (typeof value) { 26 | case 'number': 27 | return value; 28 | case 'object': 29 | return 0; 30 | default: 31 | throwDataItemTypeError(item, expectedTypeSpec); 32 | } 33 | } else if (isString(expectedTypeSpec)) { 34 | switch (typeof value) { 35 | case 'string': 36 | return value; 37 | case 'object': 38 | return ''; 39 | default: 40 | throwDataItemTypeError(item, expectedTypeSpec); 41 | } 42 | } else { 43 | throw new Error( 44 | `Unknown expected type spec: ${JSON.stringify(expectedTypeSpec)}` 45 | ); 46 | } 47 | } 48 | 49 | function throwDataItemTypeError( 50 | item: DataItem, 51 | expectedTypeSpec: DataTypeSpec 52 | ): never { 53 | const [[line, col], _] = item; 54 | throw new Error( 55 | `expected ${typeSpecName(expectedTypeSpec)}, ` + 56 | `got ${dataItemTypeName(item)} data item ` + 57 | `(defined at ${line}:${col})` 58 | ); 59 | } 60 | -------------------------------------------------------------------------------- /qbjc/src/lib/error-with-loc.ts: -------------------------------------------------------------------------------- 1 | /** Location of an error in the source file. */ 2 | export interface Loc { 3 | line: number; 4 | col: number; 5 | } 6 | 7 | /** An error with source file and location info. */ 8 | export default class ErrorWithLoc extends Error { 9 | constructor( 10 | message: string, 11 | { 12 | sourceFileName, 13 | loc, 14 | cause, 15 | }: { 16 | sourceFileName?: string; 17 | loc?: {line: number; col: number}; 18 | cause?: Error; 19 | } = {} 20 | ) { 21 | // @ts-ignore 22 | super(message, {cause}); 23 | if (loc) { 24 | const sourceFileNamePrefix = sourceFileName ? `${sourceFileName}:` : ''; 25 | const locPrefix = `${loc.line}:${loc.col}`; 26 | this.message = `${sourceFileNamePrefix}${locPrefix} - ${this.message}`; 27 | this.loc = loc; 28 | } 29 | } 30 | 31 | loc?: Loc; 32 | } 33 | -------------------------------------------------------------------------------- /qbjc/src/lib/round-half-to-even.ts: -------------------------------------------------------------------------------- 1 | /** Round a number to integer using BASIC's round-half-to-even rule. */ 2 | export default function roundHalfToEven(n: number) { 3 | if (n - Math.floor(n) === 0.5) { 4 | return Math.round(n / 2) * 2; 5 | } else { 6 | return Math.round(n); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /qbjc/src/lib/symbol-table.ts: -------------------------------------------------------------------------------- 1 | import {DataTypeSpec} from './types'; 2 | 3 | /** Type of a variable symbol. */ 4 | export enum VarType { 5 | /** Regular variable. */ 6 | VAR = 'var', 7 | /** Static variable. */ 8 | STATIC_VAR = 'staticVar', 9 | /** Constant. */ 10 | CONST = 'const', 11 | /** FUNCTION or SUB argument. */ 12 | ARG = 'arg', 13 | } 14 | 15 | /** Scope of a variable symbol. */ 16 | export enum VarScope { 17 | LOCAL = 'local', 18 | GLOBAL = 'global', 19 | } 20 | 21 | interface VarSymbolBase { 22 | name: string; 23 | typeSpec: DataTypeSpec; 24 | } 25 | 26 | interface LocalVarSymbol extends VarSymbolBase { 27 | varType: VarType.STATIC_VAR | VarType.ARG; 28 | varScope: VarScope.LOCAL; 29 | } 30 | 31 | interface LocalOrGlobalVarSymbol extends VarSymbolBase { 32 | varType: VarType.VAR | VarType.CONST; 33 | varScope: VarScope.LOCAL | VarScope.GLOBAL; 34 | } 35 | 36 | /** A variable in a symbol table. */ 37 | export type VarSymbol = LocalVarSymbol | LocalOrGlobalVarSymbol; 38 | 39 | /** Symbol table for variables. */ 40 | export type VarSymbolTable = Array; 41 | 42 | export function lookupSymbol( 43 | symbolTable: Array, 44 | name: string 45 | ) { 46 | return ( 47 | symbolTable.find( 48 | (symbol) => symbol.name.toLowerCase() === name.toLowerCase() 49 | ) ?? null 50 | ); 51 | } 52 | 53 | export function lookupSymbols( 54 | symbolTable: Array, 55 | name: string 56 | ) { 57 | return symbolTable.filter( 58 | (symbol) => symbol.name.toLowerCase() === name.toLowerCase() 59 | ); 60 | } 61 | -------------------------------------------------------------------------------- /qbjc/src/lib/types.ts: -------------------------------------------------------------------------------- 1 | /** Overall type of the value. */ 2 | export enum DataType { 3 | /** 16-bit integer */ 4 | INTEGER = 'integer', 5 | /** 32-bit integer */ 6 | LONG = 'long', 7 | /** 32-bit floating point. */ 8 | SINGLE = 'single', 9 | /** 64-bit floating point. */ 10 | DOUBLE = 'double', 11 | /** String. */ 12 | STRING = 'string', 13 | /** Array. */ 14 | ARRAY = 'array', 15 | /** User-defined data type, a.k.a. records. */ 16 | UDT = 'udt', 17 | } 18 | 19 | export type NumericDataType = 20 | | DataType.INTEGER 21 | | DataType.LONG 22 | | DataType.SINGLE 23 | | DataType.DOUBLE; 24 | 25 | export type ElementaryDataType = NumericDataType | DataType.STRING; 26 | 27 | export type SingularDataType = ElementaryDataType | DataType.UDT; 28 | 29 | /** Full data type specification. */ 30 | export type DataTypeSpec = SingularTypeSpec | ArrayTypeSpec; 31 | 32 | export type SingularTypeSpec = ElementaryTypeSpec | UdtTypeSpec; 33 | 34 | export type ElementaryTypeSpec = NumericTypeSpec | StringTypeSpec; 35 | 36 | export interface NumericTypeSpec { 37 | type: NumericDataType; 38 | } 39 | 40 | export interface StringTypeSpec { 41 | type: DataType.STRING; 42 | } 43 | 44 | export interface ArrayTypeSpec { 45 | type: DataType.ARRAY; 46 | elementTypeSpec: SingularTypeSpec; 47 | dimensionSpecs: Array; 48 | } 49 | 50 | /** Specification for a single dimension of an array. 51 | * 52 | * The format is [minIdx, maxIdx]. 53 | */ 54 | export type ArrayDimensionSpec = [number, number]; 55 | 56 | export interface UdtTypeSpec { 57 | type: DataType.UDT; 58 | name: string; 59 | fieldSpecs: Array; 60 | } 61 | 62 | /** Specification for a field in a user-defined record type. */ 63 | export interface FieldSpec { 64 | name: string; 65 | typeSpec: SingularTypeSpec; 66 | } 67 | 68 | // Helpers for creating DataTypeSpec instances. 69 | 70 | export function integerSpec(): NumericTypeSpec { 71 | return {type: DataType.INTEGER}; 72 | } 73 | 74 | export function longSpec(): NumericTypeSpec { 75 | return {type: DataType.LONG}; 76 | } 77 | 78 | export function singleSpec(): NumericTypeSpec { 79 | return {type: DataType.SINGLE}; 80 | } 81 | 82 | export function doubleSpec(): NumericTypeSpec { 83 | return {type: DataType.DOUBLE}; 84 | } 85 | 86 | export function stringSpec(): StringTypeSpec { 87 | return {type: DataType.STRING}; 88 | } 89 | 90 | export function arraySpec( 91 | elementTypeSpec: SingularTypeSpec, 92 | dimensionSpecs: Array 93 | ): ArrayTypeSpec { 94 | return {type: DataType.ARRAY, elementTypeSpec, dimensionSpecs}; 95 | } 96 | 97 | // Helpers for type checking. 98 | 99 | export function isNumeric( 100 | t: DataType | DataTypeSpec 101 | ): t is NumericDataType | NumericTypeSpec { 102 | return [ 103 | DataType.INTEGER, 104 | DataType.LONG, 105 | DataType.SINGLE, 106 | DataType.DOUBLE, 107 | ].includes(getType(t)); 108 | } 109 | 110 | export function isIntegral( 111 | t: DataType | DataTypeSpec 112 | ): t is DataType.INTEGER | DataType.LONG | NumericTypeSpec { 113 | return [DataType.INTEGER, DataType.LONG].includes(getType(t)); 114 | } 115 | 116 | export function isString( 117 | t: DataType | DataTypeSpec 118 | ): t is DataType.STRING | {type: DataType.STRING} { 119 | return getType(t) === DataType.STRING; 120 | } 121 | 122 | export function isArray( 123 | t: DataType | DataTypeSpec 124 | ): t is DataType.ARRAY | ArrayTypeSpec { 125 | return getType(t) === DataType.ARRAY; 126 | } 127 | 128 | export function isElementaryType( 129 | t: DataType | DataTypeSpec 130 | ): t is ElementaryDataType | ElementaryTypeSpec { 131 | return isNumeric(t) || isString(t); 132 | } 133 | 134 | export function isUdt( 135 | t: DataType | DataTypeSpec 136 | ): t is DataType.UDT | UdtTypeSpec { 137 | return getType(t) === DataType.UDT; 138 | } 139 | 140 | export function isSingularType( 141 | t: DataType | DataTypeSpec 142 | ): t is SingularDataType | SingularTypeSpec { 143 | return isElementaryType(t) || isUdt(t); 144 | } 145 | 146 | export function areMatchingElementaryTypes( 147 | typeSpec1: DataTypeSpec, 148 | typeSpec2: DataTypeSpec 149 | ) { 150 | return ( 151 | (isNumeric(typeSpec1) && isNumeric(typeSpec2)) || 152 | (isString(typeSpec1) && isString(typeSpec2)) 153 | ); 154 | } 155 | 156 | export function areMatchingSingularTypes( 157 | typeSpec1: DataTypeSpec, 158 | typeSpec2: DataTypeSpec 159 | ) { 160 | return ( 161 | areMatchingElementaryTypes(typeSpec1, typeSpec2) || 162 | (isUdt(typeSpec1) && isUdt(typeSpec2) && typeSpec1.name === typeSpec2.name) 163 | ); 164 | } 165 | 166 | export function typeSpecName(t: DataTypeSpec): string { 167 | switch (t.type) { 168 | case DataType.ARRAY: 169 | return `${typeSpecName(t.elementTypeSpec)} array`; 170 | case DataType.UDT: 171 | return `"${t.name}"`; 172 | default: 173 | return t.type; 174 | } 175 | } 176 | 177 | function getType(t: DataType | DataTypeSpec) { 178 | return typeof t === 'object' ? t.type : t; 179 | } 180 | 181 | /** Type of a procedure. */ 182 | export enum ProcType { 183 | /** SUB procedure. */ 184 | SUB = 'sub', 185 | /** FUNCTION procedure. */ 186 | FN = 'fn', 187 | /** DEF FN procedure. */ 188 | DEF_FN = 'defFn', 189 | } 190 | 191 | export function isFnOrDefFn( 192 | procType: ProcType 193 | ): procType is ProcType.FN | ProcType.DEF_FN { 194 | return procType === ProcType.FN || procType === ProcType.DEF_FN; 195 | } 196 | 197 | /** Returns the user-facing name of a ProcType. */ 198 | export function procTypeName(procType: ProcType) { 199 | const NAMES = { 200 | [ProcType.SUB]: 'SUB procedure', 201 | [ProcType.FN]: 'FUNCTION procedure', 202 | [ProcType.DEF_FN]: 'DEF FN procedure', 203 | }; 204 | return NAMES[procType]; 205 | } 206 | 207 | /** The type of a procedure definition. */ 208 | export enum ProcDefType { 209 | /** Built-in function. */ 210 | BUILTIN = 'builtin', 211 | /** User-defined function in the current module. */ 212 | MODULE = 'module', 213 | } 214 | -------------------------------------------------------------------------------- /qbjc/src/parser/lexer.ts: -------------------------------------------------------------------------------- 1 | import moo from 'moo'; 2 | 3 | /** QBasic keywords. 4 | * 5 | * Note that the values must be lowercase! 6 | */ 7 | export enum Keywords { 8 | AND = 'and', 9 | AS = 'as', 10 | CALL = 'call', 11 | CASE = 'case', 12 | COLOR = 'color', 13 | CONST = 'const', 14 | DATA = 'data', 15 | DECLARE = 'declare', 16 | DEF = 'def', 17 | DEFINT = 'defint', 18 | DEFSNG = 'defsng', 19 | DEFDBL = 'defdbl', 20 | DEFLNG = 'deflng', 21 | DEFSTR = 'defstr', 22 | DIM = 'dim', 23 | DO = 'do', 24 | DOUBLE = 'double', 25 | ELSE = 'else', 26 | ELSEIF = 'elseif', 27 | END = 'end', 28 | EXIT = 'exit', 29 | FUNCTION = 'function', 30 | FOR = 'for', 31 | GOSUB = 'gosub', 32 | GOTO = 'goto', 33 | IF = 'if', 34 | IS = 'is', 35 | INPUT = 'input', 36 | INTEGER = 'integer', 37 | LET = 'let', 38 | LINE = 'line', 39 | LOCATE = 'locate', 40 | LONG = 'long', 41 | LOOP = 'loop', 42 | MOD = 'mod', 43 | NEXT = 'next', 44 | NOT = 'not', 45 | OR = 'or', 46 | PRINT = 'print', 47 | READ = 'read', 48 | REM = 'rem', 49 | RESTORE = 'restore', 50 | RETURN = 'return', 51 | SEG = 'seg', 52 | SELECT = 'select', 53 | SHARED = 'shared', 54 | SINGLE = 'single', 55 | STATIC = 'static', 56 | STEP = 'step', 57 | STOP = 'stop', 58 | STRING = 'string', 59 | SUB = 'sub', 60 | SYSTEM = 'system', 61 | SWAP = 'swap', 62 | THEN = 'then', 63 | TO = 'to', 64 | TYPE = 'type', 65 | UNTIL = 'until', 66 | USING = 'using', 67 | VIEW = 'view', 68 | WEND = 'wend', 69 | WHILE = 'while', 70 | } 71 | 72 | /** moo lexer used internally by Lexer. */ 73 | const mooLexer = moo.states( 74 | { 75 | main: { 76 | WHITESPACE: { 77 | match: /\s+/, 78 | type: (text) => (text.includes('\n') ? 'NEWLINE' : ''), 79 | lineBreaks: true, 80 | }, 81 | COMMENT: { 82 | match: /'/, 83 | push: 'comment', 84 | }, 85 | IDENTIFIER: { 86 | match: /[a-zA-Z_][a-zA-Z0-9_]*(?:\$|%|#|&|!)?/, 87 | type: caseInsensitiveKeywords(Keywords), 88 | }, 89 | 90 | STRING_LITERAL: { 91 | match: /"[^"]*"/, 92 | value: (text) => text.substr(1, text.length - 2), 93 | }, 94 | NUMERIC_LITERAL: /(?:\d*\.\d+|\d+)/, 95 | HEX_LITERAL: { 96 | match: /&[hH][\da-fA-F]+&?/, 97 | type: () => 'NUMERIC_LITERAL', 98 | value: (text) => `${parseInt(text.substr(2), 16)}`, 99 | }, 100 | OCT_LITERAL: { 101 | match: /&[oO]?[0-7]+&?/, 102 | type: () => 'NUMERIC_LITERAL', 103 | value: (text) => `${parseInt(text.match(/[0-7]+/)![0], 8)}`, 104 | }, 105 | 106 | COLON: ':', 107 | SEMICOLON: ';', 108 | COMMA: ',', 109 | DOT: '.', 110 | LPAREN: '(', 111 | RPAREN: ')', 112 | ADD: '+', 113 | SUB: '-', // Note: must be after NUMERIC_LITERAL 114 | MUL: '*', 115 | EXP: '^', 116 | DIV: '/', 117 | INTDIV: '\\', 118 | // Note: order matters in the comparison operators! 119 | EQ: '=', 120 | NE: '<>', 121 | GTE: '>=', 122 | LTE: '<=', 123 | GT: '>', 124 | LT: '<', 125 | }, 126 | comment: { 127 | COMMENT: { 128 | match: /[^\n]+/, 129 | pop: 1, 130 | }, 131 | NEWLINE: { 132 | match: /\n/, 133 | pop: 1, 134 | lineBreaks: true, 135 | }, 136 | }, 137 | }, 138 | 'main' 139 | ); 140 | 141 | const TOKEN_TYPES_TO_DISCARD = ['WHITESPACE', 'COMMENT', 'REM']; 142 | 143 | /** Extended token. */ 144 | export interface Token extends moo.Token { 145 | isFirstTokenOnLine?: boolean; 146 | } 147 | 148 | /** Lexer for QBasic. 149 | * 150 | * This class wraps the moo lexer with some additional capabilities: 151 | * 152 | * - Discard irrelevant tokens, based on https://github.com/no-context/moo/issues/81. 153 | * - Set isFirstTokenOnLine flag on tokens to help disambiguate labels from statements. 154 | * - Support lookahead with peek(). 155 | * - Store the last token for debugging output. 156 | */ 157 | class Lexer { 158 | constructor(private readonly mooLexer: moo.Lexer) {} 159 | 160 | next(): Token | undefined { 161 | if (this.tokenQueue.length > 0) { 162 | return this.tokenQueue.pop(); 163 | } 164 | 165 | let token: Token | undefined; 166 | do { 167 | token = this.mooLexer.next(); 168 | if (token) { 169 | this.lastToken = token; 170 | token.isFirstTokenOnLine = this.isNextTokenFirstOnLine; 171 | 172 | if (token.type === 'NEWLINE') { 173 | this.isNextTokenFirstOnLine = true; 174 | } else { 175 | this.isNextTokenFirstOnLine = false; 176 | } 177 | 178 | if (token.type === 'REM') { 179 | this.mooLexer.pushState('comment'); 180 | } 181 | } 182 | } while ( 183 | token && 184 | token.type && 185 | TOKEN_TYPES_TO_DISCARD.includes(token.type) 186 | ); 187 | return token; 188 | } 189 | 190 | peek(): Token | undefined { 191 | const token = this.next(); 192 | this.tokenQueue.push(token); 193 | return token; 194 | } 195 | 196 | save(): moo.LexerState { 197 | return this.mooLexer.save(); 198 | } 199 | 200 | reset(chunk?: string, state?: moo.LexerState) { 201 | this.mooLexer.reset(chunk, state); 202 | this.isNextTokenFirstOnLine = true; 203 | this.lastToken = undefined; 204 | this.tokenQueue = []; 205 | return this; 206 | } 207 | 208 | formatError(token: Token, message?: string) { 209 | return this.mooLexer.formatError(token, message); 210 | } 211 | 212 | has(tokenType: string) { 213 | return this.mooLexer.has(tokenType); 214 | } 215 | 216 | lastToken: Token | undefined = undefined; 217 | private isNextTokenFirstOnLine: boolean = true; 218 | private tokenQueue: Array = []; 219 | } 220 | 221 | // Based on https://github.com/no-context/moo/pull/85#issue-178701835 222 | function caseInsensitiveKeywords(map: {[k: string]: string | string[]}) { 223 | const keywordsTransformFn = moo.keywords(map); 224 | return (text: string) => keywordsTransformFn(text.toLowerCase()); 225 | } 226 | 227 | const lexer = new Lexer(mooLexer); 228 | export default lexer; 229 | -------------------------------------------------------------------------------- /qbjc/src/parser/parser.ts: -------------------------------------------------------------------------------- 1 | import {Grammar, Parser, ParserOptions} from 'nearley'; 2 | import {Module} from '../lib/ast'; 3 | import grammar from './grammar'; 4 | import lexer from './lexer'; 5 | import ErrorWithLoc from '../lib/error-with-loc'; 6 | 7 | export function createParser(opts?: ParserOptions) { 8 | return new Parser(Grammar.fromCompiled(grammar), opts); 9 | } 10 | 11 | /** Entry point for parsing an input string into an AST module. */ 12 | export default function parse( 13 | input: string, 14 | opts?: {sourceFileName?: string} & ParserOptions 15 | ): Module { 16 | const parser = createParser(opts); 17 | try { 18 | parser.feed(input); 19 | // Add terminating line break (necessary due to lack of EOF terminal). 20 | parser.feed('\n'); 21 | } catch (e: any) { 22 | if ('token' in e) { 23 | throw new ErrorWithLoc( 24 | // Hack to reduce verbosity of nearley error messages... 25 | e.message.replace( 26 | / Instead, I was expecting to see one of the following:$[\s\S]*/gm, 27 | '' 28 | ), 29 | { 30 | sourceFileName: opts?.sourceFileName, 31 | loc: e.token, 32 | } 33 | ); 34 | } else { 35 | throw e; 36 | } 37 | } 38 | if (parser.results.length === 0) { 39 | throw new ErrorWithLoc(`Unexpected end of input`, { 40 | sourceFileName: opts?.sourceFileName, 41 | loc: lexer.lastToken, 42 | }); 43 | } else if (parser.results.length !== 1) { 44 | throw new ErrorWithLoc( 45 | `${parser.results.length} parse trees:\n\n` + 46 | parser.results 47 | .map( 48 | (tree, idx) => 49 | `Parse tree #${idx + 1}:\n${JSON.stringify(tree, null, 4)}` 50 | ) 51 | .join('\n\n'), 52 | { 53 | sourceFileName: opts?.sourceFileName, 54 | loc: lexer.lastToken, 55 | } 56 | ); 57 | } 58 | return parser.results[0]; 59 | } 60 | -------------------------------------------------------------------------------- /qbjc/src/qbjc.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import {program} from 'commander'; 4 | import fs from 'fs-extra'; 5 | import path from 'path'; 6 | import compile, {CompileArgs, CompileResult} from './compile'; 7 | import {NodeExecutor} from './runtime/node-platform'; 8 | // Not using resolveJsonModule because it causes the output to be generated relative to the root 9 | // directory instead of src/. 10 | const packageJson = require('../package.json'); 11 | 12 | export interface CompileFileArgs 13 | extends Omit { 14 | sourceFilePath: string; 15 | outputFilePath?: string; 16 | enableSourceMap?: boolean; 17 | } 18 | 19 | export interface CompileFileResult extends CompileResult { 20 | source: string; 21 | outputFilePath: string; 22 | } 23 | 24 | /** Compiles a QBasic source file and write out the compiled program and source map. */ 25 | export async function compileFile({ 26 | sourceFilePath, 27 | outputFilePath: outputFilePathArg, 28 | enableSourceMap, 29 | enableBundling, 30 | enableMinify, 31 | }: CompileFileArgs): Promise { 32 | // 1. Read source file. 33 | const source = await fs.readFile(sourceFilePath, 'utf-8'); 34 | 35 | // 2. Compile code. 36 | const sourceFileName = path.basename(sourceFilePath); 37 | const compileResult = await compile({ 38 | source, 39 | sourceFileName, 40 | enableBundling, 41 | enableMinify, 42 | }); 43 | let {code, map: sourceMapContent} = compileResult; 44 | 45 | // 3. Write compiled program and source map. 46 | const outputFilePath = outputFilePathArg || `${sourceFilePath}.js`; 47 | if (enableSourceMap) { 48 | const sourceMapFileName = `${path.basename(outputFilePath)}.map`; 49 | await fs.writeFile( 50 | path.join(path.dirname(outputFilePath), sourceMapFileName), 51 | sourceMapContent 52 | ); 53 | 54 | code += `\n//# sourceMappingURL=${sourceMapFileName}\n`; 55 | } 56 | await fs.writeFile(outputFilePath, code); 57 | if (enableBundling) { 58 | await fs.chmod(outputFilePath, '755'); 59 | } 60 | 61 | return { 62 | ...compileResult, 63 | source, 64 | code, 65 | map: sourceMapContent, 66 | outputFilePath, 67 | }; 68 | } 69 | 70 | // Entrypoint for the "qbjc" CLI tool. 71 | if (require.main === module) { 72 | (async () => { 73 | program 74 | .name(packageJson.name) 75 | .version(packageJson.version) 76 | .arguments('') 77 | .option('-o, --output ', 'output file path') 78 | .option('-r, --run', 'run the compiled program after compilation') 79 | .option('--minify', 'minify the compiled program') 80 | .option('--source-map', 'enable source map generation') 81 | .option('--no-bundle', 'disable bundling with runtime code') 82 | .option( 83 | '--debug-ast', 84 | 'enable generation of AST file for debugging compilation' 85 | ) 86 | .option('--debug-trace', `enable stack trace for debugging compilation`) 87 | .parse(); 88 | 89 | if (program.args.length === 0) { 90 | console.error('Error: No input files specified.'); 91 | process.exit(1); 92 | } else if (program.args.length > 1) { 93 | console.error('Error: Please provide a single input file.'); 94 | process.exit(1); 95 | } 96 | const sourceFilePath = program.args[0]; 97 | 98 | const opts = program.opts(); 99 | 100 | const compileFileResult = await compileFile({ 101 | sourceFilePath, 102 | outputFilePath: opts.output, 103 | enableSourceMap: opts.sourceMap, 104 | enableBundling: opts.bundle, 105 | enableMinify: opts.minify, 106 | }); 107 | 108 | if (opts.debugAst) { 109 | await fs.writeJson( 110 | `${sourceFilePath}.ast.json`, 111 | compileFileResult.astModule, 112 | { 113 | spaces: 4, 114 | } 115 | ); 116 | } 117 | 118 | if (opts.run) { 119 | const executor = new NodeExecutor(); 120 | await executor.executeModule(compileFileResult.code); 121 | } 122 | })().catch((e) => { 123 | if ('message' in e) { 124 | if (program.opts().debugTrace) { 125 | console.trace(e); 126 | } else { 127 | console.error(e.message); 128 | } 129 | } 130 | process.exit(1); 131 | }); 132 | } 133 | -------------------------------------------------------------------------------- /qbjc/src/runtime/ansi-terminal-key-code-map.ts: -------------------------------------------------------------------------------- 1 | /** Returns a VT100 escape sequence prefixed by "ESC [". */ 2 | function ESC(c: string) { 3 | return `\u001B[${c}`; 4 | } 5 | 6 | /** Returns a DOS keyboard scan code prefixed by NUL. */ 7 | function NUL(n: number) { 8 | return String.fromCharCode(0, n); 9 | } 10 | 11 | /** Map ANSI / VT100 sequences to DOS keyboard scan codes. */ 12 | const ANSI_TERMINAL_KEY_CODE_MAP: {[key: string]: string} = { 13 | [ESC('A')]: NUL(72), // Up arrow 14 | [ESC('B')]: NUL(80), // Down arrow 15 | [ESC('C')]: NUL(77), // Right arrow 16 | [ESC('D')]: NUL(75), // Left arrow 17 | // More TODO 18 | }; 19 | 20 | export default ANSI_TERMINAL_KEY_CODE_MAP; 21 | -------------------------------------------------------------------------------- /qbjc/src/runtime/ansi-terminal-platform.ts: -------------------------------------------------------------------------------- 1 | import ansiEscapes from 'ansi-escapes'; 2 | import ansiStyles from 'ansi-styles'; 3 | import ANSI_TERMINAL_KEY_CODE_MAP from './ansi-terminal-key-code-map'; 4 | import {ColorName, RuntimePlatform} from './runtime'; 5 | 6 | /** RuntimePlatform implementing screen manipulation using ANSI escape codes. 7 | * 8 | * Ideally this functionality could be made into a mixin, but 1) it relies on this.print() etc and 9 | * so must be a class and 2) abstract classes don't support mixins yet: 10 | * https://github.com/microsoft/TypeScript/issues/35356 11 | */ 12 | abstract class AnsiTerminalPlatform extends RuntimePlatform { 13 | async moveCursorTo(x: number, y: number) { 14 | await this.print(ansiEscapes.cursorTo(x, y)); 15 | } 16 | 17 | async setCursorVisibility(isCursorVisible: boolean) { 18 | await this.print( 19 | isCursorVisible ? ansiEscapes.cursorShow : ansiEscapes.cursorHide 20 | ); 21 | } 22 | 23 | async clearScreen() { 24 | await this.print(ansiEscapes.eraseScreen); 25 | await this.moveCursorTo(0, 0); 26 | } 27 | 28 | async setFgColor(colorName: ColorName) { 29 | await this.print(ansiStyles[colorName].open); 30 | } 31 | 32 | async setBgColor(colorName: ColorName) { 33 | const bgColorName = `bg${colorName[0].toUpperCase()}${colorName.substr( 34 | 1 35 | )}` as keyof ansiStyles.BackgroundColor; 36 | await this.print(ansiStyles[bgColorName].open); 37 | } 38 | 39 | async beep() { 40 | await this.print(ansiEscapes.beep); 41 | } 42 | 43 | /** Translates an ANSI / VT100 key code to the DOS equivalent. */ 44 | translateKeyCode(c: string) { 45 | return ANSI_TERMINAL_KEY_CODE_MAP[c] ?? c; 46 | } 47 | } 48 | 49 | export default AnsiTerminalPlatform; 50 | -------------------------------------------------------------------------------- /qbjc/src/runtime/browser-platform.ts: -------------------------------------------------------------------------------- 1 | // Must polyfill global 'process' variable before importing ansi-escapes. 2 | self.process = self.process ?? require('process'); 3 | import ansiEscapes from 'ansi-escapes'; 4 | import {Terminal} from 'xterm'; 5 | import AnsiTerminalPlatform from './ansi-terminal-platform'; 6 | import {CompiledModule} from './compiled-code'; 7 | import Executor, {ExecutionOpts} from './executor'; 8 | // Polyfill Buffer for iconv-lite. 9 | self.Buffer = self.Buffer ?? require('buffer').Buffer; 10 | 11 | /** RuntimePlatform for the browser environment based on xterm.js. */ 12 | export class BrowserPlatform extends AnsiTerminalPlatform { 13 | constructor(private readonly terminal: Terminal) { 14 | super(); 15 | } 16 | 17 | // When running in the browser, delay is implemented as a busy loop as 18 | // setImmediate will block input processing. So disabling delay by default. 19 | defaultStmtExecutionDelayUs = 0; 20 | 21 | async loadCompiledModule(code: string) { 22 | // Drop "#!/usr/bin/env node" shebang line. 23 | const match = code.match(/^(?:#![^\n]+\n)?([\s\S]+)$/); 24 | if (!match) { 25 | throw new Error(`Empty code string`); 26 | } 27 | 28 | const fn = new Function('module', match[1]); 29 | const moduleObj = {exports: {}}; 30 | fn(moduleObj); 31 | return moduleObj.exports as CompiledModule; 32 | } 33 | 34 | async delay(delayInUs: number) { 35 | if ( 36 | typeof window === 'undefined' || 37 | typeof window.performance === 'undefined' 38 | ) { 39 | await new Promise((resolve) => 40 | setTimeout(resolve, Math.round(delayInUs / 1000)) 41 | ); 42 | } else { 43 | const t0 = performance.now(); 44 | while ((performance.now() - t0) * 1000 < delayInUs) {} 45 | } 46 | } 47 | 48 | async print(s: string) { 49 | this.terminal.write(s); 50 | } 51 | 52 | async inputLine(): Promise { 53 | const text: Array = []; 54 | let cursorIdx = 0; 55 | let {x: initialX} = await this.getCursorPosition(); 56 | const {cols} = await this.getScreenSize(); 57 | const disposeFns: Array<() => void> = []; 58 | await new Promise((resolve) => { 59 | const {dispose: removeOnDataListenerFn} = this.terminal.onData( 60 | async (chunk: string) => { 61 | switch (chunk) { 62 | case '\r': 63 | this.print('\n'); 64 | resolve(); 65 | break; 66 | case '\x7f': // backspace 67 | if (text.length > 0 && cursorIdx > 0) { 68 | text.splice(--cursorIdx, 1); 69 | this.print(ansiEscapes.cursorBackward()); 70 | this.print(ansiEscapes.cursorSavePosition); 71 | this.print(ansiEscapes.eraseEndLine); 72 | this.print(text.slice(cursorIdx).join('')); 73 | this.print(ansiEscapes.cursorRestorePosition); 74 | } 75 | break; 76 | case '\x1b[3~': // delete 77 | if (cursorIdx < text.length) { 78 | text.splice(cursorIdx, 1); 79 | this.print(ansiEscapes.cursorSavePosition); 80 | this.print(ansiEscapes.eraseEndLine); 81 | this.print(text.slice(cursorIdx).join('')); 82 | this.print(ansiEscapes.cursorRestorePosition); 83 | } 84 | break; 85 | case '\x1b[C': // right 86 | if (cursorIdx < text.length) { 87 | ++cursorIdx; 88 | this.print(chunk); 89 | } 90 | break; 91 | case '\x1b[D': // left 92 | if (cursorIdx > 0) { 93 | --cursorIdx; 94 | this.print(chunk); 95 | } 96 | break; 97 | case '\x1b[F': // end 98 | if (cursorIdx < text.length) { 99 | this.print(ansiEscapes.cursorForward(text.length - cursorIdx)); 100 | cursorIdx = text.length; 101 | } 102 | break; 103 | case '\x1b[H': // home 104 | if (cursorIdx > 0) { 105 | this.print(ansiEscapes.cursorBackward(cursorIdx)); 106 | cursorIdx = 0; 107 | } 108 | break; 109 | default: 110 | // TODO: Support multi-line line editing. 111 | if ( 112 | chunk.match(/^[\x20-\x7e]$/) && 113 | initialX + text.length < cols - 2 114 | ) { 115 | // Printable ASCII 116 | text.splice(cursorIdx++, 0, chunk); 117 | this.print(chunk); 118 | this.print(ansiEscapes.cursorSavePosition); 119 | this.print(text.slice(cursorIdx).join('')); 120 | this.print(ansiEscapes.cursorRestorePosition); 121 | } else { 122 | console.log( 123 | chunk 124 | .split('') 125 | .map((c) => c.charCodeAt(0)) 126 | .join(', ') 127 | ); 128 | } 129 | break; 130 | } 131 | } 132 | ); 133 | disposeFns.push(removeOnDataListenerFn); 134 | const checkShouldStopExecutionIntervalId = setInterval(() => { 135 | if (this.shouldStopExecution) { 136 | resolve(); 137 | } 138 | }, 20); 139 | disposeFns.push(() => clearInterval(checkShouldStopExecutionIntervalId)); 140 | }); 141 | disposeFns.forEach((fn) => fn()); 142 | return text.join(''); 143 | } 144 | 145 | async getChar(): Promise { 146 | let result: string | null = null; 147 | const {dispose} = this.terminal.onData((chunk: string) => { 148 | result = this.translateKeyCode(chunk.toString()); 149 | }); 150 | return new Promise((resolve) => { 151 | setTimeout(() => { 152 | dispose(); 153 | resolve(result); 154 | }, 20); 155 | }); 156 | } 157 | 158 | async getCursorPosition(): Promise<{x: number; y: number}> { 159 | return new Promise<{x: number; y: number}>((resolve, reject) => { 160 | const {dispose} = this.terminal.onData((chunk: string) => { 161 | dispose(); 162 | const match = chunk.toString().match(/\[(\d+)\;(\d+)R/); 163 | if (match) { 164 | const [row, col] = [match[1], match[2]]; 165 | const result = { 166 | // ANSI row & col numbers are 1-based. 167 | x: parseInt(col) - 1, 168 | y: parseInt(row) - 1, 169 | }; 170 | resolve(result); 171 | } else { 172 | reject(); 173 | } 174 | }); 175 | this.print(ansiEscapes.cursorGetPosition); 176 | }); 177 | } 178 | 179 | async getScreenSize(): Promise<{rows: number; cols: number}> { 180 | return { 181 | rows: this.terminal.rows, 182 | cols: this.terminal.cols, 183 | }; 184 | } 185 | } 186 | 187 | export class BrowserExecutor extends Executor { 188 | constructor(terminal: Terminal, opts: ExecutionOpts = {}) { 189 | super(new BrowserPlatform(terminal), opts); 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /qbjc/src/runtime/compiled-code.ts: -------------------------------------------------------------------------------- 1 | import {DataItems} from '../lib/data-item'; 2 | import {VarSymbolTable} from '../lib/symbol-table'; 3 | import {DataTypeSpec, ProcType} from '../lib/types'; 4 | import Runtime from './runtime'; 5 | 6 | /** A compiled module produced by CodeGenerator. */ 7 | export interface CompiledModule { 8 | sourceFileName?: string; 9 | localSymbols: VarSymbolTable; 10 | globalSymbols: VarSymbolTable; 11 | stmts: Array; 12 | procs: Array; 13 | } 14 | 15 | interface CompiledComponentBase { 16 | /** Location of this statement in the source code. */ 17 | loc?: [number, number]; 18 | } 19 | 20 | /** A compiled procedure (SUB or FUNCTION). */ 21 | export interface CompiledProc { 22 | type: ProcType; 23 | name: string; 24 | localSymbols: VarSymbolTable; 25 | paramSymbols: VarSymbolTable; 26 | stmts: Array; 27 | } 28 | 29 | /** A compiled statement. */ 30 | export type CompiledStmt = 31 | | CompiledLabelStmt 32 | | CompiledCodeStmt 33 | | CompiledDataStmt; 34 | 35 | /** A label in the compiled program. */ 36 | export interface CompiledLabelStmt extends CompiledComponentBase { 37 | label: string; 38 | } 39 | 40 | /** A compiled statement with executable code. */ 41 | export interface CompiledCodeStmt extends CompiledComponentBase { 42 | run(ctx: ExecutionContext): Promise; 43 | } 44 | 45 | /** A compiled DATA statement. */ 46 | export interface CompiledDataStmt extends CompiledComponentBase { 47 | data: DataItems; 48 | } 49 | 50 | /** Return value from a compiled statement. */ 51 | export type ExecutionDirective = 52 | | GotoDirective 53 | | GosubDirective 54 | | ReturnDirective 55 | | EndDirective 56 | | ExitProcDirective; 57 | 58 | export enum ExecutionDirectiveType { 59 | GOTO = 'goto', 60 | GOSUB = 'gosub', 61 | RETURN = 'return', 62 | END = 'end', 63 | EXIT_PROC = 'exitProc', 64 | } 65 | 66 | /** Jump to a label. */ 67 | export interface GotoDirective { 68 | type: ExecutionDirectiveType.GOTO; 69 | destLabel: string; 70 | } 71 | 72 | /** Push return address and jump to a label. */ 73 | export interface GosubDirective { 74 | type: ExecutionDirectiveType.GOSUB; 75 | destLabel: string; 76 | } 77 | 78 | /** Pop return address and jump to it (or another label if provided). */ 79 | export interface ReturnDirective { 80 | type: ExecutionDirectiveType.RETURN; 81 | destLabel?: string; 82 | } 83 | 84 | /** End program execution. */ 85 | export interface EndDirective { 86 | type: ExecutionDirectiveType.END; 87 | } 88 | 89 | /** End execution of current proc. */ 90 | export interface ExitProcDirective { 91 | type: ExecutionDirectiveType.EXIT_PROC; 92 | } 93 | 94 | /** A map holding variables at runtime, indexed by variable name. */ 95 | export type VarContainer = {[name: string]: any}; 96 | 97 | /** Pointer to a variable. 98 | * 99 | * The underlying variable can be referenced with ptr[0][ptr[1]]. 100 | */ 101 | export type Ptr = [T, keyof T]; 102 | 103 | /** Arguments to a procedure. */ 104 | export type ArgsContainer = {[name: string]: Ptr}; 105 | 106 | /** Compiled statement execution context. */ 107 | export interface ExecutionContext { 108 | /** Handle to the Runtime instance. */ 109 | runtime: Runtime; 110 | /** Executes a procedure (FUNCTION or SUB). */ 111 | executeProc: ( 112 | prevCtx: ExecutionContext, 113 | name: string, 114 | ...args: Array 115 | ) => Promise; 116 | /** Runtime implementation of READ. */ 117 | read: ( 118 | ...resultTypes: Array 119 | ) => Promise>; 120 | /** Runtime implementation of RESTORE. */ 121 | restore: (destLabel?: string) => void; 122 | 123 | /** Arguments to the current procedure. */ 124 | args: ArgsContainer; 125 | /** Local variables. */ 126 | localVars: VarContainer; 127 | /** Static variables. */ 128 | localStaticVars: VarContainer; 129 | /** Global variables. */ 130 | globalVars: VarContainer; 131 | /** Temp variables. */ 132 | tempVars: VarContainer; 133 | } 134 | 135 | /** Type of an argument to print(). */ 136 | export enum PrintArgType { 137 | COMMA = 'comma', 138 | SEMICOLON = 'semicolon', 139 | VALUE = 'value', 140 | } 141 | 142 | /** Argument to print(). */ 143 | export type PrintArg = 144 | | {type: PrintArgType.COMMA | PrintArgType.SEMICOLON} 145 | | ValuePrintArg; 146 | 147 | export interface ValuePrintArg { 148 | type: PrintArgType.VALUE; 149 | value: string | number; 150 | } 151 | -------------------------------------------------------------------------------- /qbjc/src/runtime/init-value.ts: -------------------------------------------------------------------------------- 1 | import {DataType, DataTypeSpec, isElementaryType} from '../lib/types'; 2 | import QbArray from './qb-array'; 3 | import QbUdt from './qb-udt'; 4 | 5 | /** Initial value for elementary data types. */ 6 | const ELEMENTARY_TYPE_INIT_VALUES = { 7 | [DataType.INTEGER]: 0, 8 | [DataType.LONG]: 0, 9 | [DataType.SINGLE]: 0.0, 10 | [DataType.DOUBLE]: 0.0, 11 | [DataType.STRING]: '', 12 | }; 13 | 14 | export default function initValue(typeSpec: DataTypeSpec) { 15 | const {type: dataType} = typeSpec; 16 | if (isElementaryType(dataType)) { 17 | return ELEMENTARY_TYPE_INIT_VALUES[dataType]; 18 | } 19 | switch (typeSpec.type) { 20 | case DataType.ARRAY: 21 | return new QbArray(typeSpec); 22 | case DataType.UDT: 23 | return new QbUdt(typeSpec); 24 | default: 25 | throw new Error(`Unknown type: ${JSON.stringify(typeSpec)}`); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /qbjc/src/runtime/node-platform.ts: -------------------------------------------------------------------------------- 1 | import ansiEscapes from 'ansi-escapes'; 2 | import {performance} from 'perf_hooks'; 3 | import requireFromString from 'require-from-string'; 4 | import AnsiTerminalPlatform from './ansi-terminal-platform'; 5 | import {CompiledModule} from './compiled-code'; 6 | import Executor, {ExecutionOpts} from './executor'; 7 | 8 | /** RuntimePlatform for Node.JS on TTY. */ 9 | export class NodePlatform extends AnsiTerminalPlatform { 10 | // When running in Node, delay is implemented using setImmediate and won't 11 | // block other functionality. So we can safely set a higher delay value for 12 | // better compatibility. 13 | defaultStmtExecutionDelayUs = 5; 14 | 15 | async loadCompiledModule(code: string) { 16 | return requireFromString(code) as CompiledModule; 17 | } 18 | 19 | async delay(delayInUs: number) { 20 | const t0 = performance.now(); 21 | while ((performance.now() - t0) * 1000 < delayInUs) { 22 | await new Promise((resolve) => setImmediate(resolve)); 23 | } 24 | } 25 | 26 | async print(s: string) { 27 | process.stdout.write(s); 28 | } 29 | 30 | async inputLine(): Promise { 31 | return new Promise((resolve) => { 32 | process.stdin.resume(); 33 | process.stdin.setRawMode(false); 34 | process.stdin.once('data', (chunk) => { 35 | process.stdin.pause(); 36 | resolve(chunk.toString()); 37 | }); 38 | }); 39 | } 40 | 41 | async getChar(): Promise { 42 | // Based on https://stackoverflow.com/a/35688423/3401268 43 | let result: string | null = null; 44 | process.stdin.resume(); 45 | process.stdin.setRawMode(true); 46 | const callbackFn = (chunk: Buffer) => { 47 | result = this.translateKeyCode(chunk.toString()); 48 | // Handle Ctrl-C 49 | if (result === String.fromCharCode(3)) { 50 | throw new Error('Received Ctrl-C, aborting'); 51 | } 52 | process.stdin.pause(); 53 | }; 54 | process.stdin.once('data', callbackFn); 55 | return new Promise((resolve) => { 56 | setTimeout(() => { 57 | if (result === null) { 58 | process.stdin.removeListener('data', callbackFn); 59 | process.stdin.pause(); 60 | } 61 | resolve(result); 62 | }, 20); 63 | }); 64 | } 65 | 66 | async getCursorPosition(): Promise<{x: number; y: number}> { 67 | // Based on https://github.com/bubkoo/get-cursor-position/blob/master/index.js 68 | process.stdin.resume(); 69 | const originalIsRaw = process.stdin.isRaw; 70 | process.stdin.setRawMode(true); 71 | return new Promise<{x: number; y: number}>((resolve, reject) => { 72 | process.stdin.once('data', (chunk) => { 73 | process.stdin.pause(); 74 | process.stdin.setRawMode(originalIsRaw); 75 | const match = chunk.toString().match(/\[(\d+)\;(\d+)R/); 76 | if (match) { 77 | const [row, col] = [match[1], match[2]]; 78 | const result = { 79 | // ANSI row & col numbers are 1-based. 80 | x: parseInt(col) - 1, 81 | y: parseInt(row) - 1, 82 | }; 83 | resolve(result); 84 | } else { 85 | reject(); 86 | } 87 | }); 88 | process.stdout.write(ansiEscapes.cursorGetPosition); 89 | }); 90 | } 91 | 92 | async getScreenSize(): Promise<{rows: number; cols: number}> { 93 | // Basd on https://stackoverflow.com/a/35688423/3401268 94 | const origPos = await this.getCursorPosition(); 95 | await this.moveCursorTo(10000, 10000); 96 | const bottomRightPos = await this.getCursorPosition(); 97 | await this.moveCursorTo(origPos.x, origPos.y); 98 | return { 99 | rows: bottomRightPos.y + 1, 100 | cols: bottomRightPos.x + 1, 101 | }; 102 | } 103 | } 104 | 105 | export class NodeExecutor extends Executor { 106 | constructor(opts: ExecutionOpts = {}) { 107 | super(new NodePlatform(), opts); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /qbjc/src/runtime/node-runtime-bundle-bootstrap.ts: -------------------------------------------------------------------------------- 1 | import {CompiledModule} from './compiled-code'; 2 | import {ExecutionOpts} from './executor'; 3 | import {NodeExecutor} from './node-platform'; 4 | 5 | /** Bundled module compiled from BASIC source code. */ 6 | declare var compiledModule: CompiledModule | undefined; 7 | 8 | /** Executes the bundled module. 9 | * 10 | * This is the main execution entrypoint to a compiled program. 11 | */ 12 | async function run(opts: ExecutionOpts = {}) { 13 | // Parse opts from environment variables. 14 | let stmtExecutionDelayUs: number | undefined; 15 | if (process.env.DELAY) { 16 | stmtExecutionDelayUs = parseInt(process.env.DELAY); 17 | if (isNaN(stmtExecutionDelayUs)) { 18 | stmtExecutionDelayUs = undefined; 19 | } 20 | } 21 | 22 | return await new NodeExecutor({ 23 | stmtExecutionDelayUs, 24 | ...opts, 25 | }).executeModule(compiledModule!); 26 | } 27 | 28 | module.exports = { 29 | ...(typeof compiledModule === 'undefined' ? {} : compiledModule), 30 | run, 31 | }; 32 | 33 | // When invoked directly as part of a compiled program, execute the bundled module. 34 | if (require.main === module) { 35 | run(); 36 | } 37 | -------------------------------------------------------------------------------- /qbjc/src/runtime/qb-array.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ArrayDimensionSpec, 3 | arraySpec, 4 | ArrayTypeSpec, 5 | SingularTypeSpec, 6 | } from '../lib/types'; 7 | import initValue from './init-value'; 8 | 9 | /** Runtime representation for QBasic arrays. */ 10 | class QbArray { 11 | constructor(arrayTypeSpec: ArrayTypeSpec) { 12 | this.init(arrayTypeSpec.elementTypeSpec, arrayTypeSpec.dimensionSpecs); 13 | } 14 | 15 | /** Initializes the array with the provided spec, discarding any previous contents. */ 16 | init( 17 | elementTypeSpec: SingularTypeSpec, 18 | dimensionSpecs: Array 19 | ) { 20 | if (dimensionSpecs.length === 0) { 21 | throw new Error('No dimension specs provided'); 22 | } 23 | this._typeSpec = arraySpec(elementTypeSpec, dimensionSpecs); 24 | 25 | this._values.length = 0; 26 | let currentDimensionArrays: Array = [this._values]; 27 | for (let i = 0; i < dimensionSpecs.length; ++i) { 28 | const [minIdx, maxIdx] = dimensionSpecs[i]; 29 | const numElements = maxIdx - minIdx + 1; 30 | const nextDimensionArrays = []; 31 | for (const dimensionArray of currentDimensionArrays) { 32 | if (i === dimensionSpecs.length - 1) { 33 | for (let j = 0; j < numElements; ++j) { 34 | dimensionArray.push(initValue(elementTypeSpec)); 35 | } 36 | } else { 37 | for (let j = 0; j < numElements; ++j) { 38 | const nextDimensionArray = new Array(); 39 | nextDimensionArrays.push(nextDimensionArray); 40 | dimensionArray.push(nextDimensionArray); 41 | } 42 | } 43 | } 44 | currentDimensionArrays = nextDimensionArrays; 45 | } 46 | } 47 | 48 | /** Translates a QBasic index into the index in the underlying array. */ 49 | getIdx(dimensionIdx: number, qbIdx: number) { 50 | const {dimensionSpecs} = this._typeSpec; 51 | if ( 52 | !Number.isFinite(dimensionIdx) || 53 | dimensionIdx < 0 || 54 | dimensionIdx >= dimensionSpecs.length 55 | ) { 56 | throw new Error( 57 | 'Invalid dimension in getIndex: ' + 58 | `expected number between 0 and ${dimensionSpecs.length - 1}, ` + 59 | `got ${dimensionIdx}` 60 | ); 61 | } 62 | const [minIdx, maxIdx] = dimensionSpecs[dimensionIdx]; 63 | if (!Number.isFinite(qbIdx) || qbIdx < minIdx || qbIdx > maxIdx) { 64 | throw new Error( 65 | `Index out of range for dimension ${dimensionIdx + 1}: ` + 66 | `expected number between ${minIdx} and ${maxIdx}, ` + 67 | `got ${qbIdx}` 68 | ); 69 | } 70 | return qbIdx - minIdx; 71 | } 72 | 73 | get values() { 74 | return this._values; 75 | } 76 | 77 | get typeSpec() { 78 | return this._typeSpec; 79 | } 80 | 81 | /** The underlying array storing the actual elements. */ 82 | private _values: Array = []; 83 | /** Current type spec. */ 84 | private _typeSpec!: ArrayTypeSpec; 85 | } 86 | 87 | export default QbArray; 88 | -------------------------------------------------------------------------------- /qbjc/src/runtime/qb-udt.ts: -------------------------------------------------------------------------------- 1 | import {isUdt, UdtTypeSpec} from '../lib/types'; 2 | import initValue from './init-value'; 3 | 4 | /** Runtime representation for QBasic user-defined types. */ 5 | class QbUdt { 6 | constructor(udtTypeSpec: UdtTypeSpec) { 7 | this._typeSpec = {...udtTypeSpec}; 8 | this.init(); 9 | } 10 | 11 | private init() { 12 | for (const fieldSpec of this._typeSpec.fieldSpecs) { 13 | this._values[fieldSpec.name] = initValue(fieldSpec.typeSpec); 14 | } 15 | } 16 | 17 | clone(): QbUdt { 18 | const newQbUdt = new QbUdt(this._typeSpec); 19 | for (const fieldSpec of this._typeSpec.fieldSpecs) { 20 | const value = this._values[fieldSpec.name]; 21 | newQbUdt._values[fieldSpec.name] = isUdt(fieldSpec.typeSpec) 22 | ? (value as QbUdt).clone() 23 | : value; 24 | } 25 | return newQbUdt; 26 | } 27 | 28 | get values() { 29 | return this._values; 30 | } 31 | 32 | get typeSpec() { 33 | return this._typeSpec; 34 | } 35 | 36 | /** The underlying array storing the actual elements. */ 37 | private _values: {[key: string]: any} = {}; 38 | /** Current type spec. */ 39 | private _typeSpec!: UdtTypeSpec; 40 | } 41 | 42 | export default QbUdt; 43 | -------------------------------------------------------------------------------- /qbjc/src/tests/compile-and-run.test.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs-extra'; 2 | import _ from 'lodash'; 3 | import path from 'path'; 4 | import stripAnsi from 'strip-ansi'; 5 | import compile from '../compile'; 6 | import Executor from '../runtime/executor'; 7 | import {NodePlatform} from '../runtime/node-platform'; 8 | const {AnsiTerminal} = require('node-ansiterminal'); 9 | const AnsiParser = require('node-ansiparser'); 10 | 11 | const TEST_SOURCE_DIR_PATH = path.join( 12 | __dirname, 13 | '..', 14 | '..', 15 | 'src', 16 | 'tests', 17 | 'testdata', 18 | 'compile-and-run' 19 | ); 20 | const TEST_FILES = fs 21 | .readdirSync(TEST_SOURCE_DIR_PATH) 22 | .filter((fileName) => fileName.endsWith('.bas')); 23 | 24 | describe('Compile and run', () => { 25 | for (const testFile of TEST_FILES) { 26 | test(testFile, () => testCompileAndRun(testFile)); 27 | } 28 | }); 29 | 30 | class NodePlatformForTest extends NodePlatform { 31 | async print(s: string) { 32 | this.stdout.push(s); 33 | } 34 | 35 | getStdout(enableAnsiTerminal: boolean) { 36 | if (enableAnsiTerminal) { 37 | const terminal = new AnsiTerminal(80, 25, 500); 38 | const parser = new AnsiParser(terminal); 39 | parser.parse(this.stdout.join('')); 40 | return terminal.toString(); 41 | } else { 42 | return stripAnsi(this.stdout.join('')); 43 | } 44 | } 45 | 46 | async inputLine(): Promise { 47 | if (this.stdin.length === 0) { 48 | throw new Error(`Input exhausted`); 49 | } 50 | return this.stdin.shift()!; 51 | } 52 | 53 | stdout: Array = []; 54 | stdin: Array = []; 55 | } 56 | 57 | interface ExpectSpec { 58 | io?: Array<{input: string} | {output: string}>; 59 | enableAnsiTerminal?: boolean; 60 | } 61 | 62 | async function testCompileAndRun(testFile: string) { 63 | const testFilePath = path.join(TEST_SOURCE_DIR_PATH, testFile); 64 | const source = await fs.readFile(testFilePath, 'utf-8'); 65 | const {code} = await compile({ 66 | source: await fs.readFile(testFilePath, 'utf-8'), 67 | sourceFileName: testFile, 68 | }); 69 | await fs.writeFile(`${testFilePath}.js`, code); 70 | 71 | const expectSpec = parseExpectSpec(source); 72 | const nodePlatformForTest = new NodePlatformForTest(); 73 | nodePlatformForTest.stdin = _.flatMap(expectSpec.io ?? [], (ioItem) => 74 | 'input' in ioItem ? [ioItem.input] : [] 75 | ); 76 | 77 | await new Executor(nodePlatformForTest, { 78 | stmtExecutionDelayUs: 0, 79 | }).executeModule(code); 80 | 81 | const expectedOutput = _.flatMap(expectSpec.io ?? [], (ioItem) => 82 | 'output' in ioItem ? [ioItem.output] : [] 83 | ); 84 | expect( 85 | nodePlatformForTest.getStdout(!!expectSpec.enableAnsiTerminal) 86 | ).toEqual(expectedOutput.join('')); 87 | } 88 | 89 | function parseExpectSpec(source: string): ExpectSpec { 90 | const expectCommentRegex = /^' EXPECT ({[\s\S]*})/m; 91 | const match = source.match(expectCommentRegex); 92 | if (!match) { 93 | return {}; 94 | } 95 | const expectSpecJson = match[1].replace(/^'\s*/gm, ''); 96 | let expectSpec = JSON.parse(expectSpecJson); 97 | return expectSpec; 98 | } 99 | -------------------------------------------------------------------------------- /qbjc/src/tests/package-test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Smoke test for verifying the published package. It runs `npm pack` and 4 | # verifies the output can be installed and imported. 5 | # 6 | 7 | TEST_SCRIPT=$(cat <<'EOF' 8 | 9 | import assert from 'assert'; 10 | import {compile} from 'qbjc'; 11 | import {NodeExecutor} from 'qbjc/node'; 12 | 13 | async function testCompileAndRun(source: string) { 14 | const {code} = await compile({ 15 | source, 16 | sourceFileName: 'test.bas', 17 | }); 18 | assert(code); 19 | await new NodeExecutor().executeModule(code); 20 | } 21 | 22 | (async () => { 23 | await testCompileAndRun('PRINT "Hello, World!"'); 24 | })(); 25 | 26 | EOF 27 | ) 28 | 29 | function TEST_CLI() { 30 | BIN=$1 31 | 32 | ( 33 | set -ex 34 | 35 | $BIN --version 36 | $BIN --help 37 | 38 | echo 'PRINT "Hello, World!"' > ./test.bas 39 | $BIN -r ./test.bas 40 | ) 41 | } 42 | 43 | cd "$(dirname "$0")/../.." 44 | SOURCE_DIR="$PWD" 45 | TEMP_DIR="$PWD/../tmp-smoke-test" 46 | 47 | 48 | cd "$SOURCE_DIR" 49 | echo "> Building package" 50 | npm pack || exit 1 51 | echo 52 | 53 | package_files=(*.tgz) 54 | if [ ${#package_files[@]} -eq 1 ]; then 55 | package_file="$SOURCE_DIR/${package_files[0]}" 56 | echo "> Found package $package_file" 57 | echo 58 | else 59 | echo "Could not identify package file" 60 | exit 1 61 | fi 62 | 63 | echo "> Installing package in temp directory $TEMP_DIR" 64 | if [ -d "$TEMP_DIR" ]; then 65 | rm -rf "$TEMP_DIR" 66 | fi 67 | mkdir -p "$TEMP_DIR" 68 | cd "$TEMP_DIR" 69 | npm init -y 70 | npm install --save ts-node typescript '@types/node' "$package_file" 71 | echo '{ "compilerOptions": { "module": "CommonJS", "esModuleInterop": true } }' > tsconfig.json 72 | echo 73 | 74 | echo "> Running test script" 75 | echo "$TEST_SCRIPT" 76 | if ./node_modules/.bin/ts-node -e "$TEST_SCRIPT"; then 77 | echo 78 | echo "> Success!" 79 | exit_code=0 80 | else 81 | exit_code=$? 82 | echo 83 | echo "> Error - script returned status ${exit_code}" 84 | fi 85 | echo 86 | 87 | echo "> Running CLI test" 88 | TEST_CLI ./node_modules/.bin/qbjc 89 | exit_code=$? 90 | if [ $exit_code -eq 0 ]; then 91 | echo 92 | echo "> Success!" 93 | else 94 | echo 95 | echo "> Error - script returned status ${exit_code}" 96 | fi 97 | echo 98 | 99 | echo "> Cleaning up" 100 | cd "$SOURCE_DIR" 101 | rm -rf "$TEMP_DIR" "$package_file" 102 | 103 | exit $exit_code 104 | -------------------------------------------------------------------------------- /qbjc/src/tests/qb-array.test.ts: -------------------------------------------------------------------------------- 1 | import {integerSpec, stringSpec, arraySpec} from '../lib/types'; 2 | import QbArray from '../runtime/qb-array'; 3 | 4 | describe('QbArray', () => { 5 | test('constructor', () => { 6 | // A1(10) 7 | const a1 = new QbArray(arraySpec(integerSpec(), [[0, 10]])); 8 | expect(a1.values).toStrictEqual(new Array(11).fill(0)); 9 | 10 | // A2(10, 10) 11 | const a2 = new QbArray( 12 | arraySpec(stringSpec(), [ 13 | [0, 10], 14 | [0, 10], 15 | ]) 16 | ); 17 | expect(a2.values).toStrictEqual(new Array(11).fill(new Array(11).fill(''))); 18 | 19 | // A3(10, 10) 20 | const a3 = new QbArray( 21 | arraySpec(integerSpec(), [ 22 | [0, 10], 23 | [0, 10], 24 | [0, 10], 25 | ]) 26 | ); 27 | expect(a3.values).toStrictEqual( 28 | new Array(11).fill(new Array(11).fill(new Array(11).fill(0))) 29 | ); 30 | 31 | // A4(100 TO 200, -3 TO 0) 32 | const a4 = new QbArray( 33 | arraySpec(stringSpec(), [ 34 | [100, 200], 35 | [-3, 0], 36 | ]) 37 | ); 38 | expect(a4.values).toStrictEqual(new Array(101).fill(new Array(4).fill(''))); 39 | }); 40 | 41 | test('getIdx', () => { 42 | // A1(10) 43 | const a1 = new QbArray(arraySpec(integerSpec(), [[0, 10]])); 44 | expect(a1.getIdx(0, 4)).toStrictEqual(4); 45 | 46 | // A2(1 TO 5, -3 TO 0) 47 | const a2 = new QbArray( 48 | arraySpec(integerSpec(), [ 49 | [1, 5], 50 | [-3, 0], 51 | ]) 52 | ); 53 | expect(a2.getIdx(0, 4)).toStrictEqual(3); 54 | expect(a2.getIdx(1, 0)).toStrictEqual(3); 55 | }); 56 | }); 57 | -------------------------------------------------------------------------------- /qbjc/src/tests/qbjc.test.ts: -------------------------------------------------------------------------------- 1 | import childProcess from 'child_process'; 2 | import fs from 'fs-extra'; 3 | import path from 'path'; 4 | 5 | const TEST_SOURCE_DIR_PATH = path.join( 6 | __dirname, 7 | '..', 8 | '..', 9 | 'src', 10 | 'tests', 11 | 'testdata', 12 | 'qbjc' 13 | ); 14 | const TEST_FILES = fs 15 | .readdirSync(TEST_SOURCE_DIR_PATH) 16 | .filter((fileName) => fileName.endsWith('.bas')); 17 | const QBJC = path.join(__dirname, '..', 'qbjc.js'); 18 | const ARGS = [ 19 | [], 20 | ['--no-bundle'], 21 | ['--minify'], 22 | ['--source-map'], 23 | ['--run'], 24 | ['--no-bundle', '--run'], 25 | ['--no-bundle', '--minify'], 26 | ['--no-bundle', '--minify', '--run'], 27 | ['--no-bundle', '--source-map'], 28 | ['--minify', '--source-map'], 29 | ['--no-bundle', '--minify', '--source-map'], 30 | ]; 31 | 32 | describe('Compile and run', () => { 33 | for (const testFile of TEST_FILES) { 34 | for (const args of ARGS) { 35 | test(`${testFile} ${args.join(' ')}`, () => 36 | testQbjcCompileAndRun(testFile, args)); 37 | } 38 | } 39 | }); 40 | 41 | async function testQbjcCompileAndRun(testFile: string, args: Array) { 42 | const testFilePath = path.resolve(path.join(TEST_SOURCE_DIR_PATH, testFile)); 43 | const proc = childProcess.fork(QBJC, [...args, testFilePath]); 44 | await new Promise((resolve) => { 45 | proc.on('exit', resolve); 46 | }); 47 | expect(proc!.exitCode).toStrictEqual(0); 48 | } 49 | -------------------------------------------------------------------------------- /qbjc/src/tests/testdata/compile-and-run/.gitignore: -------------------------------------------------------------------------------- 1 | # Intermediate debugging output generated during testing. 2 | *.ast.json 3 | *.bas.js 4 | *.bas.js.map 5 | -------------------------------------------------------------------------------- /qbjc/src/tests/testdata/compile-and-run/array.bas: -------------------------------------------------------------------------------- 1 | ' Test assignment and reference 2 | PRINT A1(5, 5) 3 | FOR i = 1 TO 10 4 | FOR j = 1 TO 10 5 | A1(i, j) = i * 10 + j 6 | NEXT j, i 7 | PRINT A1(4, 2) 8 | 9 | DIM A2(4) AS INTEGER 10 | A2(1) = 5 11 | A2(2) = 7 12 | A2(3) = 9 13 | A2(4) = -2 14 | sum = 0 15 | FOR i = LBOUND(A2) TO UBOUND(A2) 16 | sum = sum + A2(i) 17 | NEXT i 18 | PRINT sum 19 | 20 | 21 | ' Test exotic indices 22 | CONST N = 100 23 | DIM A3(1 TO N, -N TO -1) AS STRING 24 | PRINT LBOUND(A3); UBOUND(A3); LBOUND(A3, 2); UBOUND(A3, 2) 25 | FOR i = 1 TO N 26 | FOR j = -N TO -1 27 | A3(i, j) = STR$(i) + " * 10 + " + STR$(j) + " = " + STR$(i * 10 + j) 28 | NEXT j, i 29 | PRINT A3(42, -42) 30 | PRINT A3(N, -N) 31 | 32 | 33 | ' Pass array element by reference 34 | DIM A4(3) AS STRING, A5(3, 3) AS STRING 35 | FOR i = 1 TO 3 36 | f1 A4(i), i 37 | f1 A5(i, i), i 38 | NEXT i 39 | SUB f1 (s AS STRING, i) 40 | s = STR$(i * 100 + i * 10 + i) 41 | END SUB 42 | PRINT A4(3) 43 | PRINT A5(2, 2) 44 | 45 | 46 | ' EXPECT { 47 | ' "io": [ 48 | ' {"output": " 0 \n"}, 49 | ' {"output": " 42 \n"}, 50 | ' 51 | ' {"output": " 19 \n"}, 52 | ' 53 | ' {"output": " 1 100 -100 -1 \n"}, 54 | ' {"output": " 42 * 10 + -42 = 378\n"}, 55 | ' {"output": " 100 * 10 + -100 = 900\n"}, 56 | ' 57 | ' {"output": " 333\n"}, 58 | ' {"output": " 222\n"} 59 | ' ] 60 | ' } 61 | -------------------------------------------------------------------------------- /qbjc/src/tests/testdata/compile-and-run/builtins.bas: -------------------------------------------------------------------------------- 1 | ' Example from Microsoft QuickBASIC BASIC: Language Reference 2 | FOR i = 1 TO 2 3 | INPUT "Binary number = ",Binary$ 'Input binary number as 4 | 'string. 5 | Length = LEN(Binary$) 'Get length of string. 6 | Decimal = 0 7 | 8 | FOR K = 1 TO Length 9 | 'Get individual digits from string, from left to right. 10 | Digit$ = MID$(Binary$,K,1) 11 | 'Test for valid binary digit. 12 | IF Digit$="0" OR Digit$="1" THEN 13 | 'Convert digit characters to numbers. 14 | Decimal = 2*Decimal + VAL(Digit$) 15 | ELSE 16 | PRINT "Error--invalid binary digit: ";Digit$ 17 | EXIT FOR 18 | END IF 19 | NEXT 20 | PRINT "Decimal number ="; Decimal 21 | NEXT i 22 | 23 | ' Example from Microsoft QuickBASIC BASIC: Language Reference 24 | FOR i = 1 TO 3 25 | ' Get a name. 26 | DO 27 | INPUT "Enter name: ", Nm$ 28 | LOOP UNTIL LEN(Nm$)>=3 29 | 30 | ' Convert lowercase letters to uppercase. 31 | Nm$ = UCASE$(Nm$) 32 | 33 | ' Look for MS., MRS., or MR. to set Sex$. 34 | IF INSTR(Nm$,"MS.") > 0 OR INSTR(Nm$,"MRS.") > 0 THEN 35 | Sex$ = "F" 36 | ELSEIF INSTR(Nm$,"MR.") > 0 THEN 37 | Sex$ = "M" 38 | ELSE 39 | ' Can't deduce sex, so query user. 40 | DO 41 | INPUT "Enter sex (M/F): ", Sex$ 42 | Sex$ = UCASE$(Sex$) 43 | LOOP WHILE Sex$ <> "M" AND Sex$ <> "F" 44 | END IF 45 | 46 | PRINT "Sex$ = "; Sex$ 47 | NEXT 48 | 49 | ' Test built-in SUBs. 50 | RANDOMIZE TIMER 51 | 52 | ' EXPECT { 53 | ' "io": [ 54 | ' {"output": "Binary number = "}, 55 | ' {"input": "10110"}, 56 | ' {"output": "Decimal number = 22 \n"}, 57 | ' 58 | ' {"output": "Binary number = "}, 59 | ' {"input": "10001"}, 60 | ' {"output": "Decimal number = 17 \n"}, 61 | ' 62 | ' {"output": "Enter name: "}, 63 | ' {"input": "Elspeth Brandtkeep"}, 64 | ' {"output": "Enter sex (M/F): "}, 65 | ' {"input": "x"}, 66 | ' {"output": "Enter sex (M/F): "}, 67 | ' {"input": "F"}, 68 | ' {"output": "Sex$ = F\n"}, 69 | ' 70 | ' {"output": "Enter name: "}, 71 | ' {"input": "Mr. Bill Gates"}, 72 | ' {"output": "Sex$ = M\n"}, 73 | ' 74 | ' {"output": "Enter name: "}, 75 | ' {"input": "Mrs. Melinda Gates"}, 76 | ' {"output": "Sex$ = F\n"} 77 | ' ] 78 | ' } 79 | 80 | -------------------------------------------------------------------------------- /qbjc/src/tests/testdata/compile-and-run/case.bas: -------------------------------------------------------------------------------- 1 | ' Tests for case sensitivity. 2 | 3 | DIM x AS INTEGER 4 | x = 5 5 | PRINT X 6 | F1 X 7 | 8 | sub f1(t as integer) 9 | shared X 10 | print x 11 | call F2(T) 12 | PRINT T 13 | end sub 14 | 15 | sub f2(r as integer) 16 | R = 42 17 | PRINT r 18 | END SUB 19 | 20 | 21 | TYPE Person 22 | Name AS STRING 23 | END TYPE 24 | 25 | dim p as person 26 | P.name = "Bill Gates" 27 | print p.NAME 28 | 29 | gosub F3 30 | f3_end: 31 | end 32 | 33 | f3: 34 | PRINT P.name 35 | RETURN F3_END 36 | 37 | 38 | ' EXPECT { 39 | ' "io": [ 40 | ' {"output": " 5 \n"}, 41 | ' {"output": " 5 \n"}, 42 | ' {"output": " 42 \n"}, 43 | ' {"output": " 42 \n"}, 44 | ' {"output": "Bill Gates\n"}, 45 | ' {"output": "Bill Gates\n"} 46 | ' ] 47 | ' } 48 | -------------------------------------------------------------------------------- /qbjc/src/tests/testdata/compile-and-run/data.bas: -------------------------------------------------------------------------------- 1 | READ a, b, c$, d$ 2 | PRINT a; b; c$; d$ 3 | 4 | RESTORE label2 5 | READ e$ 6 | PRINT e$ 7 | 8 | SUB s1 9 | RESTORE 10 | READ a, b$, c$, d$ 11 | PRINT a; b$; c$; d$ 12 | END SUB 13 | s1 14 | 15 | END 16 | 17 | label1: 18 | DATA 42,,hello 19 | label2: 20 | DATA "hello world" 21 | 22 | 23 | ' EXPECT { 24 | ' "io": [ 25 | ' {"output": " 42 0 hellohello world\n"}, 26 | ' {"output": "hello world\n"}, 27 | ' {"output": " 42 hellohello world\n"} 28 | ' ] 29 | ' } -------------------------------------------------------------------------------- /qbjc/src/tests/testdata/compile-and-run/def-fn.bas: -------------------------------------------------------------------------------- 1 | DEF FnDouble (x) = x + x 2 | DEF FnQuadruple (x) = FnDouble(FnDouble(x)) 3 | 4 | PRINT fnquadruple(7) 5 | 6 | ' EXPECT { 7 | ' "io": [ 8 | ' {"output": " 28 \n"} 9 | ' ] 10 | ' } 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /qbjc/src/tests/testdata/compile-and-run/deftype.bas: -------------------------------------------------------------------------------- 1 | DEFSTR S-U, C 2 | DEFINT A-B 3 | 4 | s1 = "hello" 5 | FUNCTION u1(c) 6 | u1 = c + "world" 7 | END FUNCTION 8 | 9 | PRINT s1; u1(", "); a1 10 | 11 | 12 | ' EXPECT { 13 | ' "io": [ 14 | ' {"output": "hello, world 0 \n"} 15 | ' ] 16 | ' } 17 | 18 | -------------------------------------------------------------------------------- /qbjc/src/tests/testdata/compile-and-run/for-loop.bas: -------------------------------------------------------------------------------- 1 | FOR i = 1 TO 3 2 | PRINT i 3 | NEXT 4 | 5 | FOR i = 1 TO 1 6 | PRINT i 7 | NEXT i 8 | 9 | FOR i = 1 TO 0 10 | PRINT i 11 | NEXT i 12 | 13 | FOR i = 1 TO 3 14 | PRINT i 15 | FOR j = i * 10 + 1 TO i * 10 + 3 16 | PRINT j 17 | NEXT j, i 18 | 19 | FOR i = 1 TO 3 20 | PRINT i 21 | FOR j = i * 10 + 1 TO i * 10 + 3 22 | PRINT j 23 | NEXT 24 | NEXT 25 | 26 | t = 1 27 | s = 1 28 | FOR i = 3 TO t STEP 0 - s 29 | PRINT i 30 | t = 100 ' This should not affect the loop 31 | s = 5 ' This should not affect the loop 32 | NEXT 33 | 34 | FOR i = 1 TO 3 35 | FOR j = i * 10 + 1 TO i * 10 + 3 36 | PRINT j 37 | EXIT FOR 38 | NEXT 39 | PRINT i 40 | NEXT 41 | 42 | 43 | ' EXPECT { 44 | ' "io": [ 45 | ' {"output": " 1 \n"}, 46 | ' {"output": " 2 \n"}, 47 | ' {"output": " 3 \n"}, 48 | ' 49 | ' {"output": " 1 \n"}, 50 | ' 51 | ' {"output": " 1 \n"}, 52 | ' {"output": " 11 \n"}, 53 | ' {"output": " 12 \n"}, 54 | ' {"output": " 13 \n"}, 55 | ' {"output": " 2 \n"}, 56 | ' {"output": " 21 \n"}, 57 | ' {"output": " 22 \n"}, 58 | ' {"output": " 23 \n"}, 59 | ' {"output": " 3 \n"}, 60 | ' {"output": " 31 \n"}, 61 | ' {"output": " 32 \n"}, 62 | ' {"output": " 33 \n"}, 63 | ' 64 | ' {"output": " 1 \n"}, 65 | ' {"output": " 11 \n"}, 66 | ' {"output": " 12 \n"}, 67 | ' {"output": " 13 \n"}, 68 | ' {"output": " 2 \n"}, 69 | ' {"output": " 21 \n"}, 70 | ' {"output": " 22 \n"}, 71 | ' {"output": " 23 \n"}, 72 | ' {"output": " 3 \n"}, 73 | ' {"output": " 31 \n"}, 74 | ' {"output": " 32 \n"}, 75 | ' {"output": " 33 \n"}, 76 | ' 77 | ' {"output": " 3 \n"}, 78 | ' {"output": " 2 \n"}, 79 | ' {"output": " 1 \n"}, 80 | ' 81 | ' {"output": " 11 \n"}, 82 | ' {"output": " 1 \n"}, 83 | ' {"output": " 21 \n"}, 84 | ' {"output": " 2 \n"}, 85 | ' {"output": " 31 \n"}, 86 | ' {"output": " 3 \n"} 87 | ' ] 88 | ' } 89 | 90 | -------------------------------------------------------------------------------- /qbjc/src/tests/testdata/compile-and-run/function.bas: -------------------------------------------------------------------------------- 1 | ' Function without args. 2 | PRINT f1 3 | PRINT f1() 4 | FUNCTION f1 5 | PRINT "answer ="; 6 | f1 = 42 7 | END FUNCTION 8 | 9 | ' Pass by reference and by value. 10 | x = 100 11 | PRINT f2(x, 42); 12 | PRINT x 13 | FUNCTION f2(a, b) 14 | a = a + b 15 | f2 = a 16 | END FUNCTION 17 | 18 | ' Pass by reference through multiple function calls. 19 | x = 3 20 | PRINT "result ="; f3(x) 21 | FUNCTION f3 (a) 22 | IF a > 0 THEN 23 | a = a - 1 24 | PRINT f3(a) 25 | END IF 26 | f3 = a 27 | END FUNCTION 28 | 29 | ' Pass by value with parentheses. 30 | FUNCTION f4(a) 31 | a = 0 32 | f4 = a 33 | END FUNCTION 34 | x = 42 35 | PRINT f4(x); 36 | PRINT x 37 | y = 42 38 | PRINT f4((y)); 39 | PRINT y 40 | 41 | ' Explicit type declarations. 42 | FUNCTION f5$(x AS STRING) 43 | f5$ = x + x 44 | END FUNCTION 45 | PRINT f5$("aaa") 46 | 47 | ' Static vars. 48 | FUNCTION f6 49 | x = x + 1 50 | f6 = x 51 | END FUNCTION 52 | FUNCTION f7 STATIC 53 | x = x + 1 54 | f7 = x 55 | END FUNCTION 56 | PRINT f6; f6; f6 57 | PRINT f7; f7; f7 58 | 59 | ' Exit inside function. 60 | PRINT f8$ 61 | FUNCTION f8$ 62 | f8$ = "hi" 63 | EXIT FUNCTION 64 | f8$ = "hello" 65 | END FUNCTION 66 | 67 | ' End inside function. 68 | PRINT f9 69 | FUNCTION f9 70 | END 71 | END FUNCTION 72 | 73 | ' EXPECT { 74 | ' "io": [ 75 | ' {"output": "answer = 42 \n"}, 76 | ' {"output": "answer = 42 \n"}, 77 | ' {"output": " 142 142 \n"}, 78 | ' 79 | ' {"output": " 0 \n"}, 80 | ' {"output": " 0 \n"}, 81 | ' {"output": " 0 \n"}, 82 | ' {"output": "result = 0 \n"}, 83 | ' 84 | ' {"output": " 0 0 \n"}, 85 | ' {"output": " 0 42 \n"}, 86 | ' 87 | ' {"output": "aaaaaa\n"}, 88 | ' 89 | ' {"output": " 1 1 1 \n"}, 90 | ' {"output": " 1 2 3 \n"}, 91 | ' 92 | ' {"output": "hi\n"} 93 | ' ] 94 | ' } 95 | -------------------------------------------------------------------------------- /qbjc/src/tests/testdata/compile-and-run/gosub.bas: -------------------------------------------------------------------------------- 1 | Test1: 2 | PRINT "Hello"; 3 | GOSUB PrintSep 4 | PRINT "world" 5 | GOTO Test2 6 | 7 | PrintSep: 8 | PRINT ", "; 9 | RETURN 10 | 11 | Test2: 12 | n = 3 13 | GOSUB CountDown 14 | GOTO Test3 15 | 16 | CountDown: 17 | PRINT n 18 | IF n > 0 THEN 19 | n = n - 1 20 | GOSUB CountDown 21 | END IF 22 | RETURN 23 | 24 | Test3: 25 | ' Example from Microsoft QuickBASIC BASIC: Language Reference 26 | PRINT "in module-level code" 27 | GOSUB Sub1 28 | PRINT "this line in main routine should be skipped" 29 | Label1: 30 | PRINT "back in module-level code" 31 | END 32 | 33 | Sub1: 34 | PRINT "in subroutine one" 35 | GOSUB Sub2 36 | PRINT "this line in subroutine one should be skipped" 37 | Label2: 38 | PRINT "back in subroutine one" 39 | RETURN Label1 40 | 41 | Sub2: 42 | PRINT "in subroutine two" 43 | RETURN Label2 44 | 45 | ' EXPECT { 46 | ' "io": [ 47 | ' {"output": "Hello, world\n"}, 48 | ' 49 | ' {"output": " 3 \n"}, 50 | ' {"output": " 2 \n"}, 51 | ' {"output": " 1 \n"}, 52 | ' {"output": " 0 \n"}, 53 | ' 54 | ' {"output": "in module-level code\n"}, 55 | ' {"output": "in subroutine one\n"}, 56 | ' {"output": "in subroutine two\n"}, 57 | ' {"output": "back in subroutine one\n"}, 58 | ' {"output": "back in module-level code\n"} 59 | ' ] 60 | ' } 61 | 62 | -------------------------------------------------------------------------------- /qbjc/src/tests/testdata/compile-and-run/hello-world.bas: -------------------------------------------------------------------------------- 1 | REM This is a hello world program 2 | PRINT "Hello, world!" 3 | 4 | ' EXPECT { 5 | ' "io": [{"output": "Hello, world!\n"}] 6 | ' } 7 | -------------------------------------------------------------------------------- /qbjc/src/tests/testdata/compile-and-run/if-stmt.bas: -------------------------------------------------------------------------------- 1 | x = 1 2 | start: 3 | IF x < 3 THEN 4 | PRINT x; "< 3" 5 | ELSEIF x = 3 THEN 6 | PRINT x; "= 3" 7 | ELSE 8 | PRINT x; "> 3" 9 | END IF 10 | x = x + 1 11 | IF x <= 5 THEN GOTO start 12 | 13 | ' Multiple statements in single line IF. 14 | IF x >= 5 THEN PRINT x; : PRINT x; : PRINT x 15 | PRINT x 16 | 17 | ' EXPECT { 18 | ' "io": [ 19 | ' {"output": " 1 < 3\n"}, 20 | ' {"output": " 2 < 3\n"}, 21 | ' {"output": " 3 = 3\n"}, 22 | ' {"output": " 4 > 3\n"}, 23 | ' {"output": " 5 > 3\n"}, 24 | ' {"output": " 6 6 6 \n"}, 25 | ' {"output": " 6 \n"} 26 | ' ] 27 | ' } 28 | -------------------------------------------------------------------------------- /qbjc/src/tests/testdata/compile-and-run/input.bas: -------------------------------------------------------------------------------- 1 | INPUT a$ 2 | PRINT a$ 3 | 4 | INPUT "a$,b$"; a$, b$ 5 | PRINT a$; ", "; b$ 6 | 7 | INPUT "a,b,c ", a, b, c 8 | PRINT a; ","; b; ","; c 9 | 10 | LINE INPUT a$ 11 | PRINT a$ 12 | LINE INPUT "Prompt: "; b$ 13 | PRINT b$ 14 | 15 | ' EXPECT { 16 | ' "io": [ 17 | ' {"output": "? "}, 18 | ' {"input": "foo"}, 19 | ' {"output": "foo\n"}, 20 | ' 21 | ' {"output": "a$,b$? "}, 22 | ' {"input": "\"hello, world\",test\"a"}, 23 | ' {"output": "hello, world, test\"a\n"}, 24 | ' 25 | ' {"output": "a,b,c "}, 26 | ' {"input": "1,500,-30"}, 27 | ' {"output": " 1 , 500 ,-30 \n"}, 28 | ' 29 | ' {"input": "hello, world"}, 30 | ' {"output": "hello, world\n"}, 31 | ' {"output": "Prompt: "}, 32 | ' {"input": "hello, world"}, 33 | ' {"output": "hello, world\n"} 34 | ' ] 35 | ' } 36 | -------------------------------------------------------------------------------- /qbjc/src/tests/testdata/compile-and-run/loop.bas: -------------------------------------------------------------------------------- 1 | x = 1 2 | WHILE x <= 5 3 | PRINT x 4 | x = x + 1 5 | WEND 6 | 7 | x = 11 8 | DO WHILE x <= 15 9 | PRINT x 10 | x = x + 1 11 | LOOP 12 | 13 | x = 21 14 | DO UNTIL x > 25 15 | PRINT x 16 | x = x + 1 17 | LOOP 18 | 19 | x = 31 20 | DO 21 | PRINT x 22 | x = x + 1 23 | LOOP WHILE x <= 35 24 | 25 | x = 41 26 | DO 27 | PRINT x 28 | x = x + 1 29 | LOOP UNTIL x > 45 30 | 31 | x = 51 32 | DO 33 | PRINT x 34 | IF x >= 55 THEN EXIT DO 35 | x = x + 1 36 | LOOP 37 | 38 | x = 61 39 | DO 40 | PRINT x 41 | IF x >= 62 THEN EXIT DO 42 | x = x + 1 43 | LOOP UNTIL x > 65 44 | 45 | ' EXPECT { 46 | ' "io": [ 47 | ' {"output": " 1 \n"}, 48 | ' {"output": " 2 \n"}, 49 | ' {"output": " 3 \n"}, 50 | ' {"output": " 4 \n"}, 51 | ' {"output": " 5 \n"}, 52 | ' 53 | ' {"output": " 11 \n"}, 54 | ' {"output": " 12 \n"}, 55 | ' {"output": " 13 \n"}, 56 | ' {"output": " 14 \n"}, 57 | ' {"output": " 15 \n"}, 58 | ' 59 | ' {"output": " 21 \n"}, 60 | ' {"output": " 22 \n"}, 61 | ' {"output": " 23 \n"}, 62 | ' {"output": " 24 \n"}, 63 | ' {"output": " 25 \n"}, 64 | ' 65 | ' {"output": " 31 \n"}, 66 | ' {"output": " 32 \n"}, 67 | ' {"output": " 33 \n"}, 68 | ' {"output": " 34 \n"}, 69 | ' {"output": " 35 \n"}, 70 | ' 71 | ' {"output": " 41 \n"}, 72 | ' {"output": " 42 \n"}, 73 | ' {"output": " 43 \n"}, 74 | ' {"output": " 44 \n"}, 75 | ' {"output": " 45 \n"}, 76 | ' 77 | ' {"output": " 51 \n"}, 78 | ' {"output": " 52 \n"}, 79 | ' {"output": " 53 \n"}, 80 | ' {"output": " 54 \n"}, 81 | ' {"output": " 55 \n"}, 82 | ' 83 | ' {"output": " 61 \n"}, 84 | ' {"output": " 62 \n"} 85 | ' ] 86 | ' } 87 | 88 | -------------------------------------------------------------------------------- /qbjc/src/tests/testdata/compile-and-run/pascals-triangle.bas: -------------------------------------------------------------------------------- 1 | CLS 2 | ' Example from Microsoft QuickBASIC BASIC: Language Reference 3 | 4 | 'Print the first MAXCOL columns of Pascal's Triangle, in which 5 | 'each number is the sum of the number immediately above it 6 | 'and the number immediately below it in the preceding column. 7 | 8 | CONST MAXCOL=11 9 | DIM A(MAXCOL,MAXCOL) 10 | FOR M = 1 TO MAXCOL 11 | A(M,1) = 1 : A(M,M) = 1 'Top and bottom of each column is 1. 12 | NEXT 13 | 14 | FOR M = 3 TO MAXCOL 15 | FOR N = 2 TO M-1 16 | A(M,N) = A(M-1,N-1) + A(M-1,N) 17 | NEXT 18 | NEXT 19 | Startrow = 13 'Go to the middle of the screen. 20 | FOR M = 1 TO MAXCOL 21 | Col = 6 * M 22 | Row = Startrow 23 | FOR N = 1 TO M 24 | LOCATE Row,Col : PRINT A(M,N) 25 | Row = Row + 2 'Go down 2 rows to print next number. 26 | NEXT 27 | PRINT 28 | Startrow = Startrow - 1 'Next column starts 1 row above 29 | NEXT 'preceding column. 30 | 31 | ' EXPECT { 32 | ' "io": [ 33 | ' {"output": "\n"}, 34 | ' {"output": "\n"}, 35 | ' {"output": " 1 \n"}, 36 | ' {"output": " 1 \n"}, 37 | ' {"output": " 1 10 \n"}, 38 | ' {"output": " 1 9 \n"}, 39 | ' {"output": " 1 8 45 \n"}, 40 | ' {"output": " 1 7 36 \n"}, 41 | ' {"output": " 1 6 28 120 \n"}, 42 | ' {"output": " 1 5 21 84 \n"}, 43 | ' {"output": " 1 4 15 56 210 \n"}, 44 | ' {"output": " 1 3 10 35 126 \n"}, 45 | ' {"output": " 1 2 6 20 70 252 \n"}, 46 | ' {"output": " 1 3 10 35 126 \n"}, 47 | ' {"output": " 1 4 15 56 210 \n"}, 48 | ' {"output": " 1 5 21 84 \n"}, 49 | ' {"output": " 1 6 28 120 \n"}, 50 | ' {"output": " 1 7 36 \n"}, 51 | ' {"output": " 1 8 45 \n"}, 52 | ' {"output": " 1 9 \n"}, 53 | ' {"output": " 1 10 \n"}, 54 | ' {"output": " 1 \n"}, 55 | ' {"output": " 1 \n"}, 56 | ' {"output": "\n"}, 57 | ' {"output": "\n"} 58 | ' ], 59 | ' "enableAnsiTerminal": true 60 | ' } 61 | -------------------------------------------------------------------------------- /qbjc/src/tests/testdata/compile-and-run/print-numbers.bas: -------------------------------------------------------------------------------- 1 | INPUT "How many numbers to print"; n 2 | FOR i = 1 TO n 3 | PRINT i; 4 | IF i MOD 10 = 0 AND i <> n THEN PRINT 5 | NEXT 6 | PRINT 7 | 8 | ' EXPECT { 9 | ' "io": [ 10 | ' {"output": "How many numbers to print? "}, 11 | ' {"input": "100"}, 12 | ' {"output": " 1 2 3 4 5 6 7 8 9 10 \n"}, 13 | ' {"output": " 11 12 13 14 15 16 17 18 19 20 \n"}, 14 | ' {"output": " 21 22 23 24 25 26 27 28 29 30 \n"}, 15 | ' {"output": " 31 32 33 34 35 36 37 38 39 40 \n"}, 16 | ' {"output": " 41 42 43 44 45 46 47 48 49 50 \n"}, 17 | ' {"output": " 51 52 53 54 55 56 57 58 59 60 \n"}, 18 | ' {"output": " 61 62 63 64 65 66 67 68 69 70 \n"}, 19 | ' {"output": " 71 72 73 74 75 76 77 78 79 80 \n"}, 20 | ' {"output": " 81 82 83 84 85 86 87 88 89 90 \n"}, 21 | ' {"output": " 91 92 93 94 95 96 97 98 99 100 \n"} 22 | ' ] 23 | ' } -------------------------------------------------------------------------------- /qbjc/src/tests/testdata/compile-and-run/print.bas: -------------------------------------------------------------------------------- 1 | PRINT "Hello", "world" 2 | PRINT "Hello", "world", 3 | PRINT 4 | PRINT "Hello"; "world" 5 | PRINT "Hello"; "world"; 6 | PRINT 7 | PRINT ,,, 8 | PRINT ,,,"hi" 9 | PRINT 1; "+"; 2; 10 | 11 | PRINT USING "[###]"; 3.5 12 | PRINT USING "[###.##]"; 0.789 13 | PRINT USING "& ### &"; "foo"; 1000; "bar" 14 | PRINT USING "$$###.##";456.78 15 | 16 | ' Disambiguating without separators 17 | PRINT 3 ("hello") f1 ("hello") f1("hello") f2 ("hello", 3) 18 | 19 | FUNCTION f1(s AS STRING) 20 | f1 = LEN(s) 21 | END FUNCTION 22 | 23 | FUNCTION f2(s AS STRING, n AS INTEGER) 24 | f2 = LEN(s) * n 25 | END FUNCTION 26 | 27 | ' EXPECT { 28 | ' "io": [ 29 | ' {"output": "Hello world\n"}, 30 | ' {"output": "Hello world "}, 31 | ' {"output": "\n"}, 32 | ' {"output": "Helloworld\n"}, 33 | ' {"output": "Helloworld"}, 34 | ' {"output": "\n"}, 35 | ' {"output": " "}, 36 | ' {"output": " hi\n"}, 37 | ' {"output": " 1 + 2 "}, 38 | ' 39 | ' {"output": "[ 4]\n"}, 40 | ' {"output": "[ 0.79]\n"}, 41 | ' {"output": "foo 1000 bar\n"}, 42 | ' {"output": " $456.78\n"}, 43 | ' 44 | ' {"output": " 3 hello 5 5 15 \n"} 45 | ' ] 46 | ' } 47 | -------------------------------------------------------------------------------- /qbjc/src/tests/testdata/compile-and-run/rounding.bas: -------------------------------------------------------------------------------- 1 | DIM n as INTEGER 2 | FOR i = -5 TO 0 3 | n = i - 1 / 2 4 | PRINT n 5 | NEXT 6 | FOR i = 0 TO 5 7 | n = i + 1 / 2 8 | PRINT n 9 | NEXT 10 | 11 | ' EXPECT { 12 | ' "io": [ 13 | ' {"output": "-6 \n"}, 14 | ' {"output": "-4 \n"}, 15 | ' {"output": "-4 \n"}, 16 | ' {"output": "-2 \n"}, 17 | ' {"output": "-2 \n"}, 18 | ' {"output": " 0 \n"}, 19 | ' {"output": " 0 \n"}, 20 | ' {"output": " 2 \n"}, 21 | ' {"output": " 2 \n"}, 22 | ' {"output": " 4 \n"}, 23 | ' {"output": " 4 \n"}, 24 | ' {"output": " 6 \n"} 25 | ' ] 26 | ' } 27 | -------------------------------------------------------------------------------- /qbjc/src/tests/testdata/compile-and-run/select-stmt.bas: -------------------------------------------------------------------------------- 1 | FOR n = 0 TO 4 2 | SELECT CASE n 3 | CASE 0 4 | PRINT "ZERO" 5 | CASE 1, 2 6 | PRINT "ONE OR TWO" 7 | CASE ELSE 8 | PRINT "ELSE" 9 | END SELECT 10 | NEXT 11 | 12 | ' Example from Microsoft QuickBASIC BASIC: Language Reference 13 | FOR i = 1 TO 6 14 | 15 | INPUT "Enter acceptable level of risk (1-10): ", Total 16 | SELECT CASE Total 17 | 18 | CASE IS >= 10 19 | PRINT "Maximum risk and potential return" 20 | PRINT "Choose stock investment plan" 21 | 22 | CASE 6 TO 9 23 | PRINT "High risk and potential return" 24 | PRINT "Choose corporate bonds" 25 | 26 | CASE 2 TO 5 27 | PRINT "Moderate risk and return" 28 | PRINT "Choose mutual fund" 29 | 30 | CASE 1 31 | PRINT "No risk, low return" 32 | PRINT "Choose IRA" 33 | 34 | CASE ELSE 35 | PRINT "RESPONSE OUT OF RANGE" 36 | 37 | END SELECT 38 | 39 | NEXT 40 | 41 | ' EXPECT { 42 | ' "io": [ 43 | ' {"output": "ZERO\n"}, 44 | ' {"output": "ONE OR TWO\n"}, 45 | ' {"output": "ONE OR TWO\n"}, 46 | ' {"output": "ELSE\n"}, 47 | ' {"output": "ELSE\n"}, 48 | ' 49 | ' {"output": "Enter acceptable level of risk (1-10): "}, 50 | ' {"input": "10"}, 51 | ' {"output": "Maximum risk and potential return\n"}, 52 | ' {"output": "Choose stock investment plan\n"}, 53 | ' 54 | ' {"output": "Enter acceptable level of risk (1-10): "}, 55 | ' {"input": "0"}, 56 | ' {"output": "RESPONSE OUT OF RANGE\n"}, 57 | ' 58 | ' {"output": "Enter acceptable level of risk (1-10): "}, 59 | ' {"input": "1"}, 60 | ' {"output": "No risk, low return\n"}, 61 | ' {"output": "Choose IRA\n"}, 62 | ' 63 | ' {"output": "Enter acceptable level of risk (1-10): "}, 64 | ' {"input": "2"}, 65 | ' {"output": "Moderate risk and return\n"}, 66 | ' {"output": "Choose mutual fund\n"}, 67 | ' 68 | ' {"output": "Enter acceptable level of risk (1-10): "}, 69 | ' {"input": "3"}, 70 | ' {"output": "Moderate risk and return\n"}, 71 | ' {"output": "Choose mutual fund\n"}, 72 | ' 73 | ' {"output": "Enter acceptable level of risk (1-10): "}, 74 | ' {"input": "12"}, 75 | ' {"output": "Maximum risk and potential return\n"}, 76 | ' {"output": "Choose stock investment plan\n"} 77 | ' ] 78 | ' } 79 | -------------------------------------------------------------------------------- /qbjc/src/tests/testdata/compile-and-run/shell-sort.bas: -------------------------------------------------------------------------------- 1 | ' Example from Microsoft QuickBASIC BASIC: Language Reference 2 | ' 3 | ' Sort the word list using a Shell sort. 4 | SUB ShellSort (Array$(), Num%) STATIC 5 | Span% = Num% \ 2 6 | DO WHILE Span% > 0 7 | FOR I% = Span% TO Num% - 1 8 | 9 | J% = I% - Span% + 1 10 | FOR J% = (I% - Span% + 1) TO 1 STEP -Span% 11 | 12 | IF Array$(J%) <= Array$(J% + Span%) THEN EXIT FOR 13 | ' Swap array elements that are out of order. 14 | SWAP Array$(J%), Array$(J% + Span%) 15 | NEXT J% 16 | 17 | NEXT I% 18 | Span% = Span% \ 2 19 | LOOP 20 | END SUB 21 | 22 | 23 | CONST N = 9 24 | DIM A1(N) AS STRING 25 | FOR i = 1 TO N 26 | LINE INPUT A1(i) 27 | NEXT 28 | CALL ShellSort(A1(), N) 29 | FOR i = 1 TO N 30 | PRINT A1(i) 31 | NEXT 32 | 33 | ' EXPECT { 34 | ' "io": [ 35 | ' {"input": "yz"}, 36 | ' {"input": "jkl"}, 37 | ' {"input": "vwx"}, 38 | ' {"input": "mno"}, 39 | ' {"input": "pqr"}, 40 | ' {"input": "abc"}, 41 | ' {"input": "stu"}, 42 | ' {"input": "ghi"}, 43 | ' {"input": "def"}, 44 | ' 45 | ' {"output": "abc\n"}, 46 | ' {"output": "def\n"}, 47 | ' {"output": "ghi\n"}, 48 | ' {"output": "jkl\n"}, 49 | ' {"output": "mno\n"}, 50 | ' {"output": "pqr\n"}, 51 | ' {"output": "stu\n"}, 52 | ' {"output": "vwx\n"}, 53 | ' {"output": "yz\n"} 54 | ' ] 55 | ' } -------------------------------------------------------------------------------- /qbjc/src/tests/testdata/compile-and-run/strtonum.bas: -------------------------------------------------------------------------------- 1 | ' Example from QuickBASIC EXAMPLES folder. 2 | 3 | DECLARE FUNCTION Filter$ (Txt$, FilterString$) 4 | 5 | ' Input a line: 6 | LINE INPUT "Enter a number with commas: ", A$ 7 | 8 | ' Look for only valid numeric characters (0123456789.-) in the 9 | ' input string: 10 | CleanNum$ = Filter$(A$, "0123456789.-") 11 | 12 | ' Convert the string to a number: 13 | PRINT "The number's value = "; VAL(CleanNum$) 14 | END 15 | ' 16 | ' ========================== FILTER ========================== 17 | ' Takes unwanted characters out of a string by 18 | ' comparing them with a filter string containing 19 | ' only acceptable numeric characters 20 | ' ============================================================ 21 | ' 22 | FUNCTION Filter$ (Txt$, FilterString$) STATIC 23 | Temp$ = "" 24 | TxtLength = LEN(Txt$) 25 | 26 | FOR I = 1 TO TxtLength ' Isolate each character in 27 | C$ = MID$(Txt$, I, 1) ' the string. 28 | 29 | ' If the character is in the filter string, save it: 30 | IF INSTR(FilterString$, C$) <> 0 THEN 31 | Temp$ = Temp$ + C$ 32 | END IF 33 | NEXT I 34 | 35 | Filter$ = Temp$ 36 | END FUNCTION 37 | 38 | ' EXPECT { 39 | ' "io": [ 40 | ' {"output": "Enter a number with commas: "}, 41 | ' {"input": "asdf 32@19!0"}, 42 | ' {"output": "The number's value = 32190 \n"} 43 | ' ] 44 | ' } 45 | -------------------------------------------------------------------------------- /qbjc/src/tests/testdata/compile-and-run/sub.bas: -------------------------------------------------------------------------------- 1 | ' Test invocation with CALL keyword. 2 | CALL f1 3 | CALL f2("yo") 4 | 5 | ' Test short form invocation. 6 | f1: f1: f1: f1 ' First "f1: " should be parsed as label 7 | f2 "yo" 8 | 9 | SUB f1 10 | PRINT "hi" 11 | END SUB 12 | SUB f2(x AS STRING) 13 | PRINT x 14 | END SUB 15 | 16 | ' Exit from sub. 17 | count_down 3 18 | SUB count_down (x AS INTEGER) 19 | PRINT x 20 | IF x = 0 THEN EXIT SUB 21 | count_down x - 1 22 | END SUB 23 | 24 | ' Static vars. 25 | count_up 3 26 | SUB count_up (x AS INTEGER) 27 | STATIC t 28 | PRINT t 29 | IF t < x THEN 30 | t = t + 1 31 | count_up x 32 | END IF 33 | END SUB 34 | 35 | ' EXPECT { 36 | ' "io": [ 37 | ' {"output": "hi\n"}, 38 | ' {"output": "yo\n"}, 39 | ' 40 | ' {"output": "hi\n"}, 41 | ' {"output": "hi\n"}, 42 | ' {"output": "hi\n"}, 43 | ' {"output": "yo\n"}, 44 | ' 45 | ' {"output": " 3 \n"}, 46 | ' {"output": " 2 \n"}, 47 | ' {"output": " 1 \n"}, 48 | ' {"output": " 0 \n"}, 49 | ' 50 | ' {"output": " 0 \n"}, 51 | ' {"output": " 1 \n"}, 52 | ' {"output": " 2 \n"}, 53 | ' {"output": " 3 \n"} 54 | ' ] 55 | ' } 56 | -------------------------------------------------------------------------------- /qbjc/src/tests/testdata/compile-and-run/token.bas: -------------------------------------------------------------------------------- 1 | ' Example from QuickBASIC EXAMPLES folder. 2 | ' 3 | ' TOKEN.BAS 4 | ' 5 | ' Demonstrates a BASIC version of the strtok C function. 6 | ' 7 | DECLARE FUNCTION StrTok$(Source$,Delimiters$) 8 | 9 | LINE INPUT "Enter string: ",P$ 10 | ' Set up the characters that separate tokens. 11 | Delimiters$=" ,;:().?"+CHR$(9)+CHR$(34) 12 | ' Invoke StrTok$ with the string to tokenize. 13 | Token$=StrTok$(P$,Delimiters$) 14 | WHILE Token$<>"" 15 | PRINT Token$ 16 | ' Call StrTok$ with a null string so it knows this 17 | ' isn't the first call. 18 | Token$=StrTok$("",Delimiters$) 19 | WEND 20 | 21 | FUNCTION StrTok$(Srce$,Delim$) 22 | STATIC Start%, SaveStr$ 23 | 24 | ' If first call, make a copy of the string. 25 | IF Srce$<>"" THEN 26 | Start%=1 : SaveStr$=Srce$ 27 | END IF 28 | 29 | BegPos%=Start% : Ln%=LEN(SaveStr$) 30 | ' Look for start of a token (character that isn't delimiter). 31 | WHILE BegPos%<=Ln% AND INSTR(Delim$,MID$(SaveStr$,BegPos%,1))<>0 32 | BegPos%=BegPos%+1 33 | WEND 34 | ' Test for token start found. 35 | IF BegPos% > Ln% THEN 36 | StrTok$="" : EXIT FUNCTION 37 | END IF 38 | ' Find the end of the token. 39 | EndPos%=BegPos% 40 | WHILE EndPos% <= Ln% AND INSTR(Delim$,MID$(SaveStr$,EndPos%,1))=0 41 | EndPos%=EndPos%+1 42 | WEND 43 | StrTok$=MID$(SaveStr$,BegPos%,EndPos%-BegPos%) 44 | ' Set starting point for search for next token. 45 | Start%=EndPos% 46 | 47 | END FUNCTION 48 | 49 | ' EXPECT { 50 | ' "io": [ 51 | ' {"output": "Enter string: "}, 52 | ' {"input": "foo(a, b) baz"}, 53 | ' {"output": "foo\n"}, 54 | ' {"output": "a\n"}, 55 | ' {"output": "b\n"}, 56 | ' {"output": "baz\n"} 57 | ' ] 58 | ' } -------------------------------------------------------------------------------- /qbjc/src/tests/testdata/compile-and-run/udt.bas: -------------------------------------------------------------------------------- 1 | TYPE EmployeeRecord 2 | employee AS Employee 3 | startDate AS STRING 4 | END TYPE 5 | 6 | TYPE Contact 7 | name AS STRING 8 | email AS STRING 9 | END TYPE 10 | 11 | TYPE Employee 12 | contact AS Contact 13 | id AS LONG 14 | END TYPE 15 | 16 | ' Test initialization and direct usage. 17 | DIM r1 AS EmployeeRecord 18 | PRINT "["; r1.employee.id; "] "; r1.employee.contact.name 19 | r1.startDate = "1975-04-04" 20 | r1.employee.id = 1 21 | r1.employee.contact.name = "Bill Gates" 22 | PRINT "["; r1.employee.id; "] "; r1.employee.contact.name 23 | 24 | ' Test assignment. 25 | DIM r2 AS EmployeeRecord 26 | r2 = r1 27 | r2.employee.id = 2 28 | r2.employee.contact.name = "Paul Allen" 29 | PRINT "["; r1.employee.id; "] "; r1.employee.contact.name 30 | PRINT "["; r2.employee.id; "] "; r2.employee.contact.name 31 | 32 | 33 | ' Test array of UDTs. 34 | CONST numEmployees = 5 35 | DIM a1(1 TO numEmployees) AS EmployeeRecord 36 | PRINT a1(numEmployees).employee.id 37 | FOR i = 1 TO numEmployees 38 | a1(i).employee.id = 2 ^ (i - 1) 39 | a1(i).employee.contact.name = "Test" + STR$(i) 40 | NEXT i 41 | PRINT a1(numEmployees).employee.id 42 | 43 | ' Test passing UDT to procedures. 44 | SUB PrintEmployeeRecord (r AS EmployeeRecord) 45 | PRINT "["; r.employee.id; "] "; r.employee.contact.name 46 | END SUB 47 | SUB PrintEmployeeRecords (a() AS EmployeeRecord) 48 | FOR i = 1 TO UBOUND(a) 49 | PrintEmployeeRecord a(i) 50 | NEXT 51 | END SUB 52 | PrintEmployeeRecords a1 53 | 54 | SUB ClearEmployeeRecord (r AS EmployeeRecord) 55 | DIM newRecord AS EmployeeRecord 56 | r = newRecord 57 | END SUB 58 | 59 | ClearEmployeeRecord (r1) ' This should have no effect 60 | PrintEmployeeRecord r1 61 | ClearEmployeeRecord r1 ' This should actually clear out r1 62 | PrintEmployeeRecord r1 63 | 64 | ClearEmployeeRecord (a1(1)) ' This should have no effect 65 | PrintEmployeeRecord a1(1) 66 | ClearEmployeeRecord a1(1) ' This should actually clear out r1 67 | PrintEmployeeRecord a1(1) 68 | 69 | ' EXPECT { 70 | ' "io": [ 71 | ' {"output": "[ 0 ] \n"}, 72 | ' {"output": "[ 1 ] Bill Gates\n"}, 73 | ' 74 | ' {"output": "[ 1 ] Bill Gates\n"}, 75 | ' {"output": "[ 2 ] Paul Allen\n"}, 76 | ' 77 | ' {"output": " 0 \n"}, 78 | ' {"output": " 16 \n"}, 79 | ' 80 | ' {"output": "[ 1 ] Test 1\n"}, 81 | ' {"output": "[ 2 ] Test 2\n"}, 82 | ' {"output": "[ 4 ] Test 3\n"}, 83 | ' {"output": "[ 8 ] Test 4\n"}, 84 | ' {"output": "[ 16 ] Test 5\n"}, 85 | ' 86 | ' {"output": "[ 1 ] Bill Gates\n"}, 87 | ' {"output": "[ 0 ] \n"}, 88 | ' 89 | ' {"output": "[ 1 ] Test 1\n"}, 90 | ' {"output": "[ 0 ] \n"} 91 | ' ] 92 | ' } 93 | -------------------------------------------------------------------------------- /qbjc/src/tests/testdata/compile-and-run/var-decl-init.bas: -------------------------------------------------------------------------------- 1 | ' Test initial values for undeclared variables. 2 | PRINT a; "'"; b$; "'" 3 | PRINT f1 4 | FUNCTION f1 5 | f1 = x 6 | END FUNCTION 7 | 8 | ' Test declaring local and global variables via DIM 9 | DIM c AS STRING 10 | DIM SHARED d AS STRING 11 | PRINT "'"; c; "' '"; d; "'" 12 | c = "foo" 13 | d = "hello" 14 | LET z = f2 15 | FUNCTION f2 16 | PRINT "'"; c; "' '"; d; "'" 17 | END FUNCTION 18 | PRINT "'"; c; "' '"; d; "'" 19 | 20 | ' Test CONST. 21 | CONST e = "test", f = 500 22 | LET z = f3 23 | FUNCTION f3 24 | CONST g = 42 25 | PRINT e; f; g 26 | END FUNCTION 27 | PRINT e; f; g 28 | 29 | ' Test SHARED. 30 | DIM h$ AS STRING 31 | SUB s1 32 | SHARED h$ 33 | h$ = "hello" 34 | END SUB 35 | SUB s2 36 | SHARED h$ 37 | PRINT h$ 38 | END SUB 39 | s1 40 | s2 41 | 42 | ' EXPECT { 43 | ' "io": [ 44 | ' {"output": " 0 ''\n"}, 45 | ' {"output": " 0 \n"}, 46 | ' 47 | ' {"output": "'' ''\n"}, 48 | ' {"output": "' 0 ' 'hello'\n"}, 49 | ' {"output": "'foo' 'hello'\n"}, 50 | ' 51 | ' {"output": "test 500 42 \n"}, 52 | ' {"output": "test 500 0 \n"}, 53 | ' 54 | ' {"output": "hello\n"} 55 | ' ] 56 | ' } 57 | -------------------------------------------------------------------------------- /qbjc/src/tests/testdata/qbjc/.gitignore: -------------------------------------------------------------------------------- 1 | # Intermediate debugging output generated during testing. 2 | *.ast.json 3 | *.bas.js 4 | *.bas.js.map 5 | -------------------------------------------------------------------------------- /qbjc/src/tests/testdata/qbjc/hello-world.bas: -------------------------------------------------------------------------------- 1 | ../compile-and-run/hello-world.bas -------------------------------------------------------------------------------- /qbjc/src/tools/build-runtime-bundle.ts: -------------------------------------------------------------------------------- 1 | /** @file Script to bundle up runtime code as a single string. */ 2 | import ncc from '@vercel/ncc'; 3 | import {program} from 'commander'; 4 | import fs from 'fs-extra'; 5 | import path from 'path'; 6 | 7 | if (require.main === module) { 8 | (async () => { 9 | program 10 | .arguments(' ') 11 | .option('--minify', 'Whether to minify the output bundle') 12 | .parse(); 13 | const opts = program.opts(); 14 | if (program.args.length !== 2) { 15 | console.error(program.usage()); 16 | process.exit(1); 17 | } 18 | const [sourceFilePath, outputFilePath] = [ 19 | path.resolve(program.args[0]), // ncc requires absolute paths 20 | program.args[1], 21 | ]; 22 | for (const filePath of [sourceFilePath, outputFilePath]) { 23 | if (!filePath.endsWith('.js')) { 24 | console.error( 25 | `Error: File path ${filePath} does not have .js extension` 26 | ); 27 | process.exit(1); 28 | } 29 | } 30 | 31 | const {code} = await ncc(sourceFilePath, { 32 | minify: opts.minify, 33 | quiet: true, 34 | }); 35 | 36 | const outputCode = `module.exports = { default: ${JSON.stringify(code)} };`; 37 | await fs.writeFile(outputFilePath, outputCode); 38 | })(); 39 | } 40 | -------------------------------------------------------------------------------- /qbjc/src/tools/extract-stmt-fn-ref.ts: -------------------------------------------------------------------------------- 1 | /** Script to extract statement & function references to Airtable. */ 2 | 3 | import Airtable from 'airtable'; 4 | import {program} from 'commander'; 5 | import fs from 'fs-extra'; 6 | import _ from 'lodash'; 7 | import path from 'path'; 8 | 9 | const REFERENCE_FILE_PATH = path.join( 10 | __dirname, 11 | '..', 12 | '..', 13 | 'docs', 14 | 'Microsoft QuickBASIC BASIC: Language Reference.txt' 15 | ); 16 | const REFERENCE_START_LINE = 2864; 17 | const REFERENCE_END_LINE = 19086; 18 | const ACTION_REGEX = /■ Action\n([^■]+)(?=\n■ )/; 19 | const SYNTAX_REGEX = /■ Syntax(?: [^\n]*)?\n([^■]+)(?=\n■ )/; 20 | const REMARKS_REGEX = /^\n*■ Remarks\n*/; 21 | 22 | function isSep(line: string) { 23 | return line.match(/^[─]+$/); 24 | } 25 | 26 | function findNextName(lines: Array, startLineIdx: number) { 27 | let lineIdx = startLineIdx; 28 | let name = ''; 29 | for (;;) { 30 | // Find initial separator line. 31 | while (lineIdx < lines.length && !isSep(lines[lineIdx])) { 32 | ++lineIdx; 33 | } 34 | if (lineIdx >= lines.length - 3) { 35 | return null; 36 | } 37 | // Next line is the name. 38 | name = lines[++lineIdx]; 39 | // If next line is separator, we've found a name. 40 | if (isSep(lines[++lineIdx])) { 41 | return {name, descLineIdx: lineIdx + 1}; 42 | } 43 | // Otherwise, keep going. 44 | ++lineIdx; 45 | } 46 | } 47 | 48 | if (require.main === module) { 49 | (async () => { 50 | program 51 | .requiredOption('--api-key ', 'Airtable API key') 52 | .requiredOption('--base ', 'Airtable base ID') 53 | .requiredOption('--table ', 'Airtable table ID') 54 | .parse(); 55 | const opts = program.opts(); 56 | 57 | try { 58 | const lines = (await fs.readFile(REFERENCE_FILE_PATH, 'utf-8')) 59 | .split(/\r?\n/) 60 | .slice(REFERENCE_START_LINE - 1, REFERENCE_END_LINE - 1); 61 | 62 | const records: Array<{ 63 | name: string; 64 | action: string; 65 | syntax: string; 66 | desc: string; 67 | }> = []; 68 | let lineIdx = 0; 69 | for (;;) { 70 | // 1. Parse name. 71 | const nextName = findNextName(lines, lineIdx); 72 | if (nextName === null) { 73 | break; 74 | } 75 | const {name} = nextName; 76 | lineIdx = nextName.descLineIdx; 77 | 78 | // 2. Parse description. 79 | const descStartLineIdx = lineIdx; 80 | while (lineIdx < lines.length && !isSep(lines[lineIdx])) { 81 | ++lineIdx; 82 | } 83 | const descEndLineIdx = lineIdx; 84 | let desc = lines 85 | .slice(descStartLineIdx, descEndLineIdx) 86 | .join('\n') 87 | .trim(); 88 | 89 | let action = ''; 90 | const actionMatch = desc.match(ACTION_REGEX); 91 | if (actionMatch) { 92 | action = actionMatch[1].trim().replace(/\n\s*/g, ' '); 93 | desc = desc.replace(ACTION_REGEX, ''); 94 | } 95 | 96 | let syntaxList: Array = []; 97 | for (;;) { 98 | const syntaxMatch = desc.match(SYNTAX_REGEX); 99 | if (syntaxMatch) { 100 | syntaxList.push(syntaxMatch[1].trim().replace(/\n\s*/g, '\n')); 101 | desc = desc.replace(SYNTAX_REGEX, ''); 102 | } else { 103 | break; 104 | } 105 | } 106 | const syntax = ['```', ...syntaxList, '```'].join('\n'); 107 | 108 | desc = desc.replace(REMARKS_REGEX, ''); 109 | 110 | desc = ['```', desc, '```'].join('\n'); 111 | 112 | records.push({name, desc, action, syntax}); 113 | } 114 | 115 | console.log(`Found ${records.length} records`); 116 | 117 | const recordsToCreate: Array<{ 118 | fields: { 119 | Name: string; 120 | Description: string; 121 | Action: string; 122 | Syntax: string; 123 | }; 124 | }> = []; 125 | const recordsToUpdate: Array<{ 126 | id: string; 127 | fields: {Description: string; Action: string; Syntax: string}; 128 | }> = []; 129 | const airtable = new Airtable({apiKey: opts.apiKey}); 130 | const table = airtable.base(opts.base)(opts.table); 131 | 132 | const existingRecords = await table.select().all(); 133 | for (const {name, action, syntax, desc} of records) { 134 | const existingRecord = existingRecords.find( 135 | (existingRecord) => existingRecord.get('Name') === name 136 | ); 137 | if (existingRecord) { 138 | recordsToUpdate.push({ 139 | id: existingRecord.id, 140 | fields: {Description: desc, Action: action, Syntax: syntax}, 141 | }); 142 | } else { 143 | recordsToCreate.push({ 144 | fields: { 145 | Name: name, 146 | Description: desc, 147 | Action: action, 148 | Syntax: syntax, 149 | }, 150 | }); 151 | } 152 | } 153 | 154 | if (recordsToUpdate.length > 0) { 155 | console.log(`Updating ${recordsToUpdate.length} records`); 156 | for (const chunk of _.chunk(recordsToUpdate, 10)) { 157 | await table.update(chunk); 158 | } 159 | } 160 | if (recordsToCreate.length > 0) { 161 | console.log(`Inserting ${recordsToCreate.length} records`); 162 | for (const chunk of _.chunk(recordsToCreate, 10)) { 163 | await table.create(chunk); 164 | } 165 | } 166 | } catch (e: any) { 167 | console.error(`Got error: ${e.message ?? JSON.stringify(e)}`); 168 | } 169 | })(); 170 | } 171 | -------------------------------------------------------------------------------- /qbjc/tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "strict": true, 6 | "esModuleInterop": true, 7 | "declaration": true, 8 | "baseUrl": "." 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /qbjc/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.base", 3 | "compilerOptions": { 4 | "outDir": "dist" 5 | }, 6 | "include": ["src/**/*"] 7 | } 8 | -------------------------------------------------------------------------------- /qbjc/tsconfig.platforms.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.base", 3 | "compilerOptions": {}, 4 | "include": ["node/**/*", "browser/**/*"] 5 | } 6 | --------------------------------------------------------------------------------