Gotchas

Every item on this page was found the same way: code that ran, produced numbers, and was wrong, with no error and output that looked like output.

Why this page exists

An array language compresses a lot of behaviour into very little text, so a one-character mistake is a whole different program rather than a syntax error. Vigilance is no defence. What works is a differential test: run anything numerically important against a second implementation and compare exactly. Several of these were caught that way and no other.

Fixed since, kept here so old code makes sense

A comment line of just / used to swallow the rest of the file without a word: it opens a block comment that a line of just \ closes, and since 2.0.0 one left open is a parse error. Dotted names such as bt.idx:{…} now survive \l, and a / note at the end of a line inside a multi-line lambda comments out that line only. A lambda with too many locals fails with a limit error (error[E0108]) instead of a vague one; split it into smaller ones.

1. til #x parses as a train, not a count

til followed by # does not mean "the first n integers where n is the count of x". It parses as a composition and gives you something else entirely, without complaint. Write !#x.

2. A fully-applied projection under each evaluates once

This is the worst one on the page, because the output looks completely normal.

{[U] 3#U?U}[U]'til NB     / WRONG: returns the SAME basket NB times
{[i;U] 3#U?U}[;U]'til NB  / RIGHT: leave the iterated slot open

In the first form every argument is already supplied, so the expression is evaluated once and the result is repeated. You get a list of the right length, of the right type, containing the right kind of value, all identical. In a randomised sweep that produced a 13× flattering benchmark before anyone noticed the variance was zero.

The ten-second check

After any sampled or randomised each, run #?r for the count of distinct results. If it is 1 and you expected n, you have this bug.

3. An out-of-range gather returns nulls, silently

Indexing a vector past its end yields nulls where NumPy raises IndexError. On floats the 0ns then spread through every downstream scan and reduction. On ints it's quieter: since 2.9.0 an int null stays null in arithmetic, as q's does, but sums skip it, so +/(1 2 3)@4 5 is just 0. Either way the run completes with output that looks like output.

This surfaced when a 3,600-day start grid plus 756-day observation offsets overran a 2,600-day price history: the entire tensor filled with nulls and every summary statistic came back plausible. Assert your bounds explicitly before the gather:

if[T <= (|/S) + |/off; '"price history too short"];

4. -0.0 and 0.0: one value (and now they sort that way too)

Since 2.2, -0.0 and 0.0 are one value everywhere it matters: =, ~, in, = (group), ? (distinct) and find all treat them as the same number, as q does. Before 2.2 ? and find disagreed with the rest, and worse, which answer you got depended on the vector’s length, because the short and long generic paths used different comparisons:

#?(-0.0 0.0)          / 2   <- before 2.2
#?(300#-0.0 0.0)      / 1   <- the same two values, six hundred times
#?(-0.0 0.0)          / 1   <- 2.2, at every length

Until 2.4 the collation still put -0.0 before 0.0. Since 2.4.0 it doesn’t: the two sort as one value and grades keep them in place, so <(0.0;-0.0) is 0 1, as in q. Every NaN is one value too, sorting first. The keys-only sort (asc, x@<x) never changes an item, so equal zeros can come out in bit order, which is still ascending by value. Still, don’t write code that depends on where a signed zero lands.

5. 2.9.0 changed a few meanings, to q's

Code written for 2.8 can read differently now. The ones that bite: find of a missing item is the count (1 2 3?4 is 3, it was 0N), 0N+1 is 0N, -8! and -9! are q's bytes (Amber's own are `ser and `des), 09:30:00 is a second (a time keeps its ms: 09:30:00.000), and a missing item of a list of lists is its empty list, not nulls. The changelog has the full list.

Habits that catch all of them

  • Differential-test anything numerical. A second implementation in a language with different failure modes (NumPy is ideal, because it raises where Amber returns nulls) catches items 6, 7 and 8 immediately and items 1–5 by their consequences.
  • Check variance, not just shape. #?r on any randomised result.
  • Check for nulls after every gather that uses computed indices: +/^r should be 0.
  • Load your scripts in a fresh process and list what got defined. Items 1, 2 and 5 all manifest as "the definition is just not there".