Language & qSQL
Amber is a terse array notation with a q vocabulary bolted firmly on top. If you have written q you already know most of it; the rules below are the handful of places where the host grammar shows through, and they are properties of the host, not bugs.
Dialect notes (read these first)
- Any two-argument function can be applied infix, right-to-left. Built-in verbs,
library dyads and your own functions alike:
f:{x+y}; 3 f 4is7,2 3 in 2 4 5works, and`sym xasc tsorts. The one exception is an inline lambda literal between two args:x {x+y} ydoes not go infix, so assign it first (f:{x+y}; x f y). Bracket form always works too:lj[t;kt],in[x;y],except[a;b],xasc[`sym;t]. <>>=<=work as in q after a name, a number or a closing bracket (since 2.8.0):x<>y,x>=y. With nothing on the left,<and>keep their k reading, so<>xis the grade of the grade down.- Symbols cannot contain
_.`a_bis a parse error because_is a verb (drop, and floor). Use a quoted symbol`"a_b", a dotted name (my.name) or camel case. - Nested lambdas are not closures. An inner
{…}sees only its own parameters and globals, never the enclosing function's locals. Pass captured values in by projection:f[captured]'list. (qSQL in a lambda is the exception: since 2.8.0{[c]select from t where s=c}seesc, as in q.) .xafter a name is dyadic apply; after a verb it is monadic "value". Prefer thevaluefunction (value x≡. x) to avoid the ambiguity.
The first of those rules and the qSQL-in-any-script change both arrived in 2.0.0; two point oh is the account of what they cost to add.
Recursion and tail calls. o is the function you’re in, so
{$[x;o x-1;0]} calls itself. Since 2.4.0 a call whose result is the function’s
result (the last thing it does, in either branch of a $[..]) reuses the frame, so that
kind of recursion goes as deep as you like: {$[x;o x-1;0]}1000000 is fine, and so are two
functions calling each other. Anything else still counts against a depth of 2048 ('stack),
e.g. x*o x-1, which has to come back to multiply.
Types and nulls
| k type | list / atom | example | null |
|---|---|---|---|
| int | `I / `i | 0 1 2, !5, 101b | 0N |
| float | `F / `f | 1.5 2.5 | 0n |
| char | `C / `c | "abc", "x" | " " |
| symbol | `S / `s | `a`b`c | ` |
| dict | `m | `a`b!1 2 | (none) |
| table | `M | +`a`b!(1 2;3 4) | (none) |
There’s no separate bool type: a b literal like 101b is an int vector, and comparisons give ints. @x returns the type symbol, and type x q’s type number (7 for an int list, since 2.8.0). !n gives 0..n-1, a compact range
whose @ reads `I.
Amber stores an integer vector in the narrowest type that holds every element, so
1 2 3 is physically int8 and 1 2 3000000000 is
int64, and both are "long vectors" as far as the language is concerned. Arrays handed to
NumPy or Arrow carry the true dtype rather than being widened behind your back.
Scalar, aggregation and uniform functions
All live in amber.k. Monadic functions apply prefix (sum x); dyadic ones
use brackets (wavg[x;y]).
/ scalar (element-wise)
neg not null reciprocal sqrt floor ceiling signum abs exp log sin cos
til enlist string type key value first last reverse distinct group where
flip count mod div xbar xlog round
/ aggregation (vector -> atom)
sum prd min max avg med var dev svar sdev cov scov cor wsum wavg all any
/ uniform (vector -> vector of the same length)
sums prds mins maxs deltas ratios differ prev next
/ ordering and set operations
rank iasc idesc asc desc xrank rotate xprev in except inter union raze sublist cross
/ grouped aggregation in one pass (2.1) -- keys!values, first-appearance key order like =k
gsum gavg gmin gmax gcount gfirst glastFused idioms (since 2.1)
The compiler recognises a few expression shapes and compiles each to one fused primitive instead
of a chain that materialises intermediates. The result is identical to the unfused expression in every case;
\disasm shows the fused call.
+/x*y +/x=y +/x<y +/x>y / sum of products / count of matches: no product or mask vector
+/x@&m / sum of the elements selected by a mask: no compressed vector
x@&m / compress by a 0/1 mask: no index vector
a+s*b a-s*b (s a literal) / one pass, no s*b vector
x@<x x@>x / sort by value: a counting sort for integral-valued data, else grade+index
gsum[sym;px] / one pass: acc[group]+=px (select sum px by sym from t takes the same path)
h in kr / one pass, one byte per element (`memb underneath)Fused trees (since 2.6)
Inside a lambda, a whole tree of + - * % & | over float or int vectors and numbers (with
< > = at the top, and +/ on top of that) runs as one pass in cache-sized blocks,
split over the threads, once the vectors have 2048 items or more. Same bits as the unfused code.
f:{[a;b;c]+/(a*b)-c%2.0} / one pass, no a*b or c%2.0 vectorsThe two everyone gets backwards
wavg[sz; px] / WEIGHTS FIRST. wavg[px;sz] is a plausible wrong number.
xbar[w; x] / bucket width first: xbar[5;0 1 2 3 4 5 6 7] -> 0 0 0 0 0 5 5 5Tacit trains: hooks and forks
A parenthesised, semicolon-separated list whose every element is a function is
applied as a train (since 2.0.1): (f;g) is a hook, (f;g;h) a
fork. It lets you write a whole class of little functions with no argument name at all.
avg:(+/;%;#) / fork: mean = sum % count
avg 2 4 6 8 10 / 6.0
(|/;-;&/) 5 2 9 1 / 8 range = max - min
(%;+/) 3 1 4 1 5 / normalise to sum 1
(,;|) 1 2 3 / 1 2 3 3 2 1 hook: a vector then its reverse
(+;*;-)[10;3] / 91 dyadic fork: (10+3)*(10-3)A space-separated group (f g) is ordinary composition f(g x), not a
train. The semicolon is the switch. See the full Tacit programming
guide for hooks, forks, dyadic trains and the exact rules.
Dictionaries, tables and keyed tables
d:`a`b`c!1 2 3 / dictionary
d`b / 2
key d / `a`b`c
value d / 1 2 3
t:([]sym:`AAPL`MSFT`AAPL; px:187.3 411.2 187.4; sz:100 250 50)
count t / 3
cols t / `sym`px`sz
meta t / c | t f a -- column, type, foreign key, attribute
t`px / 187.3 411.2 187.4 (a column IS a vector)
t 0 / the first row, as a dictionary
kt:([sym:`AAPL`MSFT] lot:100 10) / keyed table
keys kt / `sym
xkey[`sym; t] / key an unkeyed table
unkey kt / and back
xcols[`px`sym`sz; t] / reorder columns
xasc[`sym; t] / sort ascending by a column (sets the `s attribute)
insert[`t; (`GOOG; 141.2; 300)] / append a rowA bare table at the interactive prompt auto-renders as a grid. Big tables print Q-style, the
first CROWS rows (default 20) then .., and the cap is applied
before formatting, so previewing a million-row table is instant.
qSQL: select, exec, update, delete
At the prompt, type the query. Bare column names like wavg[sz;px] just work.
How this layer was put on top of a K interpreter, tables, the join family and a parser that accepts
bare qSQL, is in teaching K to speak q.
gentq 100000 / sets the globals: trades, quotes
select from trades where px>150
select sym, px from trades where in[sym;`AAPL`MSFT] / brackets (infix sym in `AAPL`MSFT works too)
select last px by sym from trades
select vwap:wavg[sz;px], n:#px by sym from trades where px>0
select vwap:wavg[sz;px] by time:minbar[1;time] from trades
select[5] from trades / the first 5 rows (select[-5] -> the last 5)
select[>px] from trades / sorted by px descending (<px ascending)
select[5;>px] from trades / sorted, THEN limited -- q's clause order
select[<sym;>px] from trades / several keys: the first listed is primary
select[3;>v] v:sum px by sym from trades / on a grouped result too
exec px from trades where sym=`AAPL / one column -> a plain vector, not a table
exec px,sz from trades where sym=`AAPL / several columns -> a `px`sz dict of vectors
update px:px*1.01 from trades where sym=`AAPL
delete from trades where sz<100Nothing is loaded for you. ./amber script.k runs the file with no
standard library, so neither the bare form nor sel"…" exists there.
Load amber.k, std.k and qsql.k first, which is exactly what
the ./a launcher and the REPL do. Once they are loaded, a file pulled in with
\l gets the same treatment as a typed line: the loader runs its source through the
rewriter, so bare qSQL works in a script with no wrapper. The string and
functional forms below need the library too, since they are ordinary definitions in
qsql.k.
\l amber.k
\l std.k
\l qsql.k
t:([]sym:`AAPL`MSFT`AAPL; px:187.3 411.2 187.4; sz:100 250 50; ex:`N`Q`Q)
sel"select vwap:wavg[sz;px] by sym from t" / string form; also exq"…" upd"…" del"…"
qwhere[t; t[`px]>200] / filter: table + boolean mask
qselect[t; (,`v)!,{x[`px]*x`sz}] / project: name -> function
qby[t; `sym; (,`vwap)!,{wavg[x`sz;x`px]}] / group + aggregate
xgroup[`sym; t] / group into a keyed table of lists
ungroup xgroup[`sym; t] / and flatten it back
/ fby: an aggregate broadcast back over the rows of its group, as in q.
/ Usable INFIX, including inside a where-clause:
sel"select from t where px=(max;px) fby sym" / each sym's top-priced rows
sel"select sym, m:(max;px) fby sym from t" / ... or as a column
fby[(sum;t`sz); t`sym] / functional form
fby[(sum;t`sz); (t`sym;t`ex)] / grouped on several columns
fby[(sum;t`sz); ([]s:t`sym;e:t`ex)] / the same thing, as a table
qexec[t; t[`px]>100; ,`sym; (,`n)!,{#x`px}] / where + by + aggregate, in one callqby[t; keys; aggs] takes a table, a symbol (or symbol vector) of grouping columns,
and a dictionary mapping output names to functions. Each function receives the sub-table for
one group, so {wavg[x`sz;x`px]} reads two columns of that group and returns one
number.
Joins
| Verb | What it does |
|---|---|
lj[t;kt] | left join a table against a keyed table |
ij[t;kt] | inner join: rows present in both |
uj[x;y] | union join: the union of columns and rows |
pj[t;kt] | plus join: numeric columns are added rather than replaced |
ej[c;x;y] | equi join on the named columns |
aj[c;x;y] · aj0[c;x;y] | as-of: for each left row, the most recent right row at or before it |
wj[w;c;x;(y;a..)] | window: aggregate right rows inside a window around each left row, plus the one in force when it opens (as q) |
wj1[w;c;x;(y;a..)] | The same, only the rows inside the window |
asof[x;y] | the scalar as-of lookup |
As-of join
The join every tick shop needs, and the one Amber implements natively in C: aj
matches each trade to its most recent quote with a branch-free lower_bound
binary search over each symbol group's sorted nanosecond timestamp slice. The pure-K reference
(ajmK) is kept alongside it.
trade:([]sym:`a`b`a; time:3 4 9; px:100 200 300)
quote:([]sym:`a`a`b`a; time:1 5 2 8; bid:10 11 20 12)
aj[`sym`time; trade; quote]
/ ╭─────┬──────────────┬─────┬─────╮ (a `time` column prints as a time-of-day)
/ │ sym │ time │ px │ bid │
/ ├─────┼──────────────┼─────┼─────┤
/ │ a │ 00:00:00.003 │ 100 │ 10 │
/ │ b │ 00:00:00.004 │ 200 │ 20 │
/ │ a │ 00:00:00.009 │ 300 │ 12 │
/ ╰─────┴──────────────┴─────┴─────╯
m:aj[`sym`time; trades; quotes] / 5M trades against 10M quotes: ~704 msThe kernel needs rows sorted by (sym, time) with the `s attribute
stamped on time, so it binary-searches instead of scanning. xasc sets the
attribute for you; meta shows it in the a column. Correct on 64-bit ns
timestamps, empty groups, and no-match rows (which become null).
Window join
/ aggregate quotes inside [-1s, +1s] around every trade
gentq 100000 / sets the globals: trades, quotes
w:(-1000000000 1000000000)+\:trades`time
/ as q: (quotes; (function; column)..); an aggregate may also name its column, (name; function; column)
wj[w; `sym`time; trades; (quotes; (`mbid;max;`bid); (`mask;min;`ask))]See examples/wj.k for a full walkthrough.
Attributes: the C-level change
All four q-style attributes are implemented in C, and since 2.7 you set them the q way: `s#x,
`u#x, `p#x, `g#x, `#x to take one off, and attr x to read it. They
behave the way kdb+ 4.1 does (checked case by case against q.exe): every list can carry one, an atom is
'type, `s#t wants the table in row order, and a dict made `s looks up by steps:
(`s#1 3 5!`a`b`c) 4 is `b. The one difference: an unparted `p# is 'p-fail (q says
'u-fail). `at also reads them; meta
shows them; every ascending value sort (asc, x@<x, `srt, xasc) returns its result flagged `s (since 2.1), so a later ?, in, bin or aj on it takes the O(log n) path without an explicit `sa.
| Attribute | Set with | Effect |
|---|---|---|
`s sorted | `s#v · asc · x@<x · xasc | ? (find) becomes a kernel binary search, O(log n) |
`u unique | `u#v | asserts distinctness; lookups skip duplicate handling |
`p parted | `p#v | equal values are contiguous; O(log n) group slicing |
`g grouped | `g#v | a group index, giving O(1) per-symbol slicing |
v:asc 2000000?1000000000 / asc sets `s
attr v / `s
v ? 12345 67890 / O(log n) — see bench.k| rows | linear scan | binary (`s) | speedup |
|---|---|---|---|
| 100 k | 87 ms | 0.6 ms | 141× |
| 500 k | 417 ms | 0.9 ms | 470× |
| 2 M | 1.73 s | 1.4 ms | 1244× |
| 5 M | 4.23 s | 1.9 ms | 2261× |
Results are identical; only the time differs.
Strings
lower upper ltrim rtrim trim ss ssr sv vs like lk1ssr["abcabc"; "b"; "X"] / "aXcaXc"
vs[","; "a,b,c"] / ("a";"b";"c")
sv[","; ("a";"b";"c")] / "a,b,c"
like["AAPL"; "AA*"] / 1bOn disk: splayed and partitioned tables (since 2.7)
Binary column files laid out the way q lays out a database. A numeric column is one mmap, so a
10M-row table loads in about 6 ms and its pages come in as a query reads them. Symbols live in the
database's sym file, writes go through a temp file and a rename, and changing a loaded vector never
touches the disk.
`:db/t/ set t / splay t: one file per column, db/t/.d, db/sym
t2:get `:db/t / mapped back
Q.dpft[`:hdb;2026.01.02;`sym;`trade] / one day of trade, sorted by sym with `p on it
\l hdb / map a date-partitioned database
select sum sz by sym from trade where date=2026.01.02A select on a partitioned table keeps only the partitions its where-clause allows on the
partition column and maps only the columns it names. Grouped by date, it runs once per partition, so
select sum sz by date from trade over 10 days of 1M rows takes about 6 ms.
update/delete on one is 'par, as in q.
Since 2.9.1 a write survives a crash: a table goes to disk whole or not at all, even if the process is killed or the
power goes halfway through. Files are synced before they're renamed in, a new or rewritten table is built under a
hidden name and swapped in, and two processes adding symbols to one database take turns on its sym file.
AMBER_FSYNC=0 skips the syncs, for scratch data you'd rather write fast.
Binary serialization: -8! and -9!
-8!x gives q's bytes for any value and -9!y reads them back, the same bytes q sends over
a socket (since 2.9.0, see talking to q). Amber's own compact format, the one
peach and column files use, is `ser / `des. How it started is in
from language to platform.
b:-8!+`a`b!(1 2 3;4 5 6) / table -> q's bytes
(-9!b)~+`a`b!(1 2 3;4 5 6) / 1b — exactParallelism: peach
peach[f;y] is real multi-core: it runs on a pool of AMBER_THREADS threads
(default the online CPU count), so heavy per-item work scales across cores with no GIL. The result is
identical to f'y.
peach[{avg x?1.0}; 8#1000000] / 8 heavy tasks across the poolHanding out tasks costs more than a cheap reduction. peach pays off on per-task-heavy
work, not on sum. examples/peach.k shows the crossover.
Threads inside primitives (since 2.6)
You don't ask for these. Once a vector is big, sort and grade, find and in, the
aj match, gather, distinct, group aggregates, float sum/dot/max/min
and the fused trees above split over the same AMBER_THREADS. Small vectors never touch the
pool, and AMBER_THREADS=1 turns it off.
Float sums of 1M+ items add in fixed 64K blocks, so the last digits can differ from a plain left-to-right sum (and from 2.5). They're the same at any thread count.
Terminal charts and grid modes
plot (14*{sin x%7}'!74;60;9) / Braille line chart
plot (a;b;c) / several series: one labelled line each
plot exec px,ask from trades taq quotes where sym=`AAPL / a line per column, straight from a query
xyplot (trades`time; trades`px) / a time-of-day x axis -> HH:MM:SS ticks
candle bars[10; select from trades where sym=`AAPL] / Unicode candlesticks, clock-labelled
tplot (trades`time; trades`px) / x axis read as a time: 09:30:00
dplot (days; values) / ... as a date: 2026.09.18
chart `x`y`xunit!(t;v;`stamp) / ... as an ns timestamp
chart `y`yunit!(durations;`time) / a time on the Y axis
\grid clean | rounded | sharp | heavy / table frame style
CROWS:10 / preview height
COLOR:0 / disable ANSI highlighting
PREC:4 / float precisionA plain int or float vector carries no unit, only numbers: a column of milliseconds and a column of prices
look the same to a plot. xunit/yunit
take `num `time `sec `date `stamp
or `auto. `auto claims a narrow range on purpose: only whole numbers at
or above 1e15 (ns timestamps) and whole numbers inside one day and at least an hour in
(ms of day), because guessing wrong turns an ordinary index into a clock. Ticks land on round clock
and calendar boundaries, and the tick count comes from how wide that unit's labels are, so they
never overlap.
REPL diagnostics
| Command | What it prints |
|---|---|
\v | a workspace inspector. Every global as a table of Name / Type / Shape / Memory, with a recursive deep-footprint walker so nested and table sizes are real |
\ast expr | a colour-coded parse tree; nothing is executed. Leaves carry their literal type; tacit forms get explicit Hook/Fork/Projection labels |
\disasm expr | locals, constant pool and instruction stream of the real compiled bytecode, without executing it |
\trace expr | a 4-phase profiler (parse → arena → execute → format) with a bar chart and the arena's true high-water mark |
locals (0):
constants (2):
#0 -1
#1 3
bytecode (6 bytes):
0 MONAD 1
1 CONST #0
2 CONSTDYAD const#1 dyad=3
5 MONAD 0Amber's real compiler constant-folds 1+2 and 3-4, so
the constant pool holds 3 and -1 rather than the original literals. That is the
kind of detail a disassembler for the real VM surfaces that a from-scratch reimplementation
would not.
Error diagnostics
An error prints as a Rust-compiler-style report. AMBER_DIAG=0 gives the terse k form instead, the
message and a ^ caret under the failing token:
error[E0104]: Vector length mismatch
--> test.k:12:8
|
12 | prices + sizes
| ^^^^^^ ^^^^^
|
= help: Both vectors must have matching lengths for element-wise `+`.An error a trap catches (.[f;args;handler], @[f;x;handler]) prints nothing, as in
q (since 2.9.0). `diag switches the report at run time:
.[{1+`a};,0;{"caught"}] / "caught", and nothing printed
prev:`diag 0 / the terse k form from here on; gives the previous setting
`diag prev / and backFinance / HFT module (fin.k)
Auto-loaded after amber.k.
gentq 100000 / generate a session: sets globals `trades` and `quotes`
genopt 2000 / random option chain into the global `options`
m:aj[`sym`time; trades; quotes] / TAQ: prevailing quote for every trade
tsign m / Lee-Ready trade sign (+1 buy / -1 sell)
effspread m / effective spread = 2|px-mid|
qby[trades;`sym; `vwap!enlist {wavg[x`sz;x`px]}]
bars[1; trades] / 1-minute OHLCV bars| Group | Functions |
|---|---|
| book | mid spread spreadbps micro imbal |
| trades | vwap twap tsign signedvol effspread notional |
| returns / vol | ret logret rvol movavg movsum movmax movmin ema rollstd |
| aggregation | bars symstats |
| index | bysym symrows gidx (O(1) per-symbol slicing) |
| generators | genopt gentq |
Extended modules
Loaded automatically by repl.k after amber.k / fin.k.
| Module | Adds |
|---|---|
std.k | vectorised moving aggregates (mcount msum mavg mprd mvar mdev mmin mmax, O(n) prefix sums), dot / mmu, parse/eval/reval, a text ser/deser round-trip, protect (like .Q.trp), typed casts, peach, ts |
sys.k | .z clocks and handlers, .Q utilities, .j JSON (j.j/j.k), a minimal .h HTML renderer, plot/candle |
hdb.k | on-disk data, binary and memory-mapped since 2.7: set/get (`:db/t/ set t splays), dset/dget, splay/dload, Q.en/Q.dpft, loaddb and \l db for partitioned databases, ptab, and the older partsave/partload/parts. Text files from before 2.7 still read |
ipc.k | an in-process tickerplant (u.def u.sub u.pub u.get u.end). q's IPC (hopen, h x, neg[h], \p) is in the engine since 2.9.0: talking to q |
temporal.k | the native date / time / timestamp layer. See Temporal mechanics |
Built-in help
\ / the menu: every page, one key each
\0 \+ \' \: \` / the language: types, verbs, adverbs, files, built-in names
\h / the whole language on one screen
\q \j \z \m / the q layer: functions, tables and qSQL, dates and times, finance
\o \i / on disk, and talking to q
\g \r \e / charts, speed, errors
\k / every REPL command and editing key