commit e6583a3b753672202355cae0de56df7d3d011d31 Author: Chris Wanstrath Date: Sun Oct 5 13:43:13 2025 -0700 it's alive diff --git a/.cursor/rules/use-bun-instead-of-node-vite-npm-pnpm.mdc b/.cursor/rules/use-bun-instead-of-node-vite-npm-pnpm.mdc new file mode 120000 index 0000000..6100270 --- /dev/null +++ b/.cursor/rules/use-bun-instead-of-node-vite-npm-pnpm.mdc @@ -0,0 +1 @@ +../../CLAUDE.md \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a14702c --- /dev/null +++ b/.gitignore @@ -0,0 +1,34 @@ +# dependencies (bun install) +node_modules + +# output +out +dist +*.tgz + +# code coverage +coverage +*.lcov + +# logs +logs +_.log +report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# caches +.eslintcache +.cache +*.tsbuildinfo + +# IntelliJ based IDEs +.idea + +# Finder (MacOS) folder config +.DS_Store diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..b8100b7 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,111 @@ +--- +description: Use Bun instead of Node.js, npm, pnpm, or vite. +globs: "*.ts, *.tsx, *.html, *.css, *.js, *.jsx, package.json" +alwaysApply: false +--- + +Default to using Bun instead of Node.js. + +- Use `bun ` instead of `node ` or `ts-node ` +- Use `bun test` instead of `jest` or `vitest` +- Use `bun build ` instead of `webpack` or `esbuild` +- Use `bun install` instead of `npm install` or `yarn install` or `pnpm install` +- Use `bun run + + +``` + +With the following `frontend.tsx`: + +```tsx#frontend.tsx +import React from "react"; + +// import .css files directly and it works +import './index.css'; + +import { createRoot } from "react-dom/client"; + +const root = createRoot(document.body); + +export default function Frontend() { + return

Hello, world!

