A minimal, high-performance multi-type expression language for the browser
Littlewing is a multi-type expression language with seven value types: numbers, strings, booleans, dates, times, datetimes, and arrays. It's designed for evaluating expressions, financial calculations, temporal arithmetic, and conditional logic with type-safe execution.
- Seven types - Numbers, strings, booleans, dates (
Temporal.PlainDate), times (Temporal.PlainTime), datetimes (Temporal.PlainDateTime), and homogeneous arrays - No implicit coercion - Explicit type conversion via
STR(),NUM(), etc. - Strict boolean logic -
!,&&,||, andifconditions require booleans - Control flow -
if/then/elseexpressions andfor/in/thencomprehensions with optionalwhenguard andintoaccumulator - Pipe operator -
x |> FUN(?) |> OTHER(?, 1)chains values through function calls - Bracket indexing -
arr[0],str[1], with negative indexing and chaining - Range expressions -
1..5(exclusive),1..=5(inclusive) - Deep equality -
[1, 2] == [1, 2]evaluates totrue - Temporal arithmetic - Built-in date, time, and datetime functions using Temporal API
- Zero runtime dependencies - Requires global
TemporalAPI (native or user-provided polyfill) - Linear time execution - O(n) parsing and evaluation
- Context variables - Runtime value injection
- Custom functions - Extensible with JavaScript functions
- Comments supported - Single-line comments with
// - Optional semicolons - Semicolons between statements are optional
// Basic arithmetic
2 + 3 * 4 // → 14
// String concatenation
"hello" + " world" // → "hello world"
// Boolean logic
5 > 3 && 10 != 5 // → true
// Variables and assignment
radius = 5
area = 3.14159 * radius ^ 2 // → 78.54
// Conditional expressions
score = 85
grade = if score >= 90 then "A" else if score >= 80 then "B" else "C" // → "B"
// Date arithmetic
today = TODAY()
deadline = ADD_DAYS(today, 7) // 7 days from now
daysLeft = DIFFERENCE_IN_DAYS(today, deadline)
// Arrays with bracket indexing
items = [10, 20, 30]
first = items[0] // → 10
last = items[-1] // → 30
// Range expressions
nums = 1..=5 // → [1, 2, 3, 4, 5]
// Pipe operator — chain values through functions
-5 |> ABS(?) // → 5
-5 |> ABS(?) |> STR(?) // → "5"
150 |> CLAMP(?, 0, 100) // → 100
// For comprehensions
doubled = for x in 1..=5 then x * 2 // → [2, 4, 6, 8, 10]
evens = for x in 1..=10 when x % 2 == 0 then x // → [2, 4, 6, 8, 10]
// Financial calculations
principal = 1000
rate = 0.05
years = 3
futureValue = principal * (1 + rate) ^ years // → 1157.625
Littlewing has seven value types:
| Type | Examples | Description |
|---|---|---|
number |
42, 3.14, 0.5 |
IEEE 754 double-precision floats |
string |
"hello", "line1\nline2" |
Double-quoted, with escape sequences |
boolean |
true, false |
Logical values |
date |
DATE(2024, 6, 15) |
Temporal.PlainDate (no time, no timezone) |
time |
TIME(14, 30, 0) |
Temporal.PlainTime (no date, no timezone) |
datetime |
DATETIME(2024, 6, 15, 14, 30, 0) |
Temporal.PlainDateTime (no timezone) |
array |
[1, 2, 3], ["a", "b"] |
Homogeneous (all elements same type) |
There is no implicit type coercion. Use explicit conversion functions:
// Convert between types explicitly
STR(42) // → "42"
NUM("3.14") // → 3.14
TYPE(true) // → "boolean"
- Integers:
42,0,-17 - Decimals:
3.14159,0.5,-0.5
x = 42 // integer
y = 3.14159 // decimal
z = 0.5 // decimal (leading zero required)
Double-quoted strings with escape sequences:
greeting = "hello world"
escaped = "line1\nline2" // newline
quoted = "say \"hi\"" // escaped quotes
path = "C:\\Users\\file" // escaped backslash
empty = "" // empty string
Supported escape sequences: \", \\, \n, \t
String concatenation uses +:
first = "hello"
last = " world"
full = first + last // → "hello world"
Strings support bracket indexing (returns single-character strings):
msg = "hello"
msg[0] // → "h"
msg[-1] // → "o"
true and false are boolean literals:
x = true
y = false
result = x && y // → false
Booleans cannot be used as numbers. Use explicit conversion:
// This is a TypeError:
// true + 1
// Use NUM() to convert:
NUM(true) + 1 // → 2
Dates are Temporal.PlainDate values (date only, no time, no timezone):
today = TODAY() // current date
christmas = DATE(2024, 12, 25) // specific date
nextWeek = ADD_DAYS(today, 7) // date arithmetic
Dates can be compared with <, >, <=, >=, ==, !=:
d1 = DATE(2024, 1, 1)
d2 = DATE(2024, 12, 31)
d1 < d2 // → true
Times are Temporal.PlainTime values (time only, no date, no timezone):
t = TIME(14, 30, 0) // 2:30 PM
now = NOW_TIME() // current time
later = ADD_HOURS(t, 3) // time arithmetic (wraps at midnight)
Times can be compared with <, >, <=, >=, ==, !=:
t1 = TIME(10, 0, 0)
t2 = TIME(14, 0, 0)
t1 < t2 // → true
DateTimes are Temporal.PlainDateTime values (date + time, no timezone):
dt = DATETIME(2024, 6, 15, 14, 30, 0) // June 15, 2024 at 2:30 PM
now = NOW() // current date and time
d = TO_DATE(dt) // extract date portion
t = TO_TIME(dt) // extract time portion
combined = COMBINE(d, t) // combine date + time into datetime
DateTimes are fully supported by date functions (extractors, arithmetic, period boundaries) and time functions (extractors, arithmetic). The input type is preserved:
// Date functions preserve datetime type
ADD_DAYS(dt, 7) // → PlainDateTime (7 days later, same time)
START_OF_MONTH(dt) // → PlainDateTime (first day of month, same time)
// Time functions work on datetime
HOUR(dt) // → 14
ADD_HOURS(dt, 5) // → PlainDateTime (date may change)
Homogeneous arrays (all elements must be the same type):
nums = [1, 2, 3] // array of numbers
names = ["alice", "bob"] // array of strings
flags = [true, false, true] // array of booleans
empty = [] // empty array (type determined on first operation)
nested = [[1, 2], [3, 4]] // nested arrays
Mixed-type arrays are a TypeError:
// TypeError: [1, "two", true]
Array concatenation uses +:
[1, 2] + [3, 4] // → [1, 2, 3, 4]
Access elements with bracket indexing:
items = [10, 20, 30]
items[0] // → 10 (first element)
items[-1] // → 30 (last element)
items[1] // → 20
// Chaining works
matrix = [[1, 2], [3, 4]]
matrix[0][1] // → 2
Out-of-bounds access is a RangeError. Non-integer indices are a TypeError.
Ranges generate arrays of sequential integers:
1..5 // → [1, 2, 3, 4] (exclusive end)
1..=5 // → [1, 2, 3, 4, 5] (inclusive end)
0..0 // → [] (empty range)
3..=3 // → [3] (single element)
Both bounds must be integers. Start must not exceed end:
// TypeError: 1.5..3 (non-integer bounds)
// RangeError: 5..3 (start > end)
Ranges combine naturally with indexing and comprehensions:
(1..=5)[0] // → 1
for x in 1..=10 then x * x // → [1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
Variables store values of any type and can be assigned and reassigned:
x = 10 // number
name = "alice" // string
active = true // boolean
// Chain assignments (right-associative)
a = b = c = 5 // all get value 5
Variable names:
- Start with letter or underscore
- Contain letters, numbers, underscores
- Case-sensitive (
myVar≠myvar) true,false,if,then,else,for,in,when,intoare reserved
Single-line comments are supported using //:
// This is a comment
x = 10 // Comment after code
// y = 20 // This line is ignored
z = 30
Semicolons between statements are optional:
// With semicolons
x = 10; y = 20; x + y
// Without semicolons (also valid)
x = 10
y = 20
x + y
| Operator | Name | Operand Types | Example | Result |
|---|---|---|---|---|
+ |
Addition / Concatenation | number+number, string+string, array+array | 5 + 3 |
8 |
- |
Subtraction | number | 10 - 3 |
7 |
* |
Multiplication | number | 4 * 3 |
12 |
/ |
Division | number | 10 / 2 |
5 |
% |
Modulo | number | 10 % 3 |
1 |
^ |
Exponentiation | number | 2 ^ 3 |
8 |
- (unary) |
Negation | number | -5 |
-5 |
All comparisons return boolean (true or false):
| Operator | Name | Supported Types | Example | Result |
|---|---|---|---|---|
== |
Equal | any (deep equality) | 5 == 5 |
true |
!= |
Not equal | any (deep equality) | 5 != 3 |
true |
< |
Less than | number, string, date, time, datetime | 3 < 5 |
true |
> |
Greater than | number, string, date, time, datetime | 5 > 3 |
true |
<= |
Less or equal | number, string, date, time, datetime | 3 <= 3 |
true |
>= |
Greater or equal | number, string, date, time, datetime | 5 >= 3 |
true |
Deep equality for arrays: [1, 2] == [1, 2] → true
Cross-type equality: 1 == "1" → false (no coercion)
Logical operators require boolean operands (no truthiness):
| Operator | Name | Example | Result |
|---|---|---|---|
! |
NOT | !false |
true |
&& |
AND | true && true |
true |
|| |
OR | false || true |
true |
// Logical NOT
!false // → true
!true // → false
// !0 // → TypeError (use 0 != 0 or NUM() conversion)
// Logical operators with comparisons
x = 10
result = x > 5 && x < 20 // → true
notResult = !(x > 5) // → false
// Short-circuit evaluation
y = 0
safe = y != 0 && 100 / y > 5 // → false (right side not evaluated)
Syntax: if condition then consequent else alternate
The condition must be a boolean. The else clause is required:
// Basic conditional
age = 18
canVote = age >= 18 // → true
// Nested conditionals
score = 85
grade = if score >= 90 then "A" else if score >= 80 then "B" else if score >= 70 then "C" else "F" // → "B"
// With calculations
discount = if quantity > 100 then price * 0.2 else price * 0.1
Syntax: for variable in iterable [when guard] [into accumulator = initial] then body
Without into, creates a new array by mapping over each element (map semantics). With into, reduces to a single value (fold semantics). The optional when clause filters elements before processing.
Map (without into):
// Map over an array
nums = [1, 2, 3, 4, 5]
doubled = for x in nums then x * 2 // → [2, 4, 6, 8, 10]
// Filter with when guard
evens = for x in nums when x % 2 == 0 then x // → [2, 4]
// Map + filter
bigDoubled = for x in nums when x > 2 then x * 2 // → [6, 8, 10]
// Iterate over ranges
squares = for i in 1..=5 then i ^ 2 // → [1, 4, 9, 16, 25]
// Iterate over strings (splits into single-character strings)
chars = for c in "hello" then STR_UPPER(c) // → ["H", "E", "L", "L", "O"]
The when guard must be a boolean expression. The map result must be a homogeneous array.
Reduce (with into):
// Sum
for x in [1, 2, 3, 4] into sum = 0 then sum + x // → 10
// Product
for x in [1, 2, 3, 4] into p = 1 then p * x // → 24
// String concatenation
for x in ["a", "b", "c"] into s = "" then s + x // → "abc"
// With guard (filtered elements skip accumulator update)
for x in [1, -2, 3] when x > 0 into total = 0 then total + x // → 4
// Find maximum
for x in [3, 1, 4, 1, 5] into best = 0 then if x > best then x else best // → 5
// Flatten arrays
for x in [[1, 2], [3, 4]] into flat = [] then flat + x // → [1, 2, 3, 4]
// Empty iterable returns initial value
for x in [] into sum = 0 then sum + x // → 0
The accumulator variable is scoped to the loop body and not accessible outside.
Syntax: value |> FUNCTION(args...) where ? marks where the piped value is inserted.
The pipe operator chains a value through one or more function calls. Each step must contain at least one ? placeholder. Multiple ? in the same step all receive the same piped value.
// Basic pipe
-5 |> ABS(?) // → 5 (equivalent to ABS(-5))
// Chained pipes
-3 |> ABS(?) |> STR(?) // → "3" (equivalent to STR(ABS(-3)))
// Placeholder in any argument position
150 |> CLAMP(?, 0, 100) // → 100 (equivalent to CLAMP(150, 0, 100))
10 |> ADD(5, ?) // → 15 (equivalent to ADD(5, 10))
// Multiple placeholders (same value injected into all)
5 |> ADD(?, ?) // → 10 (equivalent to ADD(5, 5))
// With expressions on the left
2 + 3 |> ABS(?) // → 5 (parsed as (2 + 3) |> ABS(?))
// Store result in a variable
result = -7 |> ABS(?) // result = 7
The ? token is only valid inside pipe step arguments. Using ? outside a pipe expression is a syntax error. Each pipe step must be a function call with at least one ?.
From highest to lowest (use parentheses to override):
- Parentheses / Bracket indexing:
(expression),expr[index] - Unary operators:
-x,!x - Exponentiation:
x ^ y(right-associative) - Multiply/Divide/Modulo:
x * y,x / y,x % y - Add/Subtract:
x + y,x - y - Range:
x..y,x..=y - Comparisons:
<,>,<=,>=,==,!= - Logical AND:
x && y - Logical OR:
x || y - Pipe:
x |> FUN(?)(left-associative) - Assignment:
x = y(right-associative)
// Examples showing precedence
2 + 3 * 4 // → 14 (not 20)
2 ^ 3 ^ 2 // → 512 (right-associative: 2^(3^2))
-2 ^ 2 // → -4 (parsed as -(2^2))
(-2) ^ 2 // → 4 (parentheses change precedence)
1 + 2..3 + 4 // → [3, 4, 5, 6] (parsed as (1+2)..(3+4))
Separate statements with semicolons or newlines. The last expression is the return value:
// Calculate compound interest
principal = 1000
rate = 0.05
years = 3
principal * (1 + rate) ^ years // → 1157.625
// The whole program returns 1157.625
| Function | Description | Example |
|---|---|---|
STR(v) |
Convert to string | STR(42) → "42" |
NUM(v) |
Convert to number | NUM("42") → 42 |
TYPE(v) |
Get type name | TYPE(42) → "number" |
| Function | Description | Example |
|---|---|---|
ABS(x) |
Absolute value | ABS(-5) → 5 |
CEIL(x) |
Round up | CEIL(4.3) → 5 |
FLOOR(x) |
Round down | FLOOR(4.7) → 4 |
ROUND(x) |
Round to nearest | ROUND(4.5) → 5 |
SQRT(x) |
Square root | SQRT(16) → 4 |
SIN(x) |
Sine (radians) | SIN(0) → 0 |
COS(x) |
Cosine (radians) | COS(0) → 1 |
TAN(x) |
Tangent (radians) | TAN(0) → 0 |
LOG(x) |
Natural logarithm | LOG(1) → 0 |
LOG10(x) |
Base-10 logarithm | LOG10(100) → 2 |
EXP(x) |
e^x | EXP(0) → 1 |
MIN(...) |
Minimum value | MIN(3, 1, 5) → 1 |
MAX(...) |
Maximum value | MAX(3, 1, 5) → 5 |
CLAMP(val, a, b) |
Constrain to range | CLAMP(150, 0, 100) → 100 |
These functions accept both strings and arrays:
| Function | Description | String Example | Array Example |
|---|---|---|---|
LEN(x) |
Length | LEN("hello") → 5 |
LEN([1, 2, 3]) → 3 |
SLICE(x, start, end?) |
Extract section | SLICE("hello", 1, 3) → "el" |
SLICE([1, 2, 3], 1) → [2, 3] |
CONTAINS(x, search) |
Contains check | CONTAINS("hello", "ell") → true |
CONTAINS([1, 2], 2) → true |
REVERSE(x) |
Reverse | REVERSE("hello") → "olleh" |
REVERSE([1, 2, 3]) → [3, 2, 1] |
INDEX_OF(x, search) |
Find index | INDEX_OF("hello", "ll") → 2 |
INDEX_OF([10, 20], 20) → 1 |
Note: CONTAINS and INDEX_OF use substring matching for strings and deep equality for arrays. INDEX_OF returns -1 if not found.
| Function | Description | Example |
|---|---|---|
STR_UPPER(s) |
Uppercase | STR_UPPER("hello") → "HELLO" |
STR_LOWER(s) |
Lowercase | STR_LOWER("HELLO") → "hello" |
STR_TRIM(s) |
Trim whitespace | STR_TRIM(" hi ") → "hi" |
STR_SPLIT(s, sep) |
Split by separator | STR_SPLIT("a,b,c", ",") → ["a","b","c"] |
STR_REPLACE(s, search, repl) |
Replace first | STR_REPLACE("hello", "l", "r") → "herlo" |
STR_STARTS_WITH(s, prefix) |
Prefix check | STR_STARTS_WITH("hello", "he") → true |
STR_ENDS_WITH(s, suffix) |
Suffix check | STR_ENDS_WITH("hello", "lo") → true |
STR_REPEAT(s, n) |
Repeat n times | STR_REPEAT("ab", 3) → "ababab" |
Note: Use bracket indexing for character access: "hello"[0] → "h", "hello"[-1] → "o".
| Function | Description | Example |
|---|---|---|
ARR_SORT(a) |
Sort ascending | ARR_SORT([3, 1, 2]) → [1, 2, 3] |
ARR_UNIQUE(a) |
Deduplicate | ARR_UNIQUE([1, 2, 2, 3]) → [1, 2, 3] |
ARR_FLAT(a) |
Flatten one level | ARR_FLAT([[1, 2], [3]]) → [1, 2, 3] |
ARR_JOIN(a, sep) |
Join to string | ARR_JOIN(["a", "b"], "-") → "a-b" |
ARR_SUM(a) |
Sum numbers | ARR_SUM([10, 20, 30]) → 60 |
ARR_MIN(a) |
Minimum element | ARR_MIN([3, 1, 2]) → 1 |
ARR_MAX(a) |
Maximum element | ARR_MAX([3, 1, 2]) → 3 |
Note: Use arr + [item] for appending elements. Use bracket indexing for element access: arr[0], arr[-1]. ARR_SORT supports numbers, strings, dates, times, and datetimes. ARR_MIN/ARR_MAX support the same types. ARR_FLAT requires all elements to be arrays; result is validated for homogeneity. ARR_JOIN requires all elements to be strings.
Date functions operate on Temporal.PlainDate values. Most also accept Temporal.PlainDateTime (preserving the input type). TODAY() and DATE() return PlainDate only.
| Function | Description | Example |
|---|---|---|
TODAY() |
Current date | TODAY() → today's date |
DATE(y, m, d) |
Create date | DATE(2024, 6, 15) → June 15, 2024 |
| Function | Description | Returns |
|---|---|---|
YEAR(d) |
Get year | 2024 |
MONTH(d) |
Get month | 1-12 |
DAY(d) |
Get day of month | 1-31 |
WEEKDAY(d) |
Get day of week | 1-7 (1=Monday, 7=Sunday) |
DAY_OF_YEAR(d) |
Get day of year | 1-366 |
QUARTER(d) |
Get quarter | 1-4 |
| Function | Description | Example |
|---|---|---|
ADD_DAYS(d, n) |
Add days | ADD_DAYS(d, 7) → 7 days later |
ADD_MONTHS(d, n) |
Add months | ADD_MONTHS(d, 2) → 2 months later |
ADD_YEARS(d, n) |
Add years | ADD_YEARS(d, 1) → 1 year later |
| Function | Description |
|---|---|
DIFFERENCE_IN_DAYS(d1, d2) |
Absolute difference in days |
DIFFERENCE_IN_WEEKS(d1, d2) |
Absolute difference in whole weeks |
DIFFERENCE_IN_MONTHS(d1, d2) |
Absolute difference in whole months |
DIFFERENCE_IN_YEARS(d1, d2) |
Absolute difference in whole years |
| Function | Description |
|---|---|
START_OF_MONTH(d) |
First day of month |
END_OF_MONTH(d) |
Last day of month |
START_OF_YEAR(d) |
January 1st of year |
END_OF_YEAR(d) |
December 31st of year |
START_OF_WEEK(d) |
Monday of the week |
START_OF_QUARTER(d) |
First day of quarter |
| Function | Description | Returns |
|---|---|---|
IS_SAME_DAY(d1, d2) |
Same calendar day (mixed date+datetime OK) | boolean |
IS_WEEKEND(d) |
Saturday or Sunday | boolean |
IS_LEAP_YEAR(d) |
Leap year check | boolean |
AGE(birth, ref?) |
Age in complete years (defaults to today) | number |
d1 < d2 |
Before (use operators) | boolean |
d1 > d2 |
After (use operators) | boolean |
Time functions operate on Temporal.PlainTime values. Extractors and arithmetic also accept Temporal.PlainDateTime.
| Function | Description | Example |
|---|---|---|
TIME(h, m, s) |
Create time | TIME(14, 30, 0) → 2:30 PM |
NOW_TIME() |
Current time | NOW_TIME() → current wall-clock time |
| Function | Description | Returns |
|---|---|---|
HOUR(t) |
Get hour | 0-23 |
MINUTE(t) |
Get minute | 0-59 |
SECOND(t) |
Get second | 0-59 |
MILLISECOND(t) |
Get millisecond | 0-999 |
| Function | Description | Notes |
|---|---|---|
ADD_HOURS(t, n) |
Add hours | PlainTime wraps at midnight |
ADD_MINUTES(t, n) |
Add minutes | PlainTime wraps at midnight |
ADD_SECONDS(t, n) |
Add seconds | PlainTime wraps at midnight |
| Function | Description |
|---|---|
DIFFERENCE_IN_HOURS(t1, t2) |
Absolute difference in hours |
DIFFERENCE_IN_MINUTES(t1, t2) |
Absolute difference in minutes |
DIFFERENCE_IN_SECONDS(t1, t2) |
Absolute difference in seconds |
| Function | Description | Returns |
|---|---|---|
IS_SAME_TIME(t1, t2) |
Same time-of-day (mixed time+datetime OK) | boolean |
DateTime functions for construction and conversion using Temporal.PlainDateTime.
| Function | Description | Example |
|---|---|---|
DATETIME(y, mo, d, h, mi, s) |
Create datetime | DATETIME(2024, 6, 15, 14, 30, 0) |
NOW() |
Current datetime | NOW() → current date and time |
TO_DATE(dt) |
Extract date portion | TO_DATE(dt) → PlainDate |
TO_TIME(dt) |
Extract time portion | TO_TIME(dt) → PlainTime |
COMBINE(d, t) |
Combine date + time | COMBINE(d, t) → PlainDateTime |
START_OF_DAY(dt) |
Midnight (00:00:00) | START_OF_DAY(dt) |
END_OF_DAY(dt) |
End of day (23:59:59.999...) | END_OF_DAY(dt) |
// Create and compare dates
today = TODAY()
christmas = DATE(2024, 12, 25)
daysUntil = DIFFERENCE_IN_DAYS(today, christmas)
// Date arithmetic
nextWeek = ADD_DAYS(today, 7)
nextMonth = ADD_MONTHS(today, 1)
// Check properties
IS_WEEKEND(today) // → true or false
IS_LEAP_YEAR(DATE(2024, 1, 1)) // → true
WEEKDAY(today) // → 1-7
// Period boundaries
monthStart = START_OF_MONTH(today)
monthEnd = END_OF_MONTH(today)
// Time operations
meeting = TIME(14, 30, 0)
endTime = ADD_HOURS(meeting, 1) // → 15:30:00
HOUR(meeting) // → 14
// DateTime operations
event = DATETIME(2024, 6, 15, 14, 30, 0)
eventDate = TO_DATE(event) // → 2024-06-15
eventTime = TO_TIME(event) // → 14:30:00
dayStart = START_OF_DAY(event) // → 2024-06-15T00:00:00
// Combine date + time
appt = COMBINE(DATE(2024, 12, 25), TIME(10, 0, 0))
You can inject variables when executing expressions:
evaluate("radius * 2 * 3.14159", {
variables: { radius: 10 },
}); // → 62.8318
// Variables can be any type
evaluate("STR_UPPER(name)", {
functions: { ...defaultContext.functions },
variables: { name: "alice" },
}); // → "ALICE"Variables provided externally override script assignments:
// Script: x = 5; x * 2
evaluate(script); // → 10 (uses x = 5 from script)
evaluate(script, { variables: { x: 10 } }); // → 20 (external x overrides)Extend littlewing with your own functions:
import { evaluate, assertNumber } from "littlewing";
evaluate("CLAMP(x, 0, 100)", {
variables: { x: 150 },
functions: {
CLAMP: (value, min, max) => {
assertNumber(value, "CLAMP", "value");
assertNumber(min, "CLAMP", "min");
assertNumber(max, "CLAMP", "max");
return Math.min(Math.max(value, min), max);
},
},
}); // → 100All custom functions must:
- Accept zero or more
RuntimeValuearguments - Return a
RuntimeValue - Be pure (no side effects recommended)
- Use UPPERCASE naming to match built-in function convention and avoid collisions with variables
Littlewing exports the same assertion helpers used by the built-in standard library. Use them in custom functions for type-safe argument validation:
| Function | Narrows to |
|---|---|
assertNumber(v, ctx, side?) |
number |
assertString(v, ctx) |
string |
assertBoolean(v, ctx, side?) |
boolean |
assertArray(v, ctx) |
readonly RuntimeValue[] |
assertDate(v, ctx) |
Temporal.PlainDate |
assertTime(v, ctx) |
Temporal.PlainTime |
assertDateTime(v, ctx) |
Temporal.PlainDateTime |
assertDateOrDateTime(v, ctx) |
Temporal.PlainDate | Temporal.PlainDateTime |
assertTimeOrDateTime(v, ctx) |
Temporal.PlainTime | Temporal.PlainDateTime |
Each throws a TypeError with a descriptive message on type mismatch. The ctx parameter is the function name for error context, and the optional side identifies the argument (e.g., "left", "price").
// Compound interest
principal = 1000
annualRate = 0.07
years = 10
futureValue = principal * (1 + annualRate) ^ years // → 1967.15
// Monthly loan payment
loanAmount = 200000
monthlyRate = 0.04 / 12
months = 360
payment = loanAmount * monthlyRate / (1 - (1 + monthlyRate) ^ -months)
// Tiered pricing
quantity = 150
unitPrice = if quantity > 100 then 0.8 else if quantity > 50 then 0.9 else 1.0
total = quantity * unitPrice
// Shipping cost
weight = 15
distance = 500
baseCost = 10
weightCost = weight * 0.5
distanceCost = distance * 0.02
shippingTotal = baseCost + weightCost + distanceCost
// Transform arrays
prices = [10, 25, 50, 75, 100]
discounted = for p in prices then p * 0.9 // → [9, 22.5, 45, 67.5, 90]
expensive = for p in prices when p > 50 then p // → [75, 100]
// Reduce arrays
total = for p in prices into sum = 0 then sum + p // → 260
// Generate sequences
indices = 0..LEN(prices) // → [0, 1, 2, 3, 4]
// Check value ranges
age = 25
isValidAge = age >= 0 && age <= 120 // → true
// Complex validation
score = 85
isPass = score >= 60 // → true
isExcellent = score >= 90 // → false
// String manipulation
greeting = STR_UPPER("hello") // → "HELLO"
msg = "hello world"
hasWorld = CONTAINS(msg, "world") // → true
first = SLICE(msg, 0, 5) // → "hello"
initial = msg[0] // → "h"
// Type conversion
label = "Score: " + STR(95) // → "Score: 95"
- Small bundle - Minimal impact on your app size
- O(n) performance - Linear time parsing and evaluation
- Parse once, evaluate many - Reuse ASTs for repeated evaluation
- Predictable evaluation time - O(n) based on expression size
import { evaluate, parse } from "littlewing";
// Parse once, evaluate many times
const formula = parse("price * quantity * (1 - discount)");
evaluate(formula, { variables: { price: 10, quantity: 5, discount: 0.1 } }); // → 45
evaluate(formula, { variables: { price: 20, quantity: 3, discount: 0.15 } }); // → 51- No function definitions - Provide via context
- No mutations - Variables are reassigned, not mutated; arrays are immutable
- No side effects - Pure expression evaluation
- No implicit coercion - Use explicit conversion functions
- No general-purpose loops -
forcomprehensions map or reduce (no imperative looping)
These limitations are features! They ensure:
- Predictable performance
- Safe user input evaluation
- Simple mental model
- Small implementation
Littlewing is safe for evaluating untrusted user input:
- No access to global scope
- No eval() or code generation
- No infinite loops possible
- No prototype pollution
- No injection attacks
- Bounded execution time (O(n))
The worst a malicious user can do is write an expression that returns an unexpected value or triggers a type error.