diff --git a/src/prelude/index.ts b/src/prelude/index.ts index cf6e5a5..2a01236 100644 --- a/src/prelude/index.ts +++ b/src/prelude/index.ts @@ -13,8 +13,9 @@ import { load } from './load' import { list } from './list' import { math } from './math' import { str } from './str' +import { types } from './types' -export const globals = { +export const globals: Record = { dict, fs, json, @@ -34,7 +35,6 @@ export const globals = { name: Bun.argv[2] || '(shrimp)', path: resolve(join('.', Bun.argv[2] ?? '')) }, - }, // hello @@ -84,16 +84,6 @@ export const globals = { exit: (num: number) => process.exit(num ?? 0), // 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 @@ -212,4 +202,8 @@ export function formatValue(value: Value, inner = false): string { default: return String(value) } -} \ No newline at end of file +} + +// add types functions to top-level namespace +for (const [key, value] of Object.entries(types)) + globals[key] = value \ No newline at end of file diff --git a/src/prelude/tests/types.test.ts b/src/prelude/tests/types.test.ts new file mode 100644 index 0000000..6a4a587 --- /dev/null +++ b/src/prelude/tests/types.test.ts @@ -0,0 +1,143 @@ +import { expect, describe, test } from 'bun:test' + +describe('type predicates', () => { + test('boolean? checks if value is boolean', async () => { + await expect(`boolean? true`).toEvaluateTo(true) + await expect(`boolean? false`).toEvaluateTo(true) + await expect(`boolean? 42`).toEvaluateTo(false) + await expect(`boolean? 'hello'`).toEvaluateTo(false) + await expect(`boolean? null`).toEvaluateTo(false) + await expect(`boolean? [1 2 3]`).toEvaluateTo(false) + }) + + test('number? checks if value is number', async () => { + await expect(`number? 42`).toEvaluateTo(true) + await expect(`number? 3.14`).toEvaluateTo(true) + await expect(`number? 0`).toEvaluateTo(true) + await expect(`number? -5`).toEvaluateTo(true) + await expect(`number? 'hello'`).toEvaluateTo(false) + await expect(`number? true`).toEvaluateTo(false) + await expect(`number? null`).toEvaluateTo(false) + }) + + test('string? checks if value is string', async () => { + await expect(`string? 'hello'`).toEvaluateTo(true) + await expect(`string? ''`).toEvaluateTo(true) + await expect(`string? world`).toEvaluateTo(true) + await expect(`string? 42`).toEvaluateTo(false) + await expect(`string? true`).toEvaluateTo(false) + await expect(`string? null`).toEvaluateTo(false) + await expect(`string? [1 2 3]`).toEvaluateTo(false) + }) + + test('array? checks if value is array', async () => { + await expect(`array? [1 2 3]`).toEvaluateTo(true) + await expect(`array? []`).toEvaluateTo(true) + await expect(`array? ['a' 'b']`).toEvaluateTo(true) + await expect(`array? [a=1 b=2]`).toEvaluateTo(false) + await expect(`array? 42`).toEvaluateTo(false) + await expect(`array? 'hello'`).toEvaluateTo(false) + await expect(`array? null`).toEvaluateTo(false) + }) + + test('list? is alias for array?', async () => { + await expect(`list? [1 2 3]`).toEvaluateTo(true) + await expect(`list? []`).toEvaluateTo(true) + await expect(`list? [a=1 b=2]`).toEvaluateTo(false) + }) + + test('dict? checks if value is dict', async () => { + await expect(`dict? [a=1 b=2]`).toEvaluateTo(true) + await expect(`dict? [=]`).toEvaluateTo(true) + await expect(`dict? [1 2 3]`).toEvaluateTo(false) + await expect(`dict? []`).toEvaluateTo(false) + await expect(`dict? 42`).toEvaluateTo(false) + await expect(`dict? 'hello'`).toEvaluateTo(false) + }) + + test('function? checks if value is function', async () => { + await expect(` + my-fn = do x: x * 2 end + function? my-fn + `).toEvaluateTo(true) + await expect(`function? inc`).toEvaluateTo(true) + await expect(`function? list.map`).toEvaluateTo(true) + await expect(`function? 42`).toEvaluateTo(false) + await expect(`function? 'hello'`).toEvaluateTo(false) + await expect(`function? [1 2 3]`).toEvaluateTo(false) + }) + + test('null? checks if value is null', async () => { + await expect(`null? null`).toEvaluateTo(true) + await expect(`null? 0`).toEvaluateTo(false) + await expect(`null? false`).toEvaluateTo(false) + await expect(`null? ''`).toEvaluateTo(false) + await expect(`null? []`).toEvaluateTo(false) + }) +}) + +describe('type coercion', () => { + test('boolean coerces to boolean', async () => { + await expect(`boolean true`).toEvaluateTo(true) + await expect(`boolean false`).toEvaluateTo(false) + await expect(`boolean 1`).toEvaluateTo(true) + await expect(`boolean 0`).toEvaluateTo(false) + await expect(`boolean 'hello'`).toEvaluateTo(true) + await expect(`boolean ''`).toEvaluateTo(false) + await expect(`boolean null`).toEvaluateTo(false) + await expect(`boolean [1 2 3]`).toEvaluateTo(true) + }) + + test('number coerces to number', async () => { + await expect(`number 42`).toEvaluateTo(42) + await expect(`number '42'`).toEvaluateTo(42) + await expect(`number '3.14'`).toEvaluateTo(3.14) + await expect(`number true`).toEvaluateTo(1) + await expect(`number false`).toEvaluateTo(0) + }) + + test('string coerces to string', async () => { + await expect(`string 'hello'`).toEvaluateTo('hello') + await expect(`string 42`).toEvaluateTo('42') + await expect(`string true`).toEvaluateTo('true') + await expect(`string false`).toEvaluateTo('false') + await expect(`string null`).toEvaluateTo('null') + }) +}) + +describe('type predicates in conditionals', () => { + test('using type predicates in if statements', async () => { + await expect(` + x = 42 + if (number? x): + 'is-num' + else: + 'not-num' + end + `).toEvaluateTo('is-num') + }) + + test('filtering by type', async () => { + await expect(` + items = [1 'hello' 2 'world' 3] + list.filter items number? + `).toEvaluateTo([1, 2, 3]) + }) + + test('filtering strings', async () => { + await expect(` + items = [1 'hello' 2 'world' 3] + list.filter items string? + `).toEvaluateTo(['hello', 'world']) + }) + + test('checking for functions', async () => { + await expect(` + double = do x: x * 2 end + not-fn = 42 + is-fn = function? double + is-not-fn = function? not-fn + is-fn and (not is-not-fn) + `).toEvaluateTo(true) + }) +}) diff --git a/src/prelude/types.ts b/src/prelude/types.ts new file mode 100644 index 0000000..a92c4c0 --- /dev/null +++ b/src/prelude/types.ts @@ -0,0 +1,22 @@ +import { toValue } from 'reefvm' + +export const types = { + 'boolean?': (v: any) => toValue(v).type === 'boolean', + boolean: (v: any) => Boolean(v), + + 'number?': (v: any) => toValue(v).type === 'number', + number: (v: any) => Number(v), + + 'string?': (v: any) => toValue(v).type === 'string', + string: (v: any) => String(v), + + + 'array?': (v: any) => toValue(v).type === 'array', + 'list?': (v: any) => toValue(v).type === 'array', + + 'dict?': (v: any) => toValue(v).type === 'dict', + + 'function?': (v: any) => ['function', 'native'].includes(toValue(v).type), + + 'null?': (v: any) => toValue(v).type === 'null', +}