Compare commits
No commits in common. "2855b4fbe36a1f034a100de37b2e566e0f0ad96b" and "ec2b1a9b22783f835cd0464b1aca6ff69afb8d46" have entirely different histories.
2855b4fbe3
...
ec2b1a9b22
216
CLAUDE.md
216
CLAUDE.md
|
|
@ -1,149 +1,119 @@
|
||||||
# CLAUDE.md
|
---
|
||||||
|
description: Use Bun instead of Node.js, npm, pnpm, or vite.
|
||||||
|
globs: "*.ts, *.tsx, *.html, *.css, *.js, *.jsx, package.json"
|
||||||
|
alwaysApply: false
|
||||||
|
---
|
||||||
|
|
||||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
## Overview
|
||||||
|
|
||||||
## Project Overview
|
This is a stack based VM for a simple dyanmic language called Shrimp.
|
||||||
|
|
||||||
ReefVM is a stack-based bytecode virtual machine for the Shrimp programming language. It implements a complete VM with closures, tail call optimization, exception handling, variadic functions, named parameters, and Ruby-style iterators with break/continue.
|
Please read README.md, SPEC.md, src/vm.ts, and the examples/ to understand the VM.
|
||||||
|
|
||||||
**Essential reading**: Before making changes, read README.md, SPEC.md, and GUIDE.md to understand the VM architecture, instruction set, and compiler patterns.
|
## Bun
|
||||||
|
|
||||||
## Development Commands
|
Default to using Bun instead of Node.js.
|
||||||
|
|
||||||
### Running Files
|
- Use `bun <file>` instead of `node <file>` or `ts-node <file>`
|
||||||
```bash
|
- Use `bun test` instead of `jest` or `vitest`
|
||||||
bun <file.ts> # Run TypeScript files directly
|
- Use `bun build <file.html|file.ts|file.css>` instead of `webpack` or `esbuild`
|
||||||
bun examples/native.ts # Run example
|
- Use `bun install` instead of `npm install` or `yarn install` or `pnpm install`
|
||||||
|
- Use `bun run <script>` instead of `npm run <script>` or `yarn run <script>` or `pnpm run <script>`
|
||||||
|
- Bun automatically loads .env, so don't use dotenv.
|
||||||
|
|
||||||
|
## APIs
|
||||||
|
|
||||||
|
- `Bun.serve()` supports WebSockets, HTTPS, and routes. Don't use `express`.
|
||||||
|
- `bun:sqlite` for SQLite. Don't use `better-sqlite3`.
|
||||||
|
- `Bun.redis` for Redis. Don't use `ioredis`.
|
||||||
|
- `Bun.sql` for Postgres. Don't use `pg` or `postgres.js`.
|
||||||
|
- `WebSocket` is built-in. Don't use `ws`.
|
||||||
|
- Prefer `Bun.file` over `node:fs`'s readFile/writeFile
|
||||||
|
- Bun.$`ls` instead of execa.
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
Use `bun test` to run tests.
|
||||||
|
|
||||||
|
```ts#index.test.ts
|
||||||
|
import { test, expect } from "bun:test";
|
||||||
|
|
||||||
|
test("hello world", () => {
|
||||||
|
expect(1).toBe(1);
|
||||||
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
### Testing
|
## Frontend
|
||||||
```bash
|
|
||||||
bun test # Run all tests
|
|
||||||
bun test <file> # Run specific test file
|
|
||||||
bun test --watch # Watch mode
|
|
||||||
```
|
|
||||||
|
|
||||||
### Building
|
Use HTML imports with `Bun.serve()`. Don't use `vite`. HTML imports fully support React, CSS, Tailwind.
|
||||||
No build step required - Bun runs TypeScript directly.
|
|
||||||
|
|
||||||
## Architecture
|
Server:
|
||||||
|
|
||||||
### Core Components
|
```ts#index.ts
|
||||||
|
import index from "./index.html"
|
||||||
|
|
||||||
**VM Execution Model** (src/vm.ts):
|
Bun.serve({
|
||||||
- Stack-based execution with program counter (PC)
|
routes: {
|
||||||
- Call stack for function frames
|
"/": index,
|
||||||
- Exception handler stack for try/catch/finally
|
"/api/users/:id": {
|
||||||
- Lexical scope chain with parent references
|
GET: (req) => {
|
||||||
- Native function registry for TypeScript interop
|
return new Response(JSON.stringify({ id: req.params.id }));
|
||||||
|
},
|
||||||
**Key subsystems**:
|
},
|
||||||
- **bytecode.ts**: Parser that converts human-readable bytecode strings to executable bytecode. Handles label resolution, constant pool management, and function definition parsing.
|
},
|
||||||
- **value.ts**: Tagged union Value type system with type coercion functions (toNumber, toString, isTrue, isEqual)
|
// optional websocket support
|
||||||
- **scope.ts**: Linked scope chain for variable resolution with lexical scoping
|
websocket: {
|
||||||
- **frame.ts**: Call frame tracking for function calls and break targets
|
open: (ws) => {
|
||||||
- **exception.ts**: Exception handler records for try/catch/finally blocks
|
ws.send("Hello, world!");
|
||||||
- **validator.ts**: Bytecode validation to catch common errors before execution
|
},
|
||||||
- **opcode.ts**: OpCode enum defining all VM instructions
|
message: (ws, message) => {
|
||||||
|
ws.send(message);
|
||||||
### Critical Design Decisions
|
},
|
||||||
|
close: (ws) => {
|
||||||
**Relative jumps**: All JUMP instructions use PC-relative offsets (not absolute addresses), making bytecode position-independent. PUSH_TRY/PUSH_FINALLY use absolute addresses.
|
// handle close
|
||||||
|
}
|
||||||
**Truthiness semantics**: Only `null` and `false` are falsy. Unlike JavaScript, `0`, `""`, empty arrays, and empty dicts are truthy.
|
},
|
||||||
|
development: {
|
||||||
**No AND/OR opcodes**: Short-circuit logical operations are implemented at the compiler level using JUMP patterns with DUP.
|
hmr: true,
|
||||||
|
console: true,
|
||||||
**Tail call optimization**: TAIL_CALL reuses the current call frame instead of pushing a new one, enabling unbounded recursion.
|
}
|
||||||
|
|
||||||
**Break semantics**: CALL marks frames as break targets. BREAK unwinds the call stack to the most recent break target, enabling Ruby-style iterator patterns.
|
|
||||||
|
|
||||||
**Exception handling**: THROW jumps to finally (if present) or catch. The VM does NOT auto-jump to finally on successful try completion - compilers must explicitly generate JUMPs to finally blocks.
|
|
||||||
|
|
||||||
**Parameter binding priority**: Named args bind to fixed params first. Unmatched named args go to `@named` dict parameter. Fixed params bind in order: named arg > positional arg > default > null.
|
|
||||||
|
|
||||||
**Native function calling**: CALL_NATIVE consumes the entire stack as arguments (different from CALL which pops specific argument counts).
|
|
||||||
|
|
||||||
## Testing Strategy
|
|
||||||
|
|
||||||
Tests are organized by feature area:
|
|
||||||
- **basic.test.ts**: Stack ops, arithmetic, comparisons, variables, control flow
|
|
||||||
- **functions.test.ts**: Function creation, calls, closures, defaults, variadic, named args
|
|
||||||
- **tail-call.test.ts**: Tail call optimization and unbounded recursion
|
|
||||||
- **exceptions.test.ts**: Try/catch/finally, exception unwinding, nested handlers
|
|
||||||
- **native.test.ts**: Native function interop (sync and async)
|
|
||||||
- **bytecode.test.ts**: Bytecode parser, label resolution, constants
|
|
||||||
- **validator.test.ts**: Bytecode validation rules
|
|
||||||
- **examples.test.ts**: Integration tests for example programs
|
|
||||||
|
|
||||||
When adding features:
|
|
||||||
1. Add unit tests for the specific opcode/feature
|
|
||||||
2. Add integration tests showing real-world usage
|
|
||||||
3. Update SPEC.md with formal specification
|
|
||||||
4. Update GUIDE.md with compiler patterns
|
|
||||||
5. Consider adding an example to examples/
|
|
||||||
|
|
||||||
## Common Patterns
|
|
||||||
|
|
||||||
### Writing Bytecode Tests
|
|
||||||
```typescript
|
|
||||||
import { toBytecode, run } from "#reef"
|
|
||||||
|
|
||||||
const bytecode = toBytecode(`
|
|
||||||
PUSH 42
|
|
||||||
STORE x
|
|
||||||
LOAD x
|
|
||||||
HALT
|
|
||||||
`)
|
|
||||||
|
|
||||||
const result = await run(bytecode)
|
|
||||||
// result is { type: 'number', value: 42 }
|
|
||||||
```
|
|
||||||
|
|
||||||
### Native Function Registration
|
|
||||||
```typescript
|
|
||||||
const vm = new VM(bytecode)
|
|
||||||
vm.registerFunction('functionName', (...args: Value[]): Value => {
|
|
||||||
// Implementation
|
|
||||||
return toValue(result)
|
|
||||||
})
|
})
|
||||||
await vm.run()
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Label Usage (Preferred)
|
HTML files can import .tsx, .jsx or .js files directly and Bun's bundler will transpile & bundle automatically. `<link>` tags can point to stylesheets and Bun's CSS bundler will bundle.
|
||||||
Use labels instead of numeric offsets for readability:
|
|
||||||
```
|
```html#index.html
|
||||||
JUMP .skip
|
<html>
|
||||||
PUSH 42
|
<body>
|
||||||
HALT
|
<h1>Hello, world!</h1>
|
||||||
.skip:
|
<script type="module" src="./frontend.tsx"></script>
|
||||||
PUSH 99
|
</body>
|
||||||
HALT
|
</html>
|
||||||
```
|
```
|
||||||
|
|
||||||
## TypeScript Configuration
|
With the following `frontend.tsx`:
|
||||||
|
|
||||||
- Import alias: `#reef` maps to `./src/index.ts`
|
```tsx#frontend.tsx
|
||||||
- Module system: ES modules (`"type": "module"` in package.json)
|
import React from "react";
|
||||||
- Bun automatically handles TypeScript compilation
|
|
||||||
|
|
||||||
## Bun-Specific Notes
|
// import .css files directly and it works
|
||||||
|
import './index.css';
|
||||||
|
|
||||||
- Use `bun` instead of `node`, `npm`, `pnpm`, or `vite`
|
import { createRoot } from "react-dom/client";
|
||||||
- No need for dotenv - Bun loads .env automatically
|
|
||||||
- Prefer Bun APIs over Node.js equivalents when available
|
|
||||||
- See .cursor/rules/use-bun-instead-of-node-vite-npm-pnpm.mdc for detailed Bun usage
|
|
||||||
|
|
||||||
## Common Gotchas
|
const root = createRoot(document.body);
|
||||||
|
|
||||||
**Jump offsets**: JUMP/JUMP_IF_FALSE/JUMP_IF_TRUE use relative offsets from the next instruction (PC + 1). PUSH_TRY/PUSH_FINALLY use absolute instruction indices.
|
export default function Frontend() {
|
||||||
|
return <h1>Hello, world!</h1>;
|
||||||
|
}
|
||||||
|
|
||||||
**Stack operations**: Most binary operations pop in reverse order (second operand is popped first, then first operand).
|
root.render(<Frontend />);
|
||||||
|
```
|
||||||
|
|
||||||
**MAKE_ARRAY operand**: Specifies count, not a stack index. `MAKE_ARRAY #3` pops 3 items.
|
Then, run index.ts
|
||||||
|
|
||||||
**CALL_NATIVE stack behavior**: Unlike CALL, it consumes all stack values as arguments and clears the stack.
|
```sh
|
||||||
|
bun --hot ./index.ts
|
||||||
|
```
|
||||||
|
|
||||||
**Finally blocks**: The compiler must generate explicit JUMPs to finally blocks for successful try/catch completion. The VM only auto-jumps to finally on THROW.
|
For more information, read the Bun API docs in `node_modules/bun-types/docs/**.md`.
|
||||||
|
|
||||||
**Variable scoping**: STORE updates existing variables in parent scopes or creates in current scope. It does NOT shadow by default.
|
|
||||||
|
|
|
||||||
344
GUIDE.md
344
GUIDE.md
|
|
@ -1,344 +0,0 @@
|
||||||
# Reef Compiler Guide
|
|
||||||
|
|
||||||
Quick reference for compiling to Reef bytecode.
|
|
||||||
|
|
||||||
## Bytecode Syntax
|
|
||||||
|
|
||||||
### Instructions
|
|
||||||
```
|
|
||||||
OPCODE operand ; comment
|
|
||||||
```
|
|
||||||
|
|
||||||
### Operand Types
|
|
||||||
|
|
||||||
**Immediate numbers** (`#N`): Counts or relative offsets
|
|
||||||
- `MAKE_ARRAY #3` - count of 3 items
|
|
||||||
- `JUMP #5` - relative offset of 5 instructions (prefer labels)
|
|
||||||
- `PUSH_TRY #10` - absolute instruction index (prefer labels)
|
|
||||||
|
|
||||||
**Labels** (`.name`): Symbolic addresses resolved at parse time
|
|
||||||
- `.label:` - define label at current position
|
|
||||||
- `JUMP .loop` - jump to label
|
|
||||||
- `MAKE_FUNCTION (x) .body` - function body at label
|
|
||||||
|
|
||||||
**Variable names**: Plain identifiers
|
|
||||||
- `LOAD counter` - load variable
|
|
||||||
- `STORE result` - store variable
|
|
||||||
|
|
||||||
**Constants**: Literals added to constants pool
|
|
||||||
- Numbers: `PUSH 42`, `PUSH 3.14`
|
|
||||||
- Strings: `PUSH "hello"` or `PUSH 'world'`
|
|
||||||
- Booleans: `PUSH true`, `PUSH false`
|
|
||||||
- Null: `PUSH null`
|
|
||||||
|
|
||||||
**Native function names**: Registered TypeScript functions
|
|
||||||
- `CALL_NATIVE print`
|
|
||||||
|
|
||||||
### Functions
|
|
||||||
```
|
|
||||||
MAKE_FUNCTION (x y) .body ; Basic
|
|
||||||
MAKE_FUNCTION (x=10 y=20) .body ; Defaults
|
|
||||||
MAKE_FUNCTION (x ...rest) .body ; Variadic
|
|
||||||
MAKE_FUNCTION (x @named) .body ; Named args
|
|
||||||
MAKE_FUNCTION (x ...rest @named) .body ; Both
|
|
||||||
```
|
|
||||||
|
|
||||||
### Function Calls
|
|
||||||
Stack order (bottom to top):
|
|
||||||
```
|
|
||||||
LOAD fn
|
|
||||||
PUSH arg1 ; Positional args
|
|
||||||
PUSH arg2
|
|
||||||
PUSH "name" ; Named arg key
|
|
||||||
PUSH "value" ; Named arg value
|
|
||||||
PUSH 2 ; Positional count
|
|
||||||
PUSH 1 ; Named count
|
|
||||||
CALL
|
|
||||||
```
|
|
||||||
|
|
||||||
## Opcodes
|
|
||||||
|
|
||||||
### Stack
|
|
||||||
- `PUSH <const>` - Push constant
|
|
||||||
- `POP` - Remove top
|
|
||||||
- `DUP` - Duplicate top
|
|
||||||
|
|
||||||
### Variables
|
|
||||||
- `LOAD <name>` - Push variable value
|
|
||||||
- `STORE <name>` - Pop and store in variable
|
|
||||||
|
|
||||||
### Arithmetic
|
|
||||||
- `ADD`, `SUB`, `MUL`, `DIV`, `MOD` - Binary ops (pop 2, push result)
|
|
||||||
|
|
||||||
### Comparison
|
|
||||||
- `EQ`, `NEQ`, `LT`, `GT`, `LTE`, `GTE` - Pop 2, push boolean
|
|
||||||
|
|
||||||
### Logic
|
|
||||||
- `NOT` - Pop 1, push !value
|
|
||||||
|
|
||||||
### Control Flow
|
|
||||||
- `JUMP .label` - Unconditional jump
|
|
||||||
- `JUMP_IF_FALSE .label` - Jump if top is false or null (pops value)
|
|
||||||
- `JUMP_IF_TRUE .label` - Jump if top is truthy (pops value)
|
|
||||||
- `HALT` - Stop execution of the program
|
|
||||||
|
|
||||||
### Functions
|
|
||||||
- `MAKE_FUNCTION (params) .body` - Create function, push to stack
|
|
||||||
- `CALL` - Call function (see calling convention above)
|
|
||||||
- `TAIL_CALL` - Tail-recursive call (no stack growth)
|
|
||||||
- `RETURN` - Return from function (pops return value)
|
|
||||||
- `BREAK` - Exit iterator/loop (unwinds to break target)
|
|
||||||
|
|
||||||
### Arrays
|
|
||||||
- `MAKE_ARRAY #N` - Pop N items, push array
|
|
||||||
- `ARRAY_GET` - Pop index and array, push element
|
|
||||||
- `ARRAY_SET` - Pop value, index, array; mutate array
|
|
||||||
- `ARRAY_PUSH` - Pop value and array, append to array
|
|
||||||
- `ARRAY_LEN` - Pop array, push length
|
|
||||||
|
|
||||||
### Dicts
|
|
||||||
- `MAKE_DICT #N` - Pop N key-value pairs, push dict
|
|
||||||
- `DICT_GET` - Pop key and dict, push value (or null)
|
|
||||||
- `DICT_SET` - Pop value, key, dict; mutate dict
|
|
||||||
- `DICT_HAS` - Pop key and dict, push boolean
|
|
||||||
|
|
||||||
### Exceptions
|
|
||||||
- `PUSH_TRY .catch` - Register exception handler
|
|
||||||
- `PUSH_FINALLY .finally` - Add finally to current handler
|
|
||||||
- `POP_TRY` - Remove handler (try succeeded)
|
|
||||||
- `THROW` - Throw exception (pops error value)
|
|
||||||
|
|
||||||
### Native
|
|
||||||
- `CALL_NATIVE <name>` - Call registered TypeScript function (consumes entire stack as args)
|
|
||||||
|
|
||||||
## Compiler Patterns
|
|
||||||
|
|
||||||
### If-Else
|
|
||||||
```
|
|
||||||
<condition>
|
|
||||||
JUMP_IF_FALSE .else
|
|
||||||
<then-block>
|
|
||||||
JUMP .end
|
|
||||||
.else:
|
|
||||||
<else-block>
|
|
||||||
.end:
|
|
||||||
```
|
|
||||||
|
|
||||||
### While Loop
|
|
||||||
```
|
|
||||||
.loop:
|
|
||||||
<condition>
|
|
||||||
JUMP_IF_FALSE .end
|
|
||||||
<body>
|
|
||||||
JUMP .loop
|
|
||||||
.end:
|
|
||||||
```
|
|
||||||
|
|
||||||
### For Loop
|
|
||||||
```
|
|
||||||
<init>
|
|
||||||
.loop:
|
|
||||||
<condition>
|
|
||||||
JUMP_IF_FALSE .end
|
|
||||||
<body>
|
|
||||||
<increment>
|
|
||||||
JUMP .loop
|
|
||||||
.end:
|
|
||||||
```
|
|
||||||
|
|
||||||
### Continue
|
|
||||||
No CONTINUE opcode. Use backward jump to loop start:
|
|
||||||
```
|
|
||||||
.loop:
|
|
||||||
<condition>
|
|
||||||
JUMP_IF_FALSE .end
|
|
||||||
<early-check>
|
|
||||||
JUMP_IF_TRUE .loop ; continue
|
|
||||||
<body>
|
|
||||||
JUMP .loop
|
|
||||||
.end:
|
|
||||||
```
|
|
||||||
|
|
||||||
### Break in Loop
|
|
||||||
Mark iterator function as break target, use BREAK opcode:
|
|
||||||
```
|
|
||||||
MAKE_FUNCTION () .each_body
|
|
||||||
STORE each
|
|
||||||
LOAD collection
|
|
||||||
LOAD each
|
|
||||||
<call-iterator-with-break-semantics>
|
|
||||||
HALT
|
|
||||||
|
|
||||||
.each_body:
|
|
||||||
<condition>
|
|
||||||
JUMP_IF_TRUE .done
|
|
||||||
<body>
|
|
||||||
BREAK ; exits to caller
|
|
||||||
.done:
|
|
||||||
RETURN
|
|
||||||
```
|
|
||||||
|
|
||||||
### Short-Circuit AND
|
|
||||||
```
|
|
||||||
<left>
|
|
||||||
DUP
|
|
||||||
JUMP_IF_FALSE .end ; Short-circuit if false
|
|
||||||
POP
|
|
||||||
<right>
|
|
||||||
.end: ; Result on stack
|
|
||||||
```
|
|
||||||
|
|
||||||
### Short-Circuit OR
|
|
||||||
```
|
|
||||||
<left>
|
|
||||||
DUP
|
|
||||||
JUMP_IF_TRUE .end ; Short-circuit if true
|
|
||||||
POP
|
|
||||||
<right>
|
|
||||||
.end: ; Result on stack
|
|
||||||
```
|
|
||||||
|
|
||||||
### Try-Catch
|
|
||||||
```
|
|
||||||
PUSH_TRY .catch
|
|
||||||
<try-block>
|
|
||||||
POP_TRY
|
|
||||||
JUMP .end
|
|
||||||
.catch:
|
|
||||||
STORE err
|
|
||||||
<catch-block>
|
|
||||||
.end:
|
|
||||||
```
|
|
||||||
|
|
||||||
### Try-Catch-Finally
|
|
||||||
```
|
|
||||||
PUSH_TRY .catch
|
|
||||||
PUSH_FINALLY .finally
|
|
||||||
<try-block>
|
|
||||||
POP_TRY
|
|
||||||
JUMP .finally ; Compiler must generate this
|
|
||||||
.catch:
|
|
||||||
STORE err
|
|
||||||
<catch-block>
|
|
||||||
JUMP .finally ; And this
|
|
||||||
.finally:
|
|
||||||
<finally-block> ; Executes in both paths
|
|
||||||
.end:
|
|
||||||
```
|
|
||||||
|
|
||||||
**Important**: VM only auto-jumps to finally on THROW. For successful try/catch, compiler must explicitly JUMP to finally.
|
|
||||||
|
|
||||||
### Closures
|
|
||||||
Functions automatically capture current scope:
|
|
||||||
```
|
|
||||||
PUSH 0
|
|
||||||
STORE counter
|
|
||||||
MAKE_FUNCTION () .increment
|
|
||||||
RETURN
|
|
||||||
|
|
||||||
.increment:
|
|
||||||
LOAD counter ; Captured variable
|
|
||||||
PUSH 1
|
|
||||||
ADD
|
|
||||||
STORE counter
|
|
||||||
LOAD counter
|
|
||||||
RETURN
|
|
||||||
```
|
|
||||||
|
|
||||||
### Tail Recursion
|
|
||||||
Use TAIL_CALL instead of CALL for last call:
|
|
||||||
```
|
|
||||||
MAKE_FUNCTION (n acc) .factorial
|
|
||||||
STORE factorial
|
|
||||||
<...>
|
|
||||||
|
|
||||||
.factorial:
|
|
||||||
LOAD n
|
|
||||||
PUSH 0
|
|
||||||
LTE
|
|
||||||
JUMP_IF_FALSE .recurse
|
|
||||||
LOAD acc
|
|
||||||
RETURN
|
|
||||||
.recurse:
|
|
||||||
LOAD factorial
|
|
||||||
LOAD n
|
|
||||||
PUSH 1
|
|
||||||
SUB
|
|
||||||
LOAD n
|
|
||||||
LOAD acc
|
|
||||||
MUL
|
|
||||||
PUSH 2
|
|
||||||
PUSH 0
|
|
||||||
TAIL_CALL ; Reuses stack frame
|
|
||||||
```
|
|
||||||
|
|
||||||
## Key Concepts
|
|
||||||
|
|
||||||
### Truthiness
|
|
||||||
Only `null` and `false` are falsy. Everything else (including `0`, `""`, empty arrays/dicts) is truthy.
|
|
||||||
|
|
||||||
### Type Coercion
|
|
||||||
|
|
||||||
**toNumber**:
|
|
||||||
- `number` → identity
|
|
||||||
- `string` → parseFloat (or 0 if invalid)
|
|
||||||
- `boolean` → 1 (true) or 0 (false)
|
|
||||||
- `null` → 0
|
|
||||||
- Others → 0
|
|
||||||
|
|
||||||
**toString**:
|
|
||||||
- `string` → identity
|
|
||||||
- `number` → string representation
|
|
||||||
- `boolean` → "true" or "false"
|
|
||||||
- `null` → "null"
|
|
||||||
- `function` → "<function>"
|
|
||||||
- `array` → "[item, item]"
|
|
||||||
- `dict` → "{key: value, ...}"
|
|
||||||
|
|
||||||
**Arithmetic ops** (ADD, SUB, MUL, DIV, MOD) coerce both operands to numbers.
|
|
||||||
|
|
||||||
**Comparison ops** (LT, GT, LTE, GTE) coerce both operands to numbers.
|
|
||||||
|
|
||||||
**Equality ops** (EQ, NEQ) use type-aware comparison with deep equality for arrays/dicts.
|
|
||||||
|
|
||||||
**Note**: There is no string concatenation operator. ADD only works with numbers.
|
|
||||||
|
|
||||||
### Scope
|
|
||||||
- Variables resolved through parent scope chain
|
|
||||||
- STORE updates existing variable or creates in current scope
|
|
||||||
- Functions capture scope at definition time
|
|
||||||
|
|
||||||
### Break Semantics
|
|
||||||
- CALL marks current frame as break target
|
|
||||||
- BREAK unwinds call stack to that target
|
|
||||||
- Used for Ruby-style iterator pattern
|
|
||||||
|
|
||||||
### Parameter Binding Priority
|
|
||||||
For function calls, parameters bound in order:
|
|
||||||
1. Positional argument (if provided)
|
|
||||||
2. Named argument (if provided and matches param name)
|
|
||||||
3. Default value (if defined)
|
|
||||||
4. Null
|
|
||||||
|
|
||||||
### Exception Handlers
|
|
||||||
- PUSH_TRY uses absolute addresses for catch blocks
|
|
||||||
- Nested try blocks form a stack
|
|
||||||
- THROW unwinds to most recent handler and jumps to finally (if present) or catch
|
|
||||||
- VM does NOT automatically jump to finally on success - compiler must generate JUMPs
|
|
||||||
- Finally execution in all cases is compiler's responsibility, not VM's
|
|
||||||
|
|
||||||
### Calling Convention
|
|
||||||
All calls push arguments in order:
|
|
||||||
1. Function
|
|
||||||
2. Positional args (in order)
|
|
||||||
3. Named args (key1, val1, key2, val2, ...)
|
|
||||||
4. Positional count (as number)
|
|
||||||
5. Named count (as number)
|
|
||||||
6. CALL or TAIL_CALL
|
|
||||||
|
|
||||||
### CALL_NATIVE Behavior
|
|
||||||
Unlike CALL, CALL_NATIVE consumes the **entire stack** as arguments and clears the stack. The native function receives all values that were on the stack at the time of the call.
|
|
||||||
|
|
||||||
### Empty Stack
|
|
||||||
- RETURN with empty stack returns null
|
|
||||||
- HALT with empty stack returns null
|
|
||||||
|
|
@ -19,7 +19,7 @@ It's where Shrimp live.
|
||||||
- Dictionary operations (MAKE_DICT, DICT_GET, DICT_SET, DICT_HAS)
|
- Dictionary operations (MAKE_DICT, DICT_GET, DICT_SET, DICT_HAS)
|
||||||
- Function operations (MAKE_FUNCTION, CALL, TAIL_CALL, RETURN) with parameter binding
|
- Function operations (MAKE_FUNCTION, CALL, TAIL_CALL, RETURN) with parameter binding
|
||||||
- Variadic functions with positional rest parameters (`...rest`)
|
- Variadic functions with positional rest parameters (`...rest`)
|
||||||
- Named arguments (named) that collect unmatched named args into a dict (`@named`)
|
- Named arguments (kwargs) that collect unmatched named args into a dict (`@named`)
|
||||||
- Mixed positional and named arguments with proper priority binding
|
- Mixed positional and named arguments with proper priority binding
|
||||||
- Tail call optimization with unbounded recursion (10,000+ iterations without stack overflow)
|
- Tail call optimization with unbounded recursion (10,000+ iterations without stack overflow)
|
||||||
- Exception handling (PUSH_TRY, PUSH_FINALLY, POP_TRY, THROW) with nested try/finally blocks and call stack unwinding
|
- Exception handling (PUSH_TRY, PUSH_FINALLY, POP_TRY, THROW) with nested try/finally blocks and call stack unwinding
|
||||||
|
|
|
||||||
19
bin/validate
19
bin/validate
|
|
@ -1,19 +0,0 @@
|
||||||
#!/usr/bin/env bun
|
|
||||||
import { validateBytecode, formatValidationErrors } from "../src/validator"
|
|
||||||
|
|
||||||
const args = process.argv.slice(2)
|
|
||||||
|
|
||||||
if (args.length === 0) {
|
|
||||||
console.error("Usage: validate <file.reef>")
|
|
||||||
process.exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
const filePath = args[0]!
|
|
||||||
const source = await Bun.file(filePath).text()
|
|
||||||
const result = validateBytecode(source)
|
|
||||||
|
|
||||||
console.log(formatValidationErrors(result))
|
|
||||||
|
|
||||||
if (!result.valid) {
|
|
||||||
process.exit(1)
|
|
||||||
}
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
import { VM, toBytecode, type Value, toString, toNull } from "#reef"
|
|
||||||
|
|
||||||
const bytecode = toBytecode(`
|
|
||||||
PUSH 5
|
|
||||||
PUSH 10
|
|
||||||
ADD
|
|
||||||
CALL_NATIVE print
|
|
||||||
`)
|
|
||||||
|
|
||||||
const vm = new VM(bytecode)
|
|
||||||
|
|
||||||
vm.registerFunction('print', (...args: Value[]): Value => {
|
|
||||||
console.log(...args.map(toString))
|
|
||||||
return toNull()
|
|
||||||
})
|
|
||||||
|
|
||||||
console.write('5 + 10 = ')
|
|
||||||
|
|
||||||
await vm.run()
|
|
||||||
|
|
@ -1,12 +1,8 @@
|
||||||
import type { Bytecode } from "./bytecode"
|
import type { Bytecode } from "./bytecode"
|
||||||
import { type Value } from "./value"
|
import type { Value } from "./value"
|
||||||
import { VM } from "./vm"
|
import { VM } from "./vm"
|
||||||
|
|
||||||
export async function run(bytecode: Bytecode): Promise<Value> {
|
export async function run(bytecode: Bytecode): Promise<Value> {
|
||||||
const vm = new VM(bytecode)
|
const vm = new VM(bytecode)
|
||||||
return await vm.run()
|
return await vm.run()
|
||||||
}
|
}
|
||||||
|
|
||||||
export { type Bytecode, toBytecode } from "./bytecode"
|
|
||||||
export { type Value, toValue, toString, toNumber, toJs, toNull } from "./value"
|
|
||||||
export { VM } from "./vm"
|
|
||||||
|
|
@ -1,65 +1,65 @@
|
||||||
export enum OpCode {
|
export enum OpCode {
|
||||||
// stack
|
// stack
|
||||||
PUSH, // operand: constant index (number) | stack: [] → [value]
|
PUSH, // operand: constant index (number)
|
||||||
POP, // operand: none | stack: [value] → []
|
POP, // operand: none
|
||||||
DUP, // operand: none | stack: [value] → [value, value]
|
DUP, // operand: none
|
||||||
|
|
||||||
// variables
|
// variables
|
||||||
LOAD, // operand: variable name (identifier) | stack: [] → [value]
|
LOAD, // operand: variable name (string)
|
||||||
STORE, // operand: variable name (identifier) | stack: [value] → []
|
STORE, // operand: variable name (string)
|
||||||
|
|
||||||
// math (coerce to number, pop 2, push result)
|
// math
|
||||||
ADD, // operand: none | stack: [a, b] → [a + b]
|
ADD,
|
||||||
SUB, // operand: none | stack: [a, b] → [a - b]
|
SUB,
|
||||||
MUL, // operand: none | stack: [a, b] → [a * b]
|
MUL,
|
||||||
DIV, // operand: none | stack: [a, b] → [a / b]
|
DIV,
|
||||||
MOD, // operand: none | stack: [a, b] → [a % b]
|
MOD,
|
||||||
|
|
||||||
// comparison (pop 2, push boolean)
|
// comparison
|
||||||
EQ, // operand: none | stack: [a, b] → [a == b] (deep equality)
|
EQ,
|
||||||
NEQ, // operand: none | stack: [a, b] → [a != b]
|
NEQ,
|
||||||
LT, // operand: none | stack: [a, b] → [a < b] (numeric)
|
LT,
|
||||||
GT, // operand: none | stack: [a, b] → [a > b] (numeric)
|
GT,
|
||||||
LTE, // operand: none | stack: [a, b] → [a <= b] (numeric)
|
LTE,
|
||||||
GTE, // operand: none | stack: [a, b] → [a >= b] (numeric)
|
GTE,
|
||||||
|
|
||||||
// logical
|
// logical
|
||||||
NOT, // operand: none | stack: [a] → [!isTrue(a)]
|
NOT,
|
||||||
|
|
||||||
// control flow
|
// control flow
|
||||||
JUMP, // operand: relative offset (number) | PC-relative jump
|
JUMP,
|
||||||
JUMP_IF_FALSE, // operand: relative offset (number) | stack: [condition] → [] | jump if falsy
|
JUMP_IF_FALSE,
|
||||||
JUMP_IF_TRUE, // operand: relative offset (number) | stack: [condition] → [] | jump if truthy
|
JUMP_IF_TRUE,
|
||||||
BREAK, // operand: none | unwind call stack to break target
|
BREAK,
|
||||||
|
|
||||||
// exception handling
|
// exception handling
|
||||||
PUSH_TRY, // operand: absolute catch address (number) | register exception handler
|
PUSH_TRY,
|
||||||
PUSH_FINALLY, // operand: absolute finally address (number) | add finally to current handler
|
PUSH_FINALLY,
|
||||||
POP_TRY, // operand: none | remove exception handler (try completed successfully)
|
POP_TRY,
|
||||||
THROW, // operand: none | stack: [error] → (unwound) | throw exception
|
THROW,
|
||||||
|
|
||||||
// functions
|
// functions
|
||||||
MAKE_FUNCTION, // operand: function def index (number) | stack: [] → [function] | captures scope
|
MAKE_FUNCTION,
|
||||||
CALL, // operand: none | stack: [fn, ...args, posCount, namedCount] → [result] | marks break target
|
CALL,
|
||||||
TAIL_CALL, // operand: none | stack: [fn, ...args, posCount, namedCount] → [result] | reuses frame
|
TAIL_CALL,
|
||||||
RETURN, // operand: none | stack: [value] → (restored with value) | return from function
|
RETURN,
|
||||||
|
|
||||||
// arrays
|
// arrays
|
||||||
MAKE_ARRAY, // operand: item count (number) | stack: [item1, ..., itemN] → [array]
|
MAKE_ARRAY,
|
||||||
ARRAY_GET, // operand: none | stack: [array, index] → [value]
|
ARRAY_GET,
|
||||||
ARRAY_SET, // operand: none | stack: [array, index, value] → [] | mutates array
|
ARRAY_SET,
|
||||||
ARRAY_PUSH, // operand: none | stack: [array, value] → [] | mutates array
|
ARRAY_PUSH,
|
||||||
ARRAY_LEN, // operand: none | stack: [array] → [length]
|
ARRAY_LEN,
|
||||||
|
|
||||||
// dicts
|
// dicts
|
||||||
MAKE_DICT, // operand: pair count (number) | stack: [key1, val1, ..., keyN, valN] → [dict]
|
MAKE_DICT,
|
||||||
DICT_GET, // operand: none | stack: [dict, key] → [value] | returns null if missing
|
DICT_GET,
|
||||||
DICT_SET, // operand: none | stack: [dict, key, value] → [] | mutates dict
|
DICT_SET,
|
||||||
DICT_HAS, // operand: none | stack: [dict, key] → [boolean]
|
DICT_HAS,
|
||||||
|
|
||||||
// typescript interop
|
// typescript interop
|
||||||
CALL_NATIVE, // operand: function name (identifier) | stack: [...args] → [result] | consumes entire stack
|
CALL_NATIVE,
|
||||||
|
|
||||||
// special
|
// special
|
||||||
HALT // operand: none | stop execution
|
HALT
|
||||||
}
|
}
|
||||||
333
src/validator.ts
333
src/validator.ts
|
|
@ -1,333 +0,0 @@
|
||||||
import { OpCode } from "./opcode"
|
|
||||||
|
|
||||||
export type ValidationError = {
|
|
||||||
line: number
|
|
||||||
message: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export type ValidationResult = {
|
|
||||||
valid: boolean
|
|
||||||
errors: ValidationError[]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Opcodes that require operands
|
|
||||||
const OPCODES_WITH_OPERANDS = new Set([
|
|
||||||
OpCode.PUSH,
|
|
||||||
OpCode.LOAD,
|
|
||||||
OpCode.STORE,
|
|
||||||
OpCode.JUMP,
|
|
||||||
OpCode.JUMP_IF_FALSE,
|
|
||||||
OpCode.JUMP_IF_TRUE,
|
|
||||||
OpCode.PUSH_TRY,
|
|
||||||
OpCode.PUSH_FINALLY,
|
|
||||||
OpCode.MAKE_ARRAY,
|
|
||||||
OpCode.MAKE_DICT,
|
|
||||||
OpCode.MAKE_FUNCTION,
|
|
||||||
OpCode.CALL_NATIVE,
|
|
||||||
])
|
|
||||||
|
|
||||||
// Opcodes that should NOT have operands
|
|
||||||
const OPCODES_WITHOUT_OPERANDS = new Set([
|
|
||||||
OpCode.POP,
|
|
||||||
OpCode.DUP,
|
|
||||||
OpCode.ADD,
|
|
||||||
OpCode.SUB,
|
|
||||||
OpCode.MUL,
|
|
||||||
OpCode.DIV,
|
|
||||||
OpCode.MOD,
|
|
||||||
OpCode.EQ,
|
|
||||||
OpCode.NEQ,
|
|
||||||
OpCode.LT,
|
|
||||||
OpCode.GT,
|
|
||||||
OpCode.LTE,
|
|
||||||
OpCode.GTE,
|
|
||||||
OpCode.NOT,
|
|
||||||
OpCode.HALT,
|
|
||||||
OpCode.BREAK,
|
|
||||||
OpCode.POP_TRY,
|
|
||||||
OpCode.THROW,
|
|
||||||
OpCode.CALL,
|
|
||||||
OpCode.TAIL_CALL,
|
|
||||||
OpCode.RETURN,
|
|
||||||
OpCode.ARRAY_GET,
|
|
||||||
OpCode.ARRAY_SET,
|
|
||||||
OpCode.ARRAY_PUSH,
|
|
||||||
OpCode.ARRAY_LEN,
|
|
||||||
OpCode.DICT_GET,
|
|
||||||
OpCode.DICT_SET,
|
|
||||||
OpCode.DICT_HAS,
|
|
||||||
])
|
|
||||||
|
|
||||||
export function validateBytecode(source: string): ValidationResult {
|
|
||||||
const errors: ValidationError[] = []
|
|
||||||
const lines = source.split("\n")
|
|
||||||
const labels = new Map<string, number>()
|
|
||||||
const labelReferences = new Map<string, number[]>()
|
|
||||||
|
|
||||||
let instructionCount = 0
|
|
||||||
|
|
||||||
// First pass: collect labels and check for duplicates
|
|
||||||
for (let i = 0; i < lines.length; i++) {
|
|
||||||
const lineNum = i + 1
|
|
||||||
let line = lines[i]!
|
|
||||||
|
|
||||||
// Strip comments
|
|
||||||
const commentIndex = line.indexOf(';')
|
|
||||||
if (commentIndex !== -1) {
|
|
||||||
line = line.slice(0, commentIndex)
|
|
||||||
}
|
|
||||||
|
|
||||||
const trimmed = line.trim()
|
|
||||||
if (!trimmed) continue
|
|
||||||
|
|
||||||
// Check for label definition
|
|
||||||
if (/^\.[a-zA-Z_][a-zA-Z0-9_]*:$/.test(trimmed)) {
|
|
||||||
const labelName = trimmed.slice(1, -1)
|
|
||||||
if (labels.has(labelName)) {
|
|
||||||
errors.push({
|
|
||||||
line: lineNum,
|
|
||||||
message: `Duplicate label: .${labelName} (first defined at line ${labels.get(labelName)})`,
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
labels.set(labelName, lineNum)
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
instructionCount++
|
|
||||||
}
|
|
||||||
|
|
||||||
// Second pass: validate instructions
|
|
||||||
instructionCount = 0
|
|
||||||
for (let i = 0; i < lines.length; i++) {
|
|
||||||
const lineNum = i + 1
|
|
||||||
let line = lines[i]!
|
|
||||||
|
|
||||||
// Strip comments
|
|
||||||
const commentIndex = line.indexOf(';')
|
|
||||||
if (commentIndex !== -1) {
|
|
||||||
line = line.slice(0, commentIndex)
|
|
||||||
}
|
|
||||||
|
|
||||||
const trimmed = line.trim()
|
|
||||||
if (!trimmed) continue
|
|
||||||
|
|
||||||
// Skip label definitions
|
|
||||||
if (/^\.[a-zA-Z_][a-zA-Z0-9_]*:$/.test(trimmed)) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
instructionCount++
|
|
||||||
|
|
||||||
const parts = trimmed.split(/\s+/)
|
|
||||||
const opName = parts[0]!
|
|
||||||
const operand = parts.slice(1).join(' ')
|
|
||||||
|
|
||||||
// Check if opcode exists
|
|
||||||
const opCode = OpCode[opName as keyof typeof OpCode]
|
|
||||||
if (opCode === undefined) {
|
|
||||||
errors.push({
|
|
||||||
line: lineNum,
|
|
||||||
message: `Unknown opcode: ${opName}`,
|
|
||||||
})
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check operand requirements
|
|
||||||
if (OPCODES_WITH_OPERANDS.has(opCode) && !operand) {
|
|
||||||
errors.push({
|
|
||||||
line: lineNum,
|
|
||||||
message: `${opName} requires an operand`,
|
|
||||||
})
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if (OPCODES_WITHOUT_OPERANDS.has(opCode) && operand) {
|
|
||||||
errors.push({
|
|
||||||
line: lineNum,
|
|
||||||
message: `${opName} does not take an operand`,
|
|
||||||
})
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate specific operand formats
|
|
||||||
if (operand) {
|
|
||||||
// Check for label references
|
|
||||||
if (operand.startsWith('.') && !operand.includes('(')) {
|
|
||||||
const labelName = operand.slice(1)
|
|
||||||
if (!labelReferences.has(labelName)) {
|
|
||||||
labelReferences.set(labelName, [])
|
|
||||||
}
|
|
||||||
labelReferences.get(labelName)!.push(lineNum)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate MAKE_FUNCTION syntax
|
|
||||||
if (opCode === OpCode.MAKE_FUNCTION) {
|
|
||||||
if (!operand.startsWith('(')) {
|
|
||||||
errors.push({
|
|
||||||
line: lineNum,
|
|
||||||
message: `MAKE_FUNCTION requires parameter list: MAKE_FUNCTION (params) address`,
|
|
||||||
})
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
const match = operand.match(/^(\(.*?\))\s+(.+)$/)
|
|
||||||
if (!match) {
|
|
||||||
errors.push({
|
|
||||||
line: lineNum,
|
|
||||||
message: `Invalid MAKE_FUNCTION syntax: expected (params) address`,
|
|
||||||
})
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
const [, paramStr, bodyAddr] = match
|
|
||||||
|
|
||||||
// Validate parameter syntax
|
|
||||||
const paramList = paramStr!.slice(1, -1).trim()
|
|
||||||
if (paramList) {
|
|
||||||
const params = paramList.split(/\s+/)
|
|
||||||
let seenVariadic = false
|
|
||||||
let seenNamed = false
|
|
||||||
|
|
||||||
for (const param of params) {
|
|
||||||
// Check for invalid order
|
|
||||||
if (seenVariadic && !param.startsWith('@')) {
|
|
||||||
errors.push({
|
|
||||||
line: lineNum,
|
|
||||||
message: `Invalid parameter order: variadic parameter (...) must come before named parameter (@)`,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (seenNamed) {
|
|
||||||
errors.push({
|
|
||||||
line: lineNum,
|
|
||||||
message: `Invalid parameter order: named parameter (@) must be last`,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check parameter format
|
|
||||||
if (param.startsWith('...')) {
|
|
||||||
seenVariadic = true
|
|
||||||
const name = param.slice(3)
|
|
||||||
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name)) {
|
|
||||||
errors.push({
|
|
||||||
line: lineNum,
|
|
||||||
message: `Invalid variadic parameter name: ${param}`,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
} else if (param.startsWith('@')) {
|
|
||||||
seenNamed = true
|
|
||||||
const name = param.slice(1)
|
|
||||||
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name)) {
|
|
||||||
errors.push({
|
|
||||||
line: lineNum,
|
|
||||||
message: `Invalid named parameter name: ${param}`,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
} else if (param.includes('=')) {
|
|
||||||
// Default parameter
|
|
||||||
const [name, defaultValue] = param.split('=')
|
|
||||||
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name!.trim())) {
|
|
||||||
errors.push({
|
|
||||||
line: lineNum,
|
|
||||||
message: `Invalid parameter name: ${name}`,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Regular parameter
|
|
||||||
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(param)) {
|
|
||||||
errors.push({
|
|
||||||
line: lineNum,
|
|
||||||
message: `Invalid parameter name: ${param}`,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate body address
|
|
||||||
if (!bodyAddr!.startsWith('.') && !bodyAddr!.startsWith('#')) {
|
|
||||||
errors.push({
|
|
||||||
line: lineNum,
|
|
||||||
message: `Invalid body address: expected .label or #offset`,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// If it's a label, track it
|
|
||||||
if (bodyAddr!.startsWith('.')) {
|
|
||||||
const labelName = bodyAddr!.slice(1)
|
|
||||||
if (!labelReferences.has(labelName)) {
|
|
||||||
labelReferences.set(labelName, [])
|
|
||||||
}
|
|
||||||
labelReferences.get(labelName)!.push(lineNum)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate immediate numbers
|
|
||||||
if (operand.startsWith('#')) {
|
|
||||||
const numStr = operand.slice(1)
|
|
||||||
if (!/^-?\d+$/.test(numStr)) {
|
|
||||||
errors.push({
|
|
||||||
line: lineNum,
|
|
||||||
message: `Invalid immediate number: ${operand}`,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate variable names for LOAD/STORE
|
|
||||||
if ((opCode === OpCode.LOAD || opCode === OpCode.STORE) &&
|
|
||||||
!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(operand)) {
|
|
||||||
errors.push({
|
|
||||||
line: lineNum,
|
|
||||||
message: `Invalid variable name: ${operand}`,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate string constants
|
|
||||||
if ((operand.startsWith('"') || operand.startsWith("'")) &&
|
|
||||||
!(operand.endsWith('"') || operand.endsWith("'"))) {
|
|
||||||
errors.push({
|
|
||||||
line: lineNum,
|
|
||||||
message: `Unterminated string: ${operand}`,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for undefined label references
|
|
||||||
for (const [labelName, refLines] of labelReferences) {
|
|
||||||
if (!labels.has(labelName)) {
|
|
||||||
for (const refLine of refLines) {
|
|
||||||
errors.push({
|
|
||||||
line: refLine,
|
|
||||||
message: `Undefined label: .${labelName}`,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sort errors by line number
|
|
||||||
errors.sort((a, b) => a.line - b.line)
|
|
||||||
|
|
||||||
return {
|
|
||||||
valid: errors.length === 0,
|
|
||||||
errors,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function formatValidationErrors(result: ValidationResult): string {
|
|
||||||
if (result.valid) {
|
|
||||||
return "✓ Bytecode is valid"
|
|
||||||
}
|
|
||||||
|
|
||||||
const lines: string[] = [
|
|
||||||
`✗ Found ${result.errors.length} error${result.errors.length === 1 ? '' : 's'}:`,
|
|
||||||
'',
|
|
||||||
]
|
|
||||||
|
|
||||||
for (const error of result.errors) {
|
|
||||||
lines.push(` Line ${error.line}: ${error.message}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
return lines.join('\n')
|
|
||||||
}
|
|
||||||
|
|
@ -131,8 +131,4 @@ export function toJs(v: Value): any {
|
||||||
case 'dict': return Object.fromEntries(v.value.entries().map(([k, v]) => [k, toJs(v)]))
|
case 'dict': return Object.fromEntries(v.value.entries().map(([k, v]) => [k, toJs(v)]))
|
||||||
case 'function': return '<function>'
|
case 'function': return '<function>'
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
export function toNull(): Value {
|
|
||||||
return toValue(null)
|
|
||||||
}
|
}
|
||||||
14
src/vm.ts
14
src/vm.ts
|
|
@ -386,7 +386,7 @@ export class VM {
|
||||||
// Check if named argument was provided for this param
|
// Check if named argument was provided for this param
|
||||||
if (namedArgs.has(paramName)) {
|
if (namedArgs.has(paramName)) {
|
||||||
this.scope.set(paramName, namedArgs.get(paramName)!)
|
this.scope.set(paramName, namedArgs.get(paramName)!)
|
||||||
namedArgs.delete(paramName) // Remove from named args so it won't go to named
|
namedArgs.delete(paramName) // Remove from named args so it won't go to kwargs
|
||||||
} else if (positionalArgs[i] !== undefined) {
|
} else if (positionalArgs[i] !== undefined) {
|
||||||
this.scope.set(paramName, positionalArgs[i]!)
|
this.scope.set(paramName, positionalArgs[i]!)
|
||||||
} else if (fn.defaults[paramName] !== undefined) {
|
} else if (fn.defaults[paramName] !== undefined) {
|
||||||
|
|
@ -410,11 +410,11 @@ export class VM {
|
||||||
// Handle named parameter (collect remaining named args that didn't match params)
|
// Handle named parameter (collect remaining named args that didn't match params)
|
||||||
if (fn.named) {
|
if (fn.named) {
|
||||||
const namedParamName = fn.params[fn.params.length - 1]!
|
const namedParamName = fn.params[fn.params.length - 1]!
|
||||||
const namedDict = new Map<string, Value>()
|
const kwargsDict = new Map<string, Value>()
|
||||||
for (const [key, value] of namedArgs) {
|
for (const [key, value] of namedArgs) {
|
||||||
namedDict.set(key, value)
|
kwargsDict.set(key, value)
|
||||||
}
|
}
|
||||||
this.scope.set(namedParamName, { type: 'dict', value: namedDict })
|
this.scope.set(namedParamName, { type: 'dict', value: kwargsDict })
|
||||||
}
|
}
|
||||||
|
|
||||||
// subtract 1 because pc was incremented
|
// subtract 1 because pc was incremented
|
||||||
|
|
@ -488,11 +488,11 @@ export class VM {
|
||||||
// Handle named parameter
|
// Handle named parameter
|
||||||
if (tailFn.named) {
|
if (tailFn.named) {
|
||||||
const namedParamName = tailFn.params[tailFn.params.length - 1]!
|
const namedParamName = tailFn.params[tailFn.params.length - 1]!
|
||||||
const namedDict = new Map<string, Value>()
|
const kwargsDict = new Map<string, Value>()
|
||||||
for (const [key, value] of tailNamedArgs) {
|
for (const [key, value] of tailNamedArgs) {
|
||||||
namedDict.set(key, value)
|
kwargsDict.set(key, value)
|
||||||
}
|
}
|
||||||
this.scope.set(namedParamName, { type: 'dict', value: namedDict })
|
this.scope.set(namedParamName, { type: 'dict', value: kwargsDict })
|
||||||
}
|
}
|
||||||
|
|
||||||
// subtract 1 because PC was incremented
|
// subtract 1 because PC was incremented
|
||||||
|
|
|
||||||
|
|
@ -1,31 +0,0 @@
|
||||||
import { test, expect } from "bun:test"
|
|
||||||
import { readdirSync } from "fs"
|
|
||||||
import { join } from "path"
|
|
||||||
import { toBytecode } from "#bytecode"
|
|
||||||
import { VM } from "#vm"
|
|
||||||
|
|
||||||
// Get all .reef files from examples directory
|
|
||||||
const examplesDir = join(import.meta.dir, "..", "examples")
|
|
||||||
const exampleFiles = readdirSync(examplesDir)
|
|
||||||
.filter(file => file.endsWith(".reef"))
|
|
||||||
.sort()
|
|
||||||
|
|
||||||
// Create a test for each example file
|
|
||||||
for (const file of exampleFiles) {
|
|
||||||
test(`examples/${file} runs without errors`, async () => {
|
|
||||||
const filePath = join(examplesDir, file)
|
|
||||||
const content = await Bun.file(filePath).text()
|
|
||||||
|
|
||||||
// Parse and run the bytecode
|
|
||||||
const bytecode = toBytecode(content)
|
|
||||||
const vm = new VM(bytecode)
|
|
||||||
|
|
||||||
// Should not throw
|
|
||||||
const result = await vm.run()
|
|
||||||
|
|
||||||
// Result should be a valid Value
|
|
||||||
expect(result).toBeDefined()
|
|
||||||
expect(result).toHaveProperty("type")
|
|
||||||
expect(["null", "boolean", "number", "string", "array", "dict", "function"]).toContain(result.type)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
@ -201,7 +201,7 @@ test("TAIL_CALL - variadic function", async () => {
|
||||||
|
|
||||||
test("CALL - named args function with no fixed params", async () => {
|
test("CALL - named args function with no fixed params", async () => {
|
||||||
const bytecode = toBytecode(`
|
const bytecode = toBytecode(`
|
||||||
MAKE_FUNCTION (@named) #9
|
MAKE_FUNCTION (@kwargs) #9
|
||||||
PUSH "name"
|
PUSH "name"
|
||||||
PUSH "Bob"
|
PUSH "Bob"
|
||||||
PUSH "age"
|
PUSH "age"
|
||||||
|
|
@ -210,7 +210,7 @@ test("CALL - named args function with no fixed params", async () => {
|
||||||
PUSH 2
|
PUSH 2
|
||||||
CALL
|
CALL
|
||||||
HALT
|
HALT
|
||||||
LOAD named
|
LOAD kwargs
|
||||||
RETURN
|
RETURN
|
||||||
`)
|
`)
|
||||||
|
|
||||||
|
|
@ -224,7 +224,7 @@ test("CALL - named args function with no fixed params", async () => {
|
||||||
|
|
||||||
test("CALL - named args function with one fixed param", async () => {
|
test("CALL - named args function with one fixed param", async () => {
|
||||||
const bytecode = toBytecode(`
|
const bytecode = toBytecode(`
|
||||||
MAKE_FUNCTION (x @named) #8
|
MAKE_FUNCTION (x @kwargs) #8
|
||||||
PUSH 10
|
PUSH 10
|
||||||
PUSH "name"
|
PUSH "name"
|
||||||
PUSH "Alice"
|
PUSH "Alice"
|
||||||
|
|
@ -232,7 +232,7 @@ test("CALL - named args function with one fixed param", async () => {
|
||||||
PUSH 1
|
PUSH 1
|
||||||
CALL
|
CALL
|
||||||
HALT
|
HALT
|
||||||
LOAD named
|
LOAD kwargs
|
||||||
RETURN
|
RETURN
|
||||||
`)
|
`)
|
||||||
|
|
||||||
|
|
@ -244,9 +244,9 @@ test("CALL - named args function with one fixed param", async () => {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
test("CALL - named args with matching param name should bind to param not named", async () => {
|
test("CALL - named args with matching param name should bind to param not kwargs", async () => {
|
||||||
const bytecode = toBytecode(`
|
const bytecode = toBytecode(`
|
||||||
MAKE_FUNCTION (name @named) #8
|
MAKE_FUNCTION (name @kwargs) #8
|
||||||
PUSH "Bob"
|
PUSH "Bob"
|
||||||
PUSH "age"
|
PUSH "age"
|
||||||
PUSH 50
|
PUSH 50
|
||||||
|
|
@ -259,13 +259,13 @@ test("CALL - named args with matching param name should bind to param not named"
|
||||||
`)
|
`)
|
||||||
|
|
||||||
const result = await new VM(bytecode).run()
|
const result = await new VM(bytecode).run()
|
||||||
// name should be bound as regular param, not collected in named
|
// name should be bound as regular param, not collected in kwargs
|
||||||
expect(result).toEqual({ type: 'string', value: 'Bob' })
|
expect(result).toEqual({ type: 'string', value: 'Bob' })
|
||||||
})
|
})
|
||||||
|
|
||||||
test("CALL - named args that match param names should not be in named", async () => {
|
test("CALL - named args that match param names should not be in kwargs", async () => {
|
||||||
const bytecode = toBytecode(`
|
const bytecode = toBytecode(`
|
||||||
MAKE_FUNCTION (name age @named) #9
|
MAKE_FUNCTION (name age @kwargs) #9
|
||||||
PUSH "name"
|
PUSH "name"
|
||||||
PUSH "Bob"
|
PUSH "Bob"
|
||||||
PUSH "city"
|
PUSH "city"
|
||||||
|
|
@ -274,14 +274,14 @@ test("CALL - named args that match param names should not be in named", async ()
|
||||||
PUSH 2
|
PUSH 2
|
||||||
CALL
|
CALL
|
||||||
HALT
|
HALT
|
||||||
LOAD named
|
LOAD kwargs
|
||||||
RETURN
|
RETURN
|
||||||
`)
|
`)
|
||||||
|
|
||||||
const result = await new VM(bytecode).run()
|
const result = await new VM(bytecode).run()
|
||||||
expect(result.type).toBe('dict')
|
expect(result.type).toBe('dict')
|
||||||
if (result.type === 'dict') {
|
if (result.type === 'dict') {
|
||||||
// Only city should be in named, name should be bound to param
|
// Only city should be in kwargs, name should be bound to param
|
||||||
expect(result.value.get('city')).toEqual({ type: 'string', value: 'NYC' })
|
expect(result.value.get('city')).toEqual({ type: 'string', value: 'NYC' })
|
||||||
expect(result.value.has('name')).toBe(false)
|
expect(result.value.has('name')).toBe(false)
|
||||||
expect(result.value.size).toBe(1)
|
expect(result.value.size).toBe(1)
|
||||||
|
|
@ -290,7 +290,7 @@ test("CALL - named args that match param names should not be in named", async ()
|
||||||
|
|
||||||
test("CALL - mixed variadic and named args", async () => {
|
test("CALL - mixed variadic and named args", async () => {
|
||||||
const bytecode = toBytecode(`
|
const bytecode = toBytecode(`
|
||||||
MAKE_FUNCTION (x ...rest @named) #10
|
MAKE_FUNCTION (x ...rest @kwargs) #10
|
||||||
PUSH 1
|
PUSH 1
|
||||||
PUSH 2
|
PUSH 2
|
||||||
PUSH 3
|
PUSH 3
|
||||||
|
|
@ -315,9 +315,9 @@ test("CALL - mixed variadic and named args", async () => {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
test("CALL - mixed variadic and named args, check named", async () => {
|
test("CALL - mixed variadic and named args, check kwargs", async () => {
|
||||||
const bytecode = toBytecode(`
|
const bytecode = toBytecode(`
|
||||||
MAKE_FUNCTION (x ...rest @named) #10
|
MAKE_FUNCTION (x ...rest @kwargs) #10
|
||||||
PUSH 1
|
PUSH 1
|
||||||
PUSH 2
|
PUSH 2
|
||||||
PUSH 3
|
PUSH 3
|
||||||
|
|
@ -327,7 +327,7 @@ test("CALL - mixed variadic and named args, check named", async () => {
|
||||||
PUSH 1
|
PUSH 1
|
||||||
CALL
|
CALL
|
||||||
HALT
|
HALT
|
||||||
LOAD named
|
LOAD kwargs
|
||||||
RETURN
|
RETURN
|
||||||
`)
|
`)
|
||||||
|
|
||||||
|
|
@ -340,18 +340,18 @@ test("CALL - mixed variadic and named args, check named", async () => {
|
||||||
|
|
||||||
test("CALL - named args with no extra named args", async () => {
|
test("CALL - named args with no extra named args", async () => {
|
||||||
const bytecode = toBytecode(`
|
const bytecode = toBytecode(`
|
||||||
MAKE_FUNCTION (x @named) #6
|
MAKE_FUNCTION (x @kwargs) #6
|
||||||
PUSH 10
|
PUSH 10
|
||||||
PUSH 1
|
PUSH 1
|
||||||
PUSH 0
|
PUSH 0
|
||||||
CALL
|
CALL
|
||||||
HALT
|
HALT
|
||||||
LOAD named
|
LOAD kwargs
|
||||||
RETURN
|
RETURN
|
||||||
`)
|
`)
|
||||||
|
|
||||||
const result = await new VM(bytecode).run()
|
const result = await new VM(bytecode).run()
|
||||||
// named should be empty dict
|
// kwargs should be empty dict
|
||||||
expect(result.type).toBe('dict')
|
expect(result.type).toBe('dict')
|
||||||
if (result.type === 'dict') {
|
if (result.type === 'dict') {
|
||||||
expect(result.value.size).toBe(0)
|
expect(result.value.size).toBe(0)
|
||||||
|
|
@ -360,7 +360,7 @@ test("CALL - named args with no extra named args", async () => {
|
||||||
|
|
||||||
test("CALL - named args with defaults on fixed params", async () => {
|
test("CALL - named args with defaults on fixed params", async () => {
|
||||||
const bytecode = toBytecode(`
|
const bytecode = toBytecode(`
|
||||||
MAKE_FUNCTION (x=5 @named) #7
|
MAKE_FUNCTION (x=5 @kwargs) #7
|
||||||
PUSH "name"
|
PUSH "name"
|
||||||
PUSH "Alice"
|
PUSH "Alice"
|
||||||
PUSH 0
|
PUSH 0
|
||||||
|
|
|
||||||
|
|
@ -1,202 +0,0 @@
|
||||||
import { test, expect } from "bun:test"
|
|
||||||
import { validateBytecode, formatValidationErrors } from "#validator"
|
|
||||||
|
|
||||||
test("valid bytecode passes validation", () => {
|
|
||||||
const source = `
|
|
||||||
PUSH 1
|
|
||||||
PUSH 2
|
|
||||||
ADD
|
|
||||||
HALT
|
|
||||||
`
|
|
||||||
const result = validateBytecode(source)
|
|
||||||
expect(result.valid).toBe(true)
|
|
||||||
expect(result.errors).toHaveLength(0)
|
|
||||||
})
|
|
||||||
|
|
||||||
test("valid bytecode with labels passes validation", () => {
|
|
||||||
const source = `
|
|
||||||
JUMP .end
|
|
||||||
PUSH 999
|
|
||||||
.end:
|
|
||||||
PUSH 42
|
|
||||||
HALT
|
|
||||||
`
|
|
||||||
const result = validateBytecode(source)
|
|
||||||
expect(result.valid).toBe(true)
|
|
||||||
expect(result.errors).toHaveLength(0)
|
|
||||||
})
|
|
||||||
|
|
||||||
test("detects unknown opcode", () => {
|
|
||||||
const source = `
|
|
||||||
PUSH 1
|
|
||||||
INVALID_OP
|
|
||||||
HALT
|
|
||||||
`
|
|
||||||
const result = validateBytecode(source)
|
|
||||||
expect(result.valid).toBe(false)
|
|
||||||
expect(result.errors).toHaveLength(1)
|
|
||||||
expect(result.errors[0]!.message).toContain("Unknown opcode: INVALID_OP")
|
|
||||||
})
|
|
||||||
|
|
||||||
test("detects undefined label", () => {
|
|
||||||
const source = `
|
|
||||||
JUMP .nowhere
|
|
||||||
HALT
|
|
||||||
`
|
|
||||||
const result = validateBytecode(source)
|
|
||||||
expect(result.valid).toBe(false)
|
|
||||||
expect(result.errors).toHaveLength(1)
|
|
||||||
expect(result.errors[0]!.message).toContain("Undefined label: .nowhere")
|
|
||||||
})
|
|
||||||
|
|
||||||
test("detects duplicate labels", () => {
|
|
||||||
const source = `
|
|
||||||
.loop:
|
|
||||||
PUSH 1
|
|
||||||
.loop:
|
|
||||||
PUSH 2
|
|
||||||
`
|
|
||||||
const result = validateBytecode(source)
|
|
||||||
expect(result.valid).toBe(false)
|
|
||||||
expect(result.errors).toHaveLength(1)
|
|
||||||
expect(result.errors[0]!.message).toContain("Duplicate label: .loop")
|
|
||||||
})
|
|
||||||
|
|
||||||
test("detects missing operand", () => {
|
|
||||||
const source = `
|
|
||||||
PUSH
|
|
||||||
HALT
|
|
||||||
`
|
|
||||||
const result = validateBytecode(source)
|
|
||||||
expect(result.valid).toBe(false)
|
|
||||||
expect(result.errors).toHaveLength(1)
|
|
||||||
expect(result.errors[0]!.message).toContain("PUSH requires an operand")
|
|
||||||
})
|
|
||||||
|
|
||||||
test("detects unexpected operand", () => {
|
|
||||||
const source = `
|
|
||||||
ADD 42
|
|
||||||
HALT
|
|
||||||
`
|
|
||||||
const result = validateBytecode(source)
|
|
||||||
expect(result.valid).toBe(false)
|
|
||||||
expect(result.errors).toHaveLength(1)
|
|
||||||
expect(result.errors[0]!.message).toContain("ADD does not take an operand")
|
|
||||||
})
|
|
||||||
|
|
||||||
test("detects invalid MAKE_FUNCTION syntax", () => {
|
|
||||||
const source = `
|
|
||||||
MAKE_FUNCTION x y .body
|
|
||||||
HALT
|
|
||||||
`
|
|
||||||
const result = validateBytecode(source)
|
|
||||||
expect(result.valid).toBe(false)
|
|
||||||
expect(result.errors[0]!.message).toContain("MAKE_FUNCTION requires parameter list")
|
|
||||||
})
|
|
||||||
|
|
||||||
test("detects invalid parameter order", () => {
|
|
||||||
const source = `
|
|
||||||
MAKE_FUNCTION (x ...rest y) .body
|
|
||||||
HALT
|
|
||||||
.body:
|
|
||||||
RETURN
|
|
||||||
`
|
|
||||||
const result = validateBytecode(source)
|
|
||||||
expect(result.valid).toBe(false)
|
|
||||||
expect(result.errors[0]!.message).toContain("variadic parameter")
|
|
||||||
})
|
|
||||||
|
|
||||||
test("detects invalid parameter name", () => {
|
|
||||||
const source = `
|
|
||||||
MAKE_FUNCTION (123invalid) .body
|
|
||||||
HALT
|
|
||||||
.body:
|
|
||||||
RETURN
|
|
||||||
`
|
|
||||||
const result = validateBytecode(source)
|
|
||||||
expect(result.valid).toBe(false)
|
|
||||||
expect(result.errors[0]!.message).toContain("Invalid parameter name")
|
|
||||||
})
|
|
||||||
|
|
||||||
test("detects invalid variable name", () => {
|
|
||||||
const source = `
|
|
||||||
LOAD 123invalid
|
|
||||||
HALT
|
|
||||||
`
|
|
||||||
const result = validateBytecode(source)
|
|
||||||
expect(result.valid).toBe(false)
|
|
||||||
expect(result.errors[0]!.message).toContain("Invalid variable name")
|
|
||||||
})
|
|
||||||
|
|
||||||
test("detects unterminated string", () => {
|
|
||||||
const source = `
|
|
||||||
PUSH "unterminated
|
|
||||||
HALT
|
|
||||||
`
|
|
||||||
const result = validateBytecode(source)
|
|
||||||
expect(result.valid).toBe(false)
|
|
||||||
expect(result.errors[0]!.message).toContain("Unterminated string")
|
|
||||||
})
|
|
||||||
|
|
||||||
test("detects invalid immediate number", () => {
|
|
||||||
const source = `
|
|
||||||
MAKE_ARRAY #abc
|
|
||||||
`
|
|
||||||
const result = validateBytecode(source)
|
|
||||||
expect(result.valid).toBe(false)
|
|
||||||
expect(result.errors[0]!.message).toContain("Invalid immediate number")
|
|
||||||
})
|
|
||||||
|
|
||||||
test("handles comments correctly", () => {
|
|
||||||
const source = `
|
|
||||||
PUSH 1 ; this is a comment
|
|
||||||
; this entire line is a comment
|
|
||||||
PUSH 2
|
|
||||||
ADD ; another comment
|
|
||||||
`
|
|
||||||
const result = validateBytecode(source)
|
|
||||||
expect(result.valid).toBe(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
test("validates function with label reference", () => {
|
|
||||||
const source = `
|
|
||||||
MAKE_FUNCTION (x y) .body
|
|
||||||
JUMP .skip
|
|
||||||
.body:
|
|
||||||
LOAD x
|
|
||||||
LOAD y
|
|
||||||
ADD
|
|
||||||
RETURN
|
|
||||||
.skip:
|
|
||||||
HALT
|
|
||||||
`
|
|
||||||
const result = validateBytecode(source)
|
|
||||||
expect(result.valid).toBe(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
test("detects multiple errors and sorts by line", () => {
|
|
||||||
const source = `
|
|
||||||
UNKNOWN_OP
|
|
||||||
PUSH
|
|
||||||
JUMP .undefined
|
|
||||||
`
|
|
||||||
const result = validateBytecode(source)
|
|
||||||
expect(result.valid).toBe(false)
|
|
||||||
expect(result.errors.length).toBeGreaterThanOrEqual(2)
|
|
||||||
// Check that errors are sorted by line number
|
|
||||||
for (let i = 1; i < result.errors.length; i++) {
|
|
||||||
expect(result.errors[i]!.line).toBeGreaterThanOrEqual(result.errors[i-1]!.line)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
test("formatValidationErrors produces readable output", () => {
|
|
||||||
const source = `
|
|
||||||
PUSH 1
|
|
||||||
UNKNOWN
|
|
||||||
`
|
|
||||||
const result = validateBytecode(source)
|
|
||||||
const formatted = formatValidationErrors(result)
|
|
||||||
expect(formatted).toContain("error")
|
|
||||||
expect(formatted).toContain("Line")
|
|
||||||
expect(formatted).toContain("UNKNOWN")
|
|
||||||
})
|
|
||||||
|
|
@ -28,9 +28,6 @@
|
||||||
"paths": {
|
"paths": {
|
||||||
"#*": [
|
"#*": [
|
||||||
"./src/*"
|
"./src/*"
|
||||||
],
|
|
||||||
"#reef": [
|
|
||||||
"./src/index.ts"
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user