Data Types
| Type | Example | Description |
|---|---|---|
| Integer | 42, -5, 1,000, #xff, #o777, #b1010 | 64-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 |
| Ratio | 1/3, -2/5 | Exact rational number (Common Lisp ratio), always normalized; supported by all three backends |
| Double | 3.14, -0.5, 3,000.50, 1d0, 6.02e23 | 64-bit floating-point number |
| String | "hello" | String literal |
| Character | #\a, #\Space, #\Newline | Character 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 |
| Symbol | x, foo | Identifier |
| Keyword | :foo, :bar | Self-evaluating symbol starting with : |
| Nil | nil | False / empty list |
| T | t | True |
| Pi | pi | The constant π, read as the double 3.141592653589793 |
| Fixnum range | most-positive-fixnum, most-negative-fixnum | Read 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/.wasmfile reads with:rontolisp-jvm/:rontolisp-wasm, so a compiled program's feature set is fixed at compile time. Files pulled in by the compile-timeload/require/asdf:load-systeminclude 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.asdfiles, where a#.form is skipped with a warning (see the Systems guide).- The runtime reader of compiled programs (
read,read-from-string, runtimeload) does not know block comments or feature conditionals, like backquote — see Compiled read/load Limitations. :common-lispis 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: