Compare commits
6 Commits
a99080ba3f
...
018c527483
| Author | SHA1 | Date | |
|---|---|---|---|
| 018c527483 | |||
| 211fa11a65 | |||
| e60e3184fa | |||
| f8d2236292 | |||
| 4f961d3039 | |||
| d957675ac8 |
2
bun.lock
2
bun.lock
|
|
@ -62,7 +62,7 @@
|
||||||
|
|
||||||
"hono": ["hono@4.9.8", "", {}, "sha512-JW8Bb4RFWD9iOKxg5PbUarBYGM99IcxFl2FPBo2gSJO11jjUDqlP1Bmfyqt8Z/dGhIQ63PMA9LdcLefXyIasyg=="],
|
"hono": ["hono@4.9.8", "", {}, "sha512-JW8Bb4RFWD9iOKxg5PbUarBYGM99IcxFl2FPBo2gSJO11jjUDqlP1Bmfyqt8Z/dGhIQ63PMA9LdcLefXyIasyg=="],
|
||||||
|
|
||||||
"reefvm": ["reefvm@git+https://git.nose.space/defunkt/reefvm#c69b172c78853756ec8acba5bc33d93eb6a571c6", { "peerDependencies": { "typescript": "^5" } }, "c69b172c78853756ec8acba5bc33d93eb6a571c6"],
|
"reefvm": ["reefvm@git+https://git.nose.space/defunkt/reefvm#9618dd64148ccf6f0cdfd8a80a0f58efe3e0819d", { "peerDependencies": { "typescript": "^5" } }, "9618dd64148ccf6f0cdfd8a80a0f58efe3e0819d"],
|
||||||
|
|
||||||
"style-mod": ["style-mod@4.1.2", "", {}, "sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw=="],
|
"style-mod": ["style-mod@4.1.2", "", {}, "sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw=="],
|
||||||
|
|
||||||
|
|
|
||||||
292
src/compiler/tests/native-exceptions.test.ts
Normal file
292
src/compiler/tests/native-exceptions.test.ts
Normal file
|
|
@ -0,0 +1,292 @@
|
||||||
|
import { describe, test, expect } from 'bun:test'
|
||||||
|
import { Compiler } from '#compiler/compiler'
|
||||||
|
import { VM } from 'reefvm'
|
||||||
|
|
||||||
|
describe('Native Function Exceptions', () => {
|
||||||
|
test('native function error caught by try/catch', async () => {
|
||||||
|
const code = `
|
||||||
|
result = try:
|
||||||
|
failing-fn
|
||||||
|
catch e:
|
||||||
|
'caught: ' + e
|
||||||
|
end
|
||||||
|
|
||||||
|
result
|
||||||
|
`
|
||||||
|
|
||||||
|
const compiler = new Compiler(code)
|
||||||
|
const vm = new VM(compiler.bytecode)
|
||||||
|
|
||||||
|
vm.set('failing-fn', () => {
|
||||||
|
throw new Error('native function failed')
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = await vm.run()
|
||||||
|
expect(result).toEqual({ type: 'string', value: 'caught: native function failed' })
|
||||||
|
})
|
||||||
|
|
||||||
|
test('async native function error caught by try/catch', async () => {
|
||||||
|
const code = `
|
||||||
|
result = try:
|
||||||
|
async-fail
|
||||||
|
catch e:
|
||||||
|
'async caught: ' + e
|
||||||
|
end
|
||||||
|
|
||||||
|
result
|
||||||
|
`
|
||||||
|
|
||||||
|
const compiler = new Compiler(code)
|
||||||
|
const vm = new VM(compiler.bytecode)
|
||||||
|
|
||||||
|
vm.set('async-fail', async () => {
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1))
|
||||||
|
throw new Error('async error')
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = await vm.run()
|
||||||
|
expect(result).toEqual({ type: 'string', value: 'async caught: async error' })
|
||||||
|
})
|
||||||
|
|
||||||
|
test('native function with arguments throwing error', async () => {
|
||||||
|
const code = `
|
||||||
|
result = try:
|
||||||
|
read-file missing.txt
|
||||||
|
catch e:
|
||||||
|
'default content'
|
||||||
|
end
|
||||||
|
|
||||||
|
result
|
||||||
|
`
|
||||||
|
|
||||||
|
const compiler = new Compiler(code)
|
||||||
|
const vm = new VM(compiler.bytecode)
|
||||||
|
|
||||||
|
vm.set('read-file', (path: string) => {
|
||||||
|
if (path === 'missing.txt') {
|
||||||
|
throw new Error('file not found')
|
||||||
|
}
|
||||||
|
return 'file contents'
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = await vm.run()
|
||||||
|
expect(result).toEqual({ type: 'string', value: 'default content' })
|
||||||
|
})
|
||||||
|
|
||||||
|
test('native function error with finally block', async () => {
|
||||||
|
const code = `
|
||||||
|
cleanup-count = 0
|
||||||
|
|
||||||
|
result = try:
|
||||||
|
failing-fn
|
||||||
|
catch e:
|
||||||
|
'error handled'
|
||||||
|
finally:
|
||||||
|
cleanup-count = cleanup-count + 1
|
||||||
|
end
|
||||||
|
|
||||||
|
cleanup-count
|
||||||
|
`
|
||||||
|
|
||||||
|
const compiler = new Compiler(code)
|
||||||
|
const vm = new VM(compiler.bytecode)
|
||||||
|
|
||||||
|
vm.set('failing-fn', () => {
|
||||||
|
throw new Error('native error')
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = await vm.run()
|
||||||
|
expect(result).toEqual({ type: 'number', value: 1 })
|
||||||
|
})
|
||||||
|
|
||||||
|
test('native function error without catch propagates', async () => {
|
||||||
|
const code = `
|
||||||
|
failing-fn
|
||||||
|
`
|
||||||
|
|
||||||
|
const compiler = new Compiler(code)
|
||||||
|
const vm = new VM(compiler.bytecode)
|
||||||
|
|
||||||
|
vm.set('failing-fn', () => {
|
||||||
|
throw new Error('uncaught error')
|
||||||
|
})
|
||||||
|
|
||||||
|
await expect(vm.run()).rejects.toThrow('uncaught error')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('native function in function-level catch', async () => {
|
||||||
|
const code = `
|
||||||
|
safe-read = do path:
|
||||||
|
read-file path
|
||||||
|
catch e:
|
||||||
|
'default: ' + e
|
||||||
|
end
|
||||||
|
|
||||||
|
result = safe-read missing.txt
|
||||||
|
result
|
||||||
|
`
|
||||||
|
|
||||||
|
const compiler = new Compiler(code)
|
||||||
|
const vm = new VM(compiler.bytecode)
|
||||||
|
|
||||||
|
vm.set('read-file', (path: string) => {
|
||||||
|
throw new Error('file not found: ' + path)
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = await vm.run()
|
||||||
|
expect(result).toEqual({ type: 'string', value: 'default: file not found: missing.txt' })
|
||||||
|
})
|
||||||
|
|
||||||
|
test('nested native function errors', async () => {
|
||||||
|
const code = `
|
||||||
|
result = try:
|
||||||
|
try:
|
||||||
|
inner-fail
|
||||||
|
catch e:
|
||||||
|
throw 'wrapped: ' + e
|
||||||
|
end
|
||||||
|
catch e:
|
||||||
|
'outer caught: ' + e
|
||||||
|
end
|
||||||
|
|
||||||
|
result
|
||||||
|
`
|
||||||
|
|
||||||
|
const compiler = new Compiler(code)
|
||||||
|
const vm = new VM(compiler.bytecode)
|
||||||
|
|
||||||
|
vm.set('inner-fail', () => {
|
||||||
|
throw new Error('inner error')
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = await vm.run()
|
||||||
|
expect(result).toEqual({ type: 'string', value: 'outer caught: wrapped: inner error' })
|
||||||
|
})
|
||||||
|
|
||||||
|
test('native function error with multiple named args', async () => {
|
||||||
|
const code = `
|
||||||
|
result = try:
|
||||||
|
process-file path=missing.txt mode=strict
|
||||||
|
catch e:
|
||||||
|
'error: ' + e
|
||||||
|
end
|
||||||
|
|
||||||
|
result
|
||||||
|
`
|
||||||
|
|
||||||
|
const compiler = new Compiler(code)
|
||||||
|
const vm = new VM(compiler.bytecode)
|
||||||
|
|
||||||
|
vm.set('process-file', (path: string, mode: string = 'lenient') => {
|
||||||
|
if (mode === 'strict' && path === 'missing.txt') {
|
||||||
|
throw new Error('strict mode: file required')
|
||||||
|
}
|
||||||
|
return 'processed'
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = await vm.run()
|
||||||
|
expect(result).toEqual({ type: 'string', value: 'error: strict mode: file required' })
|
||||||
|
})
|
||||||
|
|
||||||
|
test('native function returning normally after other functions threw', async () => {
|
||||||
|
const code = `
|
||||||
|
result1 = try:
|
||||||
|
failing-fn
|
||||||
|
catch e:
|
||||||
|
'caught'
|
||||||
|
end
|
||||||
|
|
||||||
|
result2 = success-fn
|
||||||
|
|
||||||
|
result1 + ' then ' + result2
|
||||||
|
`
|
||||||
|
|
||||||
|
const compiler = new Compiler(code)
|
||||||
|
const vm = new VM(compiler.bytecode)
|
||||||
|
|
||||||
|
vm.set('failing-fn', () => {
|
||||||
|
throw new Error('error')
|
||||||
|
})
|
||||||
|
|
||||||
|
vm.set('success-fn', () => {
|
||||||
|
return 'success'
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = await vm.run()
|
||||||
|
expect(result).toEqual({ type: 'string', value: 'caught then success' })
|
||||||
|
})
|
||||||
|
|
||||||
|
test('native function error message preserved', async () => {
|
||||||
|
const code = `
|
||||||
|
result = try:
|
||||||
|
throw-custom-message
|
||||||
|
catch e:
|
||||||
|
e
|
||||||
|
end
|
||||||
|
|
||||||
|
result
|
||||||
|
`
|
||||||
|
|
||||||
|
const compiler = new Compiler(code)
|
||||||
|
const vm = new VM(compiler.bytecode)
|
||||||
|
|
||||||
|
vm.set('throw-custom-message', () => {
|
||||||
|
throw new Error('This is a very specific error message with details')
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = await vm.run()
|
||||||
|
expect(result).toEqual({
|
||||||
|
type: 'string',
|
||||||
|
value: 'This is a very specific error message with details'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('native function throwing non-Error value', async () => {
|
||||||
|
const code = `
|
||||||
|
result = try:
|
||||||
|
throw-string
|
||||||
|
catch e:
|
||||||
|
'caught: ' + e
|
||||||
|
end
|
||||||
|
|
||||||
|
result
|
||||||
|
`
|
||||||
|
|
||||||
|
const compiler = new Compiler(code)
|
||||||
|
const vm = new VM(compiler.bytecode)
|
||||||
|
|
||||||
|
vm.set('throw-string', () => {
|
||||||
|
throw 'plain string error'
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = await vm.run()
|
||||||
|
expect(result).toEqual({ type: 'string', value: 'caught: plain string error' })
|
||||||
|
})
|
||||||
|
|
||||||
|
test('multiple native function calls with mixed success/failure', async () => {
|
||||||
|
const code = `
|
||||||
|
r1 = try: success-fn catch e: 'error' end
|
||||||
|
r2 = try: failing-fn catch e: 'caught' end
|
||||||
|
r3 = try: success-fn catch e: 'error' end
|
||||||
|
|
||||||
|
results = [r1 r2 r3]
|
||||||
|
results
|
||||||
|
`
|
||||||
|
|
||||||
|
const compiler = new Compiler(code)
|
||||||
|
const vm = new VM(compiler.bytecode)
|
||||||
|
|
||||||
|
vm.set('success-fn', () => 'ok')
|
||||||
|
vm.set('failing-fn', () => {
|
||||||
|
throw new Error('failed')
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = await vm.run()
|
||||||
|
expect(result.type).toBe('array')
|
||||||
|
const arr = result.value as any[]
|
||||||
|
expect(arr.length).toBe(3)
|
||||||
|
expect(arr[0]).toEqual({ type: 'string', value: 'ok' })
|
||||||
|
expect(arr[1]).toEqual({ type: 'string', value: 'caught' })
|
||||||
|
expect(arr[2]).toEqual({ type: 'string', value: 'ok' })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
@ -236,7 +236,12 @@ export const getStringParts = (node: SyntaxNode, input: string) => {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
return { parts, hasInterpolation: parts.length > 0 }
|
// hasInterpolation means the string has interpolation ($var) or escape sequences (\n)
|
||||||
|
// A simple string like 'hello' has one StringFragment but no interpolation
|
||||||
|
const hasInterpolation = parts.some(
|
||||||
|
(p) => p.type.id === terms.Interpolation || p.type.id === terms.EscapeSeq
|
||||||
|
)
|
||||||
|
return { parts, hasInterpolation }
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getDotGetParts = (node: SyntaxNode, input: string) => {
|
export const getDotGetParts = (node: SyntaxNode, input: string) => {
|
||||||
|
|
|
||||||
|
|
@ -159,7 +159,7 @@ TryBlock {
|
||||||
}
|
}
|
||||||
|
|
||||||
Throw {
|
Throw {
|
||||||
@specialize[@name=keyword]<Identifier, "throw"> expression
|
@specialize[@name=keyword]<Identifier, "throw"> (BinOp | ConditionalOp | expression)
|
||||||
}
|
}
|
||||||
|
|
||||||
ConditionalOp {
|
ConditionalOp {
|
||||||
|
|
|
||||||
|
|
@ -7,9 +7,9 @@ import {highlighting} from "./highlight"
|
||||||
const spec_Identifier = {__proto__:null,catch:82, finally:88, end:90, null:96, try:106, throw:110, if:114, elseif:122, else:126}
|
const spec_Identifier = {__proto__:null,catch:82, finally:88, end:90, null:96, try:106, throw:110, if:114, elseif:122, else:126}
|
||||||
export const parser = LRParser.deserialize({
|
export const parser = LRParser.deserialize({
|
||||||
version: 14,
|
version: 14,
|
||||||
states: "8lQYQbOOO#tQcO'#CvO$qOSO'#CxO%PQbO'#E`OOQ`'#DR'#DROOQa'#DO'#DOO&SQbO'#D]O'XQcO'#ETOOQa'#ET'#ETO)cQcO'#ESO*bQRO'#CwO+WQcO'#EOO+hQcO'#EOO+rQbO'#CuO,jOpO'#CsOOQ`'#EP'#EPO,oQbO'#EOO,vQQO'#EfOOQ`'#Db'#DbO,{QbO'#DdO,{QbO'#EhOOQ`'#Df'#DfO-pQRO'#DnOOQ`'#EO'#EOO-uQQO'#D}OOQ`'#D}'#D}OOQ`'#Do'#DoQYQbOOO-}QbO'#DPOOQa'#ES'#ESOOQ`'#D`'#D`OOQ`'#Ee'#EeOOQ`'#Dv'#DvO.XQbO,59^O.rQbO'#CzO.zQWO'#C{OOOO'#EV'#EVOOOO'#Dp'#DpO/`OSO,59dOOQa,59d,59dOOQ`'#Dr'#DrO/nQbO'#DSO/vQQO,5:zOOQ`'#Dq'#DqO/{QbO,59wO0SQQO,59jOOQa,59w,59wO0_QbO,59wO,{QbO,59cO,{QbO,59cO,{QbO,59cO,{QbO,59yO,{QbO,59yO,{QbO,59yO0iQRO,59aO0pQRO,59aO1RQRO,59aO0|QQO,59aO1^QQO,59aO1fObO,59_O1qQbO'#DwO1|QbO,59]O2eQbO,5;QOOQ`,5:O,5:OO2xQRO,5;SO3PQRO,5;SO3[QbO,5:YOOQ`,5:i,5:iOOQ`-E7m-E7mOOQ`,59k,59kOOQ`-E7t-E7tOOOO,59f,59fOOOO,59g,59gOOOO-E7n-E7nOOQa1G/O1G/OOOQ`-E7p-E7pO3lQbO1G0fOOQ`-E7o-E7oO4PQQO1G/UOOQa1G/c1G/cO4[QbO1G/cOOQO'#Dt'#DtO4PQQO1G/UOOQa1G/U1G/UOOQ`'#Du'#DuO4[QbO1G/cOOQa1G.}1G.}O5TQcO1G.}O5_QcO1G.}O5iQcO1G.}OOQa1G/e1G/eO7XQcO1G/eO7`QcO1G/eO7gQcO1G/eOOQa1G.{1G.{OOQa1G.y1G.yO!aQbO'#CvO&ZQbO'#CrOOQ`,5:c,5:cOOQ`-E7u-E7uO7nQbO1G0lO7yQbO1G0mO8gQbO1G0nOOQ`1G/t1G/tO8zQbO7+&QO7yQbO7+&SO9VQQO7+$pOOQa7+$p7+$pO9bQbO7+$}OOQa7+$}7+$}OOQO-E7r-E7rOOQ`-E7s-E7sO9lQbO'#DUO9qQQO'#DXOOQ`7+&W7+&WO9vQbO7+&WO9{QbO7+&WOOQ`'#Ds'#DsO:TQQO'#DsO:YQbO'#EaOOQ`'#DW'#DWO:|QbO7+&XOOQ`'#Dh'#DhO;XQbO7+&YO;^QbO7+&ZOOQ`<<Il<<IlO;zQbO<<IlO<PQbO<<IlO<XQbO<<InOOQa<<H[<<H[OOQa<<Hi<<HiO<dQQO,59pO<iQbO,59sOOQ`<<Ir<<IrO<|QbO<<IrOOQ`,5:_,5:_OOQ`-E7q-E7qOOQ`<<Is<<IsO=RQbO<<IsO=WQbO<<IsOOQ`<<It<<ItOOQ`'#Di'#DiO=`QbO<<IuOOQ`AN?WAN?WO=kQbOAN?WOOQ`AN?YAN?YO=pQbOAN?YO=uQbOAN?YO=}QbO1G/[O>bQbO1G/_OOQ`1G/_1G/_OOQ`AN?^AN?^OOQ`AN?_AN?_O>xQbOAN?_O,{QbO'#DjOOQ`'#Dx'#DxO>}QbOAN?aO?YQQO'#DlOOQ`AN?aAN?aO?_QbOAN?aOOQ`G24rG24rOOQ`G24tG24tO?dQbOG24tO?iQbO7+$vOOQ`7+$v7+$vOOQ`7+$y7+$yOOQ`G24yG24yO@SQRO,5:UO@ZQRO,5:UOOQ`-E7v-E7vOOQ`G24{G24{O@fQbOG24{O@kQQO,5:WOOQ`LD*`LD*`OOQ`<<Hb<<HbO@pQQO1G/pOOQ`LD*gLD*gO>bQbO1G/rO;^QbO7+%[OOQ`7+%^7+%^OOQ`<<Hv<<Hv",
|
states: "8xQYQbOOO#tQcO'#CvO$qOSO'#CxO%PQbO'#E`OOQ`'#DR'#DROOQa'#DO'#DOO&SQbO'#D]O'XQcO'#ETOOQa'#ET'#ETO)cQcO'#ESO)vQRO'#CwO+SQcO'#EOO+dQcO'#EOO+nQbO'#CuO,fOpO'#CsOOQ`'#EP'#EPO,kQbO'#EOO,rQQO'#EfOOQ`'#Db'#DbO,wQbO'#DdO,wQbO'#EhOOQ`'#Df'#DfO-lQRO'#DnOOQ`'#EO'#EOO-qQQO'#D}OOQ`'#D}'#D}OOQ`'#Do'#DoQYQbOOO-yQbO'#DPOOQa'#ES'#ESOOQ`'#D`'#D`OOQ`'#Ee'#EeOOQ`'#Dv'#DvO.TQbO,59^O.nQbO'#CzO.vQWO'#C{OOOO'#EV'#EVOOOO'#Dp'#DpO/[OSO,59dOOQa,59d,59dOOQ`'#Dr'#DrO/jQbO'#DSO/rQQO,5:zOOQ`'#Dq'#DqO/wQbO,59wO0OQQO,59jOOQa,59w,59wO0ZQbO,59wO,wQbO,59cO,wQbO,59cO,wQbO,59cO,wQbO,59yO,wQbO,59yO,wQbO,59yO0eQRO,59aO0lQRO,59aO0}QRO,59aO0xQQO,59aO1YQQO,59aO1bObO,59_O1mQbO'#DwO1xQbO,59]O2aQbO,5;QO2tQcO,5:OO3jQcO,5:OO3zQcO,5:OO4pQRO,5;SO4wQRO,5;SO5SQbO,5:YOOQ`,5:i,5:iOOQ`-E7m-E7mOOQ`,59k,59kOOQ`-E7t-E7tOOOO,59f,59fOOOO,59g,59gOOOO-E7n-E7nOOQa1G/O1G/OOOQ`-E7p-E7pO5dQbO1G0fOOQ`-E7o-E7oO5wQQO1G/UOOQa1G/c1G/cO6SQbO1G/cOOQO'#Dt'#DtO5wQQO1G/UOOQa1G/U1G/UOOQ`'#Du'#DuO6SQbO1G/cOOQa1G.}1G.}O6{QcO1G.}O7VQcO1G.}O7aQcO1G.}OOQa1G/e1G/eO9PQcO1G/eO9WQcO1G/eO9_QcO1G/eOOQa1G.{1G.{OOQa1G.y1G.yO!aQbO'#CvO&ZQbO'#CrOOQ`,5:c,5:cOOQ`-E7u-E7uO9fQbO1G0lO9qQbO1G0mO:_QbO1G0nOOQ`1G/t1G/tO:rQbO7+&QO9qQbO7+&SO:}QQO7+$pOOQa7+$p7+$pO;YQbO7+$}OOQa7+$}7+$}OOQO-E7r-E7rOOQ`-E7s-E7sO;dQbO'#DUO;iQQO'#DXOOQ`7+&W7+&WO;nQbO7+&WO;sQbO7+&WOOQ`'#Ds'#DsO;{QQO'#DsO<QQbO'#EaOOQ`'#DW'#DWO<tQbO7+&XOOQ`'#Dh'#DhO=PQbO7+&YO=UQbO7+&ZOOQ`<<Il<<IlO=rQbO<<IlO=wQbO<<IlO>PQbO<<InOOQa<<H[<<H[OOQa<<Hi<<HiO>[QQO,59pO>aQbO,59sOOQ`<<Ir<<IrO>tQbO<<IrOOQ`,5:_,5:_OOQ`-E7q-E7qOOQ`<<Is<<IsO>yQbO<<IsO?OQbO<<IsOOQ`<<It<<ItOOQ`'#Di'#DiO?WQbO<<IuOOQ`AN?WAN?WO?cQbOAN?WOOQ`AN?YAN?YO?hQbOAN?YO?mQbOAN?YO?uQbO1G/[O@YQbO1G/_OOQ`1G/_1G/_OOQ`AN?^AN?^OOQ`AN?_AN?_O@pQbOAN?_O,wQbO'#DjOOQ`'#Dx'#DxO@uQbOAN?aOAQQQO'#DlOOQ`AN?aAN?aOAVQbOAN?aOOQ`G24rG24rOOQ`G24tG24tOA[QbOG24tOAaQbO7+$vOOQ`7+$v7+$vOOQ`7+$y7+$yOOQ`G24yG24yOAzQRO,5:UOBRQRO,5:UOOQ`-E7v-E7vOOQ`G24{G24{OB^QbOG24{OBcQQO,5:WOOQ`LD*`LD*`OOQ`<<Hb<<HbOBhQQO1G/pOOQ`LD*gLD*gO@YQbO1G/rO=UQbO7+%[OOQ`7+%^7+%^OOQ`<<Hv<<Hv",
|
||||||
stateData: "@x~O!oOS!pOS~O_PO`fOaWOb^OcROhWOpWOqWO!QWO!VaO!XcO!ZdO!u]O!xQO#PTO#QUO#RiO~O_mOaWOb^OcROhWOpWOqWOtlO!OnO!QWO!u]O!xQO#PTO#QUO!TjX#RjX#^jX#WjXyjX|jX}jX~OP!vXQ!vXR!vXS!vXT!vXU!vXW!vXX!vXY!vXZ!vX[!vX]!vX^!vX~P!aOmtO!xwO!zrO!{sO~O_xOwvP~O_mOaWOb^OhWOpWOqWOtlO!QWO!u]O!xQO#PTO#QUO#R{O~O#V!OO~P%XO_mOaWOb^OcROhWOpWOqWOtlO!OnO!QWO!u]O!xQO#PTO#QUO~OP!wXQ!wXR!wXS!wXT!wXU!wXW!wXX!wXY!wXZ!wX[!wX]!wX^!wX#R!wX#^!wX#W!wXy!wX|!wX}!wX~P&ZOP!vXQ!vXR!vXS!vXT!vXU!vXW!vXX!vXY!vXZ!vX[!vX]!vX^!vX~O#R!rX#^!rXy!rX|!rX}!rX~P(hOT!UOU!VOW!TOX!TOY!TOZ!TO[!TO]!TO~OP!ROQ!ROR!SOS!SO^!QO~P)vO#R!rX#^!rXy!rX|!rX}!rX~OP!ROQ!ROR!SOS!SO~P*uOT!UOU!VO~P*uO_POaWOb^OcROhWOpWOqWO!QWO!u]O!xQO#PTO#QUO~O!t!]O~O!T!^O~P*uOw!`O~O_mOaWOb^OhWOpWOqWO!QWO!u]O!xQO#PTO#QUO~OV!dO~O#R!eO#^!eO~OcRO!O!gO~P,{O!Tfa#Rfa#^fa#Wfayfa|fa}fa~P&ZO_!iO!u]O~O!x!jO!z!jO!{!jO!|!jO!}!jO#O!jO~OmtO!x!lO!zrO!{sO~O_xOwvX~Ow!nO~O#V!qO~P%XOtlO#R!sO#V!uO~O#R!vO#V!qO~P,{O#W#QO~P(hOP!ROQ!ROR!SOS!SO#W#QO~OT!UOU!VO#W#QO~O!T!^O#W#QO~O_#ROh#RO!u]O~O_#SOb^O!u]O~O!T!^O#Rea#^ea#Weayea|ea}ea~O`fO!VaO!XcO!ZdO#R#XO~P+rOw#YO~P)vOT!UOU!VOw#YO~O`fO!VaO!XcO!ZdO~P+rO`fO!VaO!XcO!ZdO#R#]O~P+rOtlO#R!sO#V#_O~O#R!vO#V#aO~P,{O^!QORkiSki#Rki#^ki#Wkiyki|ki}ki~OPkiQki~P4fOP!ROQ!RO~P4fOP!ROQ!RORkiSki#Rki#^ki#Wkiyki|ki}ki~OW!TOX!TOY!TOZ!TO[!TO]!TOT!Ri#R!Ri#^!Ri#W!Riw!Riy!Ri|!Ri}!Ri~OU!VO~P6ZOU!VO~P6mOU!Ri~P6ZOy#dO|#eO}#fO~O`fO!VaO!XcO!ZdO#R#iOy#TP|#TP}#TP~P+rO`fO!VaO!XcO!ZdO#R#pO~P+rOy#dO|#eO}#qO~OtlO#R!sO#V#uO~O#R!vO#V#vO~P,{O_#wO~Ow#xO~O}#yO~O|#eO}#yO~O#R#{O~O`fO!VaO!XcO!ZdO#R#iOy#TX|#TX}#TX!_#TX!a#TX~P+rOy#dO|#eO}#}O~O}$QO~O`fO!VaO!XcO!ZdO#R#iO}#TP!_#TP!a#TP~P+rO}$TO~O|#eO}$TO~Oy#dO|#eO}$VO~Ow$YO~O`fO!VaO!XcO!ZdO#R$ZO~P+rO}$]O~O}$^O~O|#eO}$^O~O}$dO!_$`O!a$cO~O}$fO~O}$gO~O|#eO}$gO~O`fO!VaO!XcO!ZdO#R$iO~P+rO`fO!VaO!XcO!ZdO#R#iO}#TP~P+rO}$lO~O}$pO!_$`O!a$cO~Ow$rO~O}$pO~O}$sO~O`fO!VaO!XcO!ZdO#R#iO|#TP}#TP~P+rOw$uO~P)vOT!UOU!VOw$uO~O}$vO~O#R$wO~O#R$xO~Ohq~",
|
stateData: "Bp~O!oOS!pOS~O_PO`fOaWOb^OcROhWOpWOqWO!QWO!VaO!XcO!ZdO!u]O!xQO#PTO#QUO#RiO~O_mOaWOb^OcROhWOpWOqWOtlO!OnO!QWO!u]O!xQO#PTO#QUO!TjX#RjX#^jX#WjXyjX|jX}jX~OP!vXQ!vXR!vXS!vXT!vXU!vXW!vXX!vXY!vXZ!vX[!vX]!vX^!vX~P!aOmtO!xwO!zrO!{sO~O_xOwvP~O_mOaWOb^OhWOpWOqWOtlO!QWO!u]O!xQO#PTO#QUO#R{O~O#V!OO~P%XO_mOaWOb^OcROhWOpWOqWOtlO!OnO!QWO!u]O!xQO#PTO#QUO~OP!wXQ!wXR!wXS!wXT!wXU!wXW!wXX!wXY!wXZ!wX[!wX]!wX^!wX#R!wX#^!wX#W!wXy!wX|!wX}!wX~P&ZOP!vXQ!vXR!vXS!vXT!vXU!vXW!vXX!vXY!vXZ!vX[!vX]!vX^!vX~O#R!rX#^!rXy!rX|!rX}!rX~P(hOP!ROQ!ROR!SOS!SOT!UOU!VOW!TOX!TOY!TOZ!TO[!TO]!TO^!QO~O#R!rX#^!rXy!rX|!rX}!rX~OP!ROQ!ROR!SOS!SO~P*qOT!UOU!VO~P*qO_POaWOb^OcROhWOpWOqWO!QWO!u]O!xQO#PTO#QUO~O!t!]O~O!T!^O~P*qOw!`O~O_mOaWOb^OhWOpWOqWO!QWO!u]O!xQO#PTO#QUO~OV!fO~O#R!gO#^!gO~OcRO!O!iO~P,wO!Tfa#Rfa#^fa#Wfayfa|fa}fa~P&ZO_!kO!u]O~O!x!lO!z!lO!{!lO!|!lO!}!lO#O!lO~OmtO!x!nO!zrO!{sO~O_xOwvX~Ow!pO~O#V!sO~P%XOtlO#R!uO#V!wO~O#R!xO#V!sO~P,wO#W#SO~P(hOP!ROQ!ROR!SOS!SO#W#SO~OT!UOU!VO#W#SO~O!T!^O#W#SO~O_#TOh#TO!u]O~O_#UOb^O!u]O~O!T!^O#Rea#^ea#Weayea|ea}ea~O`fO!VaO!XcO!ZdO#R#ZO~P+nO#R!Wa#^!Way!Wa|!Wa}!Wa~P)vO#R!Wa#^!Way!Wa|!Wa}!Wa~OP!ROQ!ROR!SOS!SO~P3XOT!UOU!VO~P3XOT!UOU!VOW!TOX!TOY!TOZ!TO[!TO]!TO~Ow#[O~P4UOT!UOU!VOw#[O~O`fO!VaO!XcO!ZdO~P+nO`fO!VaO!XcO!ZdO#R#_O~P+nOtlO#R!uO#V#aO~O#R!xO#V#cO~P,wO^!QORkiSki#Rki#^ki#Wkiyki|ki}ki~OPkiQki~P6^OP!ROQ!RO~P6^OP!ROQ!RORkiSki#Rki#^ki#Wkiyki|ki}ki~OW!TOX!TOY!TOZ!TO[!TO]!TOT!Ri#R!Ri#^!Ri#W!Riw!Riy!Ri|!Ri}!Ri~OU!VO~P8ROU!VO~P8eOU!Ri~P8ROy#fO|#gO}#hO~O`fO!VaO!XcO!ZdO#R#kOy#TP|#TP}#TP~P+nO`fO!VaO!XcO!ZdO#R#rO~P+nOy#fO|#gO}#sO~OtlO#R!uO#V#wO~O#R!xO#V#xO~P,wO_#yO~Ow#zO~O}#{O~O|#gO}#{O~O#R#}O~O`fO!VaO!XcO!ZdO#R#kOy#TX|#TX}#TX!_#TX!a#TX~P+nOy#fO|#gO}$PO~O}$SO~O`fO!VaO!XcO!ZdO#R#kO}#TP!_#TP!a#TP~P+nO}$VO~O|#gO}$VO~Oy#fO|#gO}$XO~Ow$[O~O`fO!VaO!XcO!ZdO#R$]O~P+nO}$_O~O}$`O~O|#gO}$`O~O}$fO!_$bO!a$eO~O}$hO~O}$iO~O|#gO}$iO~O`fO!VaO!XcO!ZdO#R$kO~P+nO`fO!VaO!XcO!ZdO#R#kO}#TP~P+nO}$nO~O}$rO!_$bO!a$eO~Ow$tO~O}$rO~O}$uO~O`fO!VaO!XcO!ZdO#R#kO|#TP}#TP~P+nOw$wO~P4UOT!UOU!VOw$wO~O}$xO~O#R$yO~O#R$zO~Ohq~",
|
||||||
goto: "3O#^PPPPPPPPPPPPPPPPPPPPP#_#t$YP%X#t&^&yP's'sPP&y'wP([({P)OP)[)ePPP&yP)}*pP*wP*wP*wP+Z+^+gP+kP*w+q+w+},T,Z,g,q,{-U-]PPPP-c-g.XPP.q0XP1VPPPPPPPP1Z1t1ZPP2R2Y2Y2l2lpgOk!`!d!n#X#Y#]#k#p#x$Y$Z$i$w$xR!Z]u_O]k!^!`!d!n#X#Y#]#k#p#x$Y$Z$i$w$xrPO]k!`!d!n#X#Y#]#k#p#x$Y$Z$i$w$xzmPUVcdlq|!P!Q!R!S!T!U!V!r!w#S#T#`$`R#S!^rVO]k!`!d!n#X#Y#]#k#p#x$Y$Z$i$w$xzWPUVcdlq|!P!Q!R!S!T!U!V!r!w#S#T#`$`Q!irQ#R!]R#T!^pZOk!`!d!n#X#Y#]#k#p#x$Y$Z$i$w$xQ!X]Q!x!RR!{!S!oWOPUV]cdklq|!P!Q!R!S!T!U!V!`!d!n!r!w#S#T#X#Y#]#`#k#p#x$Y$Z$`$i$w$xTtQvYoPVq#S#TQ}UQ!p|X!s}!p!t#^pgOk!`!d!n#X#Y#]#k#p#x$Y$Z$i$w$xYnPVq#S#TQ!Z]R!glRzRQ#h#WQ#s#[Q$P#mR$X#tQ#m#XQ$k$ZR$t$iQ#g#WQ#r#[Q#z#hQ$O#mQ$U#sQ$W#tQ$_$PR$h$Xp[Ok!`!d!n#X#Y#]#k#p#x$Y$Z$i$w$xQ!Y]Q!cdQ!|!VQ#O!UR$n$`ZoPVq#S#TqgOk!`!d!n#X#Y#]#k#p#x$Y$Z$i$w$xR#o#YQ$S#pQ$y$wR$z$xT$a$S$bQ$e$SR$q$bQkOR!fkQvQR!kvQ|UR!o|QyRR!my^#k#X#]#p$Z$i$w$xR#|#kQ!t}Q#^!pT#b!t#^Q!w!PQ#`!rT#c!w#`WqPV#S#TR!hqS!_`![R#V!_Q$b$SR$o$bTjOkShOkQ#W!`Q#Z!dQ#[!n`#j#X#]#k#p$Z$i$w$xQ#n#YQ$[#xR$j$Yp`Ok!`!d!n#X#Y#]#k#p#x$Y$Z$i$w$xQ![]R#U!^rYO]k!`!d!n#X#Y#]#k#p#x$Y$Z$i$w$xYnPVq#S#TQ!PUQ!acQ!bdQ!glQ!r|W!v!P!r!w#`Q!x!QQ!y!RQ!z!SQ!|!TQ!}!UQ#P!VR$m$`pXOk!`!d!n#X#Y#]#k#p#x$Y$Z$i$w$xzmPUVcdlq|!P!Q!R!S!T!U!V!r!w#S#T#`$`R!W]TuQv!PSOPV]klq!`!d!n#S#T#X#Y#]#k#p#x$Y$Z$i$w$xU#l#X$Z$iQ#t#]V$R#p$w$xZpPVq#S#TqbOk!`!d!n#X#Y#]#k#p#x$Y$Z$i$w$xqeOk!`!d!n#X#Y#]#k#p#x$Y$Z$i$w$x",
|
goto: "3U#^PPPPPPPPPPPPPPPPPPPPP#_#t$YP%X#t&^&|P'v'vPP&|'zP(_)OP)RP)_)hPPP&|P*Q*vP*}P*}P*}P+a+d+mP+qP*}+w+},T,Z,a,m,w-R-[-cPPPP-i-m._PP.w0_P1]PPPPPPPP1a1z1aPP2X2`2`2r2rpgOk!`!f!p#Z#[#_#m#r#z$[$]$k$y$zR!Z]u_O]k!^!`!f!p#Z#[#_#m#r#z$[$]$k$y$zrPO]k!`!f!p#Z#[#_#m#r#z$[$]$k$y$zzmPUVcdlq|!P!Q!R!S!T!U!V!t!y#U#V#b$bR#U!^rVO]k!`!f!p#Z#[#_#m#r#z$[$]$k$y$zzWPUVcdlq|!P!Q!R!S!T!U!V!t!y#U#V#b$bQ!krQ#T!]R#V!^pZOk!`!f!p#Z#[#_#m#r#z$[$]$k$y$zQ!X]Q!bcQ!z!RR!}!S!oWOPUV]cdklq|!P!Q!R!S!T!U!V!`!f!p!t!y#U#V#Z#[#_#b#m#r#z$[$]$b$k$y$zTtQvYoPVq#U#VQ}UQ!r|X!u}!r!v#`pgOk!`!f!p#Z#[#_#m#r#z$[$]$k$y$zYnPVq#U#VQ!Z]R!ilRzRQ#j#YQ#u#^Q$R#oR$Z#vQ#o#ZQ$m$]R$v$kQ#i#YQ#t#^Q#|#jQ$Q#oQ$W#uQ$Y#vQ$a$RR$j$Zp[Ok!`!f!p#Z#[#_#m#r#z$[$]$k$y$zQ!Y]Q!ccQ!edQ#O!VQ#Q!UR$p$bZoPVq#U#VqgOk!`!f!p#Z#[#_#m#r#z$[$]$k$y$zR#q#[Q$U#rQ${$yR$|$zT$c$U$dQ$g$UR$s$dQkOR!hkQvQR!mvQ|UR!q|QyRR!oy^#m#Z#_#r$]$k$y$zR$O#mQ!v}Q#`!rT#d!v#`Q!y!PQ#b!tT#e!y#bWqPV#U#VR!jqS!_`![R#X!_Q$d$UR$q$dTjOkShOkQ#Y!`Q#]!fQ#^!p`#l#Z#_#m#r$]$k$y$zQ#p#[Q$^#zR$l$[p`Ok!`!f!p#Z#[#_#m#r#z$[$]$k$y$zQ![]R#W!^rYO]k!`!f!p#Z#[#_#m#r#z$[$]$k$y$zYnPVq#U#VQ!PUQ!acQ!ddQ!ilQ!t|W!x!P!t!y#bQ!z!QQ!{!RQ!|!SQ#O!TQ#P!UQ#R!VR$o$bpXOk!`!f!p#Z#[#_#m#r#z$[$]$k$y$zzmPUVcdlq|!P!Q!R!S!T!U!V!t!y#U#V#b$bR!W]TuQv!PSOPV]klq!`!f!p#U#V#Z#[#_#m#r#z$[$]$k$y$zU#n#Z$]$kQ#v#_V$T#r$y$zZpPVq#U#VqbOk!`!f!p#Z#[#_#m#r#z$[$]$k$y$zqeOk!`!f!p#Z#[#_#m#r#z$[$]$k$y$z",
|
||||||
nodeNames: "⚠ Star Slash Plus Minus And Or Eq EqEq Neq Lt Lte Gt Gte Modulo Identifier AssignableIdentifier Word IdentifierBeforeDot Do Program PipeExpr FunctionCall DotGet Number ParenExpr FunctionCallOrIdentifier BinOp String StringFragment Interpolation EscapeSeq Boolean Regex Dict NamedArg NamedArgPrefix FunctionDef Params colon CatchExpr keyword TryBlock FinallyExpr keyword keyword Underscore Array Null ConditionalOp PositionalArg operator TryExpr keyword Throw keyword IfExpr keyword SingleLineThenBlock ThenBlock ElseIfExpr keyword ElseExpr keyword Assign",
|
nodeNames: "⚠ Star Slash Plus Minus And Or Eq EqEq Neq Lt Lte Gt Gte Modulo Identifier AssignableIdentifier Word IdentifierBeforeDot Do Program PipeExpr FunctionCall DotGet Number ParenExpr FunctionCallOrIdentifier BinOp String StringFragment Interpolation EscapeSeq Boolean Regex Dict NamedArg NamedArgPrefix FunctionDef Params colon CatchExpr keyword TryBlock FinallyExpr keyword keyword Underscore Array Null ConditionalOp PositionalArg operator TryExpr keyword Throw keyword IfExpr keyword SingleLineThenBlock ThenBlock ElseIfExpr keyword ElseExpr keyword Assign",
|
||||||
maxTerm: 106,
|
maxTerm: 106,
|
||||||
context: trackScope,
|
context: trackScope,
|
||||||
|
|
@ -23,5 +23,5 @@ export const parser = LRParser.deserialize({
|
||||||
tokenizers: [operatorTokenizer, 1, 2, 3, tokenizer, new LocalTokenGroup("[~RP!O!PU~ZO!t~~", 11)],
|
tokenizers: [operatorTokenizer, 1, 2, 3, tokenizer, new LocalTokenGroup("[~RP!O!PU~ZO!t~~", 11)],
|
||||||
topRules: {"Program":[0,20]},
|
topRules: {"Program":[0,20]},
|
||||||
specialized: [{term: 15, get: (value: any, stack: any) => (specializeKeyword(value, stack) << 1), external: specializeKeyword},{term: 15, get: (value: keyof typeof spec_Identifier) => spec_Identifier[value] || -1}],
|
specialized: [{term: 15, get: (value: any, stack: any) => (specializeKeyword(value, stack) << 1), external: specializeKeyword},{term: 15, get: (value: keyof typeof spec_Identifier) => spec_Identifier[value] || -1}],
|
||||||
tokenPrec: 1463
|
tokenPrec: 1547
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,7 @@ export const globals = {
|
||||||
switch (value.type) {
|
switch (value.type) {
|
||||||
case 'string': case 'array': return value.value.length
|
case 'string': case 'array': return value.value.length
|
||||||
case 'dict': return value.value.size
|
case 'dict': return value.value.size
|
||||||
default: return 0
|
default: throw new Error(`length: expected string, array, or dict, got ${value.type}`)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -65,7 +65,24 @@ export const globals = {
|
||||||
identity: (v: any) => v,
|
identity: (v: any) => v,
|
||||||
|
|
||||||
// collections
|
// collections
|
||||||
at: (collection: any, index: number | string) => collection[index],
|
at: (collection: any, index: number | string) => {
|
||||||
|
const value = toValue(collection)
|
||||||
|
if (value.type === 'string' || value.type === 'array') {
|
||||||
|
const idx = typeof index === 'number' ? index : parseInt(index as string)
|
||||||
|
if (idx < 0 || idx >= value.value.length) {
|
||||||
|
throw new Error(`at: index ${idx} out of bounds for ${value.type} of length ${value.value.length}`)
|
||||||
|
}
|
||||||
|
return value.value[idx]
|
||||||
|
} else if (value.type === 'dict') {
|
||||||
|
const key = String(index)
|
||||||
|
if (!value.value.has(key)) {
|
||||||
|
throw new Error(`at: key '${key}' not found in dict`)
|
||||||
|
}
|
||||||
|
return value.value.get(key)
|
||||||
|
} else {
|
||||||
|
throw new Error(`at: expected string, array, or dict, got ${value.type}`)
|
||||||
|
}
|
||||||
|
},
|
||||||
range: (start: number, end: number | null) => {
|
range: (start: number, end: number | null) => {
|
||||||
if (end === null) {
|
if (end === null) {
|
||||||
end = start
|
end = start
|
||||||
|
|
|
||||||
|
|
@ -86,8 +86,14 @@ export const list = {
|
||||||
first: (list: any[]) => list[0] ?? null,
|
first: (list: any[]) => list[0] ?? null,
|
||||||
last: (list: any[]) => list[list.length - 1] ?? null,
|
last: (list: any[]) => list[list.length - 1] ?? null,
|
||||||
rest: (list: any[]) => list.slice(1),
|
rest: (list: any[]) => list.slice(1),
|
||||||
take: (list: any[], n: number) => list.slice(0, n),
|
take: (list: any[], n: number) => {
|
||||||
drop: (list: any[], n: number) => list.slice(n),
|
if (n < 0) throw new Error(`take: count must be non-negative, got ${n}`)
|
||||||
|
return list.slice(0, n)
|
||||||
|
},
|
||||||
|
drop: (list: any[], n: number) => {
|
||||||
|
if (n < 0) throw new Error(`drop: count must be non-negative, got ${n}`)
|
||||||
|
return list.slice(n)
|
||||||
|
},
|
||||||
append: (list: any[], item: any) => [...list, item],
|
append: (list: any[], item: any) => [...list, item],
|
||||||
prepend: (list: any[], item: any) => [item, ...list],
|
prepend: (list: any[], item: any) => [item, ...list],
|
||||||
'index-of': (list: any[], item: any) => list.indexOf(item),
|
'index-of': (list: any[], item: any) => list.indexOf(item),
|
||||||
|
|
|
||||||
|
|
@ -3,12 +3,24 @@ export const math = {
|
||||||
floor: (n: number) => Math.floor(n),
|
floor: (n: number) => Math.floor(n),
|
||||||
ceil: (n: number) => Math.ceil(n),
|
ceil: (n: number) => Math.ceil(n),
|
||||||
round: (n: number) => Math.round(n),
|
round: (n: number) => Math.round(n),
|
||||||
min: (...nums: number[]) => Math.min(...nums),
|
min: (...nums: number[]) => {
|
||||||
max: (...nums: number[]) => Math.max(...nums),
|
if (nums.length === 0) throw new Error('min: expected at least one argument')
|
||||||
|
return Math.min(...nums)
|
||||||
|
},
|
||||||
|
max: (...nums: number[]) => {
|
||||||
|
if (nums.length === 0) throw new Error('max: expected at least one argument')
|
||||||
|
return Math.max(...nums)
|
||||||
|
},
|
||||||
pow: (base: number, exp: number) => Math.pow(base, exp),
|
pow: (base: number, exp: number) => Math.pow(base, exp),
|
||||||
sqrt: (n: number) => Math.sqrt(n),
|
sqrt: (n: number) => {
|
||||||
|
if (n < 0) throw new Error(`sqrt: cannot take square root of negative number ${n}`)
|
||||||
|
return Math.sqrt(n)
|
||||||
|
},
|
||||||
random: () => Math.random(),
|
random: () => Math.random(),
|
||||||
clamp: (n: number, min: number, max: number) => Math.min(Math.max(n, min), max),
|
clamp: (n: number, min: number, max: number) => {
|
||||||
|
if (min > max) throw new Error(`clamp: min (${min}) must be less than or equal to max (${max})`)
|
||||||
|
return Math.min(Math.max(n, min), max)
|
||||||
|
},
|
||||||
sign: (n: number) => Math.sign(n),
|
sign: (n: number) => Math.sign(n),
|
||||||
trunc: (n: number) => Math.trunc(n),
|
trunc: (n: number) => Math.trunc(n),
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,11 @@ export const str = {
|
||||||
'replace-all': (str: string, search: string, replacement: string) => str.replaceAll(search, replacement),
|
'replace-all': (str: string, search: string, replacement: string) => str.replaceAll(search, replacement),
|
||||||
slice: (str: string, start: number, end?: number | null) => str.slice(start, end ?? undefined),
|
slice: (str: string, start: number, end?: number | null) => str.slice(start, end ?? undefined),
|
||||||
substring: (str: string, start: number, end?: number | null) => str.substring(start, end ?? undefined),
|
substring: (str: string, start: number, end?: number | null) => str.substring(start, end ?? undefined),
|
||||||
repeat: (str: string, count: number) => str.repeat(count),
|
repeat: (str: string, count: number) => {
|
||||||
|
if (count < 0) throw new Error(`repeat: count must be non-negative, got ${count}`)
|
||||||
|
if (!Number.isInteger(count)) throw new Error(`repeat: count must be an integer, got ${count}`)
|
||||||
|
return str.repeat(count)
|
||||||
|
},
|
||||||
'pad-start': (str: string, length: number, pad: string = ' ') => str.padStart(length, pad),
|
'pad-start': (str: string, length: number, pad: string = ' ') => str.padStart(length, pad),
|
||||||
'pad-end': (str: string, length: number, pad: string = ' ') => str.padEnd(length, pad),
|
'pad-end': (str: string, length: number, pad: string = ' ') => str.padEnd(length, pad),
|
||||||
lines: (str: string) => str.split('\n'),
|
lines: (str: string) => str.split('\n'),
|
||||||
|
|
|
||||||
|
|
@ -176,9 +176,12 @@ describe('introspection', () => {
|
||||||
await expect(`length 'hello'`).toEvaluateTo(5, globals)
|
await expect(`length 'hello'`).toEvaluateTo(5, globals)
|
||||||
await expect(`length [1 2 3]`).toEvaluateTo(3, globals)
|
await expect(`length [1 2 3]`).toEvaluateTo(3, globals)
|
||||||
await expect(`length [a=1 b=2]`).toEvaluateTo(2, globals)
|
await expect(`length [a=1 b=2]`).toEvaluateTo(2, globals)
|
||||||
await expect(`length 42`).toEvaluateTo(0, globals)
|
})
|
||||||
await expect(`length true`).toEvaluateTo(0, globals)
|
|
||||||
await expect(`length null`).toEvaluateTo(0, globals)
|
test('length throws on invalid types', async () => {
|
||||||
|
await expect(`try: length 42 catch e: 'error' end`).toEvaluateTo('error', globals)
|
||||||
|
await expect(`try: length true catch e: 'error' end`).toEvaluateTo('error', globals)
|
||||||
|
await expect(`try: length null catch e: 'error' end`).toEvaluateTo('error', globals)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('inspect formats values', async () => {
|
test('inspect formats values', async () => {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user