(rontolisp) docs

Data Types

TypeExampleDescription
Integer42, -5, 1,000, #xff, #o777, #b101064-bit signed integer that auto-promotes to a big integer on overflow (interpreter and JVM), 31-bit signed integer (WASM). #x/#o/#b read hexadecimal/octal/binary literals
Ratio1/3, -2/5Exact rational number (Common Lisp ratio), always normalized; supported by all three backends
Double3.14, -0.5, 3,000.50, 1d0, 6.02e2364-bit floating-point number
String"hello"String literal
Character#\a, #\Space, #\NewlineCharacter literal (#\ plus a glyph or a standard name: Space, Newline, Tab, Return, Page, Backspace, Nul, Rubout). The WASM backend indexes strings by byte, so non-ASCII characters are out of scope there
Symbolx, fooIdentifier
Keyword:foo, :barSelf-evaluating symbol starting with :
NilnilFalse / empty list
TtTrue
PipiThe constant π, read as the double 3.141592653589793
Fixnum rangemost-positive-fixnum, most-negative-fixnumRead as self-evaluating integers like pi; the value is backend-dependent (a WASM fixnum is an unboxed 31-bit reference, the interpreter and the JVM backend use 64-bit longs)
Cons(1 2 3), (a . 1)Linked list built from cons cells; (a . b) is dotted-pair notation for a single cell
Function#'car, (lambda (x) x)Function object obtained via #'/function/lambda
Array#(1 2 3), #2A((1 2) (3 4))Fixed-size array of any rank (rank 1 = vector); #(...) and #nA(...) are self-evaluating array literals
Hash table(make-hash-table)Mutable key/value table with structural (equal) keys

Numeric literals may use , as a grouping separator between digits in the integer part, so 1,000 reads as 1000 and (+ 1,000 100) evaluates to 1100. The comma is only treated as a separator when it sits between two digits; it is stripped before parsing and applies to all three backends. This differs from Common Lisp, where , is the unquote character (not supported here).

Float literals may carry a Common Lisp exponent marker -- a mantissa followed by one of e, s, f, d, l (case-insensitive), an optional sign, and an exponent, e.g. 1d0, 1e0, 1.5d3 (1500.0), -2e-3, 6.02e23. This works in all three backends (it is a reader-level feature). Unlike Common Lisp, rontolisp has a single floating-point type, so every marker reads as the same 64-bit double -- the single/short/long-float distinction (1d0 vs 1e0 vs 1f0) is not preserved, and there is no *read-default-float-format*. A marker that is not followed by exponent digits is not a float: 1d and 1d0x read as symbols (like 1+), not numbers.

In the interpreter and the JVM compiler, integer arithmetic never silently wraps: when a long operation (+, -, *, /, 1+, 1-, abs, ...) would overflow, the result is automatically promoted to an arbitrary-precision big integer, and integer literals larger than a long are read as big integers. A big-integer result that fits back in a long is demoted again, so values keep a single canonical representation. For example, with (defun fact (n) (if (= n 0) 1 (* n (fact (- n 1))))), (fact 32) returns the exact 263130836933693530167218012160000000. The WASM compiler does not support this: its integers are limited to 31-bit (i31ref) and overflow wraps.

All three backends support Common Lisp ratios (exact rational numbers). 1/3 reads as a ratio literal, and integer division that does not divide evenly returns a ratio instead of truncating:

> 1/3
1/3
> (/ 1 2)
1/2
> (+ 1/2 1/3)
5/6
> (/ 1 2.0)
0.5
> (float 1/2)
0.5

Ratio results are always normalized -- reduced by the gcd with the sign on the numerator (2/4 reads as 1/2), and demoted to an integer when the denominator reduces to one ((/ 10 2) is 5, (+ 1/2 1/2) is 1). Arithmetic, comparisons (= < > <= >=), eq/eql, abs/min/max/1+/1-/ signum, the predicates (numberp, rationalp, zerop, plusp, minusp), truncate/floor/ceiling/round, expt with an integer exponent ((expt 2 -1) is 1/2), and numerator/denominator all handle ratios; mixing in a float switches to float contagion. Unary (/ x) is the reciprocal ((/ 2) is 1/2).

Per backend, the components follow the integer representation: the interpreter and the JVM compiler use big integers (a ratio of huge numerators/denominators stays exact), while the WASM compiler keeps them in the 31-bit i31 range with no overflow promotion, like all of its integer arithmetic. The runtime reader emitted for compiled read/load does not parse ratio literals (a 1/3 token read at runtime is a symbol), and mod, evenp/oddp, gcd/lcm and isqrt remain integer-only.

Comments, feature conditionals and *features*

Besides the ; line comment, the reader supports the Common Lisp #| ... |# block comment (nesting, per the standard) and the #+/#- feature conditionals: #+expr form keeps form only when the feature expression holds, #-expr form only when it does not. A feature expression is a feature name or an (and ...)/(or ...)/(not ...) combination (spelled bare or as keywords, case-insensitive). The active features are :rontolisp on every backend plus one backend-identifying feature — :rontolisp-interpreter, :rontolisp-jvm or :rontolisp-wasm — so one source file can select per-backend code. The variable *features* reads as the active feature list (a quoted list of keywords, fixed at read time like pi; it cannot be assigned).

