Prelude of builtin functions #7

Merged
defunkt merged 45 commits from prelude into main 2025-10-29 20:15:37 +00:00
2 changed files with 248 additions and 0 deletions
Showing only changes of commit 3c06cac36c - Show all commits

View File

@ -43,6 +43,27 @@ export const globalFunctions = {
}
},
// type predicates
'string?': (v: any) => toValue(v).type === 'string',
'number?': (v: any) => toValue(v).type === 'number',
'boolean?': (v: any) => toValue(v).type === 'boolean',
'array?': (v: any) => toValue(v).type === 'array',
'dict?': (v: any) => toValue(v).type === 'dict',
'function?': (v: any) => {
const t = toValue(v).type
return t === 'function' || t === 'native'
},
'null?': (v: any) => toValue(v).type === 'null',
'some?': (v: any) => toValue(v).type !== 'null',
// boolean/logic
not: (v: any) => !v,
// utilities
inc: (n: number) => n + 1,
dec: (n: number) => n - 1,
identity: (v: any) => v,
// strings
str: {
join: (arr: string[], sep: string = ',') => arr.join(sep),
@ -65,6 +86,10 @@ export const globalFunctions = {
'pad-end': (str: string, length: number, pad: string = ' ') => str.padEnd(length, pad),
lines: (str: string) => str.split('\n'),
chars: (str: string) => str.split(''),
'index-of': (str: string, search: string) => str.indexOf(search),
'last-index-of': (str: string, search: string) => str.lastIndexOf(search),
match: (str: string, regex: RegExp) => str.match(regex),
'test?': (str: string, regex: RegExp) => regex.test(str),
},
// list
@ -96,6 +121,18 @@ export const globalFunctions = {
// predicates
'empty?': (list: any[]) => list.length === 0,
'contains?': (list: any[], item: any) => list.includes(item),
'any?': async (list: any[], cb: Function) => {
for (const value of list) {
if (await cb(value)) return true
}
return false
},
'all?': async (list: any[], cb: Function) => {
for (const value of list) {
if (!await cb(value)) return false
}
return true
},
// sequence operations
reverse: (list: any[]) => list.slice().reverse(),
sort: (list: any[], cb?: (a: any, b: any) => number) => list.slice().sort(cb),
@ -112,6 +149,34 @@ export const globalFunctions = {
append: (list: any[], item: any) => [...list, item],
prepend: (list: any[], item: any) => [item, ...list],
'index-of': (list: any[], item: any) => list.indexOf(item),
// utilities
sum: (list: any[]) => list.reduce((acc, x) => acc + x, 0),
count: async (list: any[], cb: Function) => {
let count = 0
for (const value of list) {
if (await cb(value)) count++
}
return count
},
partition: async (list: any[], cb: Function) => {
const truthy: any[] = []
const falsy: any[] = []
for (const value of list) {
if (await cb(value)) truthy.push(value)
else falsy.push(value)
}
return [truthy, falsy]
},
compact: (list: any[]) => list.filter(x => x != null),
'group-by': async (list: any[], cb: Function) => {
const groups: Record<string, any[]> = {}
for (const value of list) {
const key = String(await cb(value))
if (!groups[key]) groups[key] = []
groups[key].push(value)
}
return groups
},
},
// collections
@ -148,6 +213,21 @@ export const globalFunctions = {
get: (dict: Record<string, any>, key: string, defaultValue: any = null) => dict[key] ?? defaultValue,
merge: (...dicts: Record<string, any>[]) => Object.assign({}, ...dicts),
'empty?': (dict: Record<string, any>) => Object.keys(dict).length === 0,
map: async (dict: Record<string, any>, cb: Function) => {
const result: Record<string, any> = {}
for (const [key, value] of Object.entries(dict)) {
result[key] = await cb(value, key)
}
return result
},
filter: async (dict: Record<string, any>, cb: Function) => {
const result: Record<string, any> = {}
for (const [key, value] of Object.entries(dict)) {
if (await cb(value, key)) result[key] = value
}
return result
},
'from-entries': (entries: [string, any][]) => Object.fromEntries(entries),
},
// math
@ -161,6 +241,9 @@ export const globalFunctions = {
pow: (base: number, exp: number) => Math.pow(base, exp),
sqrt: (n: number) => Math.sqrt(n),
random: () => Math.random(),
clamp: (n: number, min: number, max: number) => Math.min(Math.max(n, min), max),
sign: (n: number) => Math.sign(n),
trunc: (n: number) => Math.trunc(n),
// predicates
'even?': (n: number) => n % 2 === 0,
'odd?': (n: number) => n % 2 !== 0,

View File

@ -87,6 +87,78 @@ describe('string operations', () => {
test('chars splits into characters', async () => {
await expect(`str.chars 'abc'`).toEvaluateTo(['a', 'b', 'c'], globalFunctions)
})
test('index-of finds substring position', async () => {
await expect(`str.index-of 'hello world' 'world'`).toEvaluateTo(6, globalFunctions)
await expect(`str.index-of 'hello' 'bye'`).toEvaluateTo(-1, globalFunctions)
})
test('last-index-of finds last occurrence', async () => {
await expect(`str.last-index-of 'hello hello' 'hello'`).toEvaluateTo(6, globalFunctions)
})
})
describe('type predicates', () => {
test('string? checks for string type', async () => {
await expect(`string? 'hello'`).toEvaluateTo(true, globalFunctions)
await expect(`string? 42`).toEvaluateTo(false, globalFunctions)
})
test('number? checks for number type', async () => {
await expect(`number? 42`).toEvaluateTo(true, globalFunctions)
await expect(`number? 'hello'`).toEvaluateTo(false, globalFunctions)
})
test('boolean? checks for boolean type', async () => {
await expect(`boolean? true`).toEvaluateTo(true, globalFunctions)
await expect(`boolean? 42`).toEvaluateTo(false, globalFunctions)
})
test('array? checks for array type', async () => {
await expect(`array? [1 2 3]`).toEvaluateTo(true, globalFunctions)
await expect(`array? 42`).toEvaluateTo(false, globalFunctions)
})
test('dict? checks for dict type', async () => {
await expect(`dict? [a=1]`).toEvaluateTo(true, globalFunctions)
await expect(`dict? []`).toEvaluateTo(false, globalFunctions)
})
test('null? checks for null type', async () => {
await expect(`null? null`).toEvaluateTo(true, globalFunctions)
await expect(`null? 42`).toEvaluateTo(false, globalFunctions)
})
test('some? checks for non-null', async () => {
await expect(`some? 42`).toEvaluateTo(true, globalFunctions)
await expect(`some? null`).toEvaluateTo(false, globalFunctions)
})
})
describe('boolean logic', () => {
test('not negates value', async () => {
await expect(`not true`).toEvaluateTo(false, globalFunctions)
await expect(`not false`).toEvaluateTo(true, globalFunctions)
await expect(`not 42`).toEvaluateTo(false, globalFunctions)
await expect(`not null`).toEvaluateTo(true, globalFunctions)
})
})
describe('utilities', () => {
test('inc increments by 1', async () => {
await expect(`inc 5`).toEvaluateTo(6, globalFunctions)
await expect(`inc -1`).toEvaluateTo(0, globalFunctions)
})
test('dec decrements by 1', async () => {
await expect(`dec 5`).toEvaluateTo(4, globalFunctions)
await expect(`dec 0`).toEvaluateTo(-1, globalFunctions)
})
test('identity returns value as-is', async () => {
await expect(`identity 42`).toEvaluateTo(42, globalFunctions)
await expect(`identity 'hello'`).toEvaluateTo('hello', globalFunctions)
})
})
describe('introspection', () => {
@ -262,6 +334,64 @@ describe('collections', () => {
await expect(`list.index-of [1 2 3] 2`).toEvaluateTo(1, globalFunctions)
await expect(`list.index-of [1 2 3] 5`).toEvaluateTo(-1, globalFunctions)
})
test('list.any? checks if any element matches', async () => {
await expect(`
gt-three = do x: x > 3 end
list.any? [1 2 4 5] gt-three
`).toEvaluateTo(true, globalFunctions)
await expect(`
gt-ten = do x: x > 10 end
list.any? [1 2 3] gt-ten
`).toEvaluateTo(false, globalFunctions)
})
test('list.all? checks if all elements match', async () => {
await expect(`
positive = do x: x > 0 end
list.all? [1 2 3] positive
`).toEvaluateTo(true, globalFunctions)
await expect(`
positive = do x: x > 0 end
list.all? [1 -2 3] positive
`).toEvaluateTo(false, globalFunctions)
})
test('list.sum adds all numbers', async () => {
await expect(`list.sum [1 2 3 4]`).toEvaluateTo(10, globalFunctions)
await expect(`list.sum []`).toEvaluateTo(0, globalFunctions)
})
test('list.count counts matching elements', async () => {
await expect(`
gt-two = do x: x > 2 end
list.count [1 2 3 4 5] gt-two
`).toEvaluateTo(3, globalFunctions)
})
test('list.partition splits array by predicate', async () => {
await expect(`
gt-two = do x: x > 2 end
list.partition [1 2 3 4 5] gt-two
`).toEvaluateTo([[3, 4, 5], [1, 2]], globalFunctions)
})
test('list.compact removes null values', async () => {
await expect(`list.compact [1 null 2 null 3]`).toEvaluateTo([1, 2, 3], globalFunctions)
})
test('list.group-by groups by key function', async () => {
await expect(`
get-type = do x:
if (string? x):
'str'
else:
'num'
end
end
list.group-by ['a' 1 'b' 2] get-type
`).toEvaluateTo({ str: ['a', 'b'], num: [1, 2] }, globalFunctions)
})
})
describe('enumerables', () => {
@ -343,6 +473,24 @@ describe('dict operations', () => {
test('dict.merge combines dicts', async () => {
await expect(`dict.merge [a=1] [b=2]`).toEvaluateTo({ a: 1, b: 2 }, globalFunctions)
})
test('dict.map transforms values', async () => {
await expect(`
double = do v k: v * 2 end
dict.map [a=1 b=2] double
`).toEvaluateTo({ a: 2, b: 4 }, globalFunctions)
})
test('dict.filter keeps matching entries', async () => {
await expect(`
gt-one = do v k: v > 1 end
dict.filter [a=1 b=2 c=3] gt-one
`).toEvaluateTo({ b: 2, c: 3 }, globalFunctions)
})
test('dict.from-entries creates dict from array', async () => {
await expect(`dict.from-entries [['a' 1] ['b' 2]]`).toEvaluateTo({ a: 1, b: 2 }, globalFunctions)
})
})
describe('math operations', () => {
@ -405,6 +553,23 @@ describe('math operations', () => {
await expect(`math.zero? 0`).toEvaluateTo(true, globalFunctions)
await expect(`math.zero? 5`).toEvaluateTo(false, globalFunctions)
})
test('math.clamp restricts value to range', async () => {
await expect(`math.clamp 5 0 10`).toEvaluateTo(5, globalFunctions)
await expect(`math.clamp -5 0 10`).toEvaluateTo(0, globalFunctions)
await expect(`math.clamp 15 0 10`).toEvaluateTo(10, globalFunctions)
})
test('math.sign returns sign of number', async () => {
await expect(`math.sign 5`).toEvaluateTo(1, globalFunctions)
await expect(`math.sign -5`).toEvaluateTo(-1, globalFunctions)
await expect(`math.sign 0`).toEvaluateTo(0, globalFunctions)
})
test('math.trunc truncates decimal', async () => {
await expect(`math.trunc 3.7`).toEvaluateTo(3, globalFunctions)
await expect(`math.trunc -3.7`).toEvaluateTo(-3, globalFunctions)
})
})
// describe('echo', () => {