Built-in Functions

These are compiled directly to bytecode instructions — no function call overhead. They all follow the same S-expression call syntax.

Contents
  1. List / Cons
    1. cons
    2. car
    3. cdr
    4. Building and Traversing Lists
  2. I/O
    1. print
    2. println
    3. Printing Different Types
  3. Strings
    1. strlen
    2. strcc
    3. strref
    4. strslice
    5. numtostr
    6. strtonum
    7. chr
  4. Hash Maps
    1. hashmake
    2. hashset
    3. hashget
    4. hashhas?
    5. hashdel
    6. hashkeys
  5. Timing
    1. clock
  6. Errors
    1. error
  7. FFI
    1. ffi
    2. loadlibrary
  8. Structs
    1. defstruct
    2. makestruct
    3. structget / structset

List / Cons

cons

(cons <head> <tail>) → cons-cell

Allocates a new cons cell with <head> as the car and <tail> as the cdr. Both arguments can be any value.

(cons 1 2)                     ; pair (1 . 2)
(cons 1 nil)                   ; one-element list [1]
(cons 1 (cons 2 (cons 3 nil))) ; list [1, 2, 3]
(cons "a" (cons "b" nil))      ; list of strings

A proper linked list is terminated by nil:

; build a list from scratch
(let xs (cons 10 (cons 20 (cons 30 nil))))
(car xs)           ; => 10
(car (cdr xs))     ; => 20
(car (cdr (cdr xs))) ; => 30

car

(car <cons>) → value

Returns the head (first element) of a cons cell.

(car (cons 'a' 'b'))   ; => "a"
(car (cons 1 nil))     ; => 1

Calling car on a non-cons value is a runtime type error.


cdr

(cdr <cons>) → value

Returns the tail (second element) of a cons cell.

(cdr (cons 1 2))    ; => 2
(cdr (cons 1 nil))  ; => nil (0)

Calling cdr on a non-cons value is a runtime type error.


Building and Traversing Lists

; sum of a linked list
(let list-sum
  (fn list-sum (xs acc)
    (if (= xs nil)
      acc
      (list-sum (cdr xs) (+ acc (car xs))))))

(let nums (cons 1 (cons 2 (cons 3 (cons 4 (cons 5 nil))))))
(list-sum nums 0)   ; => 15
; length of a list
(let length
  (fn length (xs n)
    (if (= xs nil)
      n
      (length (cdr xs) (+ n 1)))))

(length (cons 0 (cons 0 (cons 0 nil))) 0)   ; => 3

I/O

print

(print <expr>) → <expr>

Prints the value of <expr> to stdout without a trailing newline. Returns the value that was printed.

(print "Hello")        ; outputs: Hello
(print 42)             ; outputs: 42
(print (+ 1 2))        ; outputs: 3, returns 3

Because print returns its argument, it can be used inside larger expressions:

(+ (print 10) 5)   ; prints 10, then evaluates to 15

println

(println <expr>) → <expr>

Same as print but appends a newline \n after the value.

(println "Hello, world!")   ; Hello, world!\n
(println (+ 2 3))           ; 5\n

Printing Different Types

Value type Output
Integer Decimal digits, e.g. 42, -7
Float (tagged double) Shortest decimal representation via %g, e.g. 3.14
Bigint Full decimal string
Bigfloat Full decimal string
String Raw bytes (no quotes added)
Cons cell Printed as (car . cdr) pairs recursively
Closure <closure>
Nil (empty — nothing printed)
(println (cons 1 (cons 2 nil)))   ; (1 . (2 . 0))
(println nil)                     ; (empty line)
(println (fn (x) x))              ; <closure>

Strings

strlen

(strlen <s>) → integer

Returns the byte length of string <s> (not the number of Unicode code points).

(strlen "hello")   ; => 5
(strlen "café")    ; => 5  (é is 2 bytes in UTF-8)
(strlen "")        ; => 0

strcc

(strcc <a> <b>) → string

Returns a new string that is the concatenation of <a> and <b>.

(strcc "foo" "bar")   ; => "foobar"
(strcc "" "x")        ; => "x"

strref

(strref <s> <i>) → integer

Returns the byte value (0–255) at byte index <i> of string <s>. Indices are zero-based.

(strref "ABC" 0)   ; => 65  (ASCII 'A')
(strref "ABC" 2)   ; => 67  (ASCII 'C')

strslice

(strslice <s> <start> <len>) → string

Returns a substring of <s> starting at byte index <start> with byte length <len>.

(strslice "hello" 1 3)   ; => "ell"
(strslice "hello" 0 5)   ; => "hello"

numtostr

(numtostr <n>) → string

Converts a number to its decimal string representation.

(numtostr 42)     ; => "42"
(numtostr 3.14)   ; => "3.14"
(numtostr -100)   ; => "-100"

strtonum

(strtonum <s>) → integer | float | 0

Parses a decimal string as a number. Returns 0 (nil) if <s> is not a valid number.

(strtonum "42")     ; => 42
(strtonum "3.14")   ; => 3.14
(strtonum "abc")    ; => 0

chr

(chr <codepoint>) → string

Returns a one-character UTF-8 string for the given Unicode code point.

(chr 65)      ; => "A"
(chr 955)     ; => "λ"
(chr 128512)  ; => "😀"