Notes:

  • Reading happens once, at the frontend: the interpreter reads with :rontolisp-interpreter, and compiling to a .class/.wasm file reads with :rontolisp-jvm/:rontolisp-wasm, so a compiled program's feature set is fixed at compile time. Files pulled in by the compile-time load/require/ asdf:load-system include are read with the same target features.
  • A form skipped by a failing #+/#- guard is skipped at the raw character level without being parsed, so it may use syntax rontolisp does not support (that is the point of guarding it).
  • #. read-time evaluation is not supported and is a clear read error; the one exception is .asd files, where a #. form is skipped with a warning (see the Systems guide).
  • The runtime reader of compiled programs (read, read-from-string, runtime load) does not know block comments or feature conditionals, like backquote — see Compiled read/load Limitations.
  • :common-lisp is deliberately not in *features*: rontolisp is a subset, not a conforming implementation.

Dotted pairs, association lists and property lists

The reader supports Common Lisp dotted-pair notation: (a . b) denotes a single cons cell whose car is a and whose cdr is b, and (a b . c) is a list whose final cdr is c instead of nil. This is how association-list (alist) literals are written:

Dotted tails also work in backquote templates (`(a . ,x) expands to a cons chain), and the runtime reader of compiled programs parses the same notation, so a read/read-from-string of "(a . 1)" behaves identically in all backends. A standalone . outside a list is a read error, as in Common Lisp, and ,@ cannot be combined with a dotted tail in a backquote template. A dotted tail in call position (e.g. (+ 1 . 2)) is an error in all three backends -- a dotted pair is only meaningful as data.

The alist function family -- assoc, assoc-if, rassoc, acons, pairlis and copy-alist -- works in all three backends. assoc and rassoc compare with eql by default and accept optional :test/:key keywords (:test a function designator, e.g. #'equal for string keys; :key a selector applied to each pair's car/cdr before the comparison), like member:

Property lists (plists) -- flat lists of alternating indicator/value pairs like (:a 1 :b 2) -- are the keyword-based cousin of alists. getf reads the value for an indicator (two arguments only: no &optional default), the remf macro removes an indicator/value pair from a plist held in a variable or other setf place, and &key parameters in lambda lists are parsed from the same shape. (setf (getf ...)) is not a supported place and there are no symbol plists (get/symbol-plist); to add or update an entry, rebuild the list, e.g. by prepending with list*:

Arrays

make-array, aref and (setf (aref ...)) work in all three backends. Arrays of any rank >= 1 are supported; the dimensions argument is an integer (rank 1) or a non-empty list of integers, and :initial-element sets every cell (defaulting to nil). Elements are stored row-major with O(1) access (flat rank-independent access via row-major-aref / array-row-major-index), and arrays are compared by identity (eq), so two distinct arrays are never equal. length returns the element count of a vector (rank-1 array); a multidimensional array is not a sequence, so length signals an error on it. Unlike the hash-table operators, the array operators are not exposed as first-class function values, so #'aref and #'make-array are not available (call them directly). Vectors can also be built with vector and read with svref, array shapes are inspected with array-dimensions / array-rank / array-total-size, and coerce converts between lists, vectors and strings. For numpy-style vector/matrix math on top of arrays, see the linalg package. A 2-D array indexed in nested loops:

The #(...) reader syntax denotes a self-evaluating rank-1 vector literal whose elements are read as data (not evaluated), e.g. #(1 2 3) or #(a "b"). A rank-n array is written #nA((...) ...) with its contents as nested lists of depth n (#2A for a matrix, #3A for a rank-3 array, ...); every list at the same depth must have the same length, so ragged contents are a read error. Arrays print in the same readable syntax across all backends, with prin1 quoting string elements and princ not:

Packed float arrays (#d / #f)

#d(...) and #f(...) denote a packed float array: a float-typed array whose elements are stored unboxed. #d(...) is double-float (f64) and #f(...) is single-float (f32 -- half the memory, double the SIMD lane count). They read like #(...), but every element is coerced to the array's float type, so #d(1 2 3) and #d(1.0 2.0 3.0) are the same vector and (array-element-type #d(1.0)) is double-float (single-float for #f). Higher-rank literals use nested lists -- #d((1.0 2.0) (3.0 4.0)) is a matrix -- and (make-array n :element-type 'double-float) (or 'single-float) builds one at runtime.

Scalars stay double: reading an element widens it to a double (a single-float element is widened f32 -> f64), and storing one narrows it to the array's width (f64 -> f32 for a single-float array). Storing a non-real is a type error (a general array holds any value). Otherwise a packed array behaves like a general array of the same numbers for every operation -- aref, (setf (aref ...)), length, row-major-aref, array-rank, array-dimensions and coerce all work on it -- except that it prints with its own #d(...) / #f(...) reader syntax, so its printed form reads back as a packed array of the same width (preserving the unboxed representation) rather than degrading to a general one. It is simply the unboxed, float-specialized representation the numeric kernels use, so fill pointers, adjustable and displaced arrays are not available on it (those need a general array). The double-float width is the default and what linalg produces. For fast vectorized kernels over packed arrays -- and their optional hardware acceleration -- see the vec package.

Hash tables

make-hash-table, gethash, (setf (gethash ...)), remhash, clrhash, hash-table-count, hash-table-p and maphash work in all three backends. Keys are compared structurally (as if by equal): a list key like (list r c) matches an equal list, and numbers, symbols, characters and strings match by value. :test is accepted for familiarity but does not change this -- an eql table also matches structurally-equal aggregate keys. Iteration order (maphash) is not guaranteed across backends, so portable code should not depend on it. They are also usable as first-class function values (#'gethash, #'remhash, #'clrhash, #'hash-table-count, #'hash-table-p, #'maphash, and #'make-hash-table in its no-argument form) on all three backends -- passed via fixed-arity wrappers, so gethash's optional default and make-hash-table's keyword arguments are not available through the function value. A typical use -- counting with incf on the place: