Franz is a high level, functional, interpreted, dynamically typed, general-purpose programming language, with a terse syntax, and a verbose standard library.
Rust-like safety without the complexity
Tiny, keyword‑free functional core with prototype‑oriented objects, capability‑safe effects, and deterministic replay.
It features:
- Strictly no side effects* to help you write functional code
- The ability to localize the effects of imported Franz files.
- LLVM Native Compilation - Direct compilation to native executables via LLVM IR (s 1-3 complete)
- List Literals (LLVM) - Industry-standard heterogeneous lists with bracket syntax:
[1, "hello", 3.14], nested lists:[[1,2],[3,4]]( complete, 9/9 tests passing) - List Operations (LLVM) - 7 list operations: head, tail, cons, empty?, length, nth, is_list - all working with Generic* pointers (Rust-style)
- Comparison Operators (LLVM) - Equality and ordering: is, less_than, greater_than for integers, floats, and strings ( complete, 35/35 tests passing — now also tracks void separately from numeric zero)
- Enhanced Math Functions (LLVM) - 10 mathematical functions: remainder, power, random, floor, ceil, round, abs, min, max, sqrt ( complete, 27/27 tests passing)
- Control Flow (LLVM) - if/when/unless statements and cond chains for pattern matching (-5.5 complete, 65+ tests passing)
- Type Guards (LLVM) - Compile-time type checking: is_int, is_float, is_string, is_list ( complete, 30/30 tests passing)
- Cond Chains - Pattern matching conditionals like Lisp/Scheme for elegant multi-way branching ( complete, 30/30 tests passing)
- Native Code Performance - Rust-level performance with automatic type conversion, constant folding, and LLVM optimizations
- Lexical scoping - Industry-standard lexical scoping with snapshot-based closures ( complete)
- Arbitrary-depth nested closures - Full support for 3+ level nested closures (critical fix in )
- Full stdlib closure support - All higher-order functions work with closures ( complete, 48/48 tests passing)
- Zero-leak closures - closures with reference counting (0.08% memory leak rate)
- Comprehensive examples & tests - 6 examples + 12 smoke tests demonstrating scoping patterns
- High-performance module caching - 118k+ cached loads/second with AST-level caching
- Namespace isolation - Safe multi-library usage with
use_as()and dot notation - Capability-based security - Sandboxed execution with
use_with()prevents RCE vulnerabilities - Circular dependency detection - Import stack tracking prevents stack overflow crashes
- Industry-standard error handling - try/catch/error system without process termination
- Standard Library - String Module - 10 string manipulation functions (uppercase, lowercase, trim, split, replace, repeat, starts_with, ends_with, contains, char_at)
- Standard Library - Math Module - 15 mathematical functions and constants (PI, E, abs, sqrt, floor, ceil, round, max, min, clamp, sum, average, median, factorial, gcd)
- Standard Library - List Module - 12 advanced list operations (reverse, unique, flatten, zip, take, drop, any, all, partition, filled, chunk, sort)
- Standard Library - Func Module - 7 higher-order function combinators (compose2, identity, constant, flip, apply, apply_twice, apply_n)
- Dynamic typing and garbage collection.
- 0 keywords, everything is a function.
*With the exception of IO
table = (map (range 10) {_ y ->
<- (map (range 10) {item x ->
<- (multiply (add x 1) (add y 1))
})
})
From examples/mult-table.franz
From examples/game-of-life.franz
Franz now features industry-standard lexical scoping with arbitrary-depth nested closures (s 3-5 complete).
What is lexical scoping?
- Functions capture variables from their definition-time environment (not call-time)
- Closures maintain independent captured environments
- Predictable, isolated function behavior
- Full nested closure support (3, 4, 5+ levels deep)
Basic Example:
x = 10
get_x = {-> <- x} // Captures x=10
test = {->
x = 20 // Local x in test's scope
<- (get_x) // Returns 10 (captured value)
}
(println (test)) // Output: 10
Nested Closure Example ():
// Three-level nested closure
level1 = {->
a = 1
level2 = {->
b = 2
level3 = {->
c = 3
<- (add (add a b) c) // ✅ Can access a, b, and c
}
<- level3
}
<- level2
}
result = (((level1))) // Returns 6 (1+2+3)
Smoke Tests & Examples:
# Run automated smoke tests (12 tests)
bash scripts/run-smoke-tests.sh
# Run scoping examples
./franz --scoping=lexical examples/scoping/lexical/working/01-basic-closure.franz
./franz --scoping=lexical examples/scoping/lexical/working/02-nested-closures.franz
./franz --scoping=lexical examples/scoping/lexical/patterns/01-partial-application.franzDynamic scoping (legacy mode):
./franz --scoping=dynamic program.franzFranz features industry-standard control flow with LLVM native compilation (s 5.1-5.5 complete).
If Statement:
// Basic if-then-else
result = (if (greater_than x 10) "big" "small")
// Optional else (returns 0 if false)
bonus = (if (greater_than score 80) 10)
// Multi-statement blocks
result = (if condition
{
(println "Debug: condition true")
temp = (compute_value)
<- temp
}
{<- default_value})
When/Unless Helpers:
// When: execute if condition true
bonus = (when (greater_than score 80) 10)
// Unless: execute if condition false
safe_divide = (unless (is divisor 0) (divide 100 divisor))
Lisp/Scheme-style pattern matching conditionals:
// Grade classification
grade = (cond
((greater_than score 89) "A")
((greater_than score 79) "B")
((greater_than score 69) "C")
((greater_than score 59) "D")
(else "F"))
// HTTP status codes
message = (cond
((is code 200) "OK")
((is code 404) "Not Found")
((is code 500) "Server Error")
(else "Unknown"))
// Age brackets
bracket = (cond
((less_than age 18) "minor")
((less_than age 30) "young adult")
((less_than age 50) "middle age")
(else "senior"))
Features:
- Early exit on first match
- Unlimited clauses
- Optional else for default value
- Returns 0 if no match and no else
- C-level performance (zero overhead)
Compile-time type checking:
// Type-based branching
action = (cond
((is_int x) (multiply x 2))
((is_float x) (multiply x 1.5))
((is_string x) (join x "!"))
(else 0))
// Type-safe operations
safe_add = (if (and (is_int a) (is_int b))
(add a b)
0)
Available Type Guards:
(is_int value)- Check if integer (i64)(is_float value)- Check if float (double)(is_string value)- Check if string (i8*)
Building Franz
find src -name "*.c" -not -name "check.c" | xargs gcc -Wall -lm -o franz
gcc src/*.c -Wall -lm -o franz
find src -name "*.c" -not -name "check.c" | xargs gcc -g -Wall -lm -o franz
Running Examples
Basic execution:
./franz examples/hello-world.franz ./franz examples/fizzbuzz.franz
./franz -d examples/hello-world.franz
./franz -v
./franz --scoping=dynamic examples/your-program.franz
FRANZ_SCOPING=lexical ./franz examples/your-program.franz
Pipe code directly:
echo '(print "Hello Franz!")' | ./franz echo '(print (add 5 3))' | ./franz echo '(println "Hello with newline")' | ./franz
Available Examples
The examples/ directory contains 19 Franz programs:
- hello-world.franz - Simple output
- fizzbuzz.franz - Classic FizzBuzz algorithm
- factorial.franz - Interactive factorial calculator
- fibonacci.franz - Fibonacci sequence generator
- arithmetic.franz - Armstrong numbers
- collatz.franz - Collatz conjecture
- game-of-life.franz - Conway's Game of Life
- cube.franz - 3D cube rendering
- car.franz - Car simulation
- fish.franz - Fish animation
- And more...
Testing Different Types
- Non-interactive examples (run directly): ./franz examples/hello-world.franz ./franz examples/fizzbuzz.franz ./franz examples/10-print.franz
- Interactive examples (require user input): ./franz examples/factorial.franz ./franz examples/fibonacci.franz
- Complex programs (graphics/animations): ./franz examples/game-of-life.franz ./franz examples/cube.franz ./franz examples/fish.franz
Franz uses S-expression syntax (function arg1 arg2) and supports dynamic typing with strings, integers, floats, functions, lists, and void types.
All function calls are done with s-expressions (think lisp). For example,
(print "hello world")
In this case, the function print is applied with the string "hello world" as an argument.
Recursion
- No
reckeyword needed — functions are recursive by default.
factorial = {n ->
<- (if (is n 0) {<- 1} {<- (multiply n (factorial (subtract n 1)))})
}
(println "factorial 5 =" (factorial 5))
All data in franz is one of 6 different types:
stringintegerfloatfunction/native functionlistvoid
We can store this data in variables, for example,
a = 5
b = "hello"
We can combine data together to form lists,
magic_list = (list 123 "hello" 42.0)
Lists are always passed by value.
We can encapsulate code in functions using curly braces,
f = {
(print "Funky!")
}
(f) // prints "Funky"
Functions can get arguments, denoted using the "->" symbol. For example,
add_two_things = {a b ->
(print (add a b))
}
(add_two_things 3 5) // prints 8
They can also return values using the "<-" symbol,
geometric_mean = {a b ->
<- (power (multiply a b) 0.5)
}
(print (geometric_mean 3 5) "\n") // prints 3.87...
Functions operate in a few important ways:
- Function applications are dynamically scoped.
- Functions cannot create side effects.
- Like in JavaScript and Python, all functions are first-class.
Most of the features you may expect in a programming language are implemented in the form of functions. For example, here is a Fizzbuzz program using the add, loop, if, remainder, is, and print functions,
(loop 100 {i ->
i = (add i 1)
(if (is (remainder i 15) 0) {
(print "fizzbuzz\n")
} (is (remainder i 3) 0) {
(print "fizz\n")
} (is (remainder i 5) 0) {
(print "buzz\n")
} {(print i "\n")}
)
})
You should now be ready to write your own Franz programs! More info on how to build applications with events, files, code-splitting, and more, is found in the standard library documentation below.
-
arguments- A list command line arguments, like argv in C.
- Will skip all arguments up to and including the path to the franz program.
-
(print arg1 arg2 arg3 ...)- Prints all arguments to stdout, returns nothing.
-
(println arg1 arg2 arg3 ...)- Prints all arguments separated by a single space and appends a newline. Useful for clean line output when piping.
- Important: Arguments must be separated by spaces, not commas. Franz has no commas in syntax.
- Example:
(println "Name:" name "Age:" age)✅ - Wrong:
(println "Name:", name, "Age:", age)❌ (commas treated as undefined identifiers)
-
(input)- Gets a line of input from stdin.
-
(rows)- Returns the number of rows in the terminal.
-
(columns)- Returns the number of columns in the terminal.
-
(read_file path)- Returns the contents of the file designated by
path, in a string. If the file cannot be read, returns void. path:string
- Returns the contents of the file designated by
-
(write_file path contents)- Writes the string
contentsinto the file designated bypath, returns nothing. path:stringcontents:string
- Writes the string
-
(event time)or(event)- Returns the ANSI string corresponding with the current event. This may block for up to
timeseconds, rounded up to the nearest 100 ms. If notimeis supplied, the function will not return before receiving an event. time:integerorfloat
- Returns the ANSI string corresponding with the current event. This may block for up to
-
(use path1 path2 path3 ... fn)- Franz's code splitting method. Runs code in file paths, in order, on a new scope. Then uses said scope to apply
fn. path1,path2,path3, ...:stringfn:function
- Franz's code splitting method. Runs code in file paths, in order, on a new scope. Then uses said scope to apply
-
(shell command)- Runs
commandas an sh program in a seperate process, and returns stdout of the process as astring. command:string
- Runs
-
(is a b)- Checks if
aandbare equal, returns1if so, else returns0. Ifaandbare lists, a deep comparison is made. - Safe void handling:
(is value void)works for literals, variables, and closure parameters without ever conflatingvoidwith numeric0.
- Checks if
-
(less_than a b)- Checks if
ais less thanb, returns1if so, else returns0. a:integerorfloatb:integerorfloat
- Checks if
-
(greater_than a b)- Checks if
ais greater thanb, returns1if so, else returns0. a:integerorfloatb:integerorfloat
- Checks if
-
(not a)- Returns
0ifais1, and1ifais0. a:integer, which is1or0
- Returns
-
(and arg1 arg2 arg3 ...)- Returns
1if all arguments are1, else returns0 arg1,arg2,arg3, ...:integer, which is1or0
- Returns
-
(or arg1 arg2 arg3 ...)- Returns
1if at least one argument is1, else returns0 arg1,arg2,arg3, ...:integer, which is1or0
- Returns
-
(add arg1 arg2 arg3 ...)- Returns
arg1+arg2+arg3+ ... - Requires a minimum of two args
arg1,arg2,arg3, ...:integerorfloat
- Returns
-
(subtract arg1 arg2 arg3 ...)- Returns
arg1-arg2-arg3- ... - Requires a minimum of two args
arg1,arg2,arg3, ...:integerorfloat
- Returns
-
(divide arg1 arg2 arg3 ...)- Returns
arg1/arg2/arg3/ ... - Requires a minimum of two args
arg1,arg2,arg3, ...:integerorfloat
- Returns
-
(multiply arg1 arg2 arg3 ...)- Returns
arg1*arg2*arg3* ... - Requires a minimum of two args
arg1,arg2,arg3, ...:integerorfloat
- Returns
-
(remainder a b)- Returns the remainder of
aandb. a:integerorfloatb:integerorfloat
- Returns the remainder of
-
(power a b)- Returns
ato the power ofb. a:integerorfloatb:integerorfloat
- Returns
-
(random)- Returns a random number from 0 to 1.
-
(loop count fn)- Applies
fn,counttimes. Iffnreturns, the loop breaks, andloopreturns whateverfnreturned, else repeats until loop is completed. count:integer, which is greater than or equal to0fn:function, which is in the form{n -> ...}, where n is the current loop index (starting at0).
- Applies
-
(until stop fn initial_state)or(until stop fn)- Applies
fn, and repeats untilfnreturnsstop.untilreturns whateverfnreturned, beforestop. - The return value of every past iteration is passed on to the next. The initial iteration uses
initial_stateif supplied, or returnsvoidif not. fn:function, which is in the form{state n -> ...}, where n is the current loop index (starting at0), andstateis the current state.
- Applies
-
(if condition1 fn1 condtion2 fn2 condtion3 fn3 ... fn_else)- If
condition1is1, appliesfn1. - Else if
condition2is1, appliesfn2, else if ... - If no condtions were
1, appliesfn_else. - Return whatever the result of
fn1,fn2,fn3, ..., orfn_elsewas. condition1,condition2,condition3, ...:integer, which is1or0fn1,fn2,fn3, ...,fn_else:function, which takes no arguments
- If
-
(wait time)- Blocks execution for
timeamount of seconds. time:integerorfloat.
- Blocks execution for
-
void- A value of type
void
- A value of type
-
(integer a)- Returns
aas aninteger. a:string,float, orinteger.
- Returns
-
(string a)- Returns
aas astring. a:string,float, orinteger.
- Returns
-
(float a)- Returns
aas afloat. a:string,float, orinteger.
- Returns
-
(type a)- Returns the type of
aas astring.
- Returns the type of
-
(list arg1 arg2 arg3 ...)- Returns a
list, with the arguments as it's contents.
- Returns a
-
(length x)- Returns the length of
x x:stringorlist.
- Returns the length of
-
(join arg1 arg2 arg3 ...)- Returns all args joined together.
- All args must be of the same type.
arg1,arg2,arg3, ...:stringorlist.
-
(get x index1)or(get x index1 index2)- Returns the item in
xatindex1. If x is astring, this is a single char. - If
index2is supplied, returns a sub-array or substring fromindex1toindex2, not includingindex2. x:stringorlist.index1:int.index2:int.
- Returns the item in
-
(insert x item)or(insert x item index)- Returns a
listorstring, in whichitemwas inserted intoxatindex. Does not overwrite any data. - If
indexnot supplied,itemis assumed to be put at the end ofx. x:stringorlist.item:stringifxisstring, else anyindex:int.
- Returns a
-
(set x item index)- Returns a
listorstring, in which the item located atindexinx, was replaced byitem. x:stringorlist.item:stringifxisstring, else anyindex:int.
- Returns a
-
(delete x index1)or(delete x index1 index2)- Returns a
stringorlist, whereindex1was removed fromx. - If
index2is supplied, all items fromindex1toindex2are removed, not includingindex2. x:stringorlist.index1:int.index2:int.
- Returns a
-
(map arr fn)- Returns a list created by calling
fnon every item ofarr, and using the values returned byfnto populate the returned array. arr:listfn:function, which is in the form{item i -> ...}, whereitemis the current item, andiis the current index.
- Returns a list created by calling
-
(reduce arr fn initial_acc)or(reduce arr fn)- Returns a value, computed via running
fnon every item inarr. With every iteration, the last return fromfnis passed to the next application offn. The final returned value fromfnis the value returned fromreduce. arr:list.fn:function, which is in the form{acc item i -> ...}, whereitemis the current item,accis the accumulator (the result offnfrom the last item), andiis the current index.accisinitial_accif supplied, orvoidif not.
- Returns a value, computed via running
-
(filter arr fn)- Returns a new list containing only the elements from
arrwherefnreturns a truthy value (non-zero). arr:listfn:function, which is in the form{item i -> ...}, whereitemis the current item, andiis the current index. Must return an integer (0 for false, non-zero for true).- Example:
(filter (list 1 2 3 4 5 6) {x i -> <- (is (remainder x 2) 0)})returns[List: 2, 4, 6]
- Returns a new list containing only the elements from
-
(range n)- Returns a list with the integers from
0ton, not includingn. n:integer, which is greater than or equal to 0.
- Returns a list with the integers from
-
(find x item)- Returns the index of
iteminx. Returnsvoidif not found. x:stringorlistitem:stringifxisstring, else any
- Returns the index of
-
(head lst)- Returns the first element of
lst, orvoidif empty. - Useful for recursive list processing.
lst:list
- Returns the first element of
-
(tail lst)- Returns all but the first element of
lstas a new list. - Returns empty list if
lsthas 0 or 1 elements. - Useful for recursive list processing.
lst:list
- Returns all but the first element of
-
(cons item lst)- Prepends
itemto the beginning oflst, returns new list. - Equivalent to
(insert lst item 0)but more idiomatic. item: anylst:list
- Prepends
-
(empty? lst)- Returns
1iflstis empty,0otherwise. - Useful for checking base case in recursive functions.
lst:list
- Returns
Example: Recursive list processing
// Sum all elements in a list
sum = {lst ->
<- (if (empty? lst)
{<- 0}
{<- (add (head lst) (sum (tail lst)))}
)
}
(println (sum (list 1 2 3 4 5))) // Prints: 15
Dictionaries provide immutable key-value storage for structured data (similar to records/objects in other languages). Implemented using native hash table (TYPE_DICT) with O(1) average-case lookups.
-
(dict key1 val1 key2 val2 ...)- Creates a dictionary from alternating keys and values using native hash table
- Keys can be any type (typically strings or integers)
- Values can be any type
- Performance: O(1) average-case lookups with FNV-1a hash function
- Example:
(dict "name" "Ada" "age" 37)creates{name: Ada, age: 37}
-
(dict_get dict key)- Returns the value associated with
key, orvoidif not found dict:dict(native TYPE_DICT hash table)key: any type- Performance: O(1) average-case lookup
- Example:
(dict_get user "name")returns"Ada"
- Returns the value associated with
-
(dict_set dict key value)- Returns a new dictionary with the key-value pair added/updated (immutable)
- Original dict unchanged
dict:dict(hash table)key: any typevalue: any type- Example:
user2 = (dict_set user "age" 38)creates updated dict
-
(dict_keys dict)- Returns a list of all keys in the dictionary
dict:dict(hash table)- Performance: O(n) iteration
- Example:
(dict_keys user)returns[name, age]
-
(dict_values dict)- Returns a list of all values in the dictionary
dict:dict(hash table)- Performance: O(n) iteration
- Example:
(dict_values user)returns[Ada, 37]
-
(dict_has dict key)- Returns
1if key exists in dict,0otherwise dict:dict(hash table)key: any type- Performance: O(1) average-case lookup
- Example:
(dict_has user "name")returns1
- Returns
-
(dict_merge dict1 dict2)- Returns a new dictionary combining all keys from both dicts
- If a key exists in both,
dict2's value takes precedence dict1:dict(base dictionary)dict2:dict(override dictionary)- Performance: O(n+m) where n, m are dict sizes
- Example:
(dict_merge defaults user_config)
-
(dict_remove dict key)- Returns a new dictionary with the key removed (immutable)
- Original dict unchanged
dict:dict(hash table)key: any type- Example:
user2 = (dict_remove user "age")
-
(dict_map dict fn)- Returns a new dictionary with all values transformed by
fn - Keys remain unchanged
dict:dict(hash table)fn: function with signature{key value -> new_value}- Performance: O(n) iteration with function application
- Example:
boosted = (dict_map scores {k v -> <- (add v 5)})
- Returns a new dictionary with all values transformed by
-
(dict_filter dict fn)- Returns a new dictionary containing only pairs where
fnreturns truthy dict:dict(hash table)fn: function with signature{key value -> boolean}(0=false, non-zero=true)- Performance: O(n) iteration with predicate evaluation
- Example:
high = (dict_filter scores {k v -> <- (greater_than v 90)})
- Returns a new dictionary containing only pairs where
Example: User record
// Create user
user = (dict
"name" "Ada Lovelace"
"age" 36
"occupation" "Mathematician")
// Access
(println "Name:" (dict_get user "name")) // Ada Lovelace
// Update (immutable)
user2 = (dict_set user "age" 37)
// Check existence
(if (dict_has user "email") {
(println "Email:" (dict_get user "email"))
} {
(println "No email address")
})
// Merge configs
defaults = (dict "timeout" 60 "debug" 0)
config = (dict_merge defaults (dict "debug" 1 "port" 8080))
For comprehensive documentation, see:
- docs/dict/dict.md - Complete dict documentation
- examples/dict/working/ - Working examples
- test/dict/dict-test.franz - Test suite with 15 test cases
(pipe value fn1 fn2 fn3 ... fnN)- Threads a value through a sequence of functions left-to-right. Each function receives the result of the previous function as its single argument.
- Provides a clean alternative to nested function calls or intermediate variables.
- Returns the final result after all transformations.
value: any typefn1,fn2, ...,fnN: functions that each accept one argument
Example: Data transformation pipeline
// Without pipe - nested and hard to read
result = (subtract (multiply (add 5 3) 2) 4)
// With pipe - clear left-to-right flow
result = (pipe 5
{x -> <- (add x 3)} // 5 + 3 = 8
{x -> <- (multiply x 2)} // 8 * 2 = 16
{x -> <- (subtract x 4)}) // 16 - 4 = 12
(println result) // Prints: 12
Example: List processing pipeline
// Calculate sum of squares
numbers = (list 1 2 3 4 5)
result = (pipe numbers
{lst -> <- (map lst {x i -> <- (multiply x x)})}
{lst -> <- (reduce lst {acc x i -> <- (add acc x)} 0)})
(println result) // Prints: 55 (1+4+9+16+25)
Example: Reusable named functions
double = {x -> <- (multiply x 2)}
add_ten = {x -> <- (add x 10)}
halve = {x -> <- (divide x 2)}
result = (pipe 20 double add_ten halve)
(println result) // Prints: 25
Note: Franz's collection function callbacks require specific arities:
map:{item index -> ...}reduce:{accumulator item index -> ...}filter:{item index -> boolean}
(partial function arg1 arg2 ... argN)- Creates a partially applied function with fixed arguments(call partial_fn new_args...)- Applies a partial function with additional arguments
Partial application allows you to "fix" some arguments of a function, creating a new specialized function that remembers those arguments.
Example: Creating specialized functions
// Create reusable functions
add_ten = (partial add 10)
double = (partial multiply 2)
half = (partial divide 2)
// Use with call
(println (call add_ten 5)) // 15 (add 10 5)
(println (call double 21)) // 42 (multiply 2 21)
(println (call half 100)) // 0.02 (divide 2 100)
Example: Multiple fixed arguments
add_many = (partial add 10 20 30)
(println (call add_many 40)) // 100 (10+20+30+40)
Example: With pipe for clean pipelines
add_ten = (partial add 10)
times_three = (partial multiply 3)
result = (pipe 5
{x -> <- (call add_ten x)}
{x -> <- (call times_three x)})
(println result) // 45 ((5 + 10) * 3)
Example: Reusing partials
add_hundred = (partial add 100)
(call add_hundred 1) // 101
(call add_hundred 50) // 150
(call add_hundred 200) // 300
Argument Order: Fixed args come FIRST:
(partial fn A)creates{x -> (fn A x)}
(partial subtract 10)={x -> (subtract 10 x)}=10 - x(partial divide 100)={x -> (divide 100 x)}=100 / x
(thread-first value form1 form2 ...)- Threads value through forms, inserting as FIRST argument(thread-last value form1 form2 ...)- Threads value through forms, inserting as LAST argument
Threading macros provide clean, linear data flow by automatically inserting values into function calls.
Example: Thread-first
// Without threading (nested)
result = (add (multiply (subtract 100 20) 2) 10)
// With thread-first (linear, left-to-right)
result = (thread-first 100
(list subtract 20) // 100 - 20 = 80
(list multiply 2) // 80 * 2 = 160
(list add 10)) // 160 + 10 = 170
Example: Mathematical formula
// Calculate: ((x + 3) * 2) - 4
result = (thread-first 5
(list add 3) // 5 + 3 = 8
(list multiply 2) // 8 * 2 = 16
(list subtract 4)) // 16 - 4 = 12
Example: String building
result = (thread-first "Hello"
(list join " ")
(list join "World")
(list join "!")) // "Hello World!"
Example: Multiple arguments
result = (thread-first 10
(list add 5 15) // (add 10 5 15) = 30
(list multiply 2)) // (multiply 30 2) = 60
Example: Thread-last (value as LAST argument)
// Useful for non-commutative operations where value goes last
result = (thread-last 4
(list divide 100) // (divide 100 4) = 100 / 4 = 25
(list power 2)) // (power 2 25) = 2^25
Thread-last use cases:
// Exponentiation: base ^ x
(thread-last 3 (list power 2)) // 2^3 = 8
// Division: numerator / x
(thread-last 5 (list divide 100)) // 100 / 5 = 20
// Comparison: value > x
(thread-last 50 (list greater_than 100)) // 100 > 50 = 1
✅ NEW: Thread-last now works with list operations!
Use collection-last wrappers: map-last, reduce-last, filter-last
// Thread-last with list processing
numbers = (list 1 2 3 4 5)
result = (thread-last numbers
(list map-last {x i -> <- (multiply x 2)})
(list filter-last {x i -> <- (greater_than x 5)})
(list reduce-last {a x i -> <- (add a x)} 0))
(println result) // 30 (6+8+10)
Collection Functions:
- With
thread-first: usemap,reduce- With
thread-last: usemap-last,reduce-last,filter-last
Syntax: Forms must be lists:
(list function args...)not(function args...)
Franz provides a comprehensive string manipulation library via the use function. Load stdlib/string.franz to access 10 essential string functions.
Loading the String Module:
(use "stdlib/string.franz" {
// String functions available here
result = (uppercase "hello")
(println result) // HELLO
})
Available Functions:
(uppercase s)- Convert string to uppercase(lowercase s)- Convert string to lowercase(trim s)- Remove leading and trailing whitespace(split s delimiter)- Split string by delimiter into list(replace s old new)- Replace all occurrences of substring(repeat s count)- Repeat string N times(starts_with s prefix)- Check if string starts with prefix (returns 1/0)(ends_with s suffix)- Check if string ends with suffix (returns 1/0)(contains s substr)- Check if string contains substring (returns 1/0)(char_at s index)- Get character at index (0-based)
Example: Text Processing
(use "stdlib/string.franz" {
input = " Hello World "
// Clean and transform text
cleaned = (trim input) // "Hello World"
upper = (uppercase cleaned) // "HELLO WORLD"
replaced = (replace upper "WORLD" "FRANZ") // "HELLO FRANZ"
(println replaced)
// Check patterns
(if (starts_with cleaned "Hello") {
(println "Greeting detected!")
} {})
})
Example: String Validation
(use "stdlib/string.franz" {
url = "https://example.com"
// Validate URL format
is_https = (starts_with url "https://")
has_domain = (contains url ".com")
(if (and is_https has_domain) {
(println "Valid HTTPS URL")
} {
(println "Invalid URL")
})
})
Franz provides a comprehensive mathematical library via the use function. Load stdlib/math.franz to access 15 mathematical functions and constants.
Loading the Math Module:
(use "stdlib/math.franz" {
// Math functions available here
result = (sqrt 16)
area = (multiply PI (power 5 2))
(println "Square root of 16:" result)
(println "Circle area (r=5):" area)
})
Available Functions:
- Constants:
PI(3.14159...),E(2.71828...) - Basic Math:
abs,sqrt,floor,ceil,round,max,min,clamp - Statistics:
sum,average,median - Number Theory:
factorial,gcd
Example: Statistical Analysis
(use "stdlib/math.franz" {
scores = [85, 92, 78, 95, 88]
(println "Total:" (sum scores)) // 438
(println "Average:" (average scores)) // 87.6
(println "Median:" (median scores)) // 88
// Find min/max
maximum = (reduce scores {acc item index -> <- (max acc item)} 0)
minimum = (reduce scores {acc item index -> <- (min acc item)} 100)
(println "Range:" minimum "to" maximum) // 78 to 95
})
Example: Geometry Calculations
(use "stdlib/math.franz" {
// Circle
radius = 7
circumference = (multiply 2 (multiply PI radius))
area = (multiply PI (power radius 2))
(println "Circumference:" circumference) // 43.98...
(println "Area:" area) // 153.93...
// Distance between points
distance = (sqrt (add (power 3 2) (power 4 2)))
(println "Distance:" distance) // 5.0
})
Franz provides advanced list operations beyond the built-in map, filter, and reduce functions. Load stdlib/list.franz to access 12 powerful list manipulation functions.
Loading the List Module:
(use "stdlib/list.franz" {
// List functions available here
numbers = [3, 1, 4, 1, 5, 9, 2, 6]
sorted = (sort numbers)
unique_sorted = (unique sorted)
(println "Sorted:" sorted)
(println "Unique:" unique_sorted)
})
Available Functions:
- Transformation:
reverse,unique,flatten,zip - Filtering:
take,drop,partition - Inspection:
any,all - Generation:
filled,chunk,sort
Example: Data Processing Pipeline
(use "stdlib/list.franz" {
data = (list 5 2 8 1 9 3 7 4 6)
// Sort and get top 3
sorted = (sort data) // [1, 2, 3, 4, 5, 6, 7, 8, 9]
top3 = (take (reverse sorted) 3) // [9, 8, 7]
(println "Top 3:" top3)
})
Example: Working with Predicates
(use "stdlib/list.franz" {
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9]
// Check predicates
has_large = (any numbers {x i -> <- (greater_than x 5)}) // 1 (true)
all_positive = (all numbers {x i -> <- (greater_than x 0)}) // 1 (true)
// Partition by even/odd
result = (partition numbers {x i -> <- (is (remainder x 2) 0)})
evens = (get result 0) // [2, 4, 6, 8, 10]
odds = (get result 1) // [1, 3, 5, 7, 9]
(println "Evens:" evens)
(println "Odds:" odds)
})
Example: Batch Processing
(use "stdlib/list.franz" {
items = (range 10) // [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
batches = (chunk items 3) // [[0,1,2], [3,4,5], [6,7,8], [9]]
(println "Processing" (length batches) "batches...")
(map batches {batch index ->
(println " Batch" index ":" batch)
<- void
})
})
The I/O Module (stdlib/io.franz) provides 8 essential file system utility functions for Franz programs. Status: ✅ COMPLETE (8/8 functions, 40+ tests passing).
Functions:
exists- Check if path exists (file or directory)is_file- Check if path is a regular fileis_dir- Check if path is a directorylist_dir- List directory contents as a listbasename- Extract filename from pathdirname- Extract directory from pathjoin_path- Join path components with / separator (pure Franz!)read_lines- Read file as list of lines
Quick Example:
(use "stdlib/io.franz" {
// Check if file exists
(if (exists "README.md") {
(println "README exists!")
})
// List directory contents
files = (list_dir "src")
(println "Source files:" files)
// Join path components
path = (join_path (list "docs" "stdlib" "io.md"))
(println "Full path:" path)
// Extract filename and directory
full_path = "src/main.c"
dir = (dirname full_path) // "src"
file = (basename full_path) // "main.c"
(println "Directory:" dir "File:" file)
// Read file line by line
lines = (read_lines "README.md")
line_count = (length lines)
(println "README has" line_count "lines")
})
Common Patterns:
- File Existence Check:
(if (exists "config.franz") {
config = (use "config.franz")
} {
(println "Config file not found")
})
- Directory Traversal:
files = (list_dir "examples")
(map files {item index ->
(println "Example" (add index 1) ":" item)
})
- Path Construction:
components = (list "docs" "stdlib" "io.md")
full_path = (join_path components)
// Result: "docs/stdlib/io.md"
- Path Decomposition:
path = "src/stdlib.c"
dir = (dirname path) // "src"
file = (basename path) // "stdlib.c"
- Integration with List Module:
(use "stdlib/list.franz" {
(use "stdlib/io.franz" {
files = (list_dir "src")
c_files = (filter files {item index ->
<- (ends_with item ".c") // Using string module
})
sorted = (sort c_files)
(println "Sorted C files:" sorted)
})
})
Edge Cases:
- Empty inputs:
exists("")→ 0,list_dir("nonexistent")→ [],join_path([])→ "" - Trailing slashes:
basename("src/")→ "src",join_path(["src/", "file.c"])→ "src/file.c" - Nested paths:
dirname("a/b/c.txt")→ "a/b",join_path(["a", "b", "c"])→ "a/b/c"
Implementation Highlights:
- Shell integration for reliability:
exists,is_file,is_dir,list_dir,basename,dirnameuse POSIX commands - Pure Franz implementation:
join_pathimplemented using reduce (no shell overhead) - Cross-platform: Works on all Unix-like systems (Linux, macOS, BSD)
Performance:
- Shell command functions: ~1-5ms overhead per call
- Pure Franz functions: No shell overhead
- Optimization tip: Cache
existschecks when checking same path multiple times
All 45 functions implemented and tested (100%):
| Module | Functions | Status |
|---|---|---|
| stdlib/string.franz | 17/17 | ✅ COMPLETE |
| stdlib/math.franz | 8/8 | ✅ COMPLETE |
| stdlib/list.franz | 12/12 | ✅ COMPLETE |
| stdlib/io.franz | 8/8 | ✅ COMPLETE |
| TOTAL | 45/45 | ✅ 100% |
For complete module documentation and examples, see stdlib/README.md.
To identify the current interpreter version, use the -v flag.
./franz -vWhen debugging the interpreter, it may be useful to compile with the -g flag.
gcc src/*.c -g -Wall -lm -o franzThis will allow Valgrind to provide extra information,
valgrind --leak-check=full -s ./franz -d YOURCODE.franzOn mac, you can use leaks,
leaks -atExit -- ./franz YOURCODE.franzTo obtain debug information about how your code is interpreted (Tokens, AST, etc.), add the -d flag.
./franz -d YOURCODE.franzYou can also pipe code straight into franz (passed files always take priority over piped code).
echo '(print (add 1 2) "\\n")' | ./franz- Built by Mohammad Tanzil Idrisi