Skip to content
| Marketplace
Sign in
Visual Studio Code>Programming Languages>8BitScriptNew to Visual Studio Code? Get it now.
8BitScript

8BitScript

8BitScript

| (0) | Free
Syntax highlighting, language server, and a project runner for 8BitScript (.8bs) source files.
Installation
Launch VS Code Quick Open (Ctrl+P), paste the following command, and press enter.
Copied to clipboard
More Info

8BitScript for VS Code and Cursor

Syntax highlighting, a real language server, and a project runner for .8bs files. The extension itself is still a thin client: it finds the project's 8bs toolchain and runs 8bs lsp --stdio, and every diagnostic, hover, and completion it shows comes from @8bitscript/compiler through that server — not from anything reimplemented here. The same goes for building and running: the side bar's launcher only ever starts the 8bs run and 8bs build commands you would otherwise type. See Editor support for the split between the compiler, the language server, and this extension.

New to 8BitScript?

It's a statically compiled language for 6502-based 8-bit machines and the web — closer to C than to BASIC or hand-written assembly. There's no interpreter and no garbage collector; 8BitScript's own backends lower IR to machine code and WebAssembly. 8bs build and 8bs run work end to end for all nine targets — PET, VIC-20, C64, C128, Commander X16, MEGA65, Atari 8-bit, NES, and the web — right from this extension's own launcher. A range-checked type like u8 is a compile error on overflow (300 does not fit in u8 (0..255)) instead of a silent wrap, and one source resolves per target through packages instead of #ifdef. The System list is those nine, the same RELEASE_MACHINES set the CLI uses.

Compiled size sits close to hand-written C for that same reason: the same screen built by hand in C came to 178 bytes on a C64, the equivalent through 8BitScript's portable screen/text calls to 482, and through a reusable UI component to 809 — the difference being what gets computed at compile time (the hand-written version) versus resolved at run time (the portable one). 8bs build reports exactly what a program used, and a build that doesn't fit its machine's RAM — 3583 bytes on the unexpanded VIC-20 8BitScript targets first — is refused rather than truncated.

Full byte-by-byte accounting is in docs/compiler.md; the project overview is in the root README.

What it does

  • Registers .8bs as the language 8BitScript, and .8bx as 8BitX (8bitextensible), both served by the same language server
  • Colors comments, strings and template strings (with their ${...} fields), numbers (including the 0.5 a #frames(...) duration takes), types, keywords, declarations, calls, the compile-time #frames(...) and #system() (any #name colors as compile-time), the reserved waitFrame(), and the seconds unit inside a #frames(...) call
  • In .8bx, also the component and state declarations, tags (<Foo … />, </Foo>, <>, <slot />) with their component names and attributes, and { … } expressions inside them, which color as 8BitScript again; text between tags stays plain
  • Snippets for the constructs that compile — program, loop, countdown, print, #frames, #package, const, let, for, array, table, address, namespace, asm6502, poke/peek. Nothing is offered that the compiler would reject, and the compiler's own test suite is what holds that
  • // line comments and /* */ blocks, so comment-toggling works, and a /** */ block continues its * column on Enter
  • Bracket matching, auto-closing pairs, and // #region folding
  • Starts the language server (8bs lsp --stdio) when a .8bs file is open and the toolchain is installed, giving you:
    • Diagnostics — lexical, syntax, and range errors, as you type
    • Hover — documentation for built-in types (utinyint, int, ...), constructs (string, volatile, ptr, array, asm6502, @address, memory.read/memory.write), the builtins #frames(...) and its seconds unit, #system(), #fact(...), #package("version"), and waitFrame(), and a member of a named import's own namespace — screen.blank(...) shows its signature and doc comment, read from whichever module the import actually resolves to
    • Completion — built-in type names in type position (after : or inside ptr<...>/array<...>/volatile<...>), the compile-time functions after a #, the unit words inside a #frames(...) call, and — right after object. — the members a named import brings in (screen.bl offers blank) — inside a template's ${...} field as much as outside one
    • Your own names — hover on a component, function, variable, const, parameter or state field shows its declaration and the comment above it, following an import to the file it comes from; completion offers what is visible from the cursor; Go to Definition (F12) lands on the declaration, in this file or another
    • 8BX — in an .8bx file, < offers the components in scope (and slot), a component's tag offers the props it still needs as snippets (row={|}), and </ closes the innermost open element
  • 8BitScript: View Generated Assembly — from the command palette or an .8bs/.8bx editor's right-click menu, runs 8bs build --debug and opens the whole file's generated instructions, colored by a 6502 assembly grammar, in a read-only view beside the source. Clicking a line there jumps back to the exact source span that generated it. The listing is written to be read by someone who has never seen 6502 assembly as much as by someone who has: each run of instructions is introduced by the source line it came from (; line 98: screen.blank(...)), and every instruction carries a plain-English comment saying what the machine does — LDA #$06 ; A = 6, STA $02 ; keysBefore = A, JSR $0668 ; call screen_blank, BEQ $043C ; if zero, loop back to $043C — with your own globals and functions named in place of bare addresses wherever the debug map knows them (and never guessed where it doesn't). The comment icon in the tab's title bar (Toggle Plain-English Comments, the 8bitscript.assemblyView.explain setting) turns the per-instruction column off for a reader who just wants the bytes; the source-line comments stay. View Generated Assembly For… opens the same file for a machine you pick, rather than whatever's currently selected — each machine gets its own stable tab, so several can stay open side by side; the open tab's own title-bar button (Open For Another Machine…) offers the same picker from there. Saving any .8bs/.8bx file in the same project rebuilds every open tab in place — no tab moves, gains focus, or loses its scroll position, and a mid-edit build failure just leaves the last successful listing on screen rather than blanking it. No compiler logic lives in the extension for any of this — it's all read off the compiler's own .8bs.debug.json (docs/compiler.md's "Debug output" section has the schema).

If no toolchain is found, the extension says so and falls back to syntax highlighting alone — see "Installing it while developing" below.

The side bar

The extension adds an 8BitScript icon — the pixel "8" from the project logo — to the Activity Bar, the strip of icons down the left edge where the file explorer, search, and source control live. Clicking it opens a side bar with one thing in it: a launcher.

8BITSCRIPT                              📖  🚀  ♥  ⟳  …

  8BITSCRIPT
    8BitScript     this workspace · pnpm    Update
    2048           0.7.1 · pnpm             Update

  QUICK LAUNCH
    PROGRAM  [ 2048  —  ../2048                 ▾ ] 📄
    SYSTEM   [ PET 2001 (8K)                    ▾ ]
    fitted as Commodore PET · ram=8 · NTSC

  ┌──────────────────────────────────────────────┐
  │  ▶   Run 2048                                │
  │      PET 2001 (8K) · ram=8                   │
  └──────────────────────────────────────────────┘

  RUNNING MACHINES
    ▾ 2048           run · pet · 12s        ⏹
        Emulator   xpet
        Image      dist/2048-pet.prg
        Memory     8 bytes RAM · 331 bytes program

  8bs run --system 'PET 2001 (8K)' --checkout /src/8bitscript --size

The side bar is the 8BitScript tree, each workspace program, and named-system quick launch. Update on 8BitScript refreshes the toolchain: pnpm install at this workspace if it is the 8BitScript repo, git pull plus install for a clone the editor keeps under its global storage, or Install clones https://github.com/8BitScript/8bitscript.git (trunk) into that storage when nothing is there yet. A second row appears for each program in the workspace — 2048, not hello-world, not Studio — with its own Update. Examples and apps are in the Program dropdown, not as install buttons. Hardware fitting and project details live in editor tabs — Configure System and Show Project — the same pattern as Controller Setup. The hardware matrix is not in the side bar.

  • Run — starts 8bs run --system '<name>' --size (or 8bs run <machine>) for the selected project as a task in its own terminal, from the project's directory. A Use local 8BitScript toggle runs this checkout's CLI (--checkout) instead of node_modules/.bin/8bs, so a consumer app can keep published versions in package.json. What you see is exactly what the CLI prints. --size is the per-function breakdown under the memory line; it prints before the emulator window opens. On web, Run binds an ephemeral port (--port 0) so two web runs can listen at once, and listens on the LAN (8bitscript.webLan, on by default) so a phone on the same Wi-Fi can open it. The Running machines tree shows a QR of the https://<lan-ip>:<port>/ URL — Safari will warn once; tap Advanced, then Proceed. Turn the setting off (--local) on an untrusted network. --port n on the CLI still pins HTTP to n (HTTPS on n+1) when you want a stable address. The button names the project, and the line under it names the machine, the hardware fitted to it, and the region — nothing has to be read off a dropdown to know what pressing it means. It greys out when the selected project does not target the selected system, or its emulator is missing, and says which in the line below. Run and Build work for every machine in the System list.

  • Build — the same as Run, but 8bs build --system '<name>' --size (or --target <machine>), which stops at the built file instead of starting an emulator.

  • Program — the program both buttons act on (8bitscript.project). Every directory in the workspace with an 8bitscript.config.ts is in the list, grouped as Programs, Examples, and Apps. Apps are packages whose package.json declares an 8bitscript.app, Studio being the first. The 📄 beside it opens the program's entry .8bs file. Picking a program loads what it is set up for — the first of its systems, hardware and region and all — and a program with none keeps the current machine when it targets it.

    The examples that ship with the toolchain are in the list under Examples (8bitscript.showExamples, on by default), not under Programs — including when this repo is open and they are ordinary workspace folders. They come from @8bitscript/examples, which @8bitscript/cli depends on, so any project that has installed the CLI has them — today that's hello-world and joystick. The 📖 in the title bar hides or shows them. Launch Example… reaches them either way, and one that is already selected stays in the list even with the toggle off, so hiding them never blanks the picker.

  • System — a named system from project config (advertised in 8bitscript.config.ts, this clone's .8bitscript/systems.json, or ~/.config/8bitscript/systems.json), or a bare machine under them. The list comes from 8bs targets --json, tagged with origin. Configure System is the tab that fits a machine and saves it to one of those three layers. The fitted as … line opens that tab. 8bitscript.namedSystem is the selected name; 8bitscript.system is the machine when no name is selected.

SYSTEM
[ ── This clone ────────────────────── ]
[ ── This machine ──────────────────── ]
[ ── Advertised ────────────────────── ]
[  PET 8032               —  pet · profile=8032 ]
[  The browser            —  web       ]
[ ── Machines ───────────────────────── ]
[  pet — Commodore PET                  ]
[  web — Web                            ]

A system marked (too small) is one the program cannot run on: the project's config sets a floor in its requires block — so much RAM, so much disk — and that machine, fitted that way, gives less. It is still listed and still runnable; the notice line says what is short, and 8bs build is the authority that refuses.

A named system is one choice that fits the machine, its hardware and its region together — it is a 8bs run line the project wrote down. Picking one is the same as reaching that machine by hand, and reaching it by hand is recognized as that system: the button names it either way, because the panel compares what the options resolve to rather than remembering what was clicked.

  • Running machines — a tree per 8bs task in flight. A run or boot expands to the machine that launched: the emulator, the built image, RAM and program bytes, the --size breakdown (largest function first), the hardware fitted to it, and — on the web — a QR of the LAN HTTPS URL a phone on the same Wi-Fi can scan, plus live FPS and frame count from the page. Each row has its own Stop, which ends the task and the emulator with it. The section is not there when nothing is running. VICE (xpet on the PET) has no live CPU readout here: its binary monitor pauses the machine on any command, so a PET run shows the compile report and elapsed time rather than registers.
  • The last line is the exact 8bs command the Run button will start.

The view's title bar has 📖 Show or Hide Examples, 🚀 Launch Studio, ♥ Doctor (8bs doctor — checks Node, pnpm, git, and the emulators, and offers to install missing pnpm or a packaged emulator), and ⟳ Refresh; its overflow menu adds Launch App…, Launch Example…, Configure System, Show Project, Save as a System…, and Controller Setup. Those, and every choice the panel makes, are on the command palette as well — 8BitScript: Select Project, Select System, Select Region, Run, Build, Stop, Configure System, Show Project, Use Local 8BitScript, Use Published Packages, Controller Setup, Open Entry File, Open 8bitscript.config.ts.

Configure System

8BitScript: Configure System opens an editor tab — Machine, Hardware, Region, Facts, Save — and writes the same { target, profile?, hardware?, region? } object the CLI already validates. Save destination is explicit: advertise into 8bitscript.config.ts, this clone (.8bitscript/systems.json, gitignored), or this user (~/.config/8bitscript/systems.json).

The extension lists nothing of its own here: it asks the toolchain (8bs targets --json), so a new option, or a new fact, in a package appears with no extension update.

Show Project

8BitScript: Show Project opens a tab for the selected project: path, config, entry, targets, advertised vs personal systems, package manager and lockfile, @8bitscript/* versions, and the toolchain source. Install / Refresh run the detected package manager (pnpm, npm, yarn, or bun). A Dock-launched editor does not read .zshrc; Install looks for pnpm in the locations npx get-pnpm actually uses (~/Library/pnpm on macOS) and offers Run Doctor if it still is not there. Use local 8BitScript / Use published packages persist the choice in workspace settings and .8bitscript/toolchain.json — they do not rewrite package.json.

Each choice is an ordinary setting, written at workspace level when a folder is open, so it also appears in the Settings editor and survives a restart.

Controller Setup

8BitScript: Controller Setup opens a panel — an editor tab, not a side bar view, because a gamepad silhouette does not fit in 300px — that finds the pads plugged into this machine and writes down what their buttons are called.

It detects them with the browser's Gamepad API, polled on an animation frame inside the webview: the extension host is Node and has no HID, so the page is the only part of the extension that can see a controller at all. A browser hides a pad until it has been used once, so a connected-but-untouched controller really is invisible until a button is pressed — the panel says so rather than showing an empty list.

Each controller assigns to Unassigned or Player 1-4. Under that, Live input lights every raw button and draws every axis as it moves, which is how you confirm the device is talking before worrying about what anything is called. The mapper above it is a pad you can click: click a control, press it on the device, and it is bound; or run the Guided setup, which asks for every control in turn. Each step waits for you to let go of everything first, because the previous step's button is usually still down.

Bindings are stored in 8BitScript's own names, never an emulator's or a driver's:

up down left right  a b x y  l r  start select
leftStickX leftStickY rightStickX rightStickY  lt rt

That set is what lets one profile serve every machine. How this maps onto each machine projects it: a VIC-20 has one control port carrying four directions and a fire button, an NES two pads carrying eight bits each, the X16 two SNES pads carrying twelve — and those counts are the toolchain's answer (8bs targets --json, the input.joysticks and input.pads facts each machine package publishes), resolved with the hardware that machine is actually fitted with, not a list kept here. A profile that binds only the left stick still steers a joystick port: each direction falls back to the matching half of the stick.

The result is a JSON file in ~/.config/8bitscript/ — this machine's pads, not the project's source:

{
  "version": 1,
  "controllers": {
    "devices": [
      {
        "id": "8bitdo-sn30-pro-vendor-2dc8-product-6001",
        "name": "8BitDo SN30 Pro (Vendor: 2dc8 Product: 6001)",
        "player": 1,
        "mode": "standard",
        "mapping": { "up": "button:12", "a": "button:0", "leftStickX": "axis:0" }
      }
    ]
  }
}

8bitscript.controllers.json rather than a block in 8bitscript.config.ts: the config is TypeScript source that a person reads and that Save as a System… edits through the editor's own undo stack, and eighteen machine-generated bindings per device rewritten on every button press are not that. A binding is button:N, axis:N (whole and signed, for a stick), axis:N+ / axis:N- (one half, for a direction or a trigger), or key:ArrowLeft — a keyboard key by DOM KeyboardEvent.code. mode is standard when the browser vouched for the pad's layout and custom once anything has been bound by hand. Anything in the file this cannot read is simply unbound — a file you broke by hand costs you the profile, never the panel. It is personal hardware, the way VICE's own ~/.config/vice/vicerc is: an 8BitDo at one desk is not a systems block. A copy sitting in the project directory is still honoured if present (tests, a cabinet that ships with a stick).

A keyboard key is a real binding here, and on one machine it is the only one there is: atari800 has no per-button controller mapping at all — -kbdjoy0/-kbdjoy1 turn the keyboard into stick 0 or 1 and SDL2_JOY_<n>_* says which keys, while a pad's own buttons reach nothing. So during any binding step you can press a key instead of a button, and it lights the same shapes and fills the same meters as a button does. Escape is the one key you cannot bind by pressing, because it is what cancels the step; write key:Escape in the file if you really want it.

8bs run reads this file directly — there is nothing to paste anywhere. The host joystick number is the player number minus one, so plug the pads in in player order. What each emulator can actually take a mapping in differs a lot, and the CLI names by name anything it has nowhere to put.

Projects, tasks, and what is missing

A project is any directory containing an 8bitscript.config.ts; that file is already the manifest the CLI reads for the entry file and the target list, so the launcher uses it as the marker rather than a second list to maintain. A package.json on its own does not count — every package in a monorepo has one. The search covers every workspace folder, skips node_modules, .git, and .claude/worktrees, and the launcher refreshes itself when a config file is added, removed, or edited. 8bitscript.examplesPath names a directory of your own examples instead of the shipped ones, if you have one.

Launch Studio, Launch App…, and Launch Example… start one of the shipped programs without making it the selected project: pick it (when there is a choice), pick where to open it — a program whose config declares systems offers those by name, so Studio is launched on "PET 8032" rather than on pet — and it runs as an ordinary 8bs run task. The hardware that launch fits is the program's own; it does not disturb what you have fitted for your work. An app is run with the toolchain that found it, so an installed Studio launches even though its own directory sits inside node_modules.

Every run and build is also a task of type 8bs, so Tasks: Run Task lists them, and a favorite can be pinned in .vscode/tasks.json:

{
  "version": "2.0.0",
  "tasks": [
    {
      "type": "8bs",
      "command": "run",
      "project": "packages/studio",
      "target": "pet",
      "label": "Studio on the PET"
    }
  ]
}

project is relative to the workspace folder and defaults to the folder itself; command is run, build, or doctor; pal is optional. This is the way to keep a run the launcher's two dropdowns cannot express — a second system, the other region — one keystroke away.

A project whose dependencies have never been installed — it declares some and has no node_modules of its own, which is how a freshly cloned project looks — gets a Run pnpm install button under the launch buttons (or npm/yarn, whichever lockfile is nearest), which runs it in the project as a task. Running such a project asks first, because otherwise the compiler fails on the first import it cannot resolve, with a message about the package rather than the install. A project whose emulator is missing altogether cannot be run at all, and the panel says so. pet and web build and run for real; every other target is still refused.

The grammar is provisional

8BitScript's syntax is not fully specified yet. syntaxes/8bs.tmLanguage.json covers the TypeScript-derived surface the project documentation actually shows, the primitive integer types (both the friendly spelling — utinyint, int, ... — and the low-level u8/i8-style aliases), and the hardware escape hatches that already exist: @address decorators, volatile/ptr/array, and asm6502 blocks. A keyword the language turns out not to have simply never matches, so a wrong guess costs a word that does not color rather than a broken file.

This is still only lexical coloring — it has no idea what a name refers to. The language server layers semantic information over it once it has a binder to draw on; until then, hover and completion (see above) are the only compiler-backed intelligence in the editor.

Installing it

Install 8BitScript from the Visual Studio Marketplace or from Open VSX (Cursor uses Open VSX). The extension talks to 8bs in the project: pnpm add -D @8bitscript/cli so node_modules/.bin/8bs exists.

A .vsix installed by hand does not auto-update. The editor treats that copy as pinned, so it will sit there while a newer version is latest on the gallery. Uninstall it and install from the gallery, or use the source-checkout link below.

Installing it while developing the extension

Link this directory into the editor's extensions folder so the window runs the checkout, not a frozen VSIX.

From the repository root, after pnpm install:

pnpm --filter 8bitscript-lang run link-local

That removes any previous 8bitscript.8bitscript-lang-* in ~/.cursor/extensions (including a VSIX copy) and symlinks this directory under the current version. Pass --vscode for VS Code, or --also-vscode to link both. Then Developer: Reload Window.

The entry is a tiny loader (bootstrap.cjs): if src/ is present and newer than dist/, it rebuilds the bundle before the editor loads it, so a window reload cannot pick up yesterday's bundle by accident. While the window is open it watches src/, media/, syntaxes/, and snippets/. A change does not rebuild in the background — the launcher shows Rebuild the local extension. While that runs it shows status, and only after the rebuild finishes does Reload this window appear. Grammar and media changes follow the same two-step flow so a half-written bundle is never the thing a reload loads. If the editor does not pick up the symlink, copy the directory instead of linking it — some builds scan for real directories.

Cursor's built-in npm task detector opens every package.json it finds and toasts Npm task detection: failed to parse the file when that open fails — the JSON is fine. A linked checkout makes that likely for editors/vscode and the packages/* manifests. Exclude them with npm.exclude (the detector matches the directory of the file). Root scripts still show; those packages are run with pnpm from the repo root.

There is a node --test suite for the parts that do not need the editor — config reading, toolchain lookup, the command each button runs, the source-checkout rebuild, and the shape of the launcher page and the manifest behind it — under test/; pnpm test from the repository root runs it along with everything else. Remove the link with rm on the symlink; the checkout is not touched.

Publishing

A .vsix is built with @vscode/vsce and published to the Visual Studio Marketplace and to Open VSX. The release workflow (.github/workflows/release.yml) runs both. Locally, from editors/vscode:

pnpm package

That writes dist/8bitscript-lang.vsix (gitignored, same as the minified bundle). CI publishes from /tmp; do not leave a .vsix in this directory.

vscode:prepublish minifies the bundle. Diagnostics, hover, and completion come from 8bs lsp over a small stdio JSON-RPC client in this package, not from Microsoft's vscode-languageclient. The remaining weight is the launcher side bar, the grammar, and this README — not a second copy of the compiler. See Editor support.

  • Contact us
  • Jobs
  • Privacy
  • Manage cookies
  • Terms of use
  • Trademarks
  • Your Privacy Choices
  • Consumer Health Privacy
© 2026 Microsoft