; +} + +root.render(); +``` + +Then, run index.ts + +```sh +bun --hot ./index.ts +``` + +For more information, read the Bun API docs in `node_modules/bun-types/docs/**.md`. diff --git a/README.md b/README.md new file mode 100644 index 0000000..4f9ab73 --- /dev/null +++ b/README.md @@ -0,0 +1,15 @@ +# reefvm + +To install dependencies: + +```bash +bun install +``` + +To run: + +```bash +bun run index.ts +``` + +This project was created using `bun init` in bun v1.2.20. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime. diff --git a/bun.lock b/bun.lock new file mode 100644 index 0000000..637eccb --- /dev/null +++ b/bun.lock @@ -0,0 +1,29 @@ +{ + "lockfileVersion": 1, + "workspaces": { + "": { + "name": "reefvm", + "devDependencies": { + "@types/bun": "latest", + }, + "peerDependencies": { + "typescript": "^5", + }, + }, + }, + "packages": { + "@types/bun": ["@types/bun@1.2.23", "", { "dependencies": { "bun-types": "1.2.23" } }, "sha512-le8ueOY5b6VKYf19xT3McVbXqLqmxzPXHsQT/q9JHgikJ2X22wyTW3g3ohz2ZMnp7dod6aduIiq8A14Xyimm0A=="], + + "@types/node": ["@types/node@24.6.2", "", { "dependencies": { "undici-types": "~7.13.0" } }, "sha512-d2L25Y4j+W3ZlNAeMKcy7yDsK425ibcAOO2t7aPTz6gNMH0z2GThtwENCDc0d/Pw9wgyRqE5Px1wkV7naz8ang=="], + + "@types/react": ["@types/react@19.2.0", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-1LOH8xovvsKsCBq1wnT4ntDUdCJKmnEakhsuoUSy6ExlHCkGP2hqnatagYTgFk6oeL0VU31u7SNjunPN+GchtA=="], + + "bun-types": ["bun-types@1.2.23", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-R9f0hKAZXgFU3mlrA0YpE/fiDvwV0FT9rORApt2aQVWSuJDzZOyB5QLc0N/4HF57CS8IXJ6+L5E4W1bW6NS2Aw=="], + + "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], + + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + + "undici-types": ["undici-types@7.13.0", "", {}, "sha512-Ov2Rr9Sx+fRgagJ5AX0qvItZG/JKKoBRAVITs1zk7IqZGTJUwgUr7qoYBpWwakpWilTZFM98rG/AFRocu10iIQ=="], + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..897b2a7 --- /dev/null +++ b/package.json @@ -0,0 +1,11 @@ +{ + "name": "reefvm", + "module": "src/index.ts", + "type": "module", + "devDependencies": { + "@types/bun": "latest" + }, + "peerDependencies": { + "typescript": "^5" + } +} \ No newline at end of file diff --git a/src/bytecode.ts b/src/bytecode.ts new file mode 100644 index 0000000..7532765 --- /dev/null +++ b/src/bytecode.ts @@ -0,0 +1,46 @@ +import { type Value, type FunctionDef, toValue } from "./value" +import { OpCode } from "./opcode" + +export type Bytecode = { + instructions: Instruction[] + constants: Constant[] +} + +export type Instruction = { + op: OpCode + operand?: number | string +} + +export type Constant = + | Value + | FunctionDef + +export function toBytecode(str: string): Bytecode /* throws */ { + const lines = str.trim().split("\n") + + const bytecode: Bytecode = { + instructions: [], + constants: [] + } + + for (let line of lines) { + let [op, operand] = line.trim().split(" ") + + if (operand) { + if (/^\d+/.test(operand)) { + bytecode.constants.push(toValue(parseFloat(operand))) + } else if (/^['"]\w+/.test(operand)) { + bytecode.constants.push(toValue(operand.slice(1, operand.length - 1))) + } else { + throw `Unknown operand: ${operand}` + } + } + + bytecode.instructions.push({ + op: OpCode[op as keyof typeof OpCode], + operand: operand ? bytecode.constants.length - 1 : undefined + }) + } + + return bytecode +} diff --git a/src/exception.ts b/src/exception.ts new file mode 100644 index 0000000..21a9fba --- /dev/null +++ b/src/exception.ts @@ -0,0 +1,7 @@ +import { Scope } from "./scope" + +export type ExceptionHandler = { + catchAddress: number // Where to jump when exception is caught + callStackDepth: number // Call stack depth when handler was pushed + scope: Scope // Scope to restore when catching +} \ No newline at end of file diff --git a/src/frame.ts b/src/frame.ts new file mode 100644 index 0000000..cf89b3b --- /dev/null +++ b/src/frame.ts @@ -0,0 +1,8 @@ +import { Scope } from "./scope" + +export type Frame = { + returnAddress: number + returnScope: Scope + isBreakTarget: boolean + continueAddress?: number +} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..eae608b --- /dev/null +++ b/src/index.ts @@ -0,0 +1,8 @@ +import type { Bytecode } from "./bytecode" +import type { Value } from "./value" +import { VM } from "./vm" + +export async function run(bytecode: Bytecode): Promise { + const vm = new VM(bytecode) + return await vm.run() +} \ No newline at end of file diff --git a/src/opcode.ts b/src/opcode.ts new file mode 100644 index 0000000..c222daa --- /dev/null +++ b/src/opcode.ts @@ -0,0 +1,12 @@ +export enum OpCode { + // stack + PUSH, // operand: constant index (number) + POP, // operand: none + DUP, // operand: none + + // math + ADD, + SUB, + MUL, + DIV +} \ No newline at end of file diff --git a/src/scope.ts b/src/scope.ts new file mode 100644 index 0000000..5a25a06 --- /dev/null +++ b/src/scope.ts @@ -0,0 +1,35 @@ +import type { Value } from "./value" + +export class Scope { + locals = new Map() + parent?: Scope + + constructor(parent?: Scope) { + this.parent = parent + } + + get(name: string): Value /* throws */ { + if (this.locals.has(name)) + return this.locals.get(name)! + + if (this.parent) + return this.parent.get(name) + + throw new Error(`Undefined variable: ${name}`) + } + + set(name: string, value: Value) { + if (this.locals.has(name)) + this.locals.set(name, value) + else if (this.parent?.has(name)) + this.parent.set(name, value) + else + this.locals.set(name, value) + } + + has(name: string): boolean { + return this.locals.has(name) || this.parent?.has(name) || false + } +} + + diff --git a/src/value.ts b/src/value.ts new file mode 100644 index 0000000..353bccd --- /dev/null +++ b/src/value.ts @@ -0,0 +1,61 @@ +import { Scope } from "./scope" + +export type Value = + | { type: 'null', value: null } + | { type: 'boolean', value: boolean } + | { type: 'number', value: number } + | { type: 'string', value: string } + | { type: 'array', value: Value[] } + | { type: 'dict', value: Dict } + | { + type: 'function', + params: string[], + defaults: Record, + body: number, + parentScope: Scope, + variadic: boolean, + kwargs: boolean + } + +export type Dict = Map + +export type FunctionDef = { + type: 'function_def' + params: string[] + defaults: Record + body: number + variadic: boolean + kwargs: boolean +} + +export function toValue(v: any): Value /* throws */ { + if (v === null || v === undefined) + return { type: 'null', value: null } + + if (Array.isArray(v)) + return { type: 'array', value: v.map(toValue) } + + switch (typeof v) { + case 'boolean': + return { type: 'boolean', value: v } + case 'number': + return { type: 'number', value: v } + case 'string': + return { type: 'string', value: v } + case 'function': + throw "can't toValue() a js function yet" + case 'object': + const dict: Dict = new Map() + + for (const key in Object.keys(v)) + dict.set(key, toValue(v[key])) + + return { type: 'dict', value: dict } + default: + throw `can't toValue this: ${v}` + } +} + +export function toNumber(v: Value): number { + return v.type === 'number' ? v.value : 0 +} \ No newline at end of file diff --git a/src/vm.ts b/src/vm.ts new file mode 100644 index 0000000..4bab194 --- /dev/null +++ b/src/vm.ts @@ -0,0 +1,84 @@ +import type { Bytecode, Constant, Instruction } from "./bytecode" +import type { ExceptionHandler } from "./exception" +import type { Frame } from "./frame" +import { OpCode } from "./opcode" +import { Scope } from "./scope" +import { type Value, toValue, toNumber } from "./value" + +export class VM { + pc = 0 + stopped = false + stack: Value[] = [] + callStack: Frame[] = [] + exceptionHandlers: ExceptionHandler[] = [] + scope: Scope + constants: Constant[] = [] + instructions: Instruction[] = [] + + constructor(bytecode: Bytecode) { + this.instructions = bytecode.instructions + this.constants = bytecode.constants + this.scope = new Scope() + } + + async run(): Promise { + this.pc = 0 + this.stopped = false + + while (!this.stopped && this.pc < this.instructions.length) { + const instruction = this.instructions[this.pc]! + await this.execute(instruction) + this.pc++ + } + + return this.stack[this.stack.length - 1] || toValue(null) + } + + async execute(instruction: Instruction) /* throws */ { + switch (instruction.op) { + case OpCode.PUSH: + const idx = instruction.operand as number + const constant = this.constants[idx] + + if (!constant || constant.type === 'function_def') + throw new Error(`Invalid constant index: ${idx}`) + + this.stack.push(constant) + break + + case OpCode.POP: + this.stack.pop() + break + + case OpCode.DUP: + this.stack.push(this.stack[this.stack.length - 1]!) + break + + case OpCode.ADD: + this.binaryOp((a, b) => toNumber(a) + toNumber(b)) + break + + case OpCode.SUB: + this.binaryOp((a, b) => toNumber(a) - toNumber(b)) + break + + case OpCode.MUL: + this.binaryOp((a, b) => toNumber(a) * toNumber(b)) + break + + case OpCode.DIV: + this.binaryOp((a, b) => toNumber(a) / toNumber(b)) + break + + default: + throw `Unknown op: ${instruction.op}` + } + } + + binaryOp(fn: (a: Value, b: Value) => number) { + const b = this.stack.pop()! + const a = this.stack.pop()! + const result = fn(a, b) + this.stack.push({ type: 'number', value: result }) + } +} \ No newline at end of file diff --git a/tests/basic.test.ts b/tests/basic.test.ts new file mode 100644 index 0000000..2753aaf --- /dev/null +++ b/tests/basic.test.ts @@ -0,0 +1,47 @@ +import { test, expect } from "bun:test" +import { run } from "#index" +import { toBytecode } from "#bytecode" + +test("adding numbers", async () => { + const str = ` + PUSH 1 + PUSH 5 + ADD +` + expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 6 }) + + const str2 = ` + PUSH 100 + PUSH 500 + ADD +` + expect(await run(toBytecode(str2))).toEqual({ type: 'number', value: 600 }) +}) + +test("subtracting numbers", async () => { + const str = ` + PUSH 5 + PUSH 2 + SUB +` + expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 3 }) +}) + +test("multiplying numbers", async () => { + const str = ` + PUSH 5 + PUSH 2 + MUL +` + expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 10 }) +}) + +test("dividing numbers", async () => { + const str = ` + PUSH 10 + PUSH 2 + DIV +` + expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 5 }) +}) + diff --git a/tests/bytecode.test.ts b/tests/bytecode.test.ts new file mode 100644 index 0000000..1ebbe3a --- /dev/null +++ b/tests/bytecode.test.ts @@ -0,0 +1,23 @@ +import { test, expect } from "bun:test" +import { toBytecode } from "#bytecode" +import { OpCode } from "#opcode" + +test("string compilation", () => { + const str = ` + PUSH 1 + PUSH 5 + ADD +` + expect(toBytecode(str)).toEqual({ + instructions: [ + { op: OpCode.PUSH, operand: 0 }, + { op: OpCode.PUSH, operand: 1 }, + { op: OpCode.ADD }, + ], + constants: [ + { type: 'number', value: 1 }, + { type: 'number', value: 5 } + ] + }) +}) + diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..de4c31b --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,34 @@ +{ + "compilerOptions": { + // Environment setup & latest features + "lib": [ + "ESNext" + ], + "target": "ESNext", + "module": "Preserve", + "moduleDetection": "force", + "jsx": "react-jsx", + "allowJs": true, + // Bundler mode + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, + // Best practices + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true, + "noImplicitOverride": true, + // Some stricter flags (disabled by default) + "noUnusedLocals": false, + "noUnusedParameters": false, + "noPropertyAccessFromIndexSignature": false, + "baseUrl": ".", + "paths": { + "#*": [ + "./src/*" + ] + }, + } +} \ No newline at end of file