Ondřej Hruška 3 weeks ago
parent 6e5a6dcc92
commit ec35dc911b
  1. 24
      docs/README.md
  2. 86
      docs/log.md
  3. 106
      docs/math.md
  4. 73
      docs/os.md
  5. 239
      docs/table.md
  6. 179
      docs/utils.md

@ -0,0 +1,24 @@
# `a` standard library
Reference docs for the library `a` adds on top of Lua 5.5. Every module below is
a **global** — there is no `require`; they are present in every script.
## Modules
| Module | What it covers |
|--------|----------------|
| [`utils`](utils.md) | JSON encode/decode, the `NULL` marker, error-trapping (`try`/`tryn`), value `dump`. |
| [`math`](math.md) | Additions to stock `math`: `round`, `clamp`, `isFinite`, `sign`, `scale`. |
| [`table`](table.md) | Additions to stock `table`: map/filter/reduce, merge, deep-copy, aggregates, lookups, read-only proxy. |
| [`os`](os.md) | Sandbox additions: `microtime` (sub-second clock) and async `sleep`. |
| [`log`](log.md) | Level-tagged diagnostic logging (`trace`…`error`), gated by `RUST_LOG`. |
| [`sqlite`](sqlite.md) | Embedded SQLite: connect, query, parameters, transactions. |
| [`http`](http.md) | HTTP/HTTPS client: requests, JSON, auth, cookie-jar sessions. |
## Concepts
| Page | What it covers |
|------|----------------|
| [Concurrency](concurrency.md) | How async calls suspend, and running work in parallel with `task.join`. |
See the top-level [README](../README.md) for what `a` is and how to build it.

