From f8d2236292deeb14bddbe33e8d96024b276eac64 Mon Sep 17 00:00:00 2001 From: Chris Wanstrath Date: Wed, 29 Oct 2025 15:36:18 -0700 Subject: [PATCH] add exceptions to prelude functions --- src/prelude/index.ts | 21 +++++++++++++++++++-- src/prelude/list.ts | 10 ++++++++-- src/prelude/math.ts | 20 ++++++++++++++++---- src/prelude/str.ts | 6 +++++- src/prelude/tests/prelude.test.ts | 9 ++++++--- 5 files changed, 54 insertions(+), 12 deletions(-) diff --git a/src/prelude/index.ts b/src/prelude/index.ts index facf4b8..cc46ead 100644 --- a/src/prelude/index.ts +++ b/src/prelude/index.ts @@ -39,7 +39,7 @@ export const globals = { switch (value.type) { case 'string': case 'array': return value.value.length 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, // 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) => { if (end === null) { end = start diff --git a/src/prelude/list.ts b/src/prelude/list.ts index eb013ef..8c0452d 100644 --- a/src/prelude/list.ts +++ b/src/prelude/list.ts @@ -52,8 +52,14 @@ export const list = { 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), + take: (list: any[], n: number) => { + 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], prepend: (list: any[], item: any) => [item, ...list], 'index-of': (list: any[], item: any) => list.indexOf(item), diff --git a/src/prelude/math.ts b/src/prelude/math.ts index 21f2f57..148cde9 100644 --- a/src/prelude/math.ts +++ b/src/prelude/math.ts @@ -3,12 +3,24 @@ export const math = { 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), + min: (...nums: number[]) => { + 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), - 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(), - 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), trunc: (n: number) => Math.trunc(n), diff --git a/src/prelude/str.ts b/src/prelude/str.ts index fa0d657..5aede56 100644 --- a/src/prelude/str.ts +++ b/src/prelude/str.ts @@ -21,7 +21,11 @@ export const str = { '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), + 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-end': (str: string, length: number, pad: string = ' ') => str.padEnd(length, pad), lines: (str: string) => str.split('\n'), diff --git a/src/prelude/tests/prelude.test.ts b/src/prelude/tests/prelude.test.ts index ef7d8d6..a073d86 100644 --- a/src/prelude/tests/prelude.test.ts +++ b/src/prelude/tests/prelude.test.ts @@ -176,9 +176,12 @@ describe('introspection', () => { await expect(`length 'hello'`).toEvaluateTo(5, globals) await expect(`length [1 2 3]`).toEvaluateTo(3, 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 () => {