Compare commits
9 Commits
de30d85304
...
4f092fca3f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4f092fca3f | ||
| b50dafe79d | |||
| af7f389a04 | |||
| 67a98837ed | |||
| b42675e1d9 | |||
| 354df33894 | |||
| 66d21ce72b | |||
| d7bcc590fe | |||
| 787d2f2611 |
93
bin/repl
93
bin/repl
|
|
@ -1,24 +1,12 @@
|
||||||
#!/usr/bin/env bun
|
#!/usr/bin/env bun
|
||||||
|
|
||||||
import { Compiler } from '../src/compiler/compiler'
|
import { Compiler } from '../src/compiler/compiler'
|
||||||
import { VM, type Value, Scope, bytecodeToString } from 'reefvm'
|
import { colors, formatValue, globalFunctions } from '../src/prelude'
|
||||||
|
import { VM, Scope, bytecodeToString } from 'reefvm'
|
||||||
import * as readline from 'readline'
|
import * as readline from 'readline'
|
||||||
import { readFileSync, writeFileSync } from 'fs'
|
import { readFileSync, writeFileSync } from 'fs'
|
||||||
import { basename } from 'path'
|
import { basename } from 'path'
|
||||||
|
|
||||||
const colors = {
|
|
||||||
reset: '\x1b[0m',
|
|
||||||
bright: '\x1b[1m',
|
|
||||||
dim: '\x1b[2m',
|
|
||||||
cyan: '\x1b[36m',
|
|
||||||
yellow: '\x1b[33m',
|
|
||||||
green: '\x1b[32m',
|
|
||||||
red: '\x1b[31m',
|
|
||||||
blue: '\x1b[34m',
|
|
||||||
magenta: '\x1b[35m',
|
|
||||||
pink: '\x1b[38;2;255;105;180m'
|
|
||||||
}
|
|
||||||
|
|
||||||
async function repl() {
|
async function repl() {
|
||||||
const commands = ['/clear', '/reset', '/vars', '/funcs', '/history', '/bytecode', '/exit', '/save', '/quit']
|
const commands = ['/clear', '/reset', '/vars', '/funcs', '/history', '/bytecode', '/exit', '/save', '/quit']
|
||||||
|
|
||||||
|
|
@ -60,7 +48,7 @@ async function repl() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
vm ||= new VM({ instructions: [], constants: [] }, nativeFunctions)
|
vm ||= new VM({ instructions: [], constants: [] }, globalFunctions)
|
||||||
|
|
||||||
if (['/exit', 'exit', '/quit', 'quit'].includes(trimmed)) {
|
if (['/exit', 'exit', '/quit', 'quit'].includes(trimmed)) {
|
||||||
console.log(`\n${colors.yellow}Goodbye!${colors.reset}`)
|
console.log(`\n${colors.yellow}Goodbye!${colors.reset}`)
|
||||||
|
|
@ -186,40 +174,6 @@ async function repl() {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function formatValue(value: Value, inner = false): string {
|
|
||||||
switch (value.type) {
|
|
||||||
case 'string':
|
|
||||||
return `${colors.green}'${value.value}'${colors.reset}`
|
|
||||||
case 'number':
|
|
||||||
return `${colors.cyan}${value.value}${colors.reset}`
|
|
||||||
case 'boolean':
|
|
||||||
return `${colors.yellow}${value.value}${colors.reset}`
|
|
||||||
case 'null':
|
|
||||||
return `${colors.dim}null${colors.reset}`
|
|
||||||
case 'array': {
|
|
||||||
const items = value.value.map(x => formatValue(x, true)).join(' ')
|
|
||||||
return `${inner ? '(' : ''}${colors.blue}list${colors.reset} ${items}${inner ? ')' : ''}`
|
|
||||||
}
|
|
||||||
case 'dict': {
|
|
||||||
const entries = Array.from(value.value.entries())
|
|
||||||
.map(([k, v]) => `${k}=${formatValue(v, true)}`)
|
|
||||||
.join(' ')
|
|
||||||
return `${inner ? '(' : ''}${colors.magenta}dict${colors.reset} ${entries}${inner ? ')' : ''}`
|
|
||||||
}
|
|
||||||
case 'function': {
|
|
||||||
const params = value.params.join(', ')
|
|
||||||
return `${colors.dim}<fn(${params})>${colors.reset}`
|
|
||||||
}
|
|
||||||
case 'native':
|
|
||||||
return `${colors.dim}<native-fn>${colors.reset}`
|
|
||||||
case 'regex':
|
|
||||||
return `${colors.magenta}${value.value}${colors.reset}`
|
|
||||||
default:
|
|
||||||
return String(value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatVariables(scope: Scope, onlyFunctions = false): string {
|
function formatVariables(scope: Scope, onlyFunctions = false): string {
|
||||||
const vars: string[] = []
|
const vars: string[] = []
|
||||||
|
|
||||||
|
|
@ -257,7 +211,7 @@ async function loadFile(filePath: string): Promise<{ vm: VM; codeHistory: string
|
||||||
|
|
||||||
console.log(`${colors.dim}Loading ${basename(filePath)}...${colors.reset}`)
|
console.log(`${colors.dim}Loading ${basename(filePath)}...${colors.reset}`)
|
||||||
|
|
||||||
const vm = new VM({ instructions: [], constants: [] }, nativeFunctions)
|
const vm = new VM({ instructions: [], constants: [] }, globalFunctions)
|
||||||
await vm.run()
|
await vm.run()
|
||||||
|
|
||||||
const codeHistory: string[] = []
|
const codeHistory: string[] = []
|
||||||
|
|
@ -313,43 +267,4 @@ function showWelcome() {
|
||||||
console.log()
|
console.log()
|
||||||
}
|
}
|
||||||
|
|
||||||
const nativeFunctions = {
|
|
||||||
echo: (...args: any[]) => {
|
|
||||||
console.log(...args)
|
|
||||||
},
|
|
||||||
len: (value: any) => {
|
|
||||||
if (typeof value === 'string') return value.length
|
|
||||||
if (Array.isArray(value)) return value.length
|
|
||||||
if (value && typeof value === 'object') return Object.keys(value).length
|
|
||||||
return 0
|
|
||||||
},
|
|
||||||
type: (value: any) => {
|
|
||||||
if (value === null) return 'null'
|
|
||||||
if (Array.isArray(value)) return 'array'
|
|
||||||
return typeof value
|
|
||||||
},
|
|
||||||
range: (start: number, end: number | null) => {
|
|
||||||
if (end === null) {
|
|
||||||
end = start
|
|
||||||
start = 0
|
|
||||||
}
|
|
||||||
const result: number[] = []
|
|
||||||
for (let i = start; i <= end; i++) {
|
|
||||||
result.push(i)
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
},
|
|
||||||
join: (arr: any[], sep: string = ',') => {
|
|
||||||
return arr.join(sep)
|
|
||||||
},
|
|
||||||
split: (str: string, sep: string = ',') => {
|
|
||||||
return str.split(sep)
|
|
||||||
},
|
|
||||||
upper: (str: string) => str.toUpperCase(),
|
|
||||||
lower: (str: string) => str.toLowerCase(),
|
|
||||||
trim: (str: string) => str.trim(),
|
|
||||||
list: (...args: any[]) => args,
|
|
||||||
dict: (atNamed = {}) => atNamed
|
|
||||||
}
|
|
||||||
|
|
||||||
await repl()
|
await repl()
|
||||||
|
|
|
||||||
45
bin/shrimp
45
bin/shrimp
|
|
@ -1,57 +1,18 @@
|
||||||
#!/usr/bin/env bun
|
#!/usr/bin/env bun
|
||||||
|
|
||||||
import { Compiler } from '../src/compiler/compiler'
|
import { Compiler } from '../src/compiler/compiler'
|
||||||
import { VM, toValue, fromValue, bytecodeToString } from 'reefvm'
|
import { colors, globalFunctions } from '../src/prelude'
|
||||||
|
import { VM, fromValue, bytecodeToString } from 'reefvm'
|
||||||
import { readFileSync, writeFileSync, mkdirSync } from 'fs'
|
import { readFileSync, writeFileSync, mkdirSync } from 'fs'
|
||||||
import { randomUUID } from "crypto"
|
import { randomUUID } from "crypto"
|
||||||
import { spawn } from 'child_process'
|
import { spawn } from 'child_process'
|
||||||
import { join } from 'path'
|
import { join } from 'path'
|
||||||
|
|
||||||
const colors = {
|
|
||||||
reset: '\x1b[0m',
|
|
||||||
bright: '\x1b[1m',
|
|
||||||
dim: '\x1b[2m',
|
|
||||||
red: '\x1b[31m',
|
|
||||||
yellow: '\x1b[33m',
|
|
||||||
cyan: '\x1b[36m',
|
|
||||||
magenta: '\x1b[35m',
|
|
||||||
pink: '\x1b[38;2;255;105;180m'
|
|
||||||
}
|
|
||||||
|
|
||||||
const nativeFunctions = {
|
|
||||||
echo: (...args: any[]) => console.log(...args),
|
|
||||||
len: (value: any) => {
|
|
||||||
if (typeof value === 'string') return value.length
|
|
||||||
if (Array.isArray(value)) return value.length
|
|
||||||
if (value && typeof value === 'object') return Object.keys(value).length
|
|
||||||
return 0
|
|
||||||
},
|
|
||||||
type: (value: any) => toValue(value).type,
|
|
||||||
range: (start: number, end: number | null) => {
|
|
||||||
if (end === null) {
|
|
||||||
end = start
|
|
||||||
start = 0
|
|
||||||
}
|
|
||||||
const result: number[] = []
|
|
||||||
for (let i = start; i <= end; i++) {
|
|
||||||
result.push(i)
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
},
|
|
||||||
join: (arr: any[], sep: string = ',') => arr.join(sep),
|
|
||||||
split: (str: string, sep: string = ',') => str.split(sep),
|
|
||||||
upper: (str: string) => str.toUpperCase(),
|
|
||||||
lower: (str: string) => str.toLowerCase(),
|
|
||||||
trim: (str: string) => str.trim(),
|
|
||||||
list: (...args: any[]) => args,
|
|
||||||
dict: (atNamed = {}) => atNamed
|
|
||||||
}
|
|
||||||
|
|
||||||
async function runFile(filePath: string) {
|
async function runFile(filePath: string) {
|
||||||
try {
|
try {
|
||||||
const code = readFileSync(filePath, 'utf-8')
|
const code = readFileSync(filePath, 'utf-8')
|
||||||
const compiler = new Compiler(code)
|
const compiler = new Compiler(code)
|
||||||
const vm = new VM(compiler.bytecode, nativeFunctions)
|
const vm = new VM(compiler.bytecode, globalFunctions)
|
||||||
await vm.run()
|
await vm.run()
|
||||||
return vm.stack.length ? fromValue(vm.stack[vm.stack.length - 1]) : null
|
return vm.stack.length ? fromValue(vm.stack[vm.stack.length - 1]) : null
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,8 @@
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "bun generate-parser && bun --hot src/server/server.tsx",
|
"dev": "bun generate-parser && bun --hot src/server/server.tsx",
|
||||||
"generate-parser": "lezer-generator src/parser/shrimp.grammar --typeScript -o src/parser/shrimp.ts",
|
"generate-parser": "lezer-generator src/parser/shrimp.grammar --typeScript -o src/parser/shrimp.ts",
|
||||||
"repl": "bun generate-parser && bun bin/repl"
|
"repl": "bun generate-parser && bun bin/repl",
|
||||||
|
"update-reef": "cd packages/ReefVM && git pull origin main"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"reefvm": "git+https://git.nose.space/defunkt/reefvm",
|
"reefvm": "git+https://git.nose.space/defunkt/reefvm",
|
||||||
|
|
|
||||||
141
src/prelude/index.ts
Normal file
141
src/prelude/index.ts
Normal file
|
|
@ -0,0 +1,141 @@
|
||||||
|
// The prelude creates all the builtin Shrimp functions.
|
||||||
|
|
||||||
|
import { resolve, parse } from 'path'
|
||||||
|
import { readFileSync } from 'fs'
|
||||||
|
import { Compiler } from '#compiler/compiler'
|
||||||
|
import {
|
||||||
|
VM, Scope, toValue, type Value,
|
||||||
|
extractParamInfo, isWrapped, getOriginalFunction,
|
||||||
|
} from 'reefvm'
|
||||||
|
|
||||||
|
export const colors = {
|
||||||
|
reset: '\x1b[0m',
|
||||||
|
bright: '\x1b[1m',
|
||||||
|
dim: '\x1b[2m',
|
||||||
|
cyan: '\x1b[36m',
|
||||||
|
yellow: '\x1b[33m',
|
||||||
|
green: '\x1b[32m',
|
||||||
|
red: '\x1b[31m',
|
||||||
|
blue: '\x1b[34m',
|
||||||
|
magenta: '\x1b[35m',
|
||||||
|
pink: '\x1b[38;2;255;105;180m'
|
||||||
|
}
|
||||||
|
|
||||||
|
export const globalFunctions = {
|
||||||
|
// hello
|
||||||
|
echo: (...args: any[]) => {
|
||||||
|
console.log(...args.map(a => {
|
||||||
|
const v = toValue(a)
|
||||||
|
return ['array', 'dict'].includes(v.type) ? formatValue(v, true) : v.value
|
||||||
|
}))
|
||||||
|
return toValue(null)
|
||||||
|
},
|
||||||
|
|
||||||
|
// info
|
||||||
|
type: (v: any) => toValue(v).type,
|
||||||
|
inspect: (v: any) => formatValue(toValue(v)),
|
||||||
|
length: (v: any) => {
|
||||||
|
const value = toValue(v)
|
||||||
|
switch (value.type) {
|
||||||
|
case 'string': case 'array': return value.value.length
|
||||||
|
case 'dict': return value.value.size
|
||||||
|
default: return 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// strings
|
||||||
|
join: (arr: string[], sep: string = ',') => arr.join(sep),
|
||||||
|
split: (str: string, sep: string = ',') => str.split(sep),
|
||||||
|
'to-upper': (str: string) => str.toUpperCase(),
|
||||||
|
'to-lower': (str: string) => str.toLowerCase(),
|
||||||
|
trim: (str: string) => str.trim(),
|
||||||
|
|
||||||
|
// collections
|
||||||
|
at: (collection: any, index: number | string) => collection[index],
|
||||||
|
list: (...args: any[]) => args,
|
||||||
|
dict: (atNamed = {}) => atNamed,
|
||||||
|
slice: (list: any[], start: number, end?: number) => list.slice(start, end),
|
||||||
|
range: (start: number, end: number | null) => {
|
||||||
|
if (end === null) {
|
||||||
|
end = start
|
||||||
|
start = 0
|
||||||
|
}
|
||||||
|
const result: number[] = []
|
||||||
|
for (let i = start; i <= end; i++) {
|
||||||
|
result.push(i)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
},
|
||||||
|
|
||||||
|
// enumerables
|
||||||
|
map: async (list: any[], cb: Function) => {
|
||||||
|
let acc: any[] = []
|
||||||
|
for (const value of list) acc.push(await cb(value))
|
||||||
|
return acc
|
||||||
|
},
|
||||||
|
each: async (list: any[], cb: Function) => {
|
||||||
|
for (const value of list) await cb(value)
|
||||||
|
},
|
||||||
|
|
||||||
|
// modules
|
||||||
|
use: async function (this: VM, path: string) {
|
||||||
|
const scope = this.scope
|
||||||
|
const pc = this.pc
|
||||||
|
|
||||||
|
const fullPath = resolve(path) + '.sh'
|
||||||
|
const code = readFileSync(fullPath, 'utf-8')
|
||||||
|
|
||||||
|
this.pc = this.instructions.length
|
||||||
|
this.scope = new Scope(scope)
|
||||||
|
const compiled = new Compiler(code)
|
||||||
|
this.appendBytecode(compiled.bytecode)
|
||||||
|
|
||||||
|
await this.continue()
|
||||||
|
|
||||||
|
const module: Map<string, Value> = new Map
|
||||||
|
for (const [name, value] of this.scope.locals.entries())
|
||||||
|
module.set(name, value)
|
||||||
|
|
||||||
|
this.scope = scope
|
||||||
|
this.pc = pc
|
||||||
|
this.stopped = false
|
||||||
|
|
||||||
|
this.scope.set(parse(fullPath).name, { type: 'dict', value: module })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatValue(value: Value, inner = false): string {
|
||||||
|
switch (value.type) {
|
||||||
|
case 'string':
|
||||||
|
return `${colors.green}'${value.value.replaceAll("'", "\\'")}${colors.green}'${colors.reset}`
|
||||||
|
case 'number':
|
||||||
|
return `${colors.cyan}${value.value}${colors.reset}`
|
||||||
|
case 'boolean':
|
||||||
|
return `${colors.yellow}${value.value}${colors.reset}`
|
||||||
|
case 'null':
|
||||||
|
return `${colors.dim}null${colors.reset}`
|
||||||
|
case 'array': {
|
||||||
|
const items = value.value.map(x => formatValue(x, true)).join(' ')
|
||||||
|
return `${inner ? '(' : ''}${colors.blue}list${colors.reset} ${items}${inner ? ')' : ''}`
|
||||||
|
}
|
||||||
|
case 'dict': {
|
||||||
|
const entries = Array.from(value.value.entries())
|
||||||
|
.map(([k, v]) => `${k}=${formatValue(v, true)}`)
|
||||||
|
.join(' ')
|
||||||
|
return `${inner ? '(' : ''}${colors.magenta}dict${colors.reset} ${entries}${inner ? ')' : ''}`
|
||||||
|
}
|
||||||
|
case 'function': {
|
||||||
|
const params = value.params.length ? '(' + value.params.join(' ') + ')' : ''
|
||||||
|
return `${colors.dim}<function${params}>${colors.reset}`
|
||||||
|
}
|
||||||
|
case 'native':
|
||||||
|
const fn = isWrapped(value.fn) ? getOriginalFunction(value.fn) : value.fn
|
||||||
|
const info = extractParamInfo(fn)
|
||||||
|
const params = info.params.length ? '(' + info.params.join(' ') + ')' : ''
|
||||||
|
return `${colors.dim}<native${params}>${colors.reset}`
|
||||||
|
case 'regex':
|
||||||
|
return `${colors.magenta}${value.value}${colors.reset}`
|
||||||
|
default:
|
||||||
|
return String(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
4
src/prelude/tests/math.sh
Normal file
4
src/prelude/tests/math.sh
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
🥧 = 3.14159265359
|
||||||
|
pi = 3.14
|
||||||
|
add1 = do x: x + 1 end
|
||||||
|
double = do x: x * 2 end
|
||||||
256
src/prelude/tests/prelude.test.ts
Normal file
256
src/prelude/tests/prelude.test.ts
Normal file
|
|
@ -0,0 +1,256 @@
|
||||||
|
import { expect, describe, test, mock } from 'bun:test'
|
||||||
|
import { globalFunctions, formatValue } from '#prelude'
|
||||||
|
import { toValue } from 'reefvm'
|
||||||
|
|
||||||
|
describe('string operations', () => {
|
||||||
|
test('to-upper converts to uppercase', () => {
|
||||||
|
expect(globalFunctions['to-upper']('hello')).toBe('HELLO')
|
||||||
|
expect(globalFunctions['to-upper']('Hello World!')).toBe('HELLO WORLD!')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('to-lower converts to lowercase', () => {
|
||||||
|
expect(globalFunctions['to-lower']('HELLO')).toBe('hello')
|
||||||
|
expect(globalFunctions['to-lower']('Hello World!')).toBe('hello world!')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('trim removes whitespace', () => {
|
||||||
|
expect(globalFunctions.trim(' hello ')).toBe('hello')
|
||||||
|
expect(globalFunctions.trim('\n\thello\t\n')).toBe('hello')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('split divides string by separator', () => {
|
||||||
|
expect(globalFunctions.split('a,b,c', ',')).toEqual(['a', 'b', 'c'])
|
||||||
|
expect(globalFunctions.split('hello', '')).toEqual(['h', 'e', 'l', 'l', 'o'])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('split uses comma as default separator', () => {
|
||||||
|
expect(globalFunctions.split('a,b,c')).toEqual(['a', 'b', 'c'])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('join combines array elements', () => {
|
||||||
|
expect(globalFunctions.join(['a', 'b', 'c'], '-')).toBe('a-b-c')
|
||||||
|
expect(globalFunctions.join(['hello', 'world'], ' ')).toBe('hello world')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('join uses comma as default separator', () => {
|
||||||
|
expect(globalFunctions.join(['a', 'b', 'c'])).toBe('a,b,c')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('introspection', () => {
|
||||||
|
test('type returns proper types', () => {
|
||||||
|
expect(globalFunctions.type(toValue('hello'))).toBe('string')
|
||||||
|
expect(globalFunctions.type('hello')).toBe('string')
|
||||||
|
|
||||||
|
expect(globalFunctions.type(toValue(42))).toBe('number')
|
||||||
|
expect(globalFunctions.type(42)).toBe('number')
|
||||||
|
|
||||||
|
expect(globalFunctions.type(toValue(true))).toBe('boolean')
|
||||||
|
expect(globalFunctions.type(false)).toBe('boolean')
|
||||||
|
|
||||||
|
expect(globalFunctions.type(toValue(null))).toBe('null')
|
||||||
|
|
||||||
|
expect(globalFunctions.type(toValue([1, 2, 3]))).toBe('array')
|
||||||
|
|
||||||
|
const dict = new Map([['key', toValue('value')]])
|
||||||
|
expect(globalFunctions.type({ type: 'dict', value: dict })).toBe('dict')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('length', () => {
|
||||||
|
expect(globalFunctions.length(toValue('hello'))).toBe(5)
|
||||||
|
expect(globalFunctions.length('hello')).toBe(5)
|
||||||
|
|
||||||
|
expect(globalFunctions.length(toValue([1, 2, 3]))).toBe(3)
|
||||||
|
expect(globalFunctions.length([1, 2, 3])).toBe(3)
|
||||||
|
|
||||||
|
const dict = new Map([['a', toValue(1)], ['b', toValue(2)]])
|
||||||
|
expect(globalFunctions.length({ type: 'dict', value: dict })).toBe(2)
|
||||||
|
|
||||||
|
expect(globalFunctions.length(toValue(42))).toBe(0)
|
||||||
|
expect(globalFunctions.length(toValue(true))).toBe(0)
|
||||||
|
expect(globalFunctions.length(toValue(null))).toBe(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('inspect formats values', () => {
|
||||||
|
const result = globalFunctions.inspect(toValue('hello'))
|
||||||
|
expect(result).toContain('hello')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('collections', () => {
|
||||||
|
test('list creates array from arguments', () => {
|
||||||
|
expect(globalFunctions.list(1, 2, 3)).toEqual([1, 2, 3])
|
||||||
|
expect(globalFunctions.list('a', 'b')).toEqual(['a', 'b'])
|
||||||
|
expect(globalFunctions.list()).toEqual([])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('dict creates object from named arguments', () => {
|
||||||
|
expect(globalFunctions.dict({ a: 1, b: 2 })).toEqual({ a: 1, b: 2 })
|
||||||
|
expect(globalFunctions.dict()).toEqual({})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('at retrieves element at index', () => {
|
||||||
|
expect(globalFunctions.at([10, 20, 30], 0)).toBe(10)
|
||||||
|
expect(globalFunctions.at([10, 20, 30], 2)).toBe(30)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('at retrieves property from object', () => {
|
||||||
|
expect(globalFunctions.at({ name: 'test' }, 'name')).toBe('test')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('slice extracts array subset', () => {
|
||||||
|
expect(globalFunctions.slice([1, 2, 3, 4, 5], 1, 3)).toEqual([2, 3])
|
||||||
|
expect(globalFunctions.slice([1, 2, 3, 4, 5], 2)).toEqual([3, 4, 5])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('range creates number sequence', () => {
|
||||||
|
expect(globalFunctions.range(0, 5)).toEqual([0, 1, 2, 3, 4, 5])
|
||||||
|
expect(globalFunctions.range(3, 6)).toEqual([3, 4, 5, 6])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('range with single argument starts from 0', () => {
|
||||||
|
expect(globalFunctions.range(3, null)).toEqual([0, 1, 2, 3])
|
||||||
|
expect(globalFunctions.range(0, null)).toEqual([0])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('enumerables', () => {
|
||||||
|
test('map transforms array elements', async () => {
|
||||||
|
const double = (x: number) => x * 2
|
||||||
|
const result = await globalFunctions.map([1, 2, 3], double)
|
||||||
|
expect(result).toEqual([2, 4, 6])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('map works with async callbacks', async () => {
|
||||||
|
const asyncDouble = async (x: number) => {
|
||||||
|
await Promise.resolve()
|
||||||
|
return x * 2
|
||||||
|
}
|
||||||
|
const result = await globalFunctions.map([1, 2, 3], asyncDouble)
|
||||||
|
expect(result).toEqual([2, 4, 6])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('map handles empty array', async () => {
|
||||||
|
const fn = (x: number) => x * 2
|
||||||
|
const result = await globalFunctions.map([], fn)
|
||||||
|
expect(result).toEqual([])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('each iterates over array', async () => {
|
||||||
|
const results: number[] = []
|
||||||
|
await globalFunctions.each([1, 2, 3], (x: number) => {
|
||||||
|
results.push(x * 2)
|
||||||
|
})
|
||||||
|
expect(results).toEqual([2, 4, 6])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('each works with async callbacks', async () => {
|
||||||
|
const results: number[] = []
|
||||||
|
await globalFunctions.each([1, 2, 3], async (x: number) => {
|
||||||
|
await Promise.resolve()
|
||||||
|
results.push(x * 2)
|
||||||
|
})
|
||||||
|
expect(results).toEqual([2, 4, 6])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('each handles empty array', async () => {
|
||||||
|
let called = false
|
||||||
|
await globalFunctions.each([], () => {
|
||||||
|
called = true
|
||||||
|
})
|
||||||
|
expect(called).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('echo', () => {
|
||||||
|
test('echo logs arguments to console', () => {
|
||||||
|
const spy = mock(() => { })
|
||||||
|
const originalLog = console.log
|
||||||
|
console.log = spy
|
||||||
|
|
||||||
|
globalFunctions.echo('hello', 'world')
|
||||||
|
|
||||||
|
expect(spy).toHaveBeenCalledWith('hello', 'world')
|
||||||
|
console.log = originalLog
|
||||||
|
})
|
||||||
|
|
||||||
|
test('echo returns null value', () => {
|
||||||
|
const originalLog = console.log
|
||||||
|
console.log = () => { }
|
||||||
|
|
||||||
|
const result = globalFunctions.echo('test')
|
||||||
|
|
||||||
|
expect(result).toEqual(toValue(null))
|
||||||
|
console.log = originalLog
|
||||||
|
})
|
||||||
|
|
||||||
|
test('echo formats array values', () => {
|
||||||
|
const spy = mock(() => { })
|
||||||
|
const originalLog = console.log
|
||||||
|
console.log = spy
|
||||||
|
|
||||||
|
globalFunctions.echo(toValue([1, 2, 3]))
|
||||||
|
|
||||||
|
// Should format the array, not just log the raw value
|
||||||
|
expect(spy).toHaveBeenCalled()
|
||||||
|
// @ts-ignore
|
||||||
|
const logged = spy.mock.calls[0][0]
|
||||||
|
// @ts-ignore
|
||||||
|
expect(logged).toContain('list')
|
||||||
|
|
||||||
|
console.log = originalLog
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('formatValue', () => {
|
||||||
|
test('formats string with quotes', () => {
|
||||||
|
const result = formatValue(toValue('hello'))
|
||||||
|
expect(result).toContain('hello')
|
||||||
|
expect(result).toContain("'")
|
||||||
|
})
|
||||||
|
|
||||||
|
test('formats numbers', () => {
|
||||||
|
const result = formatValue(toValue(42))
|
||||||
|
expect(result).toContain('42')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('formats booleans', () => {
|
||||||
|
expect(formatValue(toValue(true))).toContain('true')
|
||||||
|
expect(formatValue(toValue(false))).toContain('false')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('formats null', () => {
|
||||||
|
const result = formatValue(toValue(null))
|
||||||
|
expect(result).toContain('null')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('formats arrays', () => {
|
||||||
|
const result = formatValue(toValue([1, 2, 3]))
|
||||||
|
expect(result).toContain('list')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('formats nested arrays with parentheses', () => {
|
||||||
|
const inner = toValue([1, 2])
|
||||||
|
const outer = toValue([inner])
|
||||||
|
const result = formatValue(outer)
|
||||||
|
expect(result).toContain('list')
|
||||||
|
expect(result).toContain('(')
|
||||||
|
expect(result).toContain(')')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('formats dicts', () => {
|
||||||
|
const dict = new Map([
|
||||||
|
['name', toValue('test')],
|
||||||
|
['age', toValue(42)]
|
||||||
|
])
|
||||||
|
const result = formatValue({ type: 'dict', value: dict })
|
||||||
|
expect(result).toContain('dict')
|
||||||
|
expect(result).toContain('name=')
|
||||||
|
expect(result).toContain('age=')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('escapes single quotes in strings', () => {
|
||||||
|
const result = formatValue(toValue("it's"))
|
||||||
|
expect(result).toContain("\\'")
|
||||||
|
})
|
||||||
|
})
|
||||||
28
src/prelude/tests/use.test.ts
Normal file
28
src/prelude/tests/use.test.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
import { expect, describe, test } from 'bun:test'
|
||||||
|
import { globalFunctions } from '#prelude'
|
||||||
|
|
||||||
|
describe('use', () => {
|
||||||
|
test(`imports all a file's functions`, async () => {
|
||||||
|
expect(`
|
||||||
|
use ./src/prelude/tests/math
|
||||||
|
dbl = math | at double
|
||||||
|
dbl 4
|
||||||
|
`).toEvaluateTo(8, globalFunctions)
|
||||||
|
|
||||||
|
expect(`
|
||||||
|
use ./src/prelude/tests/math
|
||||||
|
math | at pi
|
||||||
|
`).toEvaluateTo(3.14, globalFunctions)
|
||||||
|
|
||||||
|
expect(`
|
||||||
|
use ./src/prelude/tests/math
|
||||||
|
math | at 🥧
|
||||||
|
`).toEvaluateTo(3.14159265359, globalFunctions)
|
||||||
|
|
||||||
|
expect(`
|
||||||
|
use ./src/prelude/tests/math
|
||||||
|
call = do x y: x y end
|
||||||
|
call (math | at add1) 5
|
||||||
|
`).toEvaluateTo(6, globalFunctions)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
@ -3,7 +3,7 @@ import { parser } from '#parser/shrimp'
|
||||||
import { $ } from 'bun'
|
import { $ } from 'bun'
|
||||||
import { assert, errorMessage } from '#utils/utils'
|
import { assert, errorMessage } from '#utils/utils'
|
||||||
import { Compiler } from '#compiler/compiler'
|
import { Compiler } from '#compiler/compiler'
|
||||||
import { run, VM } from 'reefvm'
|
import { run, VM, type TypeScriptFunction } from 'reefvm'
|
||||||
import { treeToString, VMResultToValue } from '#utils/tree'
|
import { treeToString, VMResultToValue } from '#utils/tree'
|
||||||
|
|
||||||
const regenerateParser = async () => {
|
const regenerateParser = async () => {
|
||||||
|
|
@ -33,7 +33,7 @@ declare module 'bun:test' {
|
||||||
toMatchTree(expected: string): T
|
toMatchTree(expected: string): T
|
||||||
toMatchExpression(expected: string): T
|
toMatchExpression(expected: string): T
|
||||||
toFailParse(): T
|
toFailParse(): T
|
||||||
toEvaluateTo(expected: unknown, nativeFunctions?: Record<string, Function>): Promise<T>
|
toEvaluateTo(expected: unknown, globalFunctions?: Record<string, TypeScriptFunction>): Promise<T>
|
||||||
toFailEvaluation(): Promise<T>
|
toFailEvaluation(): Promise<T>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -96,13 +96,13 @@ expect.extend({
|
||||||
async toEvaluateTo(
|
async toEvaluateTo(
|
||||||
received: unknown,
|
received: unknown,
|
||||||
expected: unknown,
|
expected: unknown,
|
||||||
nativeFunctions: Record<string, Function> = {}
|
globalFunctions: Record<string, TypeScriptFunction> = {}
|
||||||
) {
|
) {
|
||||||
assert(typeof received === 'string', 'toEvaluateTo can only be used with string values')
|
assert(typeof received === 'string', 'toEvaluateTo can only be used with string values')
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const compiler = new Compiler(received)
|
const compiler = new Compiler(received)
|
||||||
const result = await run(compiler.bytecode, nativeFunctions)
|
const result = await run(compiler.bytecode, globalFunctions)
|
||||||
let value = VMResultToValue(result)
|
let value = VMResultToValue(result)
|
||||||
|
|
||||||
// Just treat regex as strings for comparison purposes
|
// Just treat regex as strings for comparison purposes
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user