From 3c06cac36c695d7154623277cce82fe9fb2342b0 Mon Sep 17 00:00:00 2001 From: Chris Wanstrath Date: Wed, 29 Oct 2025 11:34:04 -0700 Subject: [PATCH] more prelude functions --- src/prelude/index.ts | 83 +++++++++++++++ src/prelude/tests/prelude.test.ts | 165 ++++++++++++++++++++++++++++++ 2 files changed, 248 insertions(+) diff --git a/src/prelude/index.ts b/src/prelude/index.ts index 8eb9bb3..952bca9 100644 --- a/src/prelude/index.ts +++ b/src/prelude/index.ts @@ -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 = {} + 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, key: string, defaultValue: any = null) => dict[key] ?? defaultValue, merge: (...dicts: Record[]) => Object.assign({}, ...dicts), 'empty?': (dict: Record) => Object.keys(dict).length === 0, + map: async (dict: Record, cb: Function) => { + const result: Record = {} + for (const [key, value] of Object.entries(dict)) { + result[key] = await cb(value, key) + } + return result + }, + filter: async (dict: Record, cb: Function) => { + const result: Record = {} + 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, diff --git a/src/prelude/tests/prelude.test.ts b/src/prelude/tests/prelude.test.ts index 8f3d194..358dfae 100644 --- a/src/prelude/tests/prelude.test.ts +++ b/src/prelude/tests/prelude.test.ts @@ -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', () => {