@ -0,0 +1,86 @@
# `log`
The `log` global is for diagnostic output that is separate from a script's
results. Where `print` writes to standard output, `log` emits **level-tagged**
messages whose visibility is controlled at runtime by an environment variable —
so the same script can run quietly in production and verbosely while you debug,
with no code change. It is always available as the global `log`.
```lua
log.info("starting run", os.time())
log.warn("retrying after error")
log.error("giving up")
```
## Levels
There is one function per severity level:
| Function | Level | Use for |
|----------------|---------|--------------------------------------------------|
| `log.trace` | TRACE | Very fine-grained, step-by-step detail. |
| `log.debug` | DEBUG | Developer diagnostics. |
| `log.info` | INFO | Normal high-level progress. |
| `log.warn` | WARN | Something unexpected but recoverable. |
| `log.error` | ERROR | A failure worth reporting. |
`log.warning` is an alias for `log.warn`.
Each takes any number of arguments, converts each with `tostring`, and joins them
with a **tab**, just like `print`:
```lua
log.info("user", userId, "logged in") --> user 42 logged in
```
To log a table's structure, render it first with
[`utils.dump`](utils.md#utilsdumpvalue):
```lua
log.debug("request opts:", utils.dump(opts))
```
## Controlling what is shown
Log output goes to **standard error**, and which levels are shown is set by the
`RUST_LOG` environment variable. By default only `WARN` and `ERROR` appear:
```sh
a script.lua # default: warnings and errors only
RUST_LOG=info a script.lua # info and above
RUST_LOG=debug a script.lua # debug and above
RUST_LOG=trace a script.lua # everything
```
A level shows itself and everything more severe — `RUST_LOG=info` includes
`info`, `warn`, and `error` but not `debug` or `trace`. Messages below the active
level are discarded cheaply, so leaving `log.debug` calls in place costs almost
nothing when they are not enabled.
## `log` vs. `print`
| | `print` | `log` |
|---|---|---|
| Stream | stdout | stderr |
| Always shown? | yes | only at/above the active level |
| Tagged with level? | no | yes |
Use `print` for a script's actual output — the thing a caller pipes or captures.
Use `log` for the running commentary about *how* it is going, which you want to
turn up or down without editing the script.
```lua
local rows = con:query("SELECT * FROM users")
log.info("fetched", #rows, "rows") -- diagnostic; hidden unless RUST_LOG=info
for _, r in ipairs(rows) do
print(r.name) -- the actual result
end
```
## Notes
- **Always loaded.** No `require`; `log` is a global in every script.
- **Quiet by default.** With no `RUST_LOG` set, only `warn`/`error` are emitted,
so `info`/`debug`/`trace` are silent until you opt in.
- **Arguments are stringified like `print`.** Multiple arguments are joined with
tabs; tables print as their address, so use `utils.dump` for contents.

@ -0,0 +1,106 @@
# `math`
`a` keeps Lua's standard `math` library intact and adds a handful of functions
that come up constantly in everyday scripting: rounding, clamping, sign, finiteness,
and linear rescaling. The additions live on the same global `math` table, so
`math.floor` and `math.round` sit side by side.
```lua
print(math.round(3.14159, 2)) --> 3.14
print(math.clamp(120, 0, 100)) --> 100
print(math.scale(0.5, 0, 1, 0, 255)) --> 127.5
```
Everything here errors on a non-number argument rather than coercing it, so a
stray `nil` or string surfaces at the call site instead of producing a silent
`NaN`.
## `math.round(value [, places])`
Round to the nearest integer, or to `places` decimal places. Rounding is **half
away from zero** (so `0.5``1`, `-0.5``-1`), computed exactly — it does not
suffer the `floor(x + 0.5)` error that mis-rounds values just under `.5`.
The return *type* depends on `places`:
- `places` omitted or `0` → an **integer**,
- `places > 0` → a **float** rounded to that many decimals.
```lua
math.round(2.5) --> 3 (integer)
math.round(-2.5) --> -3
math.round(3.14159, 2) --> 3.14 (float)
math.round(7) --> 7 (integers pass through)
```
`places` must be a non-negative integer. A non-finite `value` (`NaN`/infinity) or
a result outside Lua's integer range is an error.
## `math.clamp(value, min, max)`
Constrain `value` to the interval `[min, max]`. Either bound may be `nil` to
leave that side open:
```lua
math.clamp(15, 0, 10) --> 10
math.clamp(-3, 0, 10) --> 0
math.clamp(5, 0, 10) --> 5
math.clamp(-3, 0, nil) --> 0 (no upper bound)
math.clamp(99, nil, 10) --> 10 (no lower bound)
```
It is an error for `min` to exceed `max`.
## `math.isFinite(value)`
Return `true` if `value` is a number that is neither `NaN` nor infinite. Anything
that is not a number returns `false` (it does not error), making it a safe guard
before arithmetic:
```lua
math.isFinite(1.5) --> true
math.isFinite(1/0) --> false (infinity)
math.isFinite(0/0) --> false (NaN)
math.isFinite("x") --> false
```
## `math.sign(value)`
Return the sign of a number as `-1`, `0`, or `1`. Zero (and `NaN`) yield `0`.
```lua
math.sign(-42) --> -1
math.sign(0) --> 0
math.sign(3.5) --> 1
```
## `math.scale(value, inMin, inMax, outMin, outMax [, clamp])`
Linearly remap `value` from the input range `[inMin, inMax]` onto the output
range `[outMin, outMax]`. With `clamp = true`, the result is held within the
output range for inputs that fall outside the input range.
```lua
-- map a 0–1023 ADC reading to a 0–100 percentage
math.scale(512, 0, 1023, 0, 100) --> ~50.05
-- ranges may descend, and you can invert
math.scale(0, 0, 100, 100, 0) --> 100
-- clamp keeps out-of-range inputs inside the output band
math.scale(2.0, 0, 1, 0, 255) --> 510 (unclamped)
math.scale(2.0, 0, 1, 0, 255, true) --> 255 (clamped)
```
The input range cannot be empty — `inMin == inMax` is an error (it would divide
by zero).
## Notes
- **Additions, not replacements.** All of stock Lua's `math``floor`, `ceil`,
`abs`, `min`, `max`, `sqrt`, `huge`, `pi`, `tointeger`, `type`, … — is still
there unchanged.
- **`round` is the only type-changing one.** It returns an integer for
`places == 0` and a float otherwise; the rest return whatever fits naturally.
- **Aggregates over tables live in [`table`](table.md).** For the min/max/mean/sum
of a collection, see `table.min`, `table.max`, `table.mean`, `table.sum`.

@ -0,0 +1,73 @@
# `os`
`a` runs scripts in a sandbox, so the standard `os` table is trimmed to its
safe, time-related functions — `os.time`, `os.clock`, `os.date`,
`os.difftime`, `os.getenv` — while the parts that touch the system
(`os.execute`, `os.remove`, `os.exit`, …) are removed. To that trimmed table `a`
adds two functions: a high-resolution clock and an async sleep.
```lua
local t0 = os.microtime()
os.sleep(0.25)
print(string.format("waited %.3fs", os.microtime() - t0)) --> waited ~0.250s
```
## `os.microtime()`
Return the current time as a Unix timestamp in **seconds, as a float** with
sub-second precision — unlike `os.time`, which is whole seconds only. It is the
right tool for measuring elapsed time:
```lua
local start = os.microtime()
doSomeWork()
local elapsed = os.microtime() - start
log.info(string.format("took %.1f ms", elapsed * 1000))
```
It reads the system wall clock, so it tracks real time (and can jump if the clock
is adjusted); for interval timing the difference of two readings is what you
want.
## `os.sleep(seconds)`
Pause for `seconds` (a float, so `os.sleep(0.1)` is 100 ms). This is an **async**
sleep: it suspends the script on the runtime instead of blocking the OS thread,
so concurrent work keeps running while it waits.
```lua
os.sleep(1) -- one second
os.sleep(0.05) -- 50 milliseconds
```
`seconds` must be a non-negative finite number.
Because the sleep suspends rather than blocks, sibling tasks started with
[`task.join`](concurrency.md) make progress during the wait — that is what lets
several sleeps (or requests, or queries) overlap:
```lua
-- finishes in ~0.3s, not 0.6s — the sleeps overlap
task.join(
function() os.sleep(0.3) end,
function() os.sleep(0.2) end,
function() os.sleep(0.1) end
)
```
One caveat applies to coroutines you drive yourself with `coroutine.wrap` /
`coroutine.resume`: there, `os.sleep` does not actually wait. See
[Self-driven coroutines](concurrency.md#self-driven-coroutines) for the full
explanation — the rule is to run anything that needs to sleep through
`task.join`.
## Notes
- **The sandbox keeps the time functions.** `os.time`, `os.clock`, `os.date`,
`os.difftime`, and `os.getenv` work as in stock Lua; system-mutating functions
are not present.
- **`microtime` for durations, `time` for timestamps.** Use `os.microtime` when
you need sub-second precision or are timing an interval; `os.time` when whole
seconds suffice.
- **`sleep` is cooperative.** It yields to the runtime, so it is cheap to sleep
inside concurrent tasks — see [Concurrency](concurrency.md).

@ -0,0 +1,239 @@
# `table`
`a` keeps Lua's standard `table` library (`insert`, `remove`, `concat`, `sort`,
`pack`, `unpack`) and adds the higher-order and collection helpers stock Lua
leaves out: map/filter/reduce, merge and deep-copy, aggregates, lookups, and a
read-only proxy. The additions live on the same global `table`.
```lua
local nums = {1, 2, 3, 4, 5}
local evens = table.ifilter(nums, function(n) return n % 2 == 0 end)
local doubled = table.map(nums, function(n) return n * 2 end)
print(table.sum(nums), table.max(nums)) --> 15 5
```
## Sequences vs. general tables
Lua tables are both arrays and maps, so most helpers come in two flavours. The
distinction runs through the whole module:
- **Plain name** (`map`, `filter`, `reduce`) iterates with `pairs` — it visits
**every** key, preserves keys in the result, and the order is unspecified.
- **`i`-prefixed name** (`imap`-style: `ifilter`, `ireduce`, …) iterates with
`ipairs` over the sequence part `1..#t` **in order**, and produces a fresh
gapless sequence.
Reach for the `i` variants when you have an array and want array semantics
(order preserved, no gaps); use the plain ones for maps or when keys matter.
## Transforming
### `table.map(tbl, fn)`
Apply `fn(value)` to every value, **preserving keys**. Order unspecified.
```lua
table.map({a = 1, b = 2}, function(v) return v * 10 end) --> {a = 10, b = 20}
```
### `table.filter(tbl, predicate)`
Keep the entries where `predicate(value)` is truthy, **preserving keys**. On a
sequence this may leave gaps — use `ifilter` to avoid them.
```lua
table.filter({a = 1, b = 2, c = 3}, function(v) return v > 1 end) --> {b = 2, c = 3}
```
### `table.ifilter(tbl, predicate)`
Keep the sequence elements matching `predicate`, producing a new **gapless**
sequence in order.
```lua
table.ifilter({1, 2, 3, 4}, function(v) return v % 2 == 0 end) --> {2, 4}
```
### `table.filterMap(tbl, fn)`
Map and filter in one pass: apply `fn(value)`, and **drop entries where `fn`
returns `nil`**. Keys are preserved (may leave gaps on a sequence).
```lua
-- keep and square only the even numbers
table.filterMap({a = 1, b = 2, c = 4}, function(v)
if v % 2 == 0 then return v * v end
end) --> {b = 4, c = 16}
```
### `table.ifilterMap(tbl, fn)`
The sequence version of `filterMap`: map over `1..#t` in order, drop `nil`
results, and return a **gapless** sequence.
```lua
table.ifilterMap({1, 2, 3, 4}, function(v)
if v % 2 == 0 then return v * 10 end
end) --> {20, 40}
```
## Reducing
### `table.reduce(tbl, fn, init)`
Fold every value into a single accumulator: `acc = fn(acc, value)`, starting from
`init`. Iterates with `pairs`, so order is unspecified — use it for
order-independent folds (sum, product, building a set).
```lua
table.reduce({1, 2, 3}, function(acc, v) return acc + v end, 0) --> 6
```
### `table.ireduce(tbl, fn, init)`
Like `reduce` but folds the sequence part **in order**, for when order matters
(e.g. concatenation, left-to-right composition).
```lua
table.ireduce({"a", "b", "c"}, function(acc, v) return acc .. v end, "") --> "abc"
```
## Aggregates
These walk all values with `pairs`, so they work on sequences and maps alike.
Each returns `nil` for an empty table (except `sum`, which returns `0`), and a
`NaN` value anywhere propagates to the result.
| Function | Result |
|--------------------|-----------------------------------------|
| `table.min(tbl)` | smallest value, or `nil` if empty |
| `table.max(tbl)` | largest value, or `nil` if empty |
| `table.mean(tbl)` | arithmetic mean, or `nil` if empty |
| `table.sum(tbl)` | sum of values, `0` if empty |
```lua
local t = {4, 8, 15, 16, 23, 42}
print(table.min(t), table.max(t)) --> 4 42
print(table.sum(t), table.mean(t)) --> 108 18.0
```
## Querying
### `table.keys(tbl)` / `table.values(tbl)`
Return an array of all keys, or all values. Order is unspecified.
```lua
table.keys({a = 1, b = 2}) --> {"a", "b"} (some order)
table.values({a = 1, b = 2}) --> {1, 2} (matching order)
```
### `table.contains(tbl, value)`
Return `true` if any value equals `value` (compared with `==`).
```lua
table.contains({"red", "green"}, "green") --> true
```
### `table.find(tbl, predicate)`
Return the **first** value matching `predicate`, together with its key:
`(value, key)`, or `(nil, nil)` if none match. Because a matching value could
itself be `false`, test the returned **key** against `nil` to distinguish "found
`false`" from "not found".
```lua
local v, k = table.find({10, 20, 30}, function(x) return x > 15 end)
print(v, k) --> 20 2
```
### `table.isEmpty(tbl)`
Return `true` if the table has no keys at all (faster and more correct than
`#tbl == 0`, which only inspects the sequence part).
```lua
table.isEmpty({}) --> true
table.isEmpty({x = 1}) --> false
```
## Combining and copying
### `table.merge(...)`
Merge any number of tables into a **new** table (a shallow merge). Sequence parts
are **appended** in argument order; other keys are copied, with later arguments
overwriting earlier ones on a collision. Not recursive — for nested merges use
`deepMerge`.
```lua
table.merge({1, 2}, {3, 4}) --> {1, 2, 3, 4} (sequences concatenate)
table.merge({a = 1, b = 2}, {b = 9}) --> {a = 1, b = 9} (later wins)
```
### `table.deepMerge(...)`
Like `merge`, but where the same key holds a table on both sides, those tables
are merged **recursively**. The result shares no tables with the inputs —
everything is deep-copied — so mutating the result never touches the originals.
```lua
table.deepMerge(
{ db = { host = "localhost", port = 5432 } },
{ db = { port = 5433 } }
) --> { db = { host = "localhost", port = 5433 } }
```
### `table.deepCopy(value)`
Return a deep copy of a table, recursively copying nested tables. Metatables are
**not** copied. Non-table values pass through unchanged.
```lua
local original = { list = {1, 2, 3} }
local copy = table.deepCopy(original)
copy.list[1] = 99
print(original.list[1]) --> 1 (unaffected)
```
`deepMerge` and `deepCopy` cap recursion at 100 levels and error beyond that, so
a cyclic table raises rather than looping forever.
### `table.reverse(tbl)`
Return a new array with the sequence elements in reverse order.
```lua
table.reverse({1, 2, 3}) --> {3, 2, 1}
```
## Protecting
### `table.readonly(tbl [, name])`
Return a read-only proxy of `tbl`: reads pass through to the underlying table,
writes raise an error. The optional `name` is included in that error message.
Useful for exposing a constant namespace that callers must not mutate.
```lua
local config = table.readonly({ retries = 3 }, "config")
print(config.retries) --> 3
config.retries = 5 --> error: Attempt to update a read-only table config
```
**Caveat:** the proxy is an empty shell that forwards reads through a metatable,
so `pairs`, `next`, and `#` see **no** contents — it is meant for guarding API
tables you read by known key, not for iterable data.
## Notes
- **Additions, not replacements.** Stock `table.insert`, `remove`, `concat`,
`sort`, `pack`, `unpack` are all still present.
- **Map vs. sequence is the recurring choice.** Plain functions preserve keys via
`pairs` (unspecified order); `i`-prefixed ones produce gapless sequences via
`ipairs` (in order). Pick by whether you have an array or a map.
- **Most helpers return new tables.** `map`, `filter*`, `merge*`, `deepCopy`,
`reverse`, `keys`, `values` never mutate their input.
- **Type-checked.** Passing a non-table where a table is expected raises a clear
error naming the offending function.

@ -0,0 +1,179 @@
# `utils`
The `utils` module is a grab-bag of helpers that the rest of the standard
library leans on: JSON encoding and decoding, a `null` marker for round-tripping
JSON and SQL, error-trapping wrappers, and a value pretty-printer. It is always
available as the global `utils`.
```lua
local data = utils.fromJSON('{"name":"Alice","age":30}')
print(data.name, data.age) --> Alice 30
print(utils.toJSON({1, 2, 3})) --> [1,2,3]
print(utils.dump({a = 1, b = {2, 3}})) --> { a = 1, b = { 2, 3 } }
```
## JSON
### `utils.toJSON(value [, pretty])`
Serialize a Lua value to a JSON string. Pass `pretty = true` for indented,
multi-line output; the default is compact.
```lua
utils.toJSON({ name = "test", count = 5 }) --> {"count":5,"name":"test"}
utils.toJSON({ name = "test" }, true) --> pretty-printed over several lines
```
How values are converted:
| Lua | JSON |
|-----------------------------|-------------------|
| `nil` | `null` |
| `utils.NULL` | `null` |
| `boolean` | `true` / `false` |
| integer | number |
| float | number |
| `string` | string |
| table (sequence) | array |
| table (other) | object |
A table is encoded as a **JSON array** when its keys are exactly the integers
`1..#t` with no gaps, and as a **JSON object** otherwise. Object keys must be
strings or integers (an integer key becomes its decimal string); any other key
type is an error.
These cases raise an error rather than producing invalid JSON:
- `NaN` or infinity (JSON has no representation for them),
- a value that has no JSON form (function, coroutine, userdata other than
`utils.NULL`),
- nesting deeper than 64 levels.
### `utils.fromJSON(string)`
Parse a JSON string into a Lua value. The inverse of `toJSON`:
| JSON | Lua |
|-------------------|----------------|
| `null` | `utils.NULL` |
| `true` / `false` | `boolean` |
| integer number | integer |
| fractional number | float |
| string | `string` |
| array | sequence table |
| object | table |
Invalid JSON raises an error. Note that JSON `null` decodes to
[`utils.NULL`](#utilsnull), **not** Lua `nil` — so a `null` inside an array does
not create a gap in the sequence.
```lua
local arr = utils.fromJSON('[1, null, 3]')
print(#arr) --> 3
print(utils.isNull(arr[2])) --> true
```
## `null`
JSON and SQL both have a `null`/`NULL` value distinct from "absent". Lua's `nil`
cannot fill that role: a table cannot *store* `nil` (assigning `nil` deletes the
key), so a literal `nil` can never survive inside a table to reach a JSON array
slot or a SQL parameter. `utils.NULL` is a sentinel that can.
### `utils.NULL`
A unique marker standing for an explicit null. Store it in a table where you mean
"null, not missing":
```lua
http.postJSON(url, { nickname = utils.NULL }) -- sends {"nickname":null}
con:execute("UPDATE u SET nickname = ?", { utils.NULL }) -- binds SQL NULL
```
It is the value `fromJSON` produces for JSON `null`, the value `toJSON` and the
`http`/`sqlite` modules turn back into `null`/`NULL`. Comparable by identity, so
`value == utils.NULL` works, but prefer `utils.isNull` for clarity.
### `utils.isNull(value)`
Return `true` if `value` is `utils.NULL`. Everything else, including Lua `nil`,
returns `false`.
```lua
utils.isNull(utils.NULL) --> true
utils.isNull(nil) --> false
utils.isNull(0) --> false
```
## Error handling
### `utils.try(fn [, ...])`
Call `fn` and trap any error, returning `(result, nil)` on success or
`(nil, err)` on failure — a more ergonomic `pcall` for the common
single-return-value case. Extra arguments are forwarded to `fn`.
```lua
local data, err = utils.try(function()
return utils.fromJSON(input)
end)
if not data then
log.error("bad JSON: " .. tostring(err))
return
end
-- use data
```
If `fn` errors with a `nil` value, `err` is the string `"unspecified error"`, so
a falsy first return reliably means failure.
### `utils.tryn(n, fn [, ...])`
Like `try`, but for a function that returns several values. `n` is how many
values `fn` returns on success. On success it returns those `n` values followed
by a trailing `nil` (the error slot); on failure it returns `n` `nil`s followed
by the error.
```lua
-- fn returns two values
local x, y, err = utils.tryn(2, function()
return parsePoint(s) -- returns x, y
end)
if err then
log.error("parse failed: " .. tostring(err))
else
print(x, y)
end
```
`n` must be an integer between 0 and 64. Use `try` when `fn` returns at most one
value; reach for `tryn` only when you genuinely need to capture several.
## Debugging
### `utils.dump(value)`
Render any Lua value as a readable string — handy for logging and quick
inspection. Strings are quoted, sequences print as `{ a, b, c }`, other tables as
`{ key = value, ... }`, and `utils.NULL` prints as `null`. Cycles are shown as
`<circular>` rather than looping forever, and nesting is capped at 20 levels.
```lua
print(utils.dump({ id = 1, tags = {"a", "b"}, parent = utils.NULL }))
--> { id = 1, parent = null, tags = { "a", "b" } }
```
This is for human eyes, not machine parsing — use `toJSON` when you need output
you can read back.
## Notes
- **`utils` is always loaded.** No `require`; the global is present in every
script.
- **`NULL` bridges three worlds.** The same marker represents null for `toJSON`,
`fromJSON`, the `http` JSON helpers, and SQL `NULL` binding in `sqlite`, so a
null value can round-trip through any of them.
- **`toJSON`/`fromJSON` are not symmetric for `nil`.** `nil` *encodes* to `null`,
but `null` *decodes* to `utils.NULL`. This is deliberate, so decoding never
silently drops array elements.
Loading…
Cancel
Save