diff --git a/src/prelude/dict.ts b/src/prelude/dict.ts new file mode 100644 index 0000000..b3ee271 --- /dev/null +++ b/src/prelude/dict.ts @@ -0,0 +1,24 @@ +export const dict = { + keys: (dict: Record) => Object.keys(dict), + values: (dict: Record) => Object.values(dict), + entries: (dict: Record) => Object.entries(dict).map(([k, v]) => ({ key: k, value: v })), + 'has?': (dict: Record, key: string) => key in dict, + 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), +} diff --git a/src/prelude/index.ts b/src/prelude/index.ts index 145aacd..50e45e4 100644 --- a/src/prelude/index.ts +++ b/src/prelude/index.ts @@ -1,13 +1,16 @@ // 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, + type Value, toValue, extractParamInfo, isWrapped, getOriginalFunction, } from 'reefvm' +import { dict } from './dict' +import { load } from './load' +import { list } from './list' +import { math } from './math' +import { str } from './str' + export const colors = { reset: '\x1b[0m', bright: '\x1b[1m', @@ -22,6 +25,12 @@ export const colors = { } export const globals = { + dict, + load, + list, + math, + str, + // hello echo: (...args: any[]) => { console.log(...args.map(a => { @@ -64,121 +73,6 @@ export const globals = { dec: (n: number) => n - 1, identity: (v: any) => v, - // strings - str: { - 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(), - // predicates - 'starts-with?': (str: string, prefix: string) => str.startsWith(prefix), - 'ends-with?': (str: string, suffix: string) => str.endsWith(suffix), - 'contains?': (str: string, substr: string) => str.includes(substr), - 'empty?': (str: string) => str.length === 0, - // transformations - replace: (str: string, search: string, replacement: string) => str.replace(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), - substring: (str: string, start: number, end?: number | null) => str.substring(start, end ?? undefined), - repeat: (str: string, count: number) => str.repeat(count), - 'pad-start': (str: string, length: number, pad: string = ' ') => str.padStart(length, pad), - '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 - list: { - slice: (list: any[], start: number, end?: number) => list.slice(start, end), - map: async (list: any[], cb: Function) => { - let acc: any[] = [] - for (const value of list) acc.push(await cb(value)) - return acc - }, - filter: async (list: any[], cb: Function) => { - let acc: any[] = [] - for (const value of list) { - if (await cb(value)) acc.push(value) - } - return acc - }, - reduce: async (list: any[], cb: Function, initial: any) => { - let acc = initial - for (const value of list) acc = await cb(acc, value) - return acc - }, - find: async (list: any[], cb: Function) => { - for (const value of list) { - if (await cb(value)) return value - } - return null - }, - // 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), - concat: (...lists: any[][]) => lists.flat(1), - flatten: (list: any[], depth: number = 1) => list.flat(depth), - unique: (list: any[]) => Array.from(new Set(list)), - zip: (list1: any[], list2: any[]) => list1.map((item, i) => [item, list2[i]]), - // access - first: (list: any[]) => list[0] ?? null, - last: (list: any[]) => list[list.length - 1] ?? null, - rest: (list: any[]) => list.slice(1), - take: (list: any[], n: number) => list.slice(0, n), - drop: (list: any[], n: number) => list.slice(n), - 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 at: (collection: any, index: number | string) => collection[index], range: (start: number, end: number | null) => { @@ -204,85 +98,12 @@ export const globals = { } }, - // dict - dict: { - keys: (dict: Record) => Object.keys(dict), - values: (dict: Record) => Object.values(dict), - entries: (dict: Record) => Object.entries(dict).map(([k, v]) => ({ key: k, value: v })), - 'has?': (dict: Record, key: string) => key in dict, - 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 - math: { - abs: (n: number) => Math.abs(n), - floor: (n: number) => Math.floor(n), - ceil: (n: number) => Math.ceil(n), - round: (n: number) => Math.round(n), - min: (...nums: number[]) => Math.min(...nums), - max: (...nums: number[]) => Math.max(...nums), - 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, - 'positive?': (n: number) => n > 0, - 'negative?': (n: number) => n < 0, - 'zero?': (n: number) => n === 0, - }, - // enumerables each: async (list: any[], cb: Function) => { for (const value of list) await cb(value) return list }, - // modules - load: async function (this: VM, path: string): Promise> { - 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: Record = {} - for (const [name, value] of this.scope.locals.entries()) - module[name] = value - - this.scope = scope - this.pc = pc - this.stopped = false - - return module - }, } export function formatValue(value: Value, inner = false): string { diff --git a/src/prelude/list.ts b/src/prelude/list.ts new file mode 100644 index 0000000..eb013ef --- /dev/null +++ b/src/prelude/list.ts @@ -0,0 +1,89 @@ +export const list = { + slice: (list: any[], start: number, end?: number) => list.slice(start, end), + map: async (list: any[], cb: Function) => { + let acc: any[] = [] + for (const value of list) acc.push(await cb(value)) + return acc + }, + filter: async (list: any[], cb: Function) => { + let acc: any[] = [] + for (const value of list) { + if (await cb(value)) acc.push(value) + } + return acc + }, + reduce: async (list: any[], cb: Function, initial: any) => { + let acc = initial + for (const value of list) acc = await cb(acc, value) + return acc + }, + find: async (list: any[], cb: Function) => { + for (const value of list) { + if (await cb(value)) return value + } + return null + }, + + // 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), + concat: (...lists: any[][]) => lists.flat(1), + flatten: (list: any[], depth: number = 1) => list.flat(depth), + unique: (list: any[]) => Array.from(new Set(list)), + zip: (list1: any[], list2: any[]) => list1.map((item, i) => [item, list2[i]]), + + // access + first: (list: any[]) => list[0] ?? null, + last: (list: any[]) => list[list.length - 1] ?? null, + rest: (list: any[]) => list.slice(1), + take: (list: any[], n: number) => list.slice(0, n), + drop: (list: any[], n: number) => list.slice(n), + 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 + }, +} \ No newline at end of file diff --git a/src/prelude/load.ts b/src/prelude/load.ts new file mode 100644 index 0000000..3f317c1 --- /dev/null +++ b/src/prelude/load.ts @@ -0,0 +1,29 @@ +import { resolve } from 'path' +import { readFileSync } from 'fs' +import { Compiler } from '#compiler/compiler' +import { type Value, VM, Scope } from 'reefvm' + +export const load = async function (this: VM, path: string): Promise> { + 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: Record = {} + for (const [name, value] of this.scope.locals.entries()) + module[name] = value + + this.scope = scope + this.pc = pc + this.stopped = false + + return module +} \ No newline at end of file diff --git a/src/prelude/math.ts b/src/prelude/math.ts new file mode 100644 index 0000000..21f2f57 --- /dev/null +++ b/src/prelude/math.ts @@ -0,0 +1,21 @@ +export const math = { + abs: (n: number) => Math.abs(n), + floor: (n: number) => Math.floor(n), + ceil: (n: number) => Math.ceil(n), + round: (n: number) => Math.round(n), + min: (...nums: number[]) => Math.min(...nums), + max: (...nums: number[]) => Math.max(...nums), + 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, + 'positive?': (n: number) => n > 0, + 'negative?': (n: number) => n < 0, + 'zero?': (n: number) => n === 0, +} \ No newline at end of file diff --git a/src/prelude/str.ts b/src/prelude/str.ts new file mode 100644 index 0000000..fa0d657 --- /dev/null +++ b/src/prelude/str.ts @@ -0,0 +1,33 @@ +// strings +export const str = { + 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(), + + // predicates + 'starts-with?': (str: string, prefix: string) => str.startsWith(prefix), + 'ends-with?': (str: string, suffix: string) => str.endsWith(suffix), + 'contains?': (str: string, substr: string) => str.includes(substr), + 'empty?': (str: string) => str.length === 0, + + // inspection + 'index-of': (str: string, search: string) => str.indexOf(search), + 'last-index-of': (str: string, search: string) => str.lastIndexOf(search), + + // transformations + replace: (str: string, search: string, replacement: string) => str.replace(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), + substring: (str: string, start: number, end?: number | null) => str.substring(start, end ?? undefined), + repeat: (str: string, count: number) => str.repeat(count), + 'pad-start': (str: string, length: number, pad: string = ' ') => str.padStart(length, pad), + 'pad-end': (str: string, length: number, pad: string = ' ') => str.padEnd(length, pad), + lines: (str: string) => str.split('\n'), + chars: (str: string) => str.split(''), + + // regex + match: (str: string, regex: RegExp) => str.match(regex), + 'test?': (str: string, regex: RegExp) => regex.test(str), +} \ No newline at end of file