Hash Maps

hashmake

(hashmake) → hash-map

Allocates a new empty hash map. Keys can be strings or integers; values can be any Val.

(let h (hashmake))

hashset

(hashset <map> <key> <val>) → map

Inserts or updates the mapping <key> → <val> in <map>. Returns the same map (mutation in place).

(let h (hashmake))
(hashset h "name" "Alice")
(hashset h 1 100)

hashget

(hashget <map> <key>) → value | 0

Returns the value for <key>, or 0 (nil) if the key is absent.

(hashget h "name")   ; => "Alice"
(hashget h "age")    ; => 0

hashhas?

(hashhas? <map> <key>) → 0 | 1

Returns 1 if <key> is present in <map>, 0 otherwise.

(hashhas? h "name")   ; => 1
(hashhas? h "age")    ; => 0

hashdel

(hashdel <map> <key>) → map

Removes <key> from <map>. Returns the same map. No-op if the key is absent.

(hashdel h "name")
(hashhas? h "name")   ; => 0

hashkeys

(hashkeys <map>) → list

Returns a linked list of all live keys in the map (order unspecified).

(let h (hashmake))
(hashset h "a" 1)
(hashset h "b" 2)
(hashkeys h)   ; => ("a" . ("b" . 0))  or some permutation

Timing

clock

(clock) → integer

Returns the current monotonic time in microseconds. Useful for benchmarking.

(let t0 (clock))
; ... work ...
(let elapsed (- (clock) t0))
(println (strcc "µs: " (numtostr elapsed)))

Errors

error

(error <msg>) → (does not return)

Prints <msg> to stderr and exits the interpreter with status 1.

(if (< x 0)
  (error "x must be non-negative")
  x)

FFI

ffi

(ffi <name> <ret-type> <arg-type>...) → callable

Resolves the C symbol <name> against the compile-time table in ffi_syms.c (common libc symbols: fopen, fclose, sin, strlen, getchar, calloc, …) and any shared object loaded with loadlibrary. The returned value behaves like a normal closure.

Under the hood, sel uses libffi (vendored as a git submodule) to build a ffi_cif from the declared types and dispatch the call — arguments are unboxed per their type codes, marshalled through ffi_call, then the result is re-boxed.

Type strings:

String C type Notes
"void" void return only
"int" int signed 32-bit
"uint" unsigned int  
"long" long 64-bit on most ABIs
"uchar" unsigned char 1 byte; useful in struct fields
"ushort" unsigned short 2 bytes
"float" float sel boxes into a double
"double" double  
"string" char* reads from a sel string; returned strings are copied
"ptr" void* stored as integer
"<StructName>" by-value struct name of a struct registered via defstruct
; call C puts()
(let puts (ffi "puts" "int" "string"))
(puts "hello from C")

; open a file
(let fopen  (ffi "fopen"  "ptr" "string" "string"))
(let fclose (ffi "fclose" "int" "ptr"))
(let fp (fopen "/tmp/test.txt" "w"))
(fclose fp)

FFI functions are first-class values: they can be stored in variables, passed as arguments, or stored in hash maps.


loadlibrary

(loadlibrary <path>) → nil

Compile-time dlopen(path, RTLD_NOW | RTLD_LOCAL) plus registration of the handle on a global list. Symbols looked up via (ffi …) after this form fall back to the registered handles when the static table misses. Loaded libraries live for the process’s lifetime; there is no unloadlibrary.

(loadlibrary "libraylib.so")
(let CloseWindow (ffi "CloseWindow" "void"))

The path argument must be a literal string — loadlibrary is resolved at compile time so symbols are visible to subsequent (ffi …) forms in the same source file.


Structs

sel can pass and return C structs by value through libffi. A struct layout is declared once with defstruct, then referenced by name in (ffi …) argument/return positions.

defstruct

(defstruct <name> <member-type> ...) → nil

Registers a struct layout. Member types are the same set as (ffi …) (primitives plus previously-registered struct names for nested by-value members). libffi computes the size and member offsets via ffi_get_struct_offsets, matching the host C ABI.

(defstruct Color uchar uchar uchar uchar)         ; raylib's Color
(defstruct Vector2 float float)
(defstruct Camera2D Vector2 Vector2 float float)  ; nested structs OK

Inline fixed-size arrays (T arr[N] in C) are not directly expressible — the binding generator (genffi.py) expands them into N consecutive members of T, which has the same byte layout.


makestruct

(makestruct "<name>" <v0> <v1> ...) → struct

Allocates a new struct of the named layout and initialises each field from the supplied values. The first argument must be a literal string.

(let RAYWHITE (makestruct "Color" 245 245 245 255))
(let pos      (makestruct "Vector2" 100.0 200.0))

structget / structset

(structget <s> <i>) → value
(structset <s> <i> <v>) → s

Read or mutate field <i> (a literal integer index). Field types follow the layout declared in defstruct; numeric fields are returned as int/double, string fields are copied to a sel string.

(structget RAYWHITE 0)        ; => 245   (the red component)
(structset pos 0 320.0)       ; pos.x = 320.0; returns pos

structset mutates in place and returns the same struct, so calls chain. Field indices are zero-based and match the order given to defstruct.