# Bovnar (BVNR) — Full documentation This single file concatenates the Bovnar landing page and the complete documentation set, for one-fetch ingestion by LLM tools. The canonical, individually-linkable Markdown sources live under https://www.bovnar.io/doc/. # Bovnar (BVNR) **Unit-safe serialization for scientific and industrial systems — with a C99 reference implementation.** ## Links - **Website:** https://www.bovnar.io - **IANA media type (`text/vnd.bovnar`):** https://www.iana.org/assignments/media-types/text/vnd.bovnar - **DOI — Bovnar 1.1.0 Documentation and Specification:** https://zenodo.org/records/21443296 - **DOI — Bovnar 1.1.0 Source:** https://zenodo.org/records/21443009 --- ## Overview In scientific and industrial systems, the expensive failures are rarely bad syntax — they are unit confusion: a value sent in pounds-force and read as newtons, feet read as meters. The number parses fine; the dimension is wrong. Bovnar closes that gap. Every value in a `.bvnr` document carries its own type family, bit-width, numeric base, and **physical unit** — inline, in the byte stream, with no external schema. The unit is not a comment or a naming convention; it is part of the value and is validated by the parser. Annotate a measurement as `m/s` and write a mismatched inline unit, and parsing fails with `error_unit_mismatch`. Hand the file to anyone and they have everything required to interpret — and to dimensionally trust — every reading. ```bovnar #!bovnar 1.1 # optional spec-version directive # A self-describing configuration document .config = { .host = "api.example.com"; .port = 443; .limits = { .timeout = 2.5; .max_packet = 16; }; }; .acceleration = 70.5 k~m·s⁻²; .velocity = 9.81; .price = 19.99; .btc_balance = 547820000; # satoshis .created = 1750000000; # a timestamp (spec 1.1) .payload = \x00 … binary stream … \x00; .matrix = [1, 2, 3]/[4, 5, 6]; .cell = &.matrix[0][1]; # reference indexing (spec 1.1) → 2 ``` --- ## Key Features - **Strong, optional typing** — seven core families (`uint`, `sint`, `float`, `float_fix`, `float_dec`, `utf8`, `bool`), plus `datetime` (spec 1.1), with explicit bit-width (`8`, `16`, `32`, `64`, …) and numeric base (`_2`, `_16`, `_36`, `_85`, …). - **First-class physical units** — SI base units, derived SI units, and IEC binary prefixes. Compound units such as `m/s²`, `k~g·m/s²`, and `Gi~B` are written inline; no external schema is needed. - **Currency units** — 166 ISO 4217 fiat currencies and 50 cryptocurrencies are first-class units, written with a mandatory `$` sigil (` 19.99`, ` 547820000`), each carrying minor-unit metadata and prefix-validity rules. - **Inline unit suffix** — `9.81 m/s` is valid without a full type annotation. - **Native binary embedding** — Octet streams (`\x00 … \x00`) carry raw bytes without Base64 overhead. - **Multi-dimensional arrays** — Rows separated by `/`; `[1,2,3]/[4,5,6]` is a native 2D structure. - **Schema-free yet type-safe** — Omit annotations and get well-defined defaults (`uint:64`, `float:64`, …); add them and the parser validates on the fly. - **Streaming SAX-style reader** — Incremental parsing from memory, a file descriptor, or a socket via a symmetric `on_unverified` / `on_verified` callback pair. - **Error recovery** — Optional resync mode skips broken assignments and continues parsing — suitable for log streams and unreliable transports. - **Python bindings** — Pure-`ctypes`, no compiled extension required. Exposes both a high-level `loads`/`dumps` dict-like API and a low-level event-driven streaming API. - **Command-line tool** — `bovnar` validates, queries values by path, pretty-prints, converts to and from JSON, dumps the lexer/validator event stream, and benchmarks parsing throughput. - **Browser playground** — the real C reference parser, compiled to WebAssembly (`bovnar_parser_wasm.js` over `bovnar_wasm_core.js`), runs the reference verified event stream (with full type/unit/value validation) in the browser and powers an interactive web playground. - **Syntax highlighting** — Ready-made grammars for VS Code, Sublime Text, Geany, Vim, and CLion (JetBrains), all sharing one "cyberpunk" colour scheme with depth-cycling brackets. - **Extensively tested** — Unit tests, socket-pair round-trip tests, a 319-case conformance suite, fuzz harnesses (reader, writer, DOM, utils), and a built-in benchmark mode (`bovnar bench`). --- ## Format at a Glance | Construct | Syntax | Example | |---|---|---| | Assignment | `.key = value ;` | `.x = 42;` | | Comment | `# … newline` | `# a remark` | | Type annotation | `` before value | ` 1000` | | Integer | `[-]digits` | `42`, `-7` | | Float | `[-]digits[.digits][e[±]digits]` | `3.14`, `1e-6` | | Special float | `nan` `inf` `ninf` | `.x = nan;` | | Boolean | `true` `false` `on` `off` (``) | `.b = on;` | | String | `"…"` with C-style escapes (`\u{…}`/`\xHH` in spec 1.1) | `"hello\nworld"`, `"caf\u{e9}"` | | Symbol | bare identifier (no quotes) | `ok`, `Monday` | | Reference | `&.path.to.key`; array indexing `&.path[i][j]` (spec 1.1) | `&.config.host`, `&.matrix[0][1]` | | Array | `[ … ]` rows separated by `/` | `[1,2]/[3,4]` | | Struct | `{ .key = val; … }` | `{.x = 1; .y = 2;}` | | Null | empty slot or `null` keyword | `.x = ;`, `.x = null;` | | Octet stream | `\x00 … binary … \x00` | raw bytes | | Version directive (spec 1.1) | `#!bovnar .` (first line) | `#!bovnar 1.1` | | Datetime (spec 1.1) | `` signed epoch seconds | ` 1750000000` | | Datetime literal (spec 1.1) | ISO-8601 (`Z` or `±HH:MM` offset, optional fraction); bare form infers `` | `2026-06-15T12:00:00+02:00` | --- ## Documentation - [Tutorial](https://www.bovnar.io/doc/0_bovnar_tutorial.md) — A guided introduction to writing and reading Bovnar documents. - [Specification](https://www.bovnar.io/doc/1_bovnar_spec.md) — The complete format specification (spec 1.1): grammar, types, units, limits. - [Unit system](https://www.bovnar.io/doc/2_bovnar_unit_system.md) — The 163 physical units, SI/IEC prefixes, and 216 currencies, with dimensions and factors. - [Read & Write C API](https://www.bovnar.io/doc/3_bovnar_readwrite_api.md) — The C reader/writer/DOM API reference (bovnar.h, bovnar_dom.h). - [Python bindings](https://www.bovnar.io/doc/4_bovnar_python_bindings.md) — The pure-ctypes Python package: loads/dumps, streaming, DOM, Quantity, NumPy/Pint bridges. - [EBNF grammar](https://www.bovnar.io/doc/5_bovnar.ebnf) — The formal grammar, annotated against the reference lexer/validator. - [FAQ](https://www.bovnar.io/doc/6_bovnar_faq.md) — Common questions on types, units, limits, encoding, and the API. - [Conformance](https://www.bovnar.io/doc/7_bovnar_conformance.md) — The 319-case conformance suite and the IUT protocol for third-party implementations. - [Unit & currency cheatsheet](https://www.bovnar.io/doc/8_unit_cheatsheet.md) — Every unit symbol, prefix, and currency code in one reference table. - [Streaming & framing](https://www.bovnar.io/doc/9_bovnar_streaming.md) — Octet streams, frames, multiplexing/demultiplexing, and embedded documents. Browse the documentation as HTML at [https://www.bovnar.io/docs/](https://www.bovnar.io/docs/). A single-file copy of the whole set is at [https://www.bovnar.io/llms-full.txt](https://www.bovnar.io/llms-full.txt). # Bovnar (BVNR) — A Practical Tutorial **Format version:** 1.1 **Audience:** Developers building systems where data carries physical measurements. --- ## Why Bovnar Exists Bovnar is **unit-safe serialization for scientific and industrial systems**. In those domains, the expensive failures are rarely bad syntax — they are unit confusion. A thrust value sent in pounds-force and read as newtons. An altitude in feet read as meters. The number parsed perfectly; the dimension was wrong, and nothing in the data said otherwise. A bare `9.81` carries no dimension on its own: it could be meters per second, volts, or a dimensionless ratio, and the receiving application has no way to know without an external schema, a naming convention, or documentation. The unit lives outside the data, and that gap is where the expensive failures hide. Bovnar makes a different choice: every value in a Bovnar document is **self-describing and unit-safe**. The type family (signed integer, float, string…), the bit-width, the numeric base, and the physical unit all travel *with* the value, in the same byte stream, without any external schema — and the unit is validated by the parser rather than assumed. Annotate a value as `m/s` and write a mismatched inline unit, and parsing fails. Hand a `.bvnr` file to anyone, and they have everything needed to interpret *and dimensionally trust* it: a 64-bit signed integer measured in kilopascals looks different from a 32-bit unsigned integer measured in mebibytes, and the format makes that difference explicit. The format also supports raw binary embedding, multi-dimensional arrays with a clean row-separator syntax, and incremental streaming parsing — all in a text layer that is pleasant to read and write by hand. --- ## The Absolute Minimum: A Valid Bovnar File A Bovnar file is a sequence of **assignments**. Every assignment looks like this: ```bovnar .key = value; ``` Three things to notice immediately: 1. The key always starts with a dot. 2. The value comes after `=`, optionally preceded by a type annotation. 3. Every assignment ends with a semicolon. The simplest possible complete file: ```bovnar .greeting = "hello"; .count = 42; .ratio = 3.14; ``` Comments begin with `#` and run to end-of-line: ```bovnar # This is a config file .host = "localhost"; # the server address .port = 5432; # PostgreSQL default ``` The format is UTF-8 throughout. A UTF-8 BOM is accepted only at the very first byte of the stream. --- ## Keys: Identifiers A key is the bare word after the leading dot. Identifiers follow these rules: - Must begin with a letter (`A–Z`, `a–z`), an underscore, or a UTF-8 leader byte in the range 0xC3–0xF4 (i.e., most non-ASCII letters). - The body may also contain `+`, `-`, and digits. - Byte `0xC2` (the UTF-8 leader for U+0080–U+00BF) is rejected at both the start and body of an identifier. - Most ASCII punctuation is forbidden — specifically `! " # $ % & ' ( ) * , . / : ; < = > ? @ [ \ ] ^ ` { | } ~`. If you hit a parse error on what looks like a valid identifier, one of these characters is likely hiding inside it. - An empty key (`.=`) is a hard error. ```bovnar .simple = 1; .with_under = 2; .with-hyphen = 3; .with+plus = 4; .camelCase = 5; .ALL_CAPS = 6; .v2 = 7; # digit in body is fine; not at start ``` This would fail: ```bovnar .123invalid = 1; # error: starts with a digit .bad,key = 1; # error: comma is forbidden in identifiers ``` --- ## Value Tokens: What Can Appear After `=` There are ten categories of raw values: | Category | Example | |---|---| | Integer literal | `42`, `-7`, `"FF"` (base-16, quoted) | | Float literal | `3.14`, `1e6`, `-.5` | | Special number | `nan`, `inf`, `ninf` | | Boolean | `true`, `false`, `on`, `off` | | Quoted string | `"hello world"` | | Symbol (bare word) | `ok`, `Monday`, `read_only` | | Reference | `&.other.key` | | Array | `[1, 2, 3]` | | Struct | `{.x = 1; .y = 2;}` | | Null | `;` (empty) or the `null` keyword | Each is covered in depth below, starting with numbers because that is where Bovnar's type system is most visible. --- ## Numbers and the Type Annotation ### Bare Literals Numbers are written as plain text. Positive integers default to `uint:64`, negative integers to `sint:64`, and anything with a `.` or `e` to `float:64`: ```bovnar .a = 42; # → uint:64 .b = -7; # → sint:64 .c = 3.14; # → float:64 .d = 1e6; # → float:64 (1 000 000) .e = -.5; # → float:64 (leading-dot form, -0.5) .f = 123.; # → float:64 (trailing dot, valid) .g = 007; # → uint:64 (leading zeros are legal; value is 7) ``` This automatic type assignment is called **default type synthesis**. It is convenient for quick files and config work. For anything where precision matters, you add an explicit type annotation. ### Type Annotation Syntax The annotation is enclosed in angle brackets and placed **between `=` and the value** — not on the key, not after the value: ```bovnar .port = 443; .temp = 23.5; .offset = -2147483648; ``` Placing the annotation anywhere else is a hard error: ```bovnar .port = 443; # WRONG: annotation must come after '=' ``` ### Type Families There are seven core type families (spec 1.0), plus `datetime` added in spec 1.1: | Keyword | Description | |---|---| | `uint` | Unsigned integer | | `sint` | Signed integer | | `float` | IEEE 754 binary floating-point | | `float_fix` | Q-format signed fixed-point | | `float_dec` | IEEE 754-2008 decimal floating-point | | `utf8` | UTF-8 string | | `bool` | Boolean (`true`/`false`); takes no parameters | | `datetime` | Timestamp: signed epoch-seconds, or an ISO-8601 literal like `2026-06-15T12:00:00Z` (spec 1.1; needs a `#!bovnar 1.1` directive) | ### Parameters After the family keyword you may add parameters separated by commas after a `:`. The three parameter classes are **width**, **base**, and **unit**. They are identified by their syntactic form and may appear in any order: ```bovnar .a = 1000; # width only .b = "FF"; # width + base (hex) .c = 500; # width + unit (meters) .d = "1F4"; # all three — 500 meters in hex .e = "1F4"; # same thing; order doesn't matter ``` #### Width Width is a plain decimal number of bits. `0` means "use the default," which is always 64: ```bovnar .a8 = 255; .a16 = 65535; .a32 = 4294967295; .a64 = 18446744073709551615; .def = 99; # same as ``` For `float`, valid widths are `0`, `16`, and any multiple of `32` up to `32768`. Width `8` or a non-multiple-of-32 (other than 16) is a hard error. For `float_fix` and `float_dec`, valid widths are `0`, `16`, `32`, `64`, `128`, and `256`. ```bovnar .half = 3.14; # half-precision .single = 3.14; # single-precision .double = 3.14; # double-precision .quad = 3.14; # quad-precision ``` For `sint` and `uint`, any width from 1 up to 32768 bits (`BVN_MAX_INT_WIDTH`) is accepted — `` is perfectly valid for a 12-bit ADC sample. #### Base The base parameter starts with `_` followed by a decimal number. For `uint` and `sint`, every base from `_2` through `_62` (consecutive) is allowed. Bases `_64` (standard Base64) and `_85` (Ascii85) are also available, but **only for `uint`**: their alphabets claim the sign glyphs as digits — Base64 uses `+` (digit 62) and `/`, Ascii85 uses both `+` and `-` — so no unambiguous sign character remains and signed values are illegal (`error_illegal_value_type`). The base selects the numeral system used to interpret the value. This is the biggest surprise for newcomers: **non-decimal values must be provided as quoted strings**. A bare `ff` in value position is lexed as a *symbol*, not a number, and will produce a type mismatch error. Always quote non-decimal digits: ```bovnar .hex = "FF"; # correct — 255 .hex = FF; # WRONG — FF is a symbol .bin = "11010110"; # correct — 214 .oct = "755"; # correct — Unix permission rwxr-xr-x ``` Signed values use a minus prefix inside the string: ```bovnar .neg_hex = "-7FFFFFFF"; .neg_bin = "-10000000"; # -128 in binary ``` `float` accepts only `_10` (the default) and `_16`. `float_fix` and `float_dec` **do not accept a base parameter at all** — specifying one is a hard error. #### Special Number Literals Three special values bypass normal numeric parsing. They are **bare reserved keywords** — no sigil — and, like `null`/`true`/`false`, are reclassified out of the symbol space by the validator: ```bovnar .nan = nan; .inf = inf; .neg = ninf; # negative infinity ``` Only these exact spellings are reserved: any other bare word (e.g. `infinity`, `nans`) is an ordinary symbol. They may be combined with any float type annotation and are valid in untyped context too. A special-number keyword takes no inline unit suffix — give its unit via the annotation (` inf`). --- ## Units This is the feature most unique to Bovnar. Physical units are a first-class part of the type annotation, not a comment or a string field you hope someone reads. ### Simple Units The unit goes inside the angle brackets, after the other parameters: ```bovnar .timeout = 2.5; # seconds .distance = 384400000; # meters (Earth-Moon distance) .voltage = 3.3; # volts .temp = 23.5; # degrees Celsius (UTF-8 is fine) ``` ### SI Prefixes SI prefixes are written before the base unit symbol with a **mandatory `~`** separator: ```bovnar .k_ohm = 4.7; # kilo-ohm .milli = 330.0; # millivolt .micro = 50.0; # microsecond (µ = U+00B5) .giga = 2.4; # gigahertz .kibi = 1024; # kibibytes (IEC binary prefix) .mebi = 4096; # mebibytes ``` The `~` between prefix and unit is non-optional. `` (no `~`) would either fail to parse or be interpreted differently. Every prefix listed in the SI table uses the same pattern: `prefix~unit`. IEC binary prefixes (`Ki`, `Mi`, `Gi`, `Ti`, `Pi`, `Ei`, `Zi`, `Yi`, `Ri`, `Qi`) follow the same rule: ```bovnar .ram = 8; # 8 gibibytes .disk = 2; # 2 tebibytes ``` ### Compound Units Compound units are built from multiple components joined by `*` (or the middle dot `·`, U+00B7) for multiplication, and `/` for division: ```bovnar .velocity = 9.81; # meters per second .acceleration = 9.81; # m·s⁻² .force = 9.81; # Newton (kilogram × meter / second²) .energy = 1000; # Joule .pressure = 101325; # Pascal .momentum = 0.5; .field = 150.0; .moment = 1.0; # asterisk = middle dot ``` The `/` separator is a **one-way switch**: once you write `/`, every subsequent component is in the denominator. Writing another `/` does not switch back to the numerator. If you need a component back in the numerator after a divisor, use a negative exponent: ```bovnar .force_alt = 9.81; # same as k~g·m/s² ``` Exponents can be written as Unicode superscripts (`²`, `³`, `⁻¹`) or with an ASCII caret (`^2`, `^-2`). Both are valid: ```bovnar .area1 = 100.0; .area2 = 100.0; # same thing .inv1 = 50.0; .inv2 = 50.0; # same thing ``` The maximum number of unit components in a compound unit is 8. More than that causes a parse error. ### Explicitly Dimensionless: `no_unit` When you want to be explicit that a value carries no physical dimension, use the keyword `no_unit`: ```bovnar .ratio = 1.4142; .count = 42; ``` Explicitly writing `no_unit` yields `BVN_UNIT_NONE` (`num_components == 0`). Omitting the unit parameter entirely yields `BVN_UNIT_NO_PREFIX(bu_none)` (`num_components == 1`, `base == bu_none`). Both are semantically dimensionless and compare as compatible via `bvn_units_compatible`, and both serialise to `"no_unit"` via `bvn_unit_to_string`, but they are structurally distinct internal states. The explicit `no_unit` is useful for documentation: it tells a reader that the author actively chose dimensionless, not that they forgot. ### Inline Unit Suffix For untyped or partially-typed values, you can append a unit suffix directly after the value literal, separated by at least one whitespace character: ```bovnar .speed = 9.81 m/s; # valid .mass = 70.0 k~g; # result: float:64, unit = k~g .heap = 65536 B; # result: uint:64, unit = B (bytes) ``` This is forbidden: ```bovnar .bad = 9.81m; # ERROR: no separator at all ``` If a type annotation also specifies a unit and you additionally write an inline suffix, they must match exactly. A mismatch is a hard error: ```bovnar .ok = 9.81 m/s; # redundant but valid: both say m/s .bad = 9.81 s; # ERROR: annotation says m, inline says s ``` **The inline unit suffix is completely forbidden inside array elements.** This is a lexer-level restriction, not a semantic one — any letter or underscore following a value token inside `[…]` is an immediate hard error. --- ## Fixed-Point Numbers: `float_fix` Fixed-point (Q-format) numbers store a value as a signed integer, with the binary point shifted by a declared number of fractional bits. The wire representation is an integer; the mathematical value is `raw_integer × 2⁻Q`. The annotation requires a `qN` parameter specifying fractional bits, in addition to the width: ```bovnar .adc = 3.14; # 16-bit, 8 fractional bits # range: [-128, 127.996], resolution ≈ 0.0039 .pid = -1.5; # 32-bit Q16: 15 integer bits + sign + 16 fractional .vel = 9.81; # with a unit .cnt = 4096; # Q0 = no fractional bits (pure integer shell) ``` Constraints: - `Q` must be in the range `0 ≤ Q < effective_width`. `` is illegal because Q must be strictly less than the width. - The base parameter (`_N`) is forbidden. `` is an error. - Valid widths: `0` (→64), `16`, `32`, `64`, `128`, `256`. Width `8` is not valid. --- ## Decimal Floating-Point: `float_dec` `float_dec` is IEEE 754-2008 decimal floating-point — useful in financial and metrological contexts where exact decimal representation matters (binary floats cannot represent `0.1` exactly; decimal floats can): ```bovnar .price = 12.99; # 7 significant decimal digits .voltage = 101325.0; # 16 significant decimal digits .pi_big = 3.14159265358979323846264338327950288; # 34 digits ``` Rules are similar to `float_fix`: no base parameter, same set of valid widths. --- ## Strings Strings are enclosed in double quotes. The `` annotation is optional — a bare quoted literal is automatically synthesised as `utf8`: ```bovnar .greeting = "hello"; # → .typed = "world"; # explicit, same result .empty = ""; ``` ### Escape Sequences Seven escape sequences are defined in spec 1.0: | Escape | Meaning | |---|---| | `\t` | Horizontal tab | | `\n` | Line feed | | `\v` | Vertical tab | | `\f` | Form feed | | `\r` | Carriage return | | `\"` | Double quote | | `\\` | Backslash | In spec 1.0 any other character after a backslash is a hard error. A document that declares `#!bovnar 1.1` additionally gets `\xHH` (one byte) and `\u{H…}` (a Unicode scalar, UTF-8 encoded) — but a control byte (other than the whitespace ones above) is still rejected, so for arbitrary binary use an octet stream (see below). In a 1.0/unversioned document `\x`/`\u` remain hard errors. Raw whitespace bytes (HT, LF, VT, FF, CR) are accepted unescaped inside string literals, so a multi-line string is valid: ```bovnar .poem = "roses are red violets are blue"; ``` ### String Concatenation Adjacent string literals separated only by whitespace or comments are concatenated at lex time: ```bovnar .url = "https://" "api.example.com" "/v1"; .long = "first part " "second part " # a comment is fine here "third part"; ``` This is the idiomatic way to write a long string across multiple lines or to construct a non-decimal value that happens to be split for readability: ```bovnar .guid = "deadbeef" "cafebabe" "01234567" "89abcdef"; ``` --- ## Symbols A symbol is a bare word in value position — no quotes. It is its own token type, distinct from strings: ```bovnar .status = ok; .mode = read_only; .day = Monday; ``` Symbol bodies follow the same character rules as identifier bodies: letters, digits, `+`, `-`, `_`, and valid UTF-8 high bytes. The key distinction from an identifier is context: identifiers appear as keys (after the opening dot), symbols appear as values. Inside an array, `]` and `,` terminate a symbol. At assignment level, `;` terminates it. Eight bare words are **reserved keywords** and are *not* symbols: `null`, `true`, `false`, `on`, `off`, and the special floats `nan`, `inf`, `ninf` (see the special-number section). A longer word that merely starts with one — `ontology`, `nullable`, `truthy`, `infinity` — is still an ordinary symbol. --- ## Booleans `true`, `false`, `on`, and `off` are reserved keywords carrying the `bool` type family. `on` is an alias for `true` and `off` for `false`; all four collapse to a boolean value and serialize canonically as `true` / `false`: ```bovnar .enabled = true; .debug = off; # same value as false .typed = on; # explicit annotation; on == true ``` A bare boolean synthesises a `` annotation automatically. The `` family takes no parameters (`` is a hard error), and only the four keywords are valid `` values. --- ## Null Values A null is the explicit absence of a value. It appears as an empty slot — nothing between `=` and `;` — or, equivalently, the reserved keyword `null`; extra commas inside an array also produce nulls: ```bovnar .nothing = ; # null, no type .also_null = null; # the null keyword — same as the empty slot .typed_null = ; # null with an explicit type annotation ``` Inside arrays, null elements arise from leading, trailing, or consecutive commas: ```bovnar .sparse = [, 1, , 2, ]; # null, 1, null, 2, null — five elements .holes = [ , 0, ]; # typed nulls ``` --- ## References A reference is a dotted path to another key in the document, introduced by `&`. The parser stores the path as a string token; it does not resolve it — resolution is entirely up to the application: ```bovnar .host = "db.internal"; .port = 5432; .conn_host = &.host; # stored as ".host" .conn_port = &.port; # stored as ".port" ``` Multi-segment paths: ```bovnar .server = { .address = "10.0.0.1"; .tls = { .cert = "/etc/ssl/server.pem"; }; }; .proxy_addr = &.server.address; # ".server.address" .cert_path = &.server.tls.cert; # ".server.tls.cert" ``` References are valid in arrays and structs: ```bovnar .nodes = [&.master, &.replica-a, &.replica-b]; .worker = { .retries = &.defaults.retries; .delay = &.defaults.delay_s; }; ``` A reference is not a null. If a field is optional and currently has no referent, represent the absence with an explicit null: ```bovnar .override = ; # null — no reference, use internal default # .override = &.config.x; # or, non-null — follow this reference ``` **Array indexing (spec 1.1).** In a `#!bovnar 1.1` document a reference may end with one or more `[N]` index suffixes to address an array element. For a flat `/`-row matrix the two indices are `[row][col]`: ```bovnar #!bovnar 1.1 .matrix = [10, 20, 30]/[40, 50, 60]; .cell = &.matrix[1][2]; # the element at row 1, column 2 → 60 .first = &.vector[0]; # a 1-D array takes a single [i] ``` The path (including the indices) is stored verbatim; the DOM layer (`bvn_dom_lookup` / the CLI `query`) is what resolves it to a value. Index suffixes are rejected in a 1.0 or unversioned document. --- ## Arrays An array is enclosed in square brackets. A single row is a comma-separated list of values: ```bovnar .primes = [2, 3, 5, 7, 11, 13]; .names = ["Alice", "Bob", "Carol"]; ``` ### Multi-Dimensional Rows Multiple rows are separated by `/`. Each bracketed group is one row: ```bovnar .matrix = [1, 2, 3]/[4, 5, 6]; # 2 rows × 3 columns ``` The event stream reflects this: `ev_array_row_start`, elements, `ev_array_row_end`, then `ev_array_dim_start` before the second `ev_array_row_start`. Row-separator `/` and array nesting are independent concepts — do not confuse them. The `/` between `]` and `[` creates a new dimension in the *same* array, not a nested one. The `/`-separated dimension rows of a single array must have a **consistent element count**, and (since spec 1.0) array elements are **homogeneous**: sibling sub-arrays must also match in length, so bare arrays and matrices are rectangular: ```bovnar .ok1 = [1, 2, 3]/[4, 5, 6]; # valid: both /-rows of one array have 3 elements .ok2 = [[1, 2], [3, 4]]; # valid: rectangular sub-arrays # .ok3 = [[1, 2], [3, 4, 5]]; # error_array_row_size_mismatch: sibling sub-arrays differ (2 vs 3) .bad1 = [1, 2, 3]/[4, 5]; # error_array_row_size_mismatch: 3 vs 2 (/-rows of one array) .bad2 = [[1, 2]/[3, 4, 5]]; # error_array_row_size_mismatch: 2 vs 3 (one /-array's own rows) ``` ### Per-Element Type Annotations Each element may carry its own annotation, placed immediately after the comma (or the opening bracket for the first element): ```bovnar .mixed = [ 255, -128, 3.14]; ``` A single annotation before `[` applies to all elements — it is a whole-array annotation: ```bovnar .ports = [80, 443, 8080, 8443]; .temps = [23.5, 24.1, 22.8]; ``` Per-element annotations can still appear inside a whole-array-annotated array. ### Structs as Array Elements Arrays of structs are the standard idiom for a list of records: ```bovnar .users = [ {.id = 1; .name = "Alice"; .role = admin;}, {.id = 2; .name = "Bob"; .role = user;} ]; ``` --- ## Structs A struct is a nested scope, grouping zero or more assignments inside `{…}`. Every field inside is a complete assignment with `.key = value;` syntax. The struct value at the parent level ends with `};`: ```bovnar .person = { .name = "Alice"; .age = 30; .active = true; }; ``` Structs nest deeply — the limit is `max_struct_nesting`, which defaults to 64 and has a hard cap of 255: ```bovnar .config = { .database = { .primary = { .host = "db1.internal"; .port = 5432; .tls = { .enabled = true; .cert = "/etc/ssl/db.pem"; }; }; }; }; ``` An empty struct is valid: ```bovnar .placeholder = {}; ``` An unmatched closing brace — a `}` with no matching `{` — is `error_illegal_struct_close`. --- ## Octet Streams: Inline Binary When you need to embed raw binary data without Base64 overhead, a NUL byte (`0x00`) in value position switches the parser into binary chunk mode. The wire protocol for an octet stream is: ``` 0x00 ← stream open 0x01 ← one chunk: tag 0x01, length as little-endian uint16, then data 0x01 ← another chunk 0x00 ← stream close ``` A length value of `0x0000` means exactly 65536 bytes. Any tag byte other than `0x01` (chunk) or `0x00` (close) inside a stream is `error_octet_stream_out_of_sync`. In practice, you will not hand-author octet streams — the writer API does this for you. In a `.bvnr` file shown for documentation purposes it appears as an escaped binary sequence: ```bovnar .binary = \x00\x01\x05\x00hello\x01\x03\x00bye\x00; ``` This produces two data events with payloads `"hello"` (5 bytes) and `"bye"` (3 bytes). The UTF-8 validator is suspended for the duration of a binary region. --- ## Putting It Together: Realistic Examples ### Hardware Configuration ```bovnar # Sensor node configuration .node_id = 7; .firmware_ver = "2.4.1"; .radio = { .frequency = 868.1; .tx_power = 14; # dB (power level); 'dBm' is not a bovnar unit .bandwidth = 125.0; .sf = 7; # spreading factor }; .sampling = { .interval = 60; .burst_size = 8; .adc_bits = 12; }; .calibration = { .offset = -0.5; .gain = 1.002; }; ``` ### Scientific Measurement Batch ```bovnar .experiment = "thermal_runaway_001"; .timestamp = 1715000000; .measurements = [ {.channel = 0; .value = 27.3; .valid = true;}, {.channel = 1; .value = 31.7; .valid = true;}, {.channel = 2; .value = 112.4; .valid = true;} ]; .threshold = 80.0; .alarm = triggered; ``` ### Multi-Dimensional Matrix ```bovnar # 3×3 rotation matrix in float64 .R = [1.0, 0.0, 0.0] /[0.0, 0.866, -0.5] /[0.0, 0.5, 0.866]; ``` ### Mixed Binary and Text ```bovnar .packet = { .type = data_frame; .sequence = 42; .timestamp = 1715000000123456; .payload = \x00\x01\x08\x00\xDE\xAD\xBE\xEF\xCA\xFE\xBA\xBE\x00; .crc32 = "A1B2C3D4"; }; ``` --- ## Common Mistakes and the Errors They Produce Understanding the error codes is essential for debugging. The parser reports line, column, the offending byte, and a byte-offset into the stream. **Annotation on the wrong side of `=`:** ```bovnar .x = 42; # error_unexpected_input_byte (< after identifier) ``` **Non-decimal value not quoted:** ```bovnar .x = FF; # FF is parsed as a symbol → error_type_value_mismatch ``` **Signed value in unsigned type:** ```bovnar .x = -1; # error_value_out_of_range ``` **Overflow:** ```bovnar .x = 256; # error_value_out_of_range (max is 255) ``` **Float with an unsupported width:** ```bovnar .x = 1.0; # error_illegal_value_type .x = 1.0; # error_illegal_value_type (not 16 or multiple of 32) ``` **Base parameter on `float_fix` or `float_dec`:** ```bovnar .x = 1.0; # error_illegal_value_type .x = 1.0; # error_illegal_value_type ``` **Q value too large for `float_fix`:** ```bovnar .x = 1.0; # error_illegal_value_type (Q must be < width) ``` **Empty unit component:** ```bovnar .x = 1.0; # error_unit_illegal (empty component between //) ``` **Too many unit components:** ```bovnar .x = 1.0; # error_unit_illegal (9 > 8 max) ``` **Inline unit suffix inside array:** ```bovnar .x = [9.81 m/s, 3.14 m]; # error_unexpected_input_byte (units forbidden in arrays) ``` **Comma outside array:** ```bovnar .x = 42,; # error_unexpected_input_byte ``` **Unknown escape sequence:** ```bovnar .x = "\q"; # error_illegal_escape_sequence (\q is not a defined escape) ``` (`\xHH` and `\u{…}` *are* defined in spec 1.1 — see the escapes section above; they are rejected only in a 1.0 or unversioned document.) **Unmatched struct close:** ```bovnar .x = 1;} # error_illegal_struct_close (a stray '}' at the top level) ``` **A `/`-array's dimension rows must match:** ```bovnar .ok = [[1, 2]/[3, 4]]; # valid — the /-array's two rows both have 2 elements .bad = [[1, 2]/[3, 4, 5]]; # error_array_row_size_mismatch — 2 vs 3 (/-rows) # note: [[1,2],[3,4,5]] is also rejected — sibling sub-arrays must match in length (1.0 homogeneity) ``` --- ## Error Recovery Mode The parser supports an optional `continue_on_error` mode. When enabled, a parse error invokes the `on_error` callback and then enters a resync state machine that skips bytes while tracking bracket and brace nesting. The `recovery_count` counter (accessible via `bvnr_reader_get_recovery_count`) is incremented immediately when an error triggers entry into resync mode — not when the resync eventually completes. When the resync state machine reaches a `;` at the original nesting depth, it resumes normal parsing. This mode is intended for log streams and unreliable transports — situations where one malformed assignment should not discard the rest of the file. For configuration and data serialization, disable it: fail fast and reject corrupt input entirely. --- ## Size Limits All limits are configurable via `bvnr_read_flags_t`. The defaults are intentionally permissive — 2 147 483 647 for array items and text bytes, and **unlimited** (`0`) for file size. Production deployments should set explicit caps: | Field | Default | Suggested cap | |---|---|---| | `max_identifier_length` | 255 | 255 | | `max_string_length` | 65535 | application-defined | | `max_number_length` | 65535 | 65535 | | `max_symbol_length` | 255 | 255 | | `max_reference_length` | 65535 | 65535 | | `max_array_items` | 2 147 483 647 | application-defined | | `max_text_bytes` | 2 147 483 647 | application-defined | | `max_file_size` | 0 (unlimited / endless) | `16777216` (16 MiB) | | `max_array_nesting` | 0 (→64 internal) | 32 or less for most applications | | `max_struct_nesting` | 0 (→64 internal) | 32 or less for most applications | Setting most fields to `0` in `bvnr_read_flags_t` causes the reader to substitute an internal default — **64** for both nesting depths, and **2 147 483 647** for `max_array_items` and `max_text_bytes`. **`max_file_size` is the exception: `0` means unlimited / endless** (no byte-count cap), which is the default so endless streams work out of the box. Explicitly setting `max_file_size = 16777216` (16 MiB) is the recommended practice for production deployments. --- ## Event Model (C API Overview) The C reader is a two-phase, callback-driven SAX-style parser. You supply two optional callbacks: `on_unverified` receives events before semantic validation, `on_verified` receives them after. For normal consumption, `on_verified` is sufficient. The event sequence for `.port = 443;` is: ``` ev_stream_start ev_assignment_start → key = "port" ev_type_annotation_start → raw = "uint:16" ev_type_annotation_type_family → "uint" ev_type_annotation_type_family_parameter → width: 16 ev_type_annotation_end ev_data → value = "443", type = {uint, 16} ``` The `ev_type_annotation_type_family_parameter` event fires once per **present** parameter. Because the explicit annotation `` carries neither a base nor a unit parameter, only the width event is emitted. Contrast this with a bare `42;` (no annotation), where the validator **synthesises** `` and therefore emits three parameter events: ``` ev_stream_start ev_assignment_start → key = "count" ev_type_annotation_start → (synthesised) ev_type_annotation_type_family → "uint" ev_type_annotation_type_family_parameter → width: 64 ev_type_annotation_type_family_parameter → base: _10 ev_type_annotation_type_family_parameter → unit: no_unit ev_type_annotation_end ev_data → value = "42", type = {uint, 64} ``` The synthesised annotation always produces all three parameter events; explicit annotations only produce events for the parameters that are actually present. **`ev_type_annotation_start` asymmetry:** This event is emitted to `on_unverified` first (with raw annotation bytes in `data`/`length` but no `value_type` or `value_unit` populated), and then separately to `on_verified` (with `value_type` and `value_unit` filled in). All other type-annotation events and `ev_data` are emitted to both callbacks simultaneously via the same call. The `bvnr_data_t` structure passed with `ev_data` carries: - `type` — the token type (`token_is_number`, `token_is_string`, `token_is_symbol`, `token_is_reference`, `token_is_array_number`, `token_is_array_string`, `token_is_bool`, `token_is_null_value`, `token_is_octet_stream`) - `value_type` — the `value_type_spec_t` (family, width, base/Q) - `value_unit` — the `value_unit_t` (up to 8 components) - `data` — pointer to the raw value bytes - `length` — byte count For an octet stream, `ev_octet_stream_start` and `ev_octet_stream_end` bracket one or more `ev_data` events with `type == token_is_octet_stream`. For arrays, `ev_array_row_start` opens each row, `ev_array_row_end` closes it, and `ev_array_dim_start` separates dimensions (the `/` rows). For structs, `ev_struct_start` and `ev_struct_end` bracket the nested assignments. No `ev_data` event is emitted for the struct value itself. --- ## Quick-Reference: Syntax Cheat Sheet ```bovnar # ── Assignments ──────────────────────────────────────────────────── .key = value; .key = value; # ── Type families ────────────────────────────────────────────────── # unsigned 32-bit integer # signed 64-bit integer # IEEE 754 single-precision # IEEE 754 quad-precision (or any multiple of 32) # Q-format fixed-point, 32-bit, 16 fractional bits # IEEE 754-2008 decimal, 64-bit # UTF-8 string # boolean (true/false); no parameters # ── Units ────────────────────────────────────────────────────────── # seconds # kilometers (SI prefix~unit, `~` mandatory) # meters per second # meters per second squared # kilogram-meters per second squared # kibibytes # explicitly dimensionless # ── Special values ───────────────────────────────────────────────── nan inf ninf # ── Booleans and null (reserved keywords) ────────────────────────── .b = true; .b = false; .b = on; .b = off; # on==true, off==false .n = null; .n = ; # null keyword == empty # ── Inline unit suffix (scalar context only) ─────────────────────── .x = 9.81 m/s; # ── Non-decimal (must be a quoted string) ────────────────────────── .x = "DEADBEEF"; .x = "-10000000"; # ── Arrays ───────────────────────────────────────────────────────── .a = [1, 2, 3]; .a = [1, 2, 3]/[4, 5, 6]; # 2D: two rows .a = [1.0, 2.0]; # whole-array annotation .a = [ 1, -1]; # per-element annotations .a = [, 1, , 2, ]; # null, 1, null, 2, null # ── Structs ──────────────────────────────────────────────────────── .s = {.x = 1; .y = 2;}; .s = {}; # ── Symbols ──────────────────────────────────────────────────────── .state = running; .ok = true; # ── References ───────────────────────────────────────────────────── .ref = &.other_key; .deep = &.config.db.host; # ── Null ─────────────────────────────────────────────────────────── .x = ; .x = ; # ── Strings ──────────────────────────────────────────────────────── .s = "hello\nworld"; .s = "part one " "part two"; # concatenation ``` --- *Bovnar Specification (v1.1) — format by the Bovnar project.* # Bovnar Specification > **Version:** 1.1 > **Status:** Released (1.x line; additive over the frozen 1.0 baseline) > **Last updated:** 2026-06-21 --- ## Table of Contents 1. [Overview](#1-overview) 2. [File Format at a Glance](#2-file-format-at-a-glance) 3. [Character Encoding & BOM](#3-character-encoding--bom) 4. [Lexical Structure](#4-lexical-structure) 5. [Type Annotations](#5-type-annotations) 6. [Value Tokens](#6-value-tokens) 7. [Arrays](#7-arrays) 8. [Structs (Scopes)](#8-structs-scopes) 9. [Octet Streams (Binary Mode)](#9-octet-streams-binary-mode) 10. [Default Type Synthesis](#10-default-type-synthesis) 11. [Units System](#11-units-system) 12. [Validation & Constraints](#12-validation--constraints) 13. [Error Handling & Recovery](#13-error-handling--recovery) 14. [Formal EBNF](#14-formal-ebnf) 15. [Complete Examples](#15-complete-examples) 16. [Reference API](#16-reference-api) 17. [Versioning & Stability](#17-versioning--stability) - [Appendix A: Event Sequence Reference](#appendix-a-event-sequence-reference) - [Appendix B: Implementation Notes](#appendix-b-implementation-notes) - [Appendix C: Limits Summary](#appendix-c-limits-summary) --- ## 1. Overview **Bovnar** (BVNR) is **unit-safe serialization for scientific and industrial systems**: a **typed, self-describing, text-binary hybrid** format that carries a validated physical unit with every value. It combines a human-readable text layer with an efficient binary octet-stream escape mechanism and a rich, unit-aware type annotation system. ### Design Goals - **Unit-safe** – every value may carry a physical unit that is validated against a built-in unit table; a unit that conflicts with the type annotation is a parse error (`error_unit_mismatch`) - **Self-describing** – values carry explicit type metadata (family, bit-width, base, measurement unit) - **Human-readable** for common cases (numbers, strings, symbols, nested structures) - **Binary-friendly** via octet stream escape for opaque payloads - **Schema-free** but type-safe – the type annotation is per-value, optional, and validated - **Streamable** – parsed incrementally via a pull-based reader; events emitted to callbacks - **Error-recoverable** – optional resync mode for parsing through minor corruption ### Architecture The reference implementation is a **two-phase** parser: | Phase | Callback | Purpose | |-------|----------|---------| | **Unverified** (`on_unverified`) | Raw token events before semantic validation | Inspection, logging, partial consumption | | **Verified** (`on_verified`) | Fully validated events | Actual data consumption | Both phases receive the same stream of events (`bvnr_event_t`). The validator sits between them. ### Stream Model ``` stream_begin → (assignment)* → EOF ``` Each assignment is: ``` .identifier = value; ``` --- ## 2. File Format at a Glance ```bovnar # This is a comment .my_first_value = 42; .my_next_value = 3.14159; .a_string = "hello world"; .a_typed_value = 1000; .a_utf8_value = "text"; .an_array = [1, 2, 3]/[4, 5, 6]; .a_struct = { .nested = true; }; ``` ### Key Syntax Rules | Construct | Syntax | Example | |-----------|--------|---------| | Comment | `#` … newline | `# this is a comment` | | Assignment | `.key = value ;` | `.foo = 42;` | | Type annotation | `` placed after `=`, before the value | `.foo = 42;` | | Number | `[-]digits[.digits][e[+/-]digits]` | `42`, `-3.14`, `1e6` | | Special number | `nan`, `inf`, `ninf` | `.x = inf;` | | Boolean | `true` / `false` / `on` / `off` | `.b = true;`, `.b = off;` | | String | `"…"` with escapes (`\u{…}`/`\xHH` in spec 1.1) | `.s = "hello\nworld";`, `.s = "caf\u{e9}";` | | Symbol | bare identifier (no quotes) | `.s = ok;`, `.day = Monday;` | | Reference | `&.path.to.key`; array indexing `&.path[i][j]` (spec 1.1) | `.ref = &.config.host;`, `.c = &.m[0][1];` | | Array | `[ … ]` rows separated by `/` | `.a = [1,2,3]/[4,5,6];` | | Struct | `{ … }` | `.s = {.x = 1; .y = 2;};` | | Octet stream | `\x00` … binary … `\x00` | binary escape | | Null value | absent token or `null` keyword | `.x = ;`, `.x = null;`, `[,1,]` | | Version directive (spec 1.1) | `#!bovnar .` on the first line | `#!bovnar 1.1` | | Datetime (spec 1.1) | `` — signed epoch seconds | `.t = 1750000000;` | | Unit (type annotation) | inside `<…>` as a type param | `` | | Unit (inline suffix) | after scalar value, before `;` | `.speed = 9.81 m/s;` | | Fixed-point type | `` | ``, `` | | Decimal float type | `` | `` | | Unit (compound) | `unit[*·/unit…]` | `m/s²`, `k~g·m/s²`, `m*s` | --- ## 3. Character Encoding & BOM ### 3.1 UTF-8 All text-layer bytes (outside octet-stream regions) must form **valid UTF-8**. The parser runs a parallel UTF-8 validator (`bvn_utf8_feed`) alongside the state machine. Violations produce `error_invalid_utf8_byte`. Overlong sequences and surrogate halves (U+D800–U+DFFF) are rejected. ### 3.2 Byte Order Mark (BOM) The UTF-8 BOM `EF BB BF` is only legal **at byte offset 0** of a stream. The lexer runs a dedicated `first_comment_*` state machine over the first comment line to detect misplaced BOMs: - A BOM (`EF BB BF`) detected **inside the first-line comment** produces `error_invalid_byte_order_mark`. - A BOM byte (`EF`) seen **after** the first comment line (in the `first_bom` state, before the first `.` identifier) is unhandled and produces `error_unexpected_input_byte`. ```bovnar # BOM at byte offset 0: valid EF BB BF.foo = 1; # BOM bytes inside the first comment → error_invalid_byte_order_mark # comment text: # EF BB BF text # BOM bytes after the first comment line → error_unexpected_input_byte # comment line EF BB BF.foo = 1; ``` A BOM appearing **inside** any later comment or string is **valid UTF-8** and accepted (only the `first_comment_*` states guard against BOM). ### 3.3 Byte Classes | Class | Bytes | Usage | |-------|-------|-------| | Whitespace | `0x09` (HT), `0x0A` (LF), `0x0B` (VT), `0x0C` (FF), `0x0D` (CR), `0x20` (SP) | Token separators | | Control (rejected) | `0x00–0x08`, `0x0E–0x1F`, `0x7F` (DEL) | Hard errors outside strings | | Safe ASCII | `0x20–0x7E` except reserved punctuation | Identifier/symbol/reference bodies | | High bytes | `0x80–0xFF` | UTF-8 multi-byte sequences | ### 3.4 Version Directive (spec 1.1) A document **may** declare the spec version it targets with a directive on its very first line: ```bovnar #!bovnar 1.1 .x = 42; ``` **An unversioned document is spec 1.0.** A document with no directive is treated as targeting spec **1.0** — the frozen baseline grammar. The directive only ever *opts in* to a newer version; its absence is never ambiguous. Consequently, any syntax introduced after 1.0 (a future minor revision) is available only to a document that declares the matching version: a 1.1-only construct in an unversioned (i.e. 1.0) document is invalid, exactly as it would be to a 1.0 reader. `bvnr_reader_get_declared_version` returns `false` for such a document (there is nothing declared), and a consumer that wants a concrete number should default to 1.0. **Form.** `#` `!bovnar` followed by one or more spaces/tabs, a `.` decimal version, optional trailing whitespace, then end-of-line (LF/CR) or EOF. Each component is a decimal integer with no leading zero (a bare `0` excepted) and must fit in 16 bits. **Backward compatible by construction.** Lexically the directive *is* a comment (`#…`), so a spec-1.0 reader skips it transparently — declaring a version never breaks an older parser. This is the one place where a comment carries meaning; everywhere else comments remain semantically inert. **Recognition rules.** - It is recognised only as the **very first comment**, after an optional BOM and leading whitespace. A `#!bovnar …` appearing on any later line is an ordinary comment and is ignored. - `#!bovnar` not followed by whitespace (e.g. `#!bovnarish`) is an ordinary comment, not a directive. - A directive prefix followed by a malformed version (missing component, non-numeric, leading zero, or trailing junk) is `error_invalid_spec_version`. **Enforcement.** A 1.1+ reader records the declared version and exposes it (`bvnr_reader_get_declared_version`). By default a version newer than the reader supports is **accepted leniently** — recorded, but not rejected up front; the parse only fails if it later meets syntax the reader does not implement. A reader opened with `strict_version` instead rejects any unsupported version (`error_unsupported_spec_version`) at the directive. A reader supports spec `.` when `major` equals its own and `minor` is ≤ its own (`BVNR_SPEC_VERSION_*`). ```bovnar #!bovnar 1.1 # current — accepted #!bovnar 1.0 # older minor — accepted #!bovnar 1.9 # newer minor — lenient: accepted; strict: error #!bovnar 2.0 # newer major — lenient: accepted; strict: error #!bovnar 1 # malformed (no minor) — error_invalid_spec_version ``` --- ## 4. Lexical Structure ### 4.1 Whitespace & Comments Whitespace and comments are freely interleaved between all tokens. **Whitespace characters:** ``` HT (0x09), LF (0x0A), VT (0x0B), FF (0x0C), CR (0x0D), SP (0x20) ``` **Comments:** ``` # any bytes except 0x00–0x08, 0x0E–0x1F, 0x7F # terminated by LF or CR (or EOF) ``` ```bovnar # A full-line comment .foo = 42; # an inline comment ``` ### 4.2 Identifiers (Keys) Every assignment begins with `.` followed by an identifier (the key). **Syntax:** ``` id-start = A–Z | a–z | "_" | UTF-8 leader bytes 0xC3–0xF4 id-body = id-start | "+" | "-" | DIGIT | UTF-8 continuation bytes ``` **Constraints:** - At least one `id-start` character is required after `.` (`.=` is `error_empty_identifier`) - Byte `0xC2` is rejected at identifier start **and body** positions (U+0080–U+00BF are forbidden in identifiers everywhere) - The following ASCII punctuation characters are **hard errors** inside identifier bodies: `! " # $ % & ' ( ) * , . / : ; < = > ? @ [ \ ] ^ ` { | } ~` **Valid identifiers:** ```bovnar .foo = 1; .FooBar = 2; ._private = 3; .my-key = 4; # hyphen allowed .my+key = 5; # plus allowed .user_defined = 6; ``` **Invalid identifiers:** ```bovnar . = 1; # error_empty_identifier .123 = 2; # starts with digit .foo,bar = 3; # comma inside identifier – error ``` ### 4.3 String Literals **Syntax:** ``` string-literal = '"' { safe-byte | escape-seq } '"' string = string-literal { ws string-literal } # concatenation ``` **Safe bytes:** `0x09–0x0D` (HT, LF, VT, FF, CR), `0x20–0x7E` except `"` (0x22) and `\` (0x5C), and `0x80–0xFF`. `0x7F` (DEL) is rejected even inside strings. **Control bytes** `0x00–0x08`, `0x0E–0x1F`, and `0x7F` (DEL) are hard errors inside strings. The whitespace bytes HT (0x09), LF (0x0A), VT (0x0B), FF (0x0C), and CR (0x0D) are accepted as raw string content. #### Escape Sequences | Escape | Meaning | Byte | |--------|---------|------| | `\t` | Horizontal Tab | `0x09` | | `\n` | Line Feed | `0x0A` | | `\v` | Vertical Tab | `0x0B` | | `\f` | Form Feed | `0x0C` | | `\r` | Carriage Return | `0x0D` | | `\"` | Double Quote | `0x22` | | `\\` | Backslash | `0x5C` | In a **spec 1.1** document (one declaring `#!bovnar 1.1` or newer — see §3.4) two further escapes are available: | Escape | Meaning | |--------|---------| | `\xHH` | the single byte `HH` (exactly two hex digits) | | `\u{H…}` | the Unicode scalar value `U+H…` (1–6 hex digits), UTF-8 encoded | - `\u{…}` rejects surrogates (`U+D800`–`U+DFFF`) and values above `U+10FFFF` with `error_invalid_codepoint`; a missing/empty/over-long brace group or a non-hex digit is `error_illegal_escape_sequence`. - `\x` writes a raw byte, but a string's contents must still be valid UTF-8 (the `utf8` family guarantee). So `"\xC3\xA9"` is `"é"`, whereas a lone `"\xFF"` is `error_invalid_utf8_byte`. Use an octet stream for arbitrary, non-textual bytes. - A `\xHH` or `\u{…}` escape that *resolves to* an ASCII control byte (`0x00`–`0x08`, `0x0E`–`0x1F`, `0x7F` — every control except the whitespace controls `0x09`–`0x0D`) is rejected with `error_unexpected_input_byte`, exactly as a raw control byte in a string is: the escape is no way around the control-byte rule. - **Gating.** `\x` and `\u` are 1.1-only. In a 1.0 or unversioned document (§3.4) the `x`/`u` after a backslash is not a recognised escape and yields `error_illegal_escape_sequence`, exactly as a 1.0 reader reports. Any byte other than `t`, `n`, `v`, `f`, `r`, `"`, `\` (plus `x`, `u` in 1.1) after `\` causes `error_illegal_escape_sequence`. **String concatenation:** Two or more adjacent string literals (separated only by whitespace/comments) are concatenated into a single token: ```bovnar .long = "hello " "world"; # → "hello world" ``` The combined byte length must not exceed `max_string_length` (default 65535). #### Examples ```bovnar .simple = "hello"; .with_escapes = "tab:\there\nnewline"; .unicode = "café"; # UTF-8 bytes for é = 0xC3 0xA9 .empty = ""; ``` ### 4.4 Symbols A **symbol** is an unquoted bare-word token that appears in value position. It starts with an `id-start` character and continues with `id-body` characters. **Differences from identifier keys:** | Context | `,` | `]` | `;` | `=` | |---------|-----|-----|-----|-----| | Identifier body | hard error | hard error | hard error | transitions to value | | Symbol body | terminates → new array element | terminates → close array | terminates → end value | hard error | ```bovnar .status = ok; # symbol "ok" .day = Monday; # symbol "Monday" .flags = [red, green, blue]; # symbols as array elements ``` **Reserved keywords.** Eight exact bare words are *not* symbols: `null`, `true`, `false`, `on`, `off`, `nan`, `inf`, and `ninf`. Lexically they are still symbol tokens, but the validator reserves the exact spellings and reclassifies them — `null` to a null value, `true`/`false`/`on`/`off` to a `bool` value (`on` ≡ `true`, `off` ≡ `false`), and the special floats `nan`/`inf`/`ninf` to numeric special values (§6.4). A bare boolean with no annotation synthesises a `` type (§10); an explicit `` annotation accepts only the four bool keywords. A longer word that merely begins with a keyword (`ontology`, `nullable`, `truthy`, `infinity`) remains an ordinary symbol. ```bovnar .enabled = true; # bool value (vt_bool), not a symbol .debug = off; # bool false .choice = on; # explicit; on == true .label = truthy; # symbol "truthy" — not a keyword ``` ### 4.5 References A **reference** is a dotted path to another key, introduced by `&`. **Syntax:** ``` reference = "&" ref-segment { ref-segment | index } ref-segment = "." id-start { ref-body-char } ref-body-char = same as symbol-body-char | "." index = "[" digit { digit } "]" (* spec 1.1 *) ``` The stored text includes the leading dot and all intermediate dots: ```bovnar .host = "localhost"; .port = 8080; .connection = &.host; # stored as ".host" .full = &.config.host; # stored as ".config.host" ``` **Examples:** ```bovnar .config = { .host = "example.com"; }; # nested struct .ref = &.config.host; # reference → ".config.host" ``` **Resolution.** A reference is stored **unresolved** — as the path string only. The parser and library never dereference it, so: - the target **need not exist** at parse time; a reference to a missing key (`&.nope`), a forward reference (`&.x` before `.x` is defined), or a reference to a value outside the document is accepted and stored verbatim; - **cycles are not detected** (`.a = &.b; .b = &.a;` parses), and cannot hang the library, which never follows a reference (`bvn_dom_lookup` navigates literal structure only, stopping at a reference rather than dereferencing it); - **resolution is entirely the application's responsibility**, including how to treat dangling paths and cycles. A reference path is bounded by `max_reference_length` (`error_reference_too_long` otherwise). In an array, references are homogeneous by **kind** — an array of references is uniform regardless of what its targets resolve to (their target types are unknown to the parser). **Array indexing (spec 1.1).** A reference path may address array elements with `[N]` index suffixes (`&.matrix[0][1]`). Like the rest of the path the index is captured **verbatim and unresolved** at the byte layer — the library never dereferences a reference, so `bvn_dom_lookup` on the *reference* node returns its stored path string, not the target. The index is interpreted only when an application itself resolves that path string against the tree by calling `bvn_dom_lookup(doc, ".matrix[0][1]")` directly (this is the same path-walker the CLI `query` command exposes — `bovnar query .matrix[0][1]` → the element). The index syntax is a 1.1 feature: in a 1.0/unversioned document a `[` in a reference is `error_unexpected_input_byte`, exactly as a 1.0 reader reports. Resolution semantics follow the array model (§7): a flat `/`-row matrix (`[10,20,30]/[40,50,60]`) is addressed as `[row][col]` — `&.matrix[0][1]` → `20` — and a 1-D array as `[i]`; genuine nested arrays (`[[1,2],[3,4]]`) descend one index per level. A partial index of a flat matrix (`&.matrix[0]`), an out-of-range index, or indexing a non-array does not resolve (the application sees no node). ```bovnar #!bovnar 1.1 .matrix = [10, 20, 30]/[40, 50, 60]; .row0c1 = &.matrix[0][1]; # resolves to 20 ``` ### 4.6 Numbers #### Bare Number Literals ``` number = ["-"] ( int-led | dot-led ) [ dec-exponent ] int-led = DIGIT { DIGIT } [ "." { DIGIT } ] dot-led = "." DIGIT { DIGIT } dec-exponent = ("e" | "E") [ "+" | "-" ] DIGIT { DIGIT } ``` - Only `e`/`E` accepted as exponent marker in bare literals (base-16 float values in quoted string literals use `p`/`P` — see §6.3) - Leading zeros are valid (`007` is accepted) - A trailing dot without fractional digits is valid (`123.`) - `.` alone is a hard error ```bovnar .i = 42; .neg = -17; .float = 3.14; .trailing = 123.; .dot_led = .5; .sci = 1e6; .neg_sci = -2.5e-3; ``` #### Special Number Literals ``` special-number = "nan" | "inf" | "ninf" ``` The special IEEE-754 values are **bare reserved keywords** — `nan`, `inf`, and `ninf` (negative infinity) — with no sigil. Like `null`/`true`/`false`/`on`/`off`, they are lexed as symbols and reclassified to numeric special values by the validator; a bare word that is not one of these exact spellings (e.g. `infinity`, `nans`) stays an ordinary symbol. The stored token text is the keyword itself: `nan`, `inf`, `ninf`. A special-number keyword takes **no inline unit suffix** — supply a unit through the type annotation instead (` inf`). ```bovnar .not_a_number = nan; .infinite = inf; .neg_infinite = ninf; ``` ### 4.7 Null Values A null value is the **absence** of a raw-value token, or the reserved keyword `null`. It occurs when: - At assignment level: `.key = ;` (nothing between `=` and `;`), or `.key = null;` - In array context: leading/trailing commas, or consecutive commas: `[,1,,2,]`; a bare `null` element is equivalent ```bovnar .null = ; # null value .also = null; # the null keyword — identical to the empty slot .items = [,1, ,2,]; # null, 1, null, 2, null ``` When a type annotation precedes a null value, the null carries the annotated type: ```bovnar .null_typed = ; .nulls_in_array = [, , ]; ``` --- ## 5. Type Annotations ### 5.1 Syntax ``` type-annotation = "<" ws type-spec ws ">" ``` The type annotation **must** be placed in one of four positions: 1. Immediately after the `=` sign of an assignment, before the value: `.key = 42;` 2. Before the opening `[` of an array — a **whole-array annotation** that is inherited by every element that does not carry its own annotation: `.ports = [80, 443, 8080];` 3. After the opening `[` of an array, before the first element. 4. After a `,` inside an array, before the next element. In all cases the annotation comes **before** the value it describes. **Correct placement:** ```bovnar .an_int = 42; .a_float = 3.14; .arr = [ 1, -2]; # Whole-array annotation — applies to all elements that lack their own annotation .ports = [80, 443, 8080]; # Per-element annotations override the whole-array annotation .mixed = [1, -1, 255]; ``` **Incorrect placement (hard error):** ```bovnar .key = 42; # ERROR: type annotation must follow '=', not the key ``` Seven type families are recognized in spec 1.0, plus `datetime` in spec 1.1: | Family | Keyword | Parameter syntax | Default Width | |--------|---------|-----------------|---------------| | Unsigned integer | `uint` | `:width,_base,unit` | 64 | | Signed integer | `sint` | `:width,_base,unit` | 64 | | Binary floating-point | `float` | `:width,_base,unit` (base `_10` or `_16` only) | 64 | | Fixed-point (Q-format) | `float_fix` | `:width,qN,unit` | 64 | | Decimal floating-point (IEEE 754-2008) | `float_dec` | `:width,unit` | 64 | | UTF-8 string | `utf8` | none (any parameter → `error_illegal_value_type`) | — | | Boolean | `bool` | none (any parameter → `error_illegal_value_type`) | — | | Timestamp (spec 1.1) | `datetime` | `:width,epoch` (no numeric base/unit) | 64 | **The `datetime` family (spec 1.1).** A `datetime` value is a **signed integer count of seconds** since a named epoch — a timestamp, as distinct from a *duration* (which is just a number with a time unit, e.g. ``). The carrier is validated exactly like `sint` (signed, decimal, range per width; negative values denote instants before the epoch). Its one family-specific parameter is the **epoch name**, one of: `unix` (default), `tai`, `gps`, `mjd`, `ntp`, `galileo`, `glonass`, `y2000`, `beidou`. A numeric base, `q`, or physical unit parameter is `error_illegal_value_type`; a fractional or exponent *numeric carrier* (e.g. `1.5` or `1e3` — as opposed to the sub-second fraction of an ISO-8601 literal, covered below) is `error_type_value_mismatch`. As a 1.1 feature it requires a `#!bovnar 1.1` declaration (§3.4) — in a 1.0/unversioned document `datetime` is `error_illegal_value_type`. Recover the civil date/time with the `bvn_datetime.h` helpers (`bvnr_datetime_epoch_mjd()` → the epoch, then `bvn_dt_epoch_seconds_to_datetime()`). ```bovnar #!bovnar 1.1 .created = 1750000000; # 2025-06-15T...Z .tai_t = 1400000000; .before = -100; # 100 s before 1970-01-01Z ``` **ISO-8601 literals (spec 1.1).** Instead of a raw integer, a `datetime` value may be written as an ISO-8601 literal: ``` datetime-literal = YYYY-MM-DD [ "T" HH:MM:SS [ "." fraction ] [ zone ] ] zone = "Z" | ( "+" | "-" ) HH:MM ``` * `YYYY-MM-DD` — a calendar date (interpreted at `00:00:00Z`) * `YYYY-MM-DDTHH:MM:SS` — a date and time (UTC when no zone is given) * a trailing `Z` (UTC) or a numeric `±HH:MM` time-zone offset * an optional `.fraction` (one or more digits) after the seconds The literal is converted at parse time to the integer epoch-seconds carrier; that integer is what is stored and re-emitted, so a pretty-print round-trip is idempotent (`2026-06-15` becomes ` 1781481600`). A **bare** literal with no annotation infers ``, so `.t = 2026-06-15;` is a timestamp without any annotation. Fields are strictly validated (month `01`–`12`, a valid day-of-month, hour `00`–`23`, minute `00`–`59`, second `00`–`60`, offset `±HH:MM` with two-digit components); a malformed or out-of-range literal is `error_invalid_datetime_literal`. A second of `60` is a **UTC leap second** and is accepted. What it stores depends on the epoch: * On the **civil epochs** (`unix`, `mjd`, `ntp`, `y2000`) it normalises onto the following second — `2016-12-31T23:59:60Z` and `2017-01-01T00:00:00Z` store the same `unix` value. Those scales run a uniform 86 400 s day and have no second to spend on the insertion; collapsing is the correct POSIX reading. * On **`tai`** it does not. TAI is a continuous atomic count, the inserted second is a real instant on it, and the two literals above store `1861920036` and `1861920037` respectively. This makes UTC⇄TAI **injective**: every TAI second has exactly one civil spelling and every civil spelling one TAI second, so a `tai` value survives any number of write/read cycles unchanged. A `:60` at an instant the implementation's leap-second table does not record as an insertion collapses on `tai` too. The table is a static snapshot, and a build that predates an IERS announcement must not reject a document spelling a genuine future leap second. A **`±HH:MM` offset** shifts the written civil time to true UTC before the conversion (`12:00:00+02:00` is `10:00:00Z`); for `tai` the offset is applied *before* the leap-second lookup, so the atomic value stays correct. The `.`, `Z`, and `±HH:MM` parts are valid only after a full `HH:MM:SS` time. **Fractional seconds** (`.` then one or more digits — ISO 8601 sets no upper bound on the digit count) are accepted. The integer carrier is still **whole seconds**, so the fraction takes no part in the value's arithmetic or comparison (` 1781524800` and a literal flooring to that second compare equal at the carrier). But the verbatim digits are **preserved**: they are surfaced to consumers as a string (the streaming `bvnr_data_t.frac_data` / `.frac_length`, or `bvn_dom_get_datetime_fraction()` on a DOM node) and are re-emitted so the value **round-trips**. A datetime that carries a fraction is pretty-printed back as an ISO literal — `2026-06-15T12:00:00.5Z` canonicalises to ` 2026-06-15T12:00:00.5Z` (always normalised to UTC `Z`, with the annotation made explicit) and re-printing that is idempotent; a datetime written as a bare integer carrier still canonicalises to the integer. The fraction is informational only — for sub-second values that participate in computation, use a finer integer carrier (e.g. milliseconds since the epoch). The UTC→epoch conversion is **leap-second correct**: the civil epochs (`unix`, `mjd`, `ntp`, `y2000`) use the uniform 86 400 s/day scale (so `unix` is ordinary POSIX time) and `tai` applies the IERS leap-second table. The atomic GNSS epochs (`gps`, `galileo`, `glonass`, `beidou`) reject a literal with `error_datetime_literal_unsupported_epoch` — those scales have no round-trippable civil⇄seconds inverse in this implementation, so supply an integer epoch-seconds carrier for them. A literal carries no unit, and an ISO literal under a non-`datetime` annotation is `error_type_value_mismatch`. ```bovnar #!bovnar 1.1 .a = 2026-06-15; # bare -> .b = 2026-06-15T12:00:00Z; # date-time, UTC .c = 2017-01-01T00:00:00Z; # tai: leap-second correct .f = 2016-12-31T23:59:60Z; # the insertion itself, one second earlier .d = 2026-06-15T12:00:00+02:00; # offset -> 10:00:00Z .e = 2026-06-15T12:00:00.5Z; # fraction preserved; round-trips as a literal ``` **Parameter syntax:** ``` type-spec = param-type [ ":" type-param-list ] param-type = "uint" | "sint" | "float" | "float_fix" | "float_dec" | "utf8" | "bool" | "datetime" (* datetime: spec 1.1 *) type-param-list = type-param { "," type-param } type-param = width-param # decimal digits only, e.g. 32 | base-param # "_" followed by digits, e.g. _16 # (forbidden for float_fix and float_dec) | q~param # "q" followed by digits, e.g. q8, q16 # (only valid for float_fix) | unit-param # unit string, e.g. m/s, k~g·m/s² | epoch-param # datetime epoch name, e.g. unix, tai (spec 1.1) ``` > **Lexer note on `float_fix` / `float_dec`:** The lexer keyword state machine > recognises `float` as the type-family prefix and then accumulates the remaining > annotation bytes (`_fix`, `_dec`, or the parameter separator `:` / closing `>`) > via `copy_type_byte` into `type_data`. `bvn_parse_type_annotation` then > distinguishes `float`, `float_fix`, and `float_dec` from the fully accumulated > string. ### 5.2 Parameters | Parameter | Syntax | Valid Values | Applies To | |-----------|--------|--------------|------------| | Width | `N` (decimal digits) | `0`, `16`, or any multiple of `32` up to `32768` for `float`; `0`,`16`,`32`,`64`,`128`,`256` for `float_fix`/`float_dec`; `0` to `BVN_MAX_INT_WIDTH` (32768) for `uint`/`sint` | uint, sint, float, float_fix, float_dec | | Base | `_N` (underscore + decimal digits) | `2–62` (uint/sint); `64`, `85` are **uint-only** (their Base64/Ascii85 alphabets use `+`/`-` as digits, so signed values are illegal — `error_illegal_value_type`); `float` accepts only `10` or `16`; **forbidden** for `float_fix` and `float_dec` | uint, sint, float | | Q (fractional bits) | `qN` (lowercase `q` + decimal digits) | `0 ≤ N < effective_width` | **float_fix only** | | Unit | unit-string | See [Units System](#11-units-system) — supports compound units | uint, sint, float, float_fix, float_dec | Width `0` means "default width" — `bvn_effective_width` returns `64`. For `float_fix`, the Q value (stored in `value_type_spec_t.base`) is the number of fractional bits. `bvn_effective_q` returns this value; `bvn_effective_base` always returns `10` for `float_fix` and `float_dec`. For `float_dec`, the encoding is a custom binary-storage format (not DPD/BID wire format) parameterised by width: | Width | exp\_bits | coeff\_bits | bias | max decimal digits | |-------|-----------|-------------|------|--------------------| | 16 | 6 | 9 | 24 | 2 | | 32 | 8 | 23 | 101 | 7 | | 64 | 10 | 53 | 398 | 16 | | 128 | 14 | 113 | 6176 | 34 | | 256 | 20 | 235 | 611867 | 70 | For `float_fix`, the wire representation is a signed Q-format integer stored in `width` bits; the mathematical value is `raw_integer × 2^(-Q)`. ### 5.3 Parameter Order Parameters are **identified by class** — each class is recognised by its syntactic form — and at most one of each class may appear. They can appear in any order: ```bovnar # All of these annotations are equivalent (parameter order is free): .val = 42; # — identical # — identical ``` Because parameters are class-identified rather than positional, an empty positional slot is never needed, so the parameter list is parsed strictly: a comma must introduce a real parameter, and a `:` must be followed by at least one parameter. Empty, trailing, or doubled components — ``, ``, ``, and the bare `` — are `error_illegal_value_type`. ### 5.4 Examples ```bovnar # Simple types .a = 1000; .b = -32768; .c = 3.14159; .d = "text"; # Width only .g = 255; # Base (requires quoted string for non-decimal) .h = "ff"; .i = "101010"; # Unit .j = 42; # explicitly dimensionless .k = 9.81; # meters per second (compound) .l = 1024; # kibibytes .m = 9.81; # kilograms · meters per second squared .n = 1.0; # meter-seconds (product) # Type annotation with null value .o = ; # null of type uint:32 # Fixed-point (Q-format): 16-bit, 8 fractional bits → resolution 2^-8 ≈ 0.0039 .fx1 = 3.14; # Fixed-point: 32-bit, Q16 (16 fractional bits, 15 integer + 1 sign) .fx2 = -1.5; # Fixed-point with unit .fx3 = 9.81; # Fixed-point: width defaults to 64, Q=0 means pure integer fixed-point .fx4 = 42; # Decimal float: 32-bit (7 significant decimal digits) .df1 = 3.14; # Decimal float: 64-bit (16 significant decimal digits) with unit .df2 = 101325; # Decimal float: 128-bit (34 significant decimal digits) .df3 = 1.2345678901234567890123456789012345; # float_fix and float_dec do NOT accept a base parameter: # .bad = 1.0; # ERROR: base forbidden for float_fix # .bad = 1.0; # ERROR: base forbidden for float_dec ``` ### 5.5 Non-decimal Base with Bare Numbers A non-decimal base with a bare number literal is not caught by the validator (no error is raised). In practice, a bare non-decimal value such as `ff` is parsed as a symbol token, not a number, so type/value compatibility validation will flag the mismatch instead. Use a quoted string for non-decimal values: ```bovnar # CORRECT: quoted string .n = "ff"; # hex value 255 ``` --- ## 6. Value Tokens ### 6.1 Type/Value Compatibility | Type Family | Accepts | |-------------|---------| | `vt_plain` (default) | Any value | | `vt_utf8` | String only | | `vt_bool` | Boolean keyword only (`true`/`false`/`on`/`off`) | | `vt_uint` | Number or string (digits) | | `vt_sint` | Number or string (digits, may be negative) | | `vt_float` | Number or string (may have `.`, `e`/`E`; base 16 strings use `p`/`P`) — only base 10 or 16 | | `vt_float_fix` | Number or string (may have `.`, `e`/`E`) — base 10 only | | `vt_float_dec` | Number or string (may have `.`, `e`/`E`) — base 10 only | | `vt_datetime` (spec 1.1) | Number only — a decimal signed integer (epoch seconds); `.`/`e` and string carriers are rejected | ### 6.2 Validation Rules per Numeric Type **uint (unsigned integer):** ```bovnar .valid = 255; .valid = 0; .invalid = -1; # error_value_out_of_range (negative) .invalid = 256; # error_value_out_of_range (overflow) ``` **sint (signed integer):** ```bovnar .valid = 127; .valid = -128; .invalid = 128; # error_value_out_of_range .invalid = -129; # error_value_out_of_range ``` **float (binary floating-point):** Valid widths: `0` (default 64), `16`, or any multiple of `32` up to `32768`. Base `10` (default) or `16` are accepted; all other bases are rejected. ```bovnar .valid = 3.14; .valid = 1e100; .valid = nan; .valid = 3.14; # half-precision .valid = 3.14; # 256-bit extended precision .invalid = 3.14; # width 8 → error_illegal_value_type .invalid = 3.14; # not 16 or a multiple of 32 → error_illegal_value_type .invalid = 3.14; # base 2 → error_illegal_value_type ``` **float_fix (fixed-point Q-format):** Valid widths: `0` (default 64), `16`, `32`, `64`, `128`, `256`. The Q parameter (`qN`) specifies fractional bits; `0 ≤ N < effective_width`. Base parameter (`_N`) is forbidden. The mathematical value of a fixed-point datum is `raw_integer × 2^(-Q)`. **Range.** A `float_fix` value must lie within the declared format's signed range — `raw_integer = round(value × 2^Q)` must fit a signed `width`-bit field, i.e. `value ∈ [-2^(width-1-Q), 2^(width-1-Q) − 2^(-Q)]`. A value outside that range is `error_value_out_of_range`, exactly as for `uint`/`sint` overflow (special numbers `nan`/`inf`/`ninf` are range-exempt, §6.4). The C encoders `bvn_float_to_fixNN` additionally **saturate** to the representable extreme on overflow rather than wrapping, so a fixed-point datum can never silently decode to an unrelated value. ```bovnar .valid = 3.14; # Q8 in 16-bit: range [-128, 127.99609375] .valid = 127.99609375; # max in range .valid = -128; # min in range .valid = -1.5; # Q16 in 32-bit .valid = 42; # Q0 = pure integer, no fractional part .valid = 9.81; .invalid = 128; # exceeds Q8/16-bit range → error_value_out_of_range .invalid = 70000; # exceeds signed-16-bit range → error_value_out_of_range .invalid = 1.0; # Q >= width → error_illegal_value_type .invalid = 1.0; # width 8 not in {0,16,32,64,128,256} .invalid = 1.0; # base param forbidden → error_illegal_value_type ``` **float_dec (IEEE 754-2008 decimal floating-point):** Valid widths: `0` (default 64), `16`, `32`, `64`, `128`, `256`. Base parameter (`_N`) is forbidden (decimal base is implicit). Values are written as ordinary decimal literals or special numbers. ```bovnar .valid = 3.14; .valid = 101325; .valid = nan; .invalid = 1.0; # width 8 not valid → error_illegal_value_type .invalid = 1.0; # base param forbidden → error_illegal_value_type ``` ### 6.3 Digit Validation Digits in values are checked against the declared base: ```bovnar .value = "ff"; # OK: f and f are valid in base 16 .value = "fg"; # error_digit_not_in_base: 'g' > base 16 .value = "1010"; # OK: valid binary .value = "210"; # error_digit_not_in_base ``` #### Exponent Markers in Quoted String Literals Bare number literals always use `e`/`E` as the exponent separator (§4.6). Quoted string number literals follow the same rule **except** for base-16 (`_16`) float values, where `p`/`P` must be used instead of `e`/`E`. This is required because `e` and `E` are valid hexadecimal digits: with base 16 you must write `"1.8p+2"`, not `"1.8e+2"` — the `e` would be read as a mantissa digit rather than an exponent marker, leaving the trailing `+2` invalid. Float values support a decimal base (the default) and base-16 (`_16`); the `p`/`P` exponent separator applies to the base-16 form. ```bovnar .hex_float = "1.8p+2"; # OK: 1.8₁₆ × 2² = 6.0 (p = binary exponent) .hex_mant = "1.8e"; # OK: 'e' is a hex digit → mantissa 1.8e₁₆ .dec_float = 12.0; # OK: default decimal base ``` The `p`/`P` exponent value is always interpreted as a decimal integer (the binary exponent bias), matching the C99 hexadecimal floating-point literal convention. ### 6.4 Special Number Semantics `nan`, `inf`, `ninf` are accepted by any numeric type family (`uint`, `sint`, `float`, `float_fix`, `float_dec`) and in untyped context. `bvn_check_acc_range` explicitly returns `true` when `bvn_is_special_number_string` matches, bypassing all range validation regardless of the declared family. They are rejected under the non-numeric families whose token kind is incompatible with `token_is_number` — `utf8` (string-only) and `bool` (boolean-keyword-only) — both producing `error_type_value_mismatch`. (A `datetime` annotation, by contrast, accepts them: the special-string check short-circuits before the datetime carrier is parsed.) ```bovnar .okay_f64 = inf; .okay_f32 = nan; .okay_u8 = nan; # accepted — range check bypassed .okay_s16 = ninf; # accepted — range check bypassed # Fine in plain/untyped context too .untyped = inf; # defaults to float:64 ``` ### 6.5 Inline Unit Suffix A **scalar** number or string value may carry an optional unit suffix separated from the literal by **at least one whitespace character**. The suffix uses the same character set as the unit parameter inside a type annotation (`unit-param`). | Separator form | Example | Valid? | |---|---|---| | Space | `.a = 9.81 m/s;` | ✓ | | No separator | `.b = 9.81m;` | ✗ error | ```bovnar .distance = 100 m; # plain integer with inline unit: meter .speed = 9.81 m/s; # plain float with inline compound unit .mass = 70.0 k~g; # with SI prefix .storage = 4 Gi~B; # with IEC prefix .ratio = 3.14 no_unit; # explicitly dimensionless via inline suffix ``` The inline unit suffix is **forbidden inside arrays**; any character that would begin an inline unit — a letter, `_`, `$`, `%`, `(`, or a UTF-8 lead byte — following a value inside `[ … ]` is a lexical error (`error_unexpected_input_byte`). **Interaction with type annotations:** | Situation | Outcome | |-----------|---------| | No annotation, inline unit present | Inline unit is used as the effective unit | | Annotation has no unit, inline unit present | Inline unit is used as the effective unit | | Annotation unit and inline unit **match** | Valid; the common unit is used | | Annotation unit and inline unit **differ** | `error_unit_mismatch` | ```bovnar # Annotation unit with no inline unit — normal .dist = 1.5; # No annotation unit, inline unit — unit from suffix .dist = 1.5 m; # Both present and identical — valid, redundant but allowed .dist = 1.5 m; # Both present and different — error_unit_mismatch .dist = 1.5 s; # ERROR: annotation says m, inline says s ``` An invalid inline unit string (unrecognised base unit, bad prefix, etc.) produces `error_unit_illegal`. The suffix may appear after `no_unit` checks as with any other unit string. --- ## 7. Arrays ### 7.1 Row Syntax An array consists of one or more bracket-enclosed **rows** separated by `/`: ```bovnar .array_2d = [1, 2, 3]/[4, 5, 6]; ``` This produces the event sequence: ``` ev_array_row_start → 3× ev_data → ev_array_row_end ev_array_dim_start → ev_array_row_start → 3× ev_data → ev_array_row_end ``` ### 7.2 Null Elements Leading/trailing commas, consecutive commas, and the bare keyword `null` all produce null elements: ```bovnar .items = [,1,,2,]; # 5 elements: null, 1, null, 2, null .one = [null]; # 1 element: a single null ``` **Empty arrays.** A row with nothing between the brackets is **empty** — zero elements, no `ev_data` emitted. It is distinct from a one-null row: ```bovnar .empty = []; # 0 elements .null1 = [null]; # 1 element (null) ``` So `bvn_dom_array_count([])` is `0` and `bvn_dom_array_count([null])` is `1`. The canonical serialiser writes a null array element as the explicit `null` keyword (e.g. `[, 1]` round-trips through `[null, 1]`), so an empty slot never collides with the empty array. Empty rows interact with row-size consistency (§7.3): all `/`-rows must share one width, and `0` is a valid width — `[]/[]` is two empty rows, but `[]/[1]` is `error_array_row_size_mismatch`. ### 7.3 Row-Size Consistency A single array's `/`-separated dimension rows (e.g. `[1,2,3]/[4,5,6]`) must all have the same element count. A mismatch produces `error_array_row_size_mismatch`; the lexer performs this check as each row closes, so an offending row is rejected at the earliest possible byte — the element that overshoots, or the `]` that falls short. This makes one `/`-array a clean rectangular N-dimensional block. Comma-separated elements are distinct *values*, but since spec 1.0 they are no longer unconstrained: they must be **homogeneous** (§7.4). ### 7.4 Element Homogeneity Since spec 1.0 the elements of an array must be **homogeneous**. The rule is *"shape uniform, fields free"*, checked over the materialised value (above the lexer; it complements the streaming reader's per-value type and unit checks): - **Kind.** Every non-null element shares the same kind — number, string, symbol, bool, reference, octet stream, array, or struct. `[1, "two"]` and `[1, {.x=1;}]` are `error_array_element_type_mismatch`. - **Dimension** (bare scalar arrays and matrices). Numeric elements must share the same physical dimension; the numeric encodings (`uint`, `sint`, `float`, `float_fix`, `float_dec`) may mix. `[ 1.0, 2.0]` (length vs mass) is rejected; `[1, 2.5, 3]` (all dimensionless) is fine. Each currency is its own dimension, so a bare array may not mix `$USD` and `$EUR` values. - **`datetime` (spec 1.1).** A `datetime` is its own kind: it does **not** mix with the plain numeric encodings above (`[ 1, 2]` is `error_array_element_type_mismatch`). And, exactly as for currencies, its **epoch is a dimension** — a bare array may not mix epochs (`[ 1, 2]` is rejected); a homogeneous same-epoch datetime array is fine. (These are materialised-document/DOM-tier rules, like the rest of §7.4.) - **Rectangular** (nested arrays). Sibling sub-arrays must have the same length and recursively-matching element shape: `[[1,2],[3,4]]` is valid, `[[1,2],[3,4,5]]` is `error_array_row_size_mismatch`. - **Structs — same keys, fields free.** Sibling structs must share the same keys, in order, with the same per-field *kinds* and nesting. Differing keys are `error_struct_shape_mismatch`; a field that is a number in one record and a string in another is `error_array_element_type_mismatch`. But a scalar field may carry a **different unit** in each record, and a list field a **different length** — so a multi-currency ledger and per-record argument lists are valid. - **Null is a hole.** `null` / empty array elements match any shape and never establish or break homogeneity, so sparse arrays like `[1, , 3]` remain valid. ```bovnar .ok_scalars = [1, 2.5, 3]; # valid: same (no) dimension, encodings may mix .ok_matrix = [[1, 2], [3, 4]]; # valid: rectangular .ok_records = [{.cur = USD; .bal = 1.0;}, {.cur = EUR; .bal = 2.0;}]; # valid: same keys, fields free .ok_sparse = [1, , 3]; # valid: null hole .bad_kind = [1, "two"]; # error_array_element_type_mismatch .bad_dim = [ 1.0, 2.0]; # error_array_element_type_mismatch .bad_ragged = [[1, 2], [3, 4, 5]]; # error_array_row_size_mismatch .bad_keys = [{.x = 1;}, {.y = 1;}]; # error_struct_shape_mismatch ``` A bare array of measurements is therefore uniform — a consumer may treat its elements identically — while records (structs) describe genuinely different things. **Heterogeneous data is modelled with a struct, not an array.** (This is a deliberate tightening over pre-1.0 drafts, which allowed ragged and mixed-type arrays; it also means a ragged or mixed-type JSON array has no bovnar representation and the `json → bvnr` converter rejects it rather than losing structure.) ### 7.5 Array Elements with Type Annotations Individual elements may carry type annotations. The annotation appears **before** each element value, **after** the comma or opening bracket: ```bovnar .mixed = [ 1, -1, 3.14]; .nulls = [ , ]; # null, null with types ``` ### 7.6 Constraints | Constraint | Limit | |------------|-------| | Array nesting depth | `max_array_nesting` — default 64, hard cap 255 | | Total array items | `max_array_items` (configurable; 0 → 2 147 483 647 internal default) | --- ## 8. Structs (Scopes) ### 8.1 Syntax ``` struct = "{" ws { assignment } ws "}" ``` Structs group related assignments into a nested scope: ```bovnar .person = { .name = "Alice"; .age = 30; .address = { .street = "123 Main St"; .city = "Springfield"; }; }; ``` **Key uniqueness.** Keys must be unique within a single scope — a struct, and the top-level document. A repeated key is `error_duplicate_struct_key`, so lookup, references, and iteration always agree on the value of a key: ```bovnar .bad = {.x = 1; .x = 2;}; # error_duplicate_struct_key .ok = {.x = 1; .y = 2;}; # fine ``` The same key in *different* scopes is unrelated and always allowed (e.g. `.a = {.x = 1;}; .b = {.x = 2;};`). The rule is enforced over the materialised document, alongside array homogeneity (§7.4). ### 8.2 Nesting Structs can be nested up to `max_struct_nesting` levels — default 64, hard cap 255 (the limit field is a `uint8_t`; setting it to 0 selects the default of 64). Exceeding the configured limit produces `error_struct_nesting_too_high`. ### 8.3 Empty Structs ```bovnar .empty = {}; ``` ### 8.4 Structs as Array Elements ```bovnar .people = [ {.name = "Alice"; .age = 30;}, {.name = "Bob"; .age = 25;} ]; ``` ### 8.5 Unmatched Braces A `}` seen with `struct_nesting_level == 0` is `error_illegal_struct_close`. --- ## 9. Octet Streams (Binary Mode) ### 9.1 Overview A NUL byte (`0x00`) where a value is expected switches from text mode to binary chunk mode. The parallel UTF-8 validator is suspended for the duration. ### 9.2 Wire Protocol ``` octet-stream = 0x00 { os-chunk } 0x00 os-chunk = 0x01 os-length os-data os-length = uint16 (little-endian); 0x0000 encodes 65536 bytes os-data = exactly os-length bytes ``` | Byte | Meaning | |------|---------| | `0x00` | End-of-stream marker (at binary level) | | `0x01` | Data chunk follows | | `0x01` + `LL LL` + `data` | Chunk of `L` bytes (0x0000 = 65536) | | Any other tag byte | `error_octet_stream_out_of_sync` | ### 9.3 Events ``` ev_octet_stream_start → (emitted on the leading 0x00) ev_data → (emitted for each binary chunk) ev_octet_stream_end → (emitted on the trailing 0x00) ``` ### 9.4 Example ```bovnar .data = ; .binary = \x00\x01\x05\x00hello\x01\x03\x00bye\x00; ``` This binary region encodes two chunks: `"hello"` (5 bytes) and `"bye"` (3 bytes), producing: ``` ev_octet_stream_start ev_data (type=octet_stream, data="hello", length=5) ev_data (type=octet_stream, data="bye", length=3) ev_octet_stream_end ``` ### 9.5 Constraints - File size: `max_file_size` (0 → **unlimited / endless**, the default — no accumulated size limit; set to `16777216` for the recommended 16 MiB cap) - Octet stream bytes contribute to the file size limit but not to `max_text_bytes` - EOF inside an octet stream region preserves the current error code instead of overwriting with `error_got_incomplete_bvnr_stream` --- ## 10. Default Type Synthesis When a number or string value carries **no explicit** type annotation (i.e. the validator's `value_type` is still `vt_plain`), the validator **synthesises** a default type annotation before emitting `ev_data`. ### 10.1 Rules | Value Form | Synthesised Type | |------------|-----------------| | Quoted string | `` | | Boolean keyword (`true`/`false`/`on`/`off`) | `` | | Special number (`nan`, `inf`, `ninf`) | `` | | Number with `.` or `e`/`E` (float literal) | `` | | Negative integer | `` | | Plain integer | `` | | Number with an inline currency unit (`$XXX`) | `` | | ISO-8601 datetime literal, no annotation (spec 1.1) | `` | The currency row takes **precedence** over the float/integer rows: any number (integer or decimal) that carries an inline currency unit synthesises `float_dec`, so money is never stored as a binary float — `.price = 5 $USD;` becomes ``, not ``. > **A bare integer inside a `datetime` array** inherits the array's `datetime` > type (width and epoch) rather than synthesising `uint`/`sint`, so a datetime > array's canonical form — annotation on the first element, bare integers after > — stays homogeneous. > **`float_fix` is never auto-synthesised**, because it requires a Q parameter > that cannot be inferred from the value literal; it must be introduced by an > explicit type annotation. **`float_dec` is auto-synthesised in exactly one > case** — a number carrying an inline currency unit (the row above) — so that > monetary values are never held as a binary float; in every other context it > too requires an explicit annotation. ### 10.2 Event Sequence The synthesised annotation produces the **same** event sequence as an explicit one. For numeric types (`uint`, `sint`, `float`) the sequence includes three parameter events; for `utf8` no parameter events are emitted: ``` # numeric (uint / sint / float) ev_type_annotation_start ev_type_annotation_type_family → "uint" / "sint" / "float" ev_type_annotation_type_family_parameter → width:64 ev_type_annotation_type_family_parameter → base:_10 ev_type_annotation_type_family_parameter → unit:no_unit ev_type_annotation_end ev_data # string ev_type_annotation_start ev_type_annotation_type_family → "utf8" ev_type_annotation_end ev_data ``` ### 10.3 Examples ```bovnar # No annotation → synthesised .x = 42; # No annotation → synthesised .y = 3.14; .z = inf; # No annotation → synthesised .w = -7; # No annotation → synthesised .s = "hello"; ``` --- ## 11. Units System ### 11.1 Base Units The unit system supports **163 named base units** across SI, IEC-binary, Imperial/US customary, CGS electromagnetic, radiation, electrical-power, and other categories. The table below covers the SI base units, all 22 named SI-derived units (degree Celsius among them — it is listed under *Non-SI units accepted for use with SI* below, where it is conventionally grouped), and the other non-SI units accepted for use with SI. For the complete reference — including Imperial/US customary, CGS, radiation, electrical-power, rotational, textile, surveying, volume, and other unit families — see **[`doc/2_bovnar_unit_system.md`](2_bovnar_unit_system.md)**. **SI base units and digital units** | Symbol | Unit | Enum | |--------|------|------| | `b` | bit | `bu_bit` | | `B` | byte | `bu_byte` | | `s` | second | `bu_second` | | `m` | meter | `bu_meter` | | `g` | gram | `bu_gram` | | `A` | ampere | `bu_ampere` | | `K` | kelvin | `bu_kelvin` | | `mol` | mole | `bu_mol` | | `cd` | candela | `bu_candela` | **Named SI-derived units** | Symbol | Unit | Enum | |--------|------|------| | `Hz` | hertz | `bu_hertz` | | `N` | newton | `bu_newton` | | `Pa` | pascal | `bu_pascal` | | `J` | joule | `bu_joule` | | `W` | watt | `bu_watt` | | `V` | volt | `bu_volt` | | `Ω` | ohm | `bu_ohm` | | `F` | farad | `bu_farad` | | `C` | coulomb | `bu_coulomb` | | `S` | siemens | `bu_siemens` | | `Wb` | weber | `bu_weber` | | `T` | tesla | `bu_tesla` | | `H` | henry | `bu_henry` | | `lm` | lumen | `bu_lumen` | | `lx` | lux | `bu_lux` | | `Bq` | becquerel | `bu_becquerel` | | `Gy` | gray | `bu_gray` | | `Sv` | sievert | `bu_sievert` | | `kat` | katal | `bu_katal` | | `rad` | radian | `bu_radian` | | `sr` | steradian | `bu_steradian` | **Non-SI units accepted for use with SI** | Symbol | Unit | Enum | |--------|------|------| | `L`, `l` | liter | `bu_liter` | | `min` | minute | `bu_minute` | | `h` | hour | `bu_hour` | | `d` | day | `bu_day` | | `wk` | week | `bu_week` | | `yr` | year | `bu_year` | | `°`, `deg`, `degr`, `degree`, `degrees` | degree (angle) | `bu_degree` | | `°C`, `degC`, `degrC` | degree Celsius | `bu_celsius` | | `t` | tonne | `bu_tonne` | | `bar` | bar | `bu_bar` | | `eV` | electronvolt | `bu_electronvolt` | | `Da`, `dalton`, `amu`, `u` | dalton | `bu_dalton` | | `au` | astronomical unit | `bu_astronomical_unit` | | `ha` | hectare | `bu_hectare` | > For Imperial/US customary length (`in`, `ft`, `yd`, `mi`, `nmi`, `Å`, `ly`, `pc`, `fur`, `fath`, `ch`, `rd`, `thou`/`mil`), mass (`lb`, `oz`, `gr`, `st`, `tn_sh`, `tn_l`, `oz_t`, `ct`, `slug`, `dr`, `dwt`), temperature (`°F`, `Ra`), pressure (`atm`, `mmHg`, `Torr`, `psi`, `inHg`, `at`), energy (`cal`, `Btu`, `erg`, `thm`, `ft_lb`), power (`hp`, `PS`/`CV`), force (`lbf`, `dyn`, `kip`, `kgf`), speed/frequency/rotation (`kn`, `rpm`, `rev`), volume (US and UK gallons, pints, fluid ounces, and many more), area (`ac`, `barn`), angle (`arcmin`, `arcsec`, `grad`), CGS (`P`, `St`, `G`, `Mx`, `Oe`, `sb`, `ph`, `Gal`), radiation (`Ci`, `R`, `rem`), logarithmic (`Np`, `dB`), electrical power (`var`, `VA`), acceleration (`gn`), time (`mo`, `fn`), textile linear density (`tex`, `den`), and apothecary/dry volume (`fl_dr`, `minim`, `pk`, `bsh`) — see the [Unit System Reference](2_bovnar_unit_system.md). ### 11.2 SI Prefixes | Prefix | Symbol | Factor | Enum | |--------|--------|--------|------| | quetta | `Q` | 10³⁰ | `si_quetta` | | ronna | `R` | 10²⁷ | `si_ronna` | | yotta | `Y` | 10²⁴ | `si_yotta` | | zetta | `Z` | 10²¹ | `si_zetta` | | exa | `E` | 10¹⁸ | `si_exa` | | peta | `P` | 10¹⁵ | `si_peta` | | tera | `T` | 10¹² | `si_tera` | | giga | `G` | 10⁹ | `si_giga` | | mega | `M` | 10⁶ | `si_mega` | | kilo | `k` | 10³ | `si_kilo` | | hecto | `h` | 10² | `si_hecto` | | deca | `da` | 10¹ | `si_deca` | | deci | `d` | 10⁻¹ | `si_deci` | | centi | `c` | 10⁻² | `si_centi` | | milli | `m` | 10⁻³ | `si_milli` | | micro | `µ` (or `u`) | 10⁻⁶ | `si_micro` | | nano | `n` | 10⁻⁹ | `si_nano` | | pico | `p` | 10⁻¹² | `si_pico` | | femto | `f` | 10⁻¹⁵ | `si_femto` | | atto | `a` | 10⁻¹⁸ | `si_atto` | | zepto | `z` | 10⁻²¹ | `si_zepto` | | yocto | `y` | 10⁻²⁴ | `si_yocto` | | ronto | `r` | 10⁻²⁷ | `si_ronto` | | quecto | `q` | 10⁻³⁰ | `si_quecto` | > **Note:** `µ` = U+00B5 (MICRO SIGN), encoded as `0xC2 0xB5` in UTF-8. ASCII `u` is accepted as an input-only alias for the micro prefix (`u~m` = `µ~m`); `bvn_unit_to_string` and the `bvnr_write_*` helpers always render it as `µ`, though the pretty-print / canonicalize path preserves the input `u` at default `unit_flags`. ### 11.3 IEC Binary Prefixes | Prefix | Symbol | Factor | Enum | |--------|--------|--------|------| | kibi | `Ki` | 2¹⁰ | `iec_kibi` | | mebi | `Mi` | 2²⁰ | `iec_mebi` | | gibi | `Gi` | 2³⁰ | `iec_gibi` | | tebi | `Ti` | 2⁴⁰ | `iec_tebi` | | pebi | `Pi` | 2⁵⁰ | `iec_pebi` | | exbi | `Ei` | 2⁶⁰ | `iec_exbi` | | zebi | `Zi` | 2⁷⁰ | `iec_zebi` | | yobi | `Yi` | 2⁸⁰ | `iec_yobi` | | robi † | `Ri` | 2⁹⁰ | `iec_robi` | | quebi † | `Qi` | 2¹⁰⁰ | `iec_quebi` | † `Ri`/`Qi` are a forward-looking extension: IEC 80000-13 stops at yobi (`Yi`, 2⁸⁰). ### 11.4 Unit Notation The unit system supports **compound units** composed of multiple base-unit terms combined with product and division separators. ``` compound-unit = "no_unit" | unit-expr unit-expr = unit-factor { unit-sep unit-factor } unit-factor = unit-component | "(" unit-expr ")" unit-sep = "*" | "/" | "·" (* "·" = U+00B7 MIDDLE DOT *) unit-component = [ prefix "~" ] base-unit [ unit-exponent ] ``` **Separators:** | Separator | Code Point | Meaning | |-----------|-----------|---------| | `*` | U+002A | Product (multiplication) | | `·` | U+00B7 | Product (multiplication) — visually preferred | | `/` | U+002F | Division — subsequent components are in the denominator | The `·` (middle dot, U+00B7, encoded as `0xC2 0xB7`) and `*` (asterisk) are semantically equivalent; both indicate multiplication of the adjacent unit components. The `/` separator divides the preceding components by the following ones. The first `/` switches all subsequent components into the denominator; additional `/` separators do not toggle back to the numerator. Every component after the first `/` is always in the denominator. **Parenthesised grouping.** A `(…)` group is a sub-expression evaluated independently; like any factor it obeys the latching denominator, so a `/` before a group negates the group's net exponents as a whole. Thus `k~g/(m·s²)` parses to `kg·m⁻¹·s⁻²` (identical to `k~g/m·s²`), while `(k~g/m)·s²` is `kg·m⁻¹·s²`. An explicit separator is required before a group (`m·(s)`, not `m(s)`); a group is not followed by its own exponent (`(m·s)²` is rejected); parentheses must balance and nest no deeper than 16. The `bvnr_write_*` helpers and `bvn_unit_to_string`, which build the unit from a parsed `value_unit`, emit the canonical, parenless form (`k~g/(m·s²)` → `k~g/m·s²`); the reader-driven pretty-print / canonicalize path preserves the source spelling verbatim unless a unit flag such as `BVN_UNIT_REDUCE` is set. **Within each `unit-component`:** - When a prefix is present, the separator `~` between the prefix and the base unit is **mandatory**. - A bare base unit with no prefix requires no separator. ```bovnar # Simple (single-component) units — same as before .time = 2.5; # seconds .speed = 1.5; # kilometers (kilo-meter) # Compound units .velocity = 9.81; # meters per second .accel = 9.81; # meters per second squared .force = 9.81; # kilogram-meters per second squared .energy = 1000; # kilogram-square-meters per second squared .moment = 1.0; # meter-seconds .area_density = 5.0; # kilograms per square meter .three_term = 9.81; # equivalent to k~g·m/s² .pressure = 101325; # grouped denominator (= k~g/m·s²) # Explicitly dimensionless .no_unit_float = 3.14; ``` ### 11.5 Unit Exponents Exponents can be written in two forms: | Form | Example | Meaning | |------|---------|---------| | Unicode superscript | `m²`, `m⁻³` | Using U+00B2/00B3 etc. | | ASCII caret | `m^2`, `m^-3`, `m^+2` | Using `^[+-]?[1-9]` (single digit; `^0` is not a valid exponent) | **Superscript mapping:** | Glyph | Code Point | Exponent | |-------|-----------|----------| | `¹` | U+00B9 | 1 | | `²` | U+00B2 | 2 | | `³` | U+00B3 | 3 | | `⁴` | U+2074 | 4 | | `⁵` | U+2075 | 5 | | `⁶` | U+2076 | 6 | | `⁷` | U+2077 | 7 | | `⁸` | U+2078 | 8 | | `⁹` | U+2079 | 9 | | `⁺` | U+207A | positive sign (no-op) | | `⁻` | U+207B | negate exponent | ### 11.6 Examples ```bovnar .distance = 1.5; # kilometers .mass = 500; # grams .velocity = 9.81; # meters per second .acceleration = 9.81; # meters per second squared .pressure = 101325; # pascals (= N/m² = k~g/(m·s²)) .energy = 1000; # kilojoules .storage = 2; # tebibytes .frequency = 2.4; # kilohertz .force = 9.81; # kilogram-meters per second squared .momentum = 0.5; # kilogram-meters per second .density = 7800; # kilograms per cubic meter ``` ### 11.7 Compound Unit Constraints | Constraint | Limit | |------------|-------| | Maximum components per compound unit | 8 (`BVNR_MAX_UNIT_COMPONENTS`) | If a compound unit string contains more than `BVNR_MAX_UNIT_COMPONENTS` components after parsing, the validator raises `error_unit_illegal`. Empty components between separators (e.g., `m//s`, `m*·s`) produce `error_unit_illegal`. ### 11.8 The `no_unit` Keyword The literal string `no_unit` in the unit parameter position means "explicitly dimensionless": ```bovnar .dimensionless = 42; ``` Within an explicit annotation, **omitting** the unit parameter yields the same internal representation as an explicit `no_unit`: `BVN_UNIT_NONE` with `num_components == 0`. (`bvn_parse_type_annotation` initialises the unit to `BVN_UNIT_NONE` and only overwrites it when a dimensioned unit parameter is actually present.) A **fully untyped** value (no annotation at all) instead defaults to dimensionless via default-type synthesis, producing `BVN_UNIT_NO_PREFIX(bu_none)` with `num_components == 1` and `base == bu_none`. All three forms are semantically equivalent — they compare as compatible via `bvn_units_compatible` and both encodings serialize to `"no_unit"` via `bvn_unit_to_string` — but the untyped-default form (`num_components == 1`) is a structurally distinct internal state from the annotated forms (`num_components == 0`). --- ## 12. Validation & Constraints ### 12.1 UTF-8 Validation | Constraint | Error | |------------|-------| | Non-UTF-8 byte sequence | `error_invalid_utf8_byte` | | Overlong encoding | Rejected (by UTF-8 rules) | | Surrogate halves (U+D800–U+DFFF) | Rejected | ### 12.2 Size Limits | Quantity | Configurable | Default | Overflow Error | |----------|-------------|---------|----------------| | Identifier length | Yes (`max_identifier_length`) | 255 | `error_identifier_too_long` | | String length | Yes (`max_string_length`) | 65535 | `error_string_too_long` | | Number length | Yes (`max_number_length`) | 65535 | `error_number_too_long` | | Symbol length | Yes (`max_symbol_length`) | 255 | `error_symbol_too_long` | | Reference length | Yes (`max_reference_length`) | 65535 | `error_reference_too_long` | | Array items | Yes (`max_array_items`) | 2 147 483 647 | `error_too_many_array_items` | | Text bytes | Yes (`max_text_bytes`) | 2 147 483 647 | `error_text_data_too_long` | | File size | Yes (`max_file_size`) | 0 (→ unlimited / endless) | `error_file_too_long` | | Struct nesting | Yes (`max_struct_nesting`) | 0 (→64 internal) | `error_struct_nesting_too_high` | | Array nesting | Yes (`max_array_nesting`) | 0 (→64 internal, hard cap 255) | `error_array_nesting_too_high` | Setting most fields to `0` in `bvnr_read_flags_t` substitutes an internal default — **64** for both nesting depths, and **2 147 483 647** (2³¹ − 1) for `max_array_items` and `max_text_bytes`. **`max_file_size` is the exception: `0` means unlimited / endless** (no byte-count cap accumulated), so endless streams are the default; set a positive value to cap. These defaults apply to both the reader and the writer. The writer does not internally limit array items, text bytes, or file size. ### 12.3 Value Validation | Check | Error | |-------|-------| | Number in base-N string contains out-of-base digit | `error_digit_not_in_base` | | Integer value exceeds declared width | `error_value_out_of_range` | | `float_fix` value outside the declared Q-format range (§6.2) | `error_value_out_of_range` | | Negative number with `uint` type | `error_value_out_of_range` | | Mismatched type family for value token | `error_type_value_mismatch` | | Dot or exponent in integer-typed value | `error_type_value_mismatch` | | Malformed or out-of-range ISO-8601 datetime literal (spec 1.1) | `error_invalid_datetime_literal` | | ISO-8601 literal for an atomic GNSS epoch — `gps`/`galileo`/`glonass`/`beidou` (spec 1.1) | `error_datetime_literal_unsupported_epoch` | | Invalid unit string | `error_unit_illegal` | | Empty parameter component — ``, ``, ``, `` (§5.3) | `error_illegal_value_type` | | Wholly empty annotation `<>` / `< >` (no family keyword) | `error_unexpected_input_byte` | | `utf8` (string) type given a number value | `error_type_value_mismatch` | | Non-decimal base for float type | `error_illegal_value_type` | | Base `_N` (N≠10, N≠16) for `float` | `error_illegal_value_type` | | Base param (`_N`) used with `float_fix` or `float_dec` | `error_illegal_value_type` | | Q param (`qN`) used with family other than `float_fix` | `error_illegal_value_type` | | Q value ≥ effective width for `float_fix` | `error_illegal_value_type` | | Invalid float width (not 0/16/multiple-of-32) for `float` | `error_illegal_value_type` | | Invalid float_fix/float_dec width (not 0/16/32/64/128/256) | `error_illegal_value_type` | ### 12.4 Array Validation | Check | Error | Tier | |-------|-------|------| | Element count mismatch across the `/`-dimension rows of a single array | `error_array_row_size_mismatch` | Streaming | | Ragged sibling sub-arrays — differing lengths between siblings (§7.4) | `error_array_row_size_mismatch` | DOM | | Array elements of mixed kind or dimension, or a struct field that differs in kind across record elements (§7.4) | `error_array_element_type_mismatch` | DOM | | Array nesting overflow (exceeds `max_array_nesting`) | `error_array_nesting_too_high` | Streaming | | Comma outside array context | `error_unexpected_input_byte` | Streaming | ### 12.5 Struct Validation | Check | Error | Tier | |-------|-------|------| | Unmatched `}` | `error_illegal_struct_close` | Streaming | | A key repeated within one scope (struct or top-level document) (§8.1) | `error_duplicate_struct_key` | DOM | | Struct array elements with differing key sets (§7.4) | `error_struct_shape_mismatch` | DOM | | Nesting depth exceeded | `error_struct_nesting_too_high` | Streaming | > **Validation tier.** *Streaming* checks are raised by the pull-based reader > (`bvnr_read`) during the `on_verified` event stream. *DOM* checks — the > spec-1.0 homogeneity (§7.4), struct-shape, and duplicate-key (§8.1) rules — > are enforced only when a document is **materialised** into a tree > (`bvn_dom_parse`), because they require comparing sibling elements that the > streaming reader sees one at a time. A streaming-only consumer therefore > accepts a heterogeneous array, a ragged sibling sub-array, a shape-mismatched > record set, or a duplicate key without error; a full spec-1.0 implementation > must apply these four checks (`error_array_row_size_mismatch` for ragged > siblings, `error_array_element_type_mismatch`, `error_struct_shape_mismatch`, > `error_duplicate_struct_key`) in its document/tree API. See the conformance > tool's "Validation tiers" (§3 of `doc/7_bovnar_conformance.md`). ### 12.6 Identifier Validation | Check | Error | |-------|-------| | Empty key (`.=` or `.` + non-identifier char) | `error_empty_identifier` | | Invalid character in identifier | `error_unexpected_input_byte` | ### 12.7 String Validation | Check | Error | |-------|-------| | Unknown escape sequence (incl. `\x`/`\u` outside spec 1.1) | `error_illegal_escape_sequence` | | `\u{…}` surrogate or value `> U+10FFFF` (spec 1.1) | `error_invalid_codepoint` | | `\x` byte(s) that break the string's UTF-8 validity (spec 1.1) | `error_invalid_utf8_byte` | | Control byte in string | `error_unexpected_input_byte` | | String length exceeded | `error_string_too_long` | ### 12.8 Unit Validation | Check | Error | |-------|-------| | Invalid unit string | `error_unit_illegal` | | Compound unit exceeds `BVNR_MAX_UNIT_COMPONENTS` | `error_unit_illegal` | | Empty component between separators | `error_unit_illegal` | | Unit string too long | `error_unit_too_long` | | Inline unit suffix differs from type-annotation unit | `error_unit_mismatch` | | Inline unit suffix inside an array element | `error_unexpected_input_byte` | ### 12.9 Octet Stream Validation | Check | Error | |-------|-------| | Unknown tag byte in binary mode | `error_octet_stream_out_of_sync` | | Incomplete chunk read | `error_read_complete_chunk_failed` | | EOF in binary mode | Preserves current error | --- ## 13. Error Handling & Recovery ### 13.1 Error Model Errors are reported through the `on_error` callback: ```c typedef void (*bvnr_on_error_fn)( void* userdata, error_code_t err, uint64_t line, uint64_t column, uint32_t byte, uint64_t offset); ``` ### 13.2 Recovery Mode When `bvnr_read_flags_t.continue_on_error` is `true`, the parser enters **resync mode** after any error: 1. The `on_error` callback is invoked with error details 2. A **resync state machine** (`state_t: resync, resync_string, resync_string_escape, resync_comment`) skips bytes 3. Tracking bracket `[]` and brace `{}` nesting 4. On encountering `;` at the saved nesting depth, parsing resumes 5. `recovery_count` (accessible via `bvnr_reader_get_recovery_count`) is incremented immediately when an error triggers entry into resync mode **State machine behavior during resync:** | Byte(s) | Action | |---------|--------| | `0x00–0xFF` (most) | Skip (consume and continue) | | `"` | Enter `resync_string`: skip until matching `"` | | `#` | Enter `resync_comment`: skip until newline | | `[`, `{` | Increment `resync_depth` | | `]`, `}` | Decrement `resync_depth`; if 0, emit array/struct close | | `;` at depth 0 | Reset state, resume normal parsing | ### 13.3 EOF in Resync If EOF is reached while in any resync state, `error_got_incomplete_bvnr_stream` is fired as a **second** `on_error` notification in addition to the original error that triggered resync entry. Both error codes are delivered to the caller's `on_error` callback in order: the original error first, then `error_got_incomplete_bvnr_stream` when EOF is detected. --- ## 14. Formal EBNF The complete grammar is maintained as a standalone file: **[`doc/5_bovnar.ebnf`](5_bovnar.ebnf)** The grammar uses ISO/IEC 14977:1996 notation and is derived from and verified against the reference implementation. It covers: - Top-level stream and assignment structure - Type annotations (seven core families: `uint`, `sint`, `float`, `float_fix`, `float_dec`, `utf8`, `bool`; plus `datetime` in spec 1.1) - Value forms: numbers, special numbers, booleans, strings, symbols, references, arrays, structs, octet streams, inline unit suffixes - Lexical primitives and UTF-8 byte class definitions - Unit sub-grammar (SI/IEC prefixes, base units, compound units, exponents) - Constraints not expressible in context-free EBNF (UTF-8 validity, BOM placement, nesting limits, type/value compatibility, error recovery behaviour) --- ## 15. Complete Examples ### 15.1 Simple Configuration ```bovnar # Application configuration .app_name = "Bovnar Demo"; .version = 1; .debug = false; .max_connections = 100; .timeout_s = 30; ``` ### 15.2 Typed Scientific Data ```bovnar # Physical measurements with units .measurements = [ {.name = "temperature"; .value = 23.5; .precision = 0.1;}, {.name = "pressure"; .value = 101325; .precision = 100;}, {.name = "humidity"; .value = 0.45; .precision = 0.01;}, {.name = "wind_speed"; .value = 5.2; .precision = 0.1;} ]; .calibration = 1.00042; .density = 7800; .accel = 9.81; ``` ### 15.3 Inline Unit Suffix The unit may be written directly after the value literal instead of — or redundantly alongside — the type annotation: ```bovnar # No type annotation: inline unit supplies both type default and unit .distance = 1500 m; # uint:64, no_unit → unit overridden to m .speed = 9.81 m/s; # float:64, unit = m/s .mass = 70.5 k~g; # float:64, unit = k~g # Type annotation without unit: inline suffix supplies the unit .dist = 1.5 k~m; # Annotation and inline unit match: valid (redundant) .pressure = 101325 Pa; # Annotation and inline unit differ: error_unit_mismatch # .bad = 1.5 s; # ERROR ``` ### 15.4 Binary Data with Octet Stream ```bovnar # An image file embedded as binary .image = \x00 \x01\x10\x00\xFF\xD8\xFF\xE0\x00\x10JFIF\x00\x01\x01\x00\x00\x01\x00\x01\x00\x00 \x01\x00\x00\x00\xFF\xD9 \x00; # A checksum alongside .checksum = "abcd"; ``` ### 15.5 Arrays with Mixed Dimensions ```bovnar # 2D matrix (uniform rows) .matrix = [1, 2, 3]/[4, 5, 6]; # Uniform nested arrays (all inner arrays same size) .uniform_nested = [[1,2],[3,4]]; # Multi-dimensional uniform array .cube = [[1,2],[3,4]]/[[5,6],[7,8]]; # Array with typed nulls .nullable = [ 1, , 3]; # Sibling sub-arrays must match in length (homogeneity, §7.4): .rect_nested = [[1,2],[3,4]]; # These produce error_array_row_size_mismatch: # .bad1 = [1,2,3]/[4,5]; # /-row sizes differ: 3 vs 2 # .bad2 = [[1,2]/[3,4,5]]; # inner /-array's own rows differ: 2 vs 3 # .bad3 = [[1,2],[3,4,5]]; # ragged sibling sub-arrays differ: 2 vs 3 ``` ### 15.6 Deeply Nested Struct ```bovnar .api_response = { .status = ok; .code = 200; .data = { .users = [ {.id = 1; .name = "Alice"; .roles = ["admin", "user"];}, {.id = 2; .name = "Bob"; .roles = ["user"];} ]; .pagination = { .page = 1; .per_page = 50; .total = 2; }; }; }; ``` ### 15.7 References and Symbols ```bovnar # Configuration with references .config = { .host = "api.example.com"; .port = 443; .tls = true; }; .endpoint_defaults = { .host = &.config.host; .port = &.config.port; .tls = &.config.tls; }; # Using symbols as enum-like values .status = ok; .mode = readonly; .direction = north; ``` ### 15.8 Compound Unit Examples ```bovnar # Velocity .velocity = 9.81; # Acceleration .acceleration = 9.81; # Force (Newton = kg·m/s²) .force = 9.81; # Energy (Joule = kg·m²/s²) .energy = 100; # Momentum (kg·m/s) .momentum = 5.0; # Pressure (Pa = kg/(m·s²)) .pressure = 101325; # Area density (kg/m²) .area_density = 5.0; # Electric field (V/m) .electric_field = 150; # Magnetic flux density (T = kg/(A·s²)) .mag_flux_density = 0.5; # Product form with asterisk .moment = 1.0; # Alternative superscript notation for compound units .force_alt = 9.81; ``` ### 15.9 Fixed-Point and Decimal Float Examples ```bovnar # ── float_fix: fixed-point Q-format ──────────────────────────────── # 16-bit Q8: 8 fractional bits, resolution 2^-8 ≈ 0.00390625 # range: [-128.0, +127.99609375] .adc_reading = 3.14; # 32-bit Q16: 15 integer bits + sign + 16 fractional bits .fine_angle = -1.5; # 64-bit Q0: no fractional bits — pure integer in fixed-point shell .sample_count = 4096; # 32-bit Q8 with unit (meters/second at 1/256 resolution) .velocity_fx = 9.81; # null of a fixed-point type .missing_fx = ; # inline unit suffix also works with float_fix .temperature = 23.5 °C; # ── float_dec: IEEE 754-2008 decimal floating-point ───────────────── # 32-bit decimal float (7 significant decimal digits) .price = 12.99; # 64-bit decimal float with unit (16 significant decimal digits) .pressure_dec = 101325.0; # 128-bit decimal float (34 significant decimal digits) .pi_dec = 3.14159265358979323846264338327950288; # 256-bit decimal float (70 significant decimal digits) .big_constant = 1.4142135623730950488016887242096980785696718753769480731766797; # Special values are accepted .nan_dec = nan; .inf_dec = inf; # null of a decimal float type .missing_dec = ; # ── Contrast with binary float ─────────────────────────────────────── # Binary IEEE float (existing) .val_bin = 3.14; # Decimal float — same text representation, different wire encoding .val_dec = 3.14; # Fixed-point — same text representation, Q-format wire encoding .val_fix = 3.14; ``` ### 15.10 Error Examples ```bovnar # These will produce parse errors: # Empty identifier . = 42; # error_empty_identifier # Type violation (type annotation on the identifier, not the value) .x = 42; # error (annotation must be after '=') # Correct: .x = "text"; # Type violation – value doesn't match type .x = 42; # error_type_value_mismatch # Value out of range .y = 300; # error_value_out_of_range # Negative unsigned .z = -1; # error_value_out_of_range # Unmatched struct close — a '}' at the top level (struct nesting 0) .stray_close = 1;} # error_illegal_struct_close # Unknown escape .string = "\x"; # error_illegal_escape_sequence # error_array_row_size_mismatch — /-dimension rows of one array, and (since 1.0) # ragged sibling sub-arrays, must match in length: # .bad1 = [1,2,3]/[4,5]; # /-row sizes differ: 3 vs 2 # .bad2 = [[1,2]/[3,4,5]]; # inner /-array's own rows differ: 2 vs 3 # .bad3 = [[1,2],[3,4,5]]; # ragged sibling sub-arrays differ: 2 vs 3 # Uniform /-rows and rectangular sibling sub-arrays are valid: .ok1 = [1,2,3]/[4,5,6]; # valid — both dimension rows have 3 elements .ok2 = [[1,2],[3,4]]; # valid — rectangular sub-arrays # Comma outside array .comma_outside = 42,; # error_unexpected_input_byte # Non-decimal base — bare token is parsed as a symbol, causing type mismatch .hex = ff; # error: symbol value for numeric type # Empty component in compound unit .x = 1.0; # error_unit_illegal # Too many components (> 8) .y = 1.0; # error_unit_illegal (9 components) # float_fix: Q >= effective width .bad_q = 1.0; # Q=16 >= width=16 → error_illegal_value_type # float_fix: invalid width .bad_fw = 1.0; # width 8 not in {0,16,32,64,128,256} # float_fix: base param forbidden .bad_fb = 1.0; # error_illegal_value_type # float_dec: invalid width .bad_dw = 1.0; # width 24 not in {0,16,32,64,128,256} # float_dec: base param forbidden .bad_db = 1.0; # error_illegal_value_type # q~param on non-float_fix type .bad_qu = 1.0; # q~param only valid for float_fix ``` --- ## 16. Reference API ### 16.1 Core Types ```c typedef enum bvnr_event_e { ev_stream_start, ev_assignment_start, ev_octet_stream_start, ev_octet_stream_end, ev_struct_start, ev_struct_end, ev_array_row_start, ev_array_row_end, ev_array_dim_start, ev_data, ev_type_annotation_start, ev_type_annotation_end, ev_type_annotation_type_family, ev_type_annotation_type_family_parameter, ev_stream_end } bvnr_event_t; typedef enum value_type_family_e { vt_plain, vt_utf8, vt_sint, vt_uint, vt_float, vt_float_fix, /* fixed-point binary, Q-format; Q stored in value_type_spec_t.base */ vt_float_dec, /* IEEE 754-2008 decimal floating-point */ vt_bool, /* boolean (true/false/on/off); see §4.4 and §6.1 */ vt_datetime, /* spec 1.1 — timestamp: signed epoch-seconds; see §5 */ vt_illegal } value_type_family_t; typedef struct value_type_spec_s { value_type_family_t family; uint32_t width; /* bit-width; 0 = default (64) */ uint32_t base; /* for uint/sint/float: numeral base; 0 = default (10) */ /* for float_fix: Q (fractional bits) */ /* for float_dec: unused (always 0) */ } value_type_spec_t; #define BVNR_MAX_UNIT_COMPONENTS 8 typedef struct value_unit_prefix_s { prefix_system_t system; union { si_prefix_id_t si; iec_prefix_id_t iec; } id; } value_unit_prefix_t; typedef struct value_unit_component_s { value_base_unit_t base; unit_exponent_t exponent; value_unit_prefix_t prefix; } value_unit_component_t; typedef struct value_unit_s { uint32_t num_components; value_unit_component_t components[BVNR_MAX_UNIT_COMPONENTS]; } value_unit_t; typedef enum token_type_e { token_is_identifier, token_is_string, token_is_number, token_is_symbol, token_is_reference, token_is_array_number, token_is_array_string, token_is_type, token_is_octet_stream, token_is_null_value, token_is_structure, token_is_unit, token_is_type_width, token_is_type_base, token_is_type_q, token_is_bool, token_is_unknown } token_type_t; typedef struct bvnr_data_s { token_type_t type; value_type_spec_t value_type; value_unit_t value_unit; const void* data; uint32_t length; const void* frac_data; /* spec 1.1 — ISO datetime sub-second digits, else NULL */ uint32_t frac_length; /* spec 1.1 — length of frac_data, else 0 */ bool converted; /* a want_unit read-time conversion was applied (§ read-time conversion) */ bvnr_converted_t conv; /* the exact converted value (unit + text + rational); zeroed when converted is false */ } bvnr_data_t; ``` ### 16.2 Type Construction Macros ```c /* Type-spec convenience constructors (from bovnar.h) */ #define BVN_TYPE_PLAIN ((value_type_spec_t){ .family = vt_plain, .width = 0, .base = 0 }) #define BVN_TYPE_UTF8 ((value_type_spec_t){ .family = vt_utf8, .width = 0, .base = 0 }) #define BVN_TYPE_BOOL ((value_type_spec_t){ .family = vt_bool, .width = 0, .base = 0 }) #define BVN_TYPE_UINT(w) ((value_type_spec_t){ .family = vt_uint, .width = (w) }) #define BVN_TYPE_SINT(w) ((value_type_spec_t){ .family = vt_sint, .width = (w) }) #define BVN_TYPE_FLOAT(w) ((value_type_spec_t){ .family = vt_float, .width = (w) }) /* float_fix: .base is repurposed to store Q (fractional bits). */ #define BVN_TYPE_FLOAT_FIX(w,q) ((value_type_spec_t){ .family = vt_float_fix, .width = (w), .base = (q) }) /* float_dec: base field is unused (always 0). */ #define BVN_TYPE_FLOAT_DEC(w) ((value_type_spec_t){ .family = vt_float_dec, .width = (w) }) /* With explicit numeral base (uint/sint only): */ #define BVN_TYPE_UINT_BASE(w,b) ((value_type_spec_t){ .family = vt_uint, .width = (w), .base = (b) }) #define BVN_TYPE_SINT_BASE(w,b) ((value_type_spec_t){ .family = vt_sint, .width = (w), .base = (b) }) ``` ### 16.3 Unit Macros ```c #define BVN_UNIT_NO_PREFIX(b) \ ((value_unit_t){ \ .num_components = 1, \ .components = {{ \ .base = (b), .exponent = exp_linear, \ .prefix.system = prefix_si, .prefix.id.si = si_none \ }} \ }) #define BVN_UNIT_SI(b, p) \ ((value_unit_t){ \ .num_components = 1, \ .components = {{ \ .base = (b), .exponent = exp_linear, \ .prefix.system = prefix_si, .prefix.id.si = (p) \ }} \ }) #define BVN_UNIT_IEC(b, p) \ ((value_unit_t){ \ .num_components = 1, \ .components = {{ \ .base = (b), .exponent = exp_linear, \ .prefix.system = prefix_iec, .prefix.id.iec = (p) \ }} \ }) #define BVN_UNIT_SI_EXP(b, p, e) \ ((value_unit_t){ \ .num_components = 1, \ .components = {{ \ .base = (b), .exponent = (e), \ .prefix.system = prefix_si, .prefix.id.si = (p) \ }} \ }) #define BVN_UNIT_NONE \ ((value_unit_t){ .num_components = 0 }) /* Compound-unit helper: two SI-prefixed components */ #define BVN_UNIT_COMPOUND2(b1, p1, e1, b2, p2, e2) \ ((value_unit_t){ \ .num_components = 2, \ .components = { \ { .base = (b1), .exponent = (e1), \ .prefix.system = prefix_si, .prefix.id.si = (p1) }, \ { .base = (b2), .exponent = (e2), \ .prefix.system = prefix_si, .prefix.id.si = (p2) } \ } \ }) ``` ### 16.4 Reader Setup ```c typedef struct bvnr_read_flags_s { uint16_t max_identifier_length; // default 255 uint16_t max_string_length; // default 65535 uint16_t max_number_length; // default 65535 uint16_t max_symbol_length; // default 255 uint16_t max_reference_length; // default 65535 uint64_t max_array_items; // 0 → 2 147 483 647 internal default uint64_t max_text_bytes; // 0 → 2 147 483 647 internal default uint64_t max_file_size; // 0 → unlimited / endless (default); 16 777 216 recommended for a cap uint8_t max_struct_nesting; // 0 → 64 internal default; hard cap 255 uint8_t max_array_nesting; // 0 → 64 internal default; hard cap 255 void* userdata; bool (*on_unverified)(void*, bvnr_event_t, bvnr_data_t*); bool (*on_verified)(void*, bvnr_event_t, bvnr_data_t*); bool continue_on_error; bvnr_on_error_fn on_error; bool strict_version; // reject a declared spec version newer than this build bool want_unit_allow_nonterminating; // deliver a non-terminating exact conversion as a rational uint32_t max_conversion_length; // 0 → 1024; longest want_unit conversion text, in chars bool (*want_unit)(void*, const bvnr_data_t*, value_unit_t*, uint32_t*); // read-time lossless unit/base conversion hook uint64_t _reserved[2]; } bvnr_read_flags_t; ``` ### 16.5 Source/Sink Creation ```c void bvnr_source_from_fd(bvnr_source_t* s, int fd); void bvnr_source_from_mem(bvnr_source_t* s, const void* buf, uint64_t len); void bvnr_sink_to_fd(bvnr_sink_t* s, int fd); void bvnr_sink_to_mem(bvnr_sink_t* s, void* buf, uint64_t cap); uint64_t bvnr_sink_bytes_written(const bvnr_sink_t* s); ``` `bvnr_sink_bytes_written` queries the total bytes written to a memory sink created with `bvnr_sink_to_mem` **by the caller**; `bvnr_open_write_sink` copies the sink, so writer output never advances the caller's struct and this stays 0. For writer output use `bvnr_writer_bytes_written` (§16.7). ### 16.6 Reading ```c bool bvnr_open_read_source(bvnr_reader_t* r, const bvnr_source_t* src, const bvnr_sink_t* src_mirror, bvnr_read_flags_t* options); bool bvnr_open_read_mem(bvnr_reader_t* r, const void* buf, uint64_t len, void* mirror_buf, uint64_t mirror_cap, bvnr_read_flags_t* options); bool bvnr_read(bvnr_reader_t* r); ``` ### 16.7 Error Queries ```c error_code_t bvnr_reader_get_error(const bvnr_reader_t* r); uint64_t bvnr_reader_get_error_line (const bvnr_reader_t* r); uint64_t bvnr_reader_get_error_column(const bvnr_reader_t* r); uint32_t bvnr_reader_get_error_byte (const bvnr_reader_t* r); uint64_t bvnr_reader_get_error_offset(const bvnr_reader_t* r); uint64_t bvnr_reader_get_recovery_count(const bvnr_reader_t* r); const char* bvn_error_to_string(error_code_t code); ``` ### 16.8 Utility Functions ```c bool bvn_validate_identifier(const char* id); bool bvn_validate_symbol(const char* surr); bool bvn_validate_reference(const char* link); bool bvn_validate_number(const char* s); bool bvn_validate_string(const uint8_t* data, size_t len); bool bvn_is_special_number_string(const char* s); bool bvn_validate_digits_for_base(const char* s, uint32_t base); bool bvn_validate_number_in_base(const char* s, uint32_t base); bool bvn_validate_uint_range(const char* s, uint32_t w, uint32_t base); bool bvn_validate_sint_range(const char* s, uint32_t w, uint32_t base); uint32_t bvn_char_to_digit(uint32_t c, uint32_t base); uint32_t bvn_min_digits_for_type(value_type_spec_t vt); int32_t bvn_format_uint64(char* buf, size_t bufsize, uint64_t value, uint32_t base, uint32_t min_digits); int32_t bvn_format_int64(char* buf, size_t bufsize, int64_t value, uint32_t base, uint32_t min_digits); int32_t bvn_format_double(char* buf, size_t bufsize, double value, value_type_spec_t vt); bool bvn_parse_int64(const char* s, value_type_spec_t vt, int64_t* out); bool bvn_parse_uint64(const char* s, value_type_spec_t vt, uint64_t* out); bool bvn_parse_double(const char* s, value_type_spec_t vt, double* out); bool bvn_parse_double_in_base(const char* s, uint32_t base, double* out); bool bvn_looks_like_double(const char* s); /* Parse a NUL-terminated unit string into a value_unit_t. Sets *ok to false on error. */ value_unit_t bvn_parse_unit(const uint8_t* unit, bool* ok); /* Length-bounded variant; does not require a NUL terminator. */ value_unit_t bvn_parse_unit_n(const uint8_t* unit, uint32_t len, bool* ok); /* Serialize a value_unit_t (possibly compound) back to a string. Numerator components are joined by "·", followed by "/" and denominator components joined by "·". Returns bytes written, or -1 on buffer overflow. */ int32_t bvn_unit_to_string(value_unit_t u, char* buf, size_t bufsize); /* Extended variant accepting bvn_unit_flags_t: BVN_UNIT_FLAGS_NONE (0) – Unicode superscript exponents, no reduction BVN_UNIT_ASCII_EXP (1 << 1) – use ^N caret notation for exponents BVN_UNIT_REDUCE (1 << 0) – reduce compound unit before serialising Flags may be OR-combined. Returns bytes written, or -1 on overflow. */ int32_t bvn_unit_to_string_ex(value_unit_t u, char* buf, size_t bufsize, bvn_unit_flags_t flags); /* Returns true if every component in u has a valid exponent (not exp_invalid), a known base unit, and a prefix legal for that base unit per bvn_prefix_unit_valid. Both serialisation functions call this predicate internally before writing. */ bool bvn_unit_valid(value_unit_t u); /* Structural equality of two units: same num_components and the same set of components (matching base, exponent, and prefix). The comparison is ORDER-INSENSITIVE — unit multiplication is commutative, so components are matched as multisets and "N·m" equals "m·N". This is the comparison the validator uses to match an inline unit suffix against a type-annotation unit (error_unit_mismatch on disagreement). For dimensional equivalence — units that measure the same physical quantity but differ in spelling or factoring (e.g. W vs VA, or the two no_unit forms) — use bvn_units_compatible from bovnar_si_units.h (§11.8) instead. */ bool bvn_unit_equal(value_unit_t a, value_unit_t b); /* Compute the combined prefix factor across all components (ignoring base-unit conversion factors). Each component's prefix factor is raised to |exponent| and multiplied together; denominator components are inverted. */ double bvn_unit_prefix_factor(value_unit_t u); /* Compute the combined prefix exponent (sum of prefix_base_exponent × |unit_exponent| across all components, negated for denominator components). */ int32_t bvn_unit_prefix_exponent(value_unit_t u); const uint8_t* bvn_get_escape_repl_table(void); ``` ### 16.9 Typed Write Helpers Convenience functions that emit a type annotation + value in one call. All functions return `false` on serialisation error. ```c /* ── Plain scalar writers ──────────────────────────────────────────── */ bool bvnr_write_string(bvnr_writer_t* w, const char* key, const char* value); bool bvnr_write_plain (bvnr_writer_t* w, const char* key, const char* value); bool bvnr_write_null (bvnr_writer_t* w, const char* key); bool bvnr_write_bool (bvnr_writer_t* w, const char* key, bool value); /* ── Integer writers ───────────────────────────────────────────────── */ bool bvnr_write_uint(bvnr_writer_t* w, const char* key, uint32_t width, uint64_t value); bool bvnr_write_sint(bvnr_writer_t* w, const char* key, uint32_t width, int64_t value); /* ── Binary float writers ─────────────────────────────────────────── */ bool bvnr_write_float(bvnr_writer_t* w, const char* key, uint32_t width, double value); /* ── Fixed-point writers (float_fix) ─────────────────────────────── */ /* q = number of fractional bits (Q parameter). */ bool bvnr_write_float_fix(bvnr_writer_t* w, const char* key, uint32_t width, uint32_t q, double value); /* ── Decimal float writers (float_dec) ───────────────────────────── */ bool bvnr_write_float_dec(bvnr_writer_t* w, const char* key, uint32_t width, double value); /* ── Writers with explicit unit ───────────────────────────────────── */ bool bvnr_write_uint_unit (bvnr_writer_t* w, const char* key, uint32_t width, uint64_t value, value_unit_t unit); bool bvnr_write_sint_unit (bvnr_writer_t* w, const char* key, uint32_t width, int64_t value, value_unit_t unit); bool bvnr_write_float_unit (bvnr_writer_t* w, const char* key, uint32_t width, double value, value_unit_t unit); bool bvnr_write_float_fix_unit(bvnr_writer_t* w, const char* key, uint32_t width, uint32_t q, double value, value_unit_t unit); bool bvnr_write_float_dec_unit(bvnr_writer_t* w, const char* key, uint32_t width, double value, value_unit_t unit); /* ── Wide-precision float writers (bvn_float_t) ─────────────────── */ /* vt.width must match f->_prec or be 0 (auto). */ bool bvnr_write_bvnf (bvnr_writer_t* w, const char* key, const bvn_float_t* f, uint32_t width); bool bvnr_write_bvnf_unit(bvnr_writer_t* w, const char* key, const bvn_float_t* f, uint32_t width, value_unit_t unit); /* ── Struct helpers ───────────────────────────────────────────────── */ bool bvnr_write_struct_start(bvnr_writer_t* w, const char* key); bool bvnr_write_struct_end (bvnr_writer_t* w); ``` ### 16.10 Error Codes ```c typedef enum error_code_e { error_none = 0, error_unknown_token_type = 1, error_array_row_size_mismatch = 2, error_identifier_too_long = 3, error_empty_identifier = 4, error_struct_nesting_too_high = 5, error_array_nesting_too_high = 6, error_illegal_struct_close = 7, error_string_too_long = 8, error_illegal_escape_sequence = 9, error_number_too_long = 10, error_symbol_too_long = 11, error_reference_too_long = 12, error_read_complete_chunk_failed = 13, error_octet_stream_out_of_sync = 14, error_unexpected_input_byte = 15, error_text_data_too_long = 16, error_reading_from_source_fd = 17, error_got_incomplete_bvnr_stream = 18, error_invalid_utf8_byte = 19, error_invalid_byte_order_mark = 20, error_type_too_long = 21, error_unit_too_long = 22, error_expected_string_in_array = 23, /* reserved; never set by the library */ error_expected_number_in_array = 24, /* reserved; never set by the library */ error_illegal_value_type = 25, error_scanner_callback_failed = 26, error_file_too_long = 27, error_invalid_argument = 28, error_too_many_array_items = 29, error_writing_to_sink = 30, error_sink_buffer_exhausted = 31, error_unit_illegal = 32, error_base_requires_string_literal = 33, error_type_value_mismatch = 34, error_value_out_of_range = 35, error_digit_not_in_base = 36, error_recovered = 37, /* reserved; never set by the library */ error_unit_mismatch = 38, /* Array element homogeneity (spec 1.0): every non-null element of an array * must share the same kind and physical dimension; sibling sub-arrays must * match in length and element shape (recursively); sibling structs must * share the same keys with recursively-matching fields. */ error_array_element_type_mismatch = 39, error_struct_shape_mismatch = 40, /* A struct (or the top-level document) repeats a key. Keys must be unique * within one scope so lookup, references and iteration always agree. */ error_duplicate_struct_key = 41, /* spec 1.1 — a leading "#!bovnar …" directive is present but malformed. */ error_invalid_spec_version = 42, /* spec 1.1 — the declared version exceeds what the reader supports and * strict_version was set. */ error_unsupported_spec_version = 43, /* spec 1.1 — a \u{…} escape names a non-scalar value (a surrogate, or a * code point above U+10FFFF). */ error_invalid_codepoint = 44, /* spec 1.1 — an ISO-8601 datetime literal is malformed or has an * out-of-range field (bad width, separator, month/day/time component). */ error_invalid_datetime_literal = 45, /* spec 1.1 — an ISO-8601 literal was given for an atomic GNSS epoch * (gps/galileo/glonass/beidou), which has no round-trippable inverse. */ error_datetime_literal_unsupported_epoch = 46, /* a lossless read-time unit/base conversion (want_unit) could not be * performed: an irrational factor, or a non-terminating expansion. */ error_unit_inexact = 47, /* a multiplexed message was still short when its octet stream ended. */ error_octet_stream_truncated = 48, } error_code_t; ``` --- ## 17. Versioning & Stability Bovnar follows semantic versioning of the **format**, independent of any implementation's version. **Implementation version.** The reference implementation exposes its own version, which tracks the format version but may advance independently for implementation-only fixes. The C header defines `BVNR_VERSION_MAJOR`, `BVNR_VERSION_MINOR`, `BVNR_VERSION_PATCH`, the comparable integer `BVNR_VERSION` (`major*10000 + minor*100 + patch`, so `#if BVNR_VERSION >= 10100` tests "≥ 1.1.0"), and `BVNR_VERSION_STRING` (`"1.1.0"`). The Python package mirrors this as `bovnar.__version__`. Separately, `BVNR_SPEC_VERSION_MAJOR` / `BVNR_SPEC_VERSION_MINOR` name the highest **spec** version the build understands (`1.1`); `bvnr_version()`, `bvnr_version_string()` and `bvnr_spec_version()` expose both at runtime. **Declaring a document's version.** Since 1.1 a document may state which spec version it targets with a leading `#!bovnar .` directive (§3.4). Because the directive is lexically a comment, this is fully backward compatible: a 1.0 reader ignores it. A 1.1+ reader records it (`bvnr_reader_get_declared_version`) and, in `strict_version` mode, rejects a version it does not support. **What 1.0 freezes.** The grammar is stable. A `.bvnr` document that is valid under spec 1.0 will remain valid, and will decode to the same values, under every 1.x revision. This covers the lexical structure, the type families and their annotations, arrays (including the homogeneity rules of §7.4), structs, octet streams, references, and the error-code values in §16.10. Conforming archives may rely on this for long-term storage. **What may still grow in 1.x (additive only).** The following may be *extended* without breaking existing documents, and such extensions ship as minor (1.x) revisions: - the **unit and currency tables** — new physical units, prefixes, and ISO 4217 / crypto currency codes may be added (a document never depends on a code being *absent*); - **new error codes** appended after the current maximum (existing numeric values never change) — 1.1 appends `error_invalid_spec_version` (42), `error_unsupported_spec_version` (43), `error_invalid_codepoint` (44), `error_invalid_datetime_literal` (45), and `error_datetime_literal_unsupported_epoch` (46); - the **optional version directive** (§3.4), added in 1.1: it is an ordinary comment to any 1.0 reader, so adding one never invalidates a document; - new optional reader/writer flags and limits whose defaults preserve current behaviour. A reader from an older 1.x point release may not recognise a unit or currency added in a newer one; that is the expected direction of forward compatibility and is not a break of the 1.0 promise. **What requires a 2.0.** Any change that could render a valid 1.x document invalid, change how it decodes, renumber an error code, or alter the grammar is a breaking change and is reserved for a major (2.0) revision. The changes that motivated the 1.0 freeze — the **mandatory `$` currency sigil** (§10.4 of the unit-system reference), **array element homogeneity** (§7.4), and **`float_fix` value-range validation** (§6.2, rejecting a value the declared Q-format cannot represent) — were exactly such breaks, so they were made *before* 1.0 and cannot be reconsidered within 1.x. --- ## Appendix A: Event Sequence Reference ### A.1 Simple Assignment (Untyped) Input: `.foo = 42;` ``` ev_stream_start ev_assignment_start data="foo" ev_type_annotation_start (synthesised) ev_type_annotation_type_family "uint" ev_type_annotation_type_family_parameter (width:64) ev_type_annotation_type_family_parameter (base:_10) ev_type_annotation_type_family_parameter (unit:no_unit) ev_type_annotation_end ev_data data="42" ev_stream_end ``` > Every stream is bracketed by `ev_stream_start` … `ev_stream_end`; the reader > emits `ev_stream_end` once after the final assignment (it is omitted from the > remaining appendix examples for brevity). ### A.2 Typed Assignment Input: `.bar = 9.81;` ``` ev_assignment_start data="bar" ev_type_annotation_start data="float:32,m/s" ev_type_annotation_type_family "float" ev_type_annotation_type_family_parameter (width:32) ev_type_annotation_type_family_parameter (unit:m/s) ev_type_annotation_end ev_data data="9.81" ``` ### A.3 Compound Unit Assignment Input: `.force = 9.81;` ``` ev_assignment_start data="force" ev_type_annotation_start data="float:64,k~g·m/s²" ev_type_annotation_type_family "float" ev_type_annotation_type_family_parameter (width:64) ev_type_annotation_type_family_parameter (unit:k~g·m/s²) → value_unit = { num_components = 3, components = [ { base=bu_gram, exponent=exp_linear, prefix={prefix_si, si_kilo} }, { base=bu_meter, exponent=exp_linear, prefix={prefix_si, si_none} }, { base=bu_second, exponent=exp_neg_square, prefix={prefix_si, si_none} } ] } ev_type_annotation_end ev_data data="9.81" ``` ### A.4 Array Input: `.arr = [1, 2]/[3, 4];` ``` ev_assignment_start data="arr" ev_array_row_start ev_type_annotation_start (synthesised for 1) ev_type_annotation_type_family "uint" ...params... ev_type_annotation_end ev_data data="1" ev_type_annotation_start (synthesised for 2) ...params... ev_type_annotation_end ev_data data="2" ev_array_row_end ev_array_dim_start ev_array_row_start ev_type_annotation_start (synthesised for 3) ...params... ev_type_annotation_end ev_data data="3" ... (4) ... ev_array_row_end ``` ### A.5 Struct Input: `.s = {.x = 1; .y = 2;};` ``` ev_assignment_start data="s" ev_struct_start ev_assignment_start data="x" ...ev_data for 1... ev_assignment_start data="y" ...ev_data for 2... ev_struct_end ``` ### A.6 Octet Stream Input: `.bin = \x00\x01\x03\x00abc\x00;` ``` ev_assignment_start data="bin" ev_octet_stream_start ev_data (octet_stream) data="abc", length=3 ev_octet_stream_end ``` --- ## Appendix B: Implementation Notes ### B.1 Keyword State Machine Type family keywords are recognised through a dedicated state machine in the lexer. The lexer fires `ACT_tf_float_done` after the shared `f→l→o→a→t` path, storing `"float"` in `type_data` and transitioning to `type_body_outro`. If the next bytes are `_fix` or `_dec`, they are accumulated via `copy_type_byte`, so the final string is `"float_fix"` or `"float_dec"`. `bvn_parse_type_annotation` then dispatches on the full accumulated string. ``` u → i → n → t → keyword "uint" u → t → f → 8 → keyword "utf8" s → i → n → t → keyword "sint" f → l → o → a → t → ACT_tf_float_done → type_body_outro → accumulate "_fix" → "float_fix" → accumulate "_dec" → "float_dec" → (nothing) → "float" ``` ### B.2 Special Number Keywords The special floats are bare reserved keywords — `nan`, `inf`, and `ninf` (negative infinity) — with no sigil. The lexer reads them as ordinary symbols; the validator then reclassifies a symbol whose text is exactly one of these three spellings into a numeric special value (`token_is_number`), the same way it reclassifies `null`/`true`/`false`/`on`/`off`: ``` symbol "nan" → reclassify → special number "nan" (3 bytes) symbol "inf" → reclassify → special number "inf" (3 bytes) symbol "ninf" → reclassify → special number "ninf" (4 bytes) ``` Any other bare word (e.g. `infinity`, `nans`) stays an ordinary symbol. The stored token text is the keyword itself: `nan`, `inf`, `ninf`. A special-number keyword takes no inline unit suffix; a unit is supplied through the type annotation (` inf`). ### B.3 Default Width, Base, and Q ```c static inline uint32_t bvn_effective_width(value_type_spec_t s) { return s.width ? s.width : 64u; } /* * For float_fix, float_dec and datetime the .base field has a different meaning * (Q for float_fix, unused for float_dec, epoch index for datetime); always * report base 10 for those — the datetime carrier is decimal epoch-seconds, so * decoding it in the epoch index stored in .base would corrupt the value. */ static inline uint32_t bvn_effective_base(value_type_spec_t s) { if (s.family == vt_float_fix || s.family == vt_float_dec || s.family == vt_datetime) return 10u; return s.base ? s.base : 10u; } /* * Returns the Q (fractional bits) for float_fix, 0 for all other families. * Q is stored in the .base field of value_type_spec_t. */ static inline uint32_t bvn_effective_q(value_type_spec_t s) { return (s.family == vt_float_fix) ? s.base : 0u; } ``` ### B.4 Type Equality ```c static inline bool bvn_type_spec_eq(value_type_spec_t a, value_type_spec_t b) { return a.family == b.family && a.width == b.width && a.base == b.base; } ``` For `float_fix`, `.base` holds Q, so two `float_fix` specs are equal only if they share the same width **and** the same Q. ### B.5 Numeric Type Check ```c static inline bool bvn_type_is_numeric(value_type_spec_t s) { return s.family == vt_sint || s.family == vt_uint || s.family == vt_float || s.family == vt_float_fix || s.family == vt_float_dec; } ``` ### B.6 Unit Component Access When iterating compound units, always check `num_components`: ```c for (uint32_t i = 0; i < u.num_components && i < BVNR_MAX_UNIT_COMPONENTS; i++) { value_unit_component_t* c = &u.components[i]; /* c->base, c->exponent, c->prefix.system, c->prefix.id */ } ``` ### B.7 Fixed-point and Decimal Float Wire Representations `float_fix` and `float_dec` are both serialised as ordinary decimal number literals in the Bovnar text layer. The type annotation is the sole indicator of wire encoding. At the C API level, the conversion path is: ``` float_fix (width ≤ 64): text literal → bvn_float_t → bvn_float_to_fixNN(f, frac_bits) → signed integer return value float_fix (width = 128, 256): text literal → bvn_float_t → bvn_float_to_fixNN(f, frac_bits, out) → wire bits in out[] float_dec: text literal → bvn_float_t → bvn_float_to_decNN(f, out) → wire bits in *out / out[] ``` The `bvn_float_t` intermediate representation is MPFR-layout-compatible (see `bvn_float.h`) and provides exact round-trip fidelity up to the declared precision. --- ## Appendix C: Limits Summary | Constant | Value | Description | |----------|-------|-------------| | reader default struct nesting | 64 | Default applied by the reader when `max_struct_nesting` is 0; hard cap is 255 | | reader default array nesting | 64 | Default applied by the reader when `max_array_nesting` is 0; hard cap is 255 | | writer default struct nesting | 64 | Default applied by the writer when `max_struct_nesting` is 0; hard cap is 255 | | writer default array nesting | 64 | Default applied by the writer when `max_array_nesting` is 0; hard cap is 255 | | reader default max_array_items | 2 147 483 647 | Default applied by the reader when `max_array_items` is 0 | | reader default max_text_bytes | 2 147 483 647 | Default applied by the reader when `max_text_bytes` is 0 | | reader default max_file_size | 0 (unlimited / endless) | A `max_file_size` of 0 imposes no byte-count cap; set to 16 777 216 (16 MiB) in production | | recommended file size cap | 16 777 216 | Suggested explicit value for `max_file_size` (16 MiB) | | `BVNR_MAX_UNIT_COMPONENTS` | 8 | Maximum number of unit components in a compound unit | | `BVN_MAX_INT_WIDTH` | 32768 | Maximum bit-width for `uint` and `sint` types. The validator and writer reject any declared width exceeding this value with `error_illegal_value_type`. | --- *End of Bovnar Specification (v1.1)* # Bovnar Quantity Annotation System — Unit and Currency Reference > **Applies to:** Bovnar (BVNR) specification version 1.1 > **Scope:** Physical units, currency codes, prefix rules, disambiguation, C/Python APIs, and validation. --- ## Table of Contents 1. [Overview](#1-overview) 2. [Syntax — Annotation as a Type Parameter](#2-syntax--annotation-as-a-type-parameter) 3. [Physical Base Units](#3-physical-base-units) - 3.1 [SI Base Units](#31-si-base-units) - 3.2 [Named SI-Derived Units](#32-named-si-derived-units) - 3.3 [Non-SI Units Accepted for Use with SI](#33-non-si-units-accepted-for-use-with-si) - 3.4 [Imperial and US Customary Units](#34-imperial-and-us-customary-units) - 3.5 [Pressure Units](#35-pressure-units) - 3.6 [Energy Units](#36-energy-units) - 3.7 [Power Units](#37-power-units) - 3.8 [Force Units](#38-force-units) - 3.9 [Speed and Rotational Frequency](#39-speed-and-rotational-frequency-units) - 3.10 [Volume Units](#310-volume-units) - 3.11 [Area Units](#311-area-units) - 3.12 [Angle Units](#312-angle-units) - 3.13 [CGS Units](#313-cgs-units) - 3.14 [Radiation Units](#314-radiation-units) - 3.15 [Logarithmic Units](#315-logarithmic-units) - 3.16 [Electrical Power Units](#316-electrical-power-units) - 3.17 [Digital Units](#317-digital-units) - 3.18 [Textile Linear Density](#318-textile-linear-density) - 3.19 [US Apothecary / Dry Volume](#319-us-apothecary--dry-volume) - 3.20 [Old German Units](#320-old-german-units) - 3.21 [Additional Length Units](#321-additional-length-units) - 3.22 [Additional Mass Units](#322-additional-mass-units) - 3.23 [Acceleration](#323-acceleration) - 3.24 [Signal Rate](#324-signal-rate) - 3.25 [Ratio and Proportion Units](#325-ratio-and-proportion-units) - 3.26 [Sentinel Value](#326-sentinel-value) 4. [Prefixes](#4-prefixes) - 4.1 [SI Prefixes](#41-si-prefixes) - 4.2 [IEC Binary Prefixes](#42-iec-binary-prefixes) 5. [Unit Notation Grammar](#5-unit-notation-grammar) - 5.1 [Simple Units](#51-simple-units) - 5.2 [Compound Units](#52-compound-units) - 5.3 [Separators](#53-separators) - 5.4 [Denominator Semantics](#54-denominator-semantics) 6. [Exponents](#6-exponents) - 6.1 [Unicode Superscript Form](#61-unicode-superscript-form) - 6.2 [ASCII Caret Form](#62-ascii-caret-form) - 6.3 [Exponent Edge Cases](#63-exponent-edge-cases) 7. [The `no_unit` Keyword](#7-the-no_unit-keyword) 8. [Constraints and Limits](#8-constraints-and-limits) 9. [Currency Codes](#9-currency-codes) - 9.1 [The `$` Sigil Rule](#91-the--sigil-rule) - 9.2 [ISO 4217 Fiat Currencies and Precious Metals](#92-iso-4217-fiat-currencies-and-precious-metals) - 9.3 [Cryptocurrencies](#93-cryptocurrencies) - 9.4 [Prefix Rules for Currency Units](#94-prefix-rules-for-currency-units) - 9.5 [Compound Currency Expressions](#95-compound-currency-expressions) - 9.6 [Compatibility Rules](#96-compatibility-rules) - 9.7 [Type Pairing Recommendations](#97-type-pairing-recommendations) 10. [Symbol Disambiguation](#10-symbol-disambiguation) - 10.1 [The Namespace Rule as Disambiguator](#101-the-namespace-rule-as-disambiguator) - 10.2 [Exhaustive Conflict Table](#102-exhaustive-conflict-table) - 10.3 [The CUP Case in Detail](#103-the-cup-case-in-detail) - 10.4 [The Mandatory Currency Sigil](#104-the-mandatory-currency-sigil) 11. [C Data Model](#11-c-data-model) - 11.1 [Enumerations](#111-enumerations) - 11.2 [Structures](#112-structures) - 11.3 [Convenience Macros](#113-convenience-macros) 12. [C API Functions](#12-c-api-functions) - 12.1 [Parsing a Unit String](#121-parsing-a-unit-string) - 12.2 [Serializing a Unit](#122-serializing-a-unit) - 12.3 [Prefix Factor and Exponent Queries](#123-prefix-factor-and-exponent-queries) - 12.4 [SI Conversion API](#124-si-conversion-api) - 12.5 [Currency API](#125-currency-api) - 12.6 [Python API](#126-python-api) 13. [Integration with the Parser Event Stream](#13-integration-with-the-parser-event-stream) 14. [Validation Errors](#14-validation-errors) 15. [Annotated Examples](#15-annotated-examples) - 15.1 [Physical Quantities](#151-physical-quantities) - 15.2 [Digital Storage](#152-digital-storage) - 15.3 [Compound SI Quantities](#153-compound-si-quantities) - 15.4 [Currency Amounts and Rates](#154-currency-amounts-and-rates) - 15.5 [Error Cases](#155-error-cases) --- ## 1. Overview The Bovnar quantity annotation system is an **optional, per-value annotation** that attaches a physical unit or currency denomination to any numeric field. It is part of the type annotation (``) and applies to the `uint`, `sint`, `float`, `float_fix`, and `float_dec` type families. Two distinct namespaces share the annotation slot: - **Physical units** — 163 named base units covering SI, Imperial, CGS, radiation, surveying, culinary, Old German, and digital storage quantities. - **Currency codes** — 216 monetary denominations: 166 ISO 4217 alphabetic codes (including precious-metal X-codes; 4 are historical: HRK retired 2023-01-01, SLL replaced by SLE 2022, ZWL superseded by ZWG 2024, BGN retired 2026-01-01) and 50 cryptocurrency tickers. Both namespaces are syntactically unified: the same grammar, the same `~` prefix separator, the same compound-unit operators (`·`, `*`, `/`), and the same `value_unit_t` data model apply to both. They are separated purely by a token-classification rule described in §9.1 and §10. Annotations are **descriptive**, not prescriptive. Bovnar validates form and type; it does not perform dimensional analysis, unit conversion, or exchange-rate arithmetic. That responsibility belongs to the consuming application. ### Design Principles - **SI-first.** All SI base units, all 22 BIPM-2019 named derived units, and all 24 current SI prefixes (quecto … quetta) are supported. - **Binary-prefix aware.** IEC 80000-13 binary prefixes (kibi … yobi) are supported for digital storage quantities, plus `Ri`/`Qi` (robi, quebi) as a forward-looking extension — those two are a proposal, not part of IEC 80000-13, which stops at yobi. - **Compound units.** Derived quantities (m/s, kg·m/s², USD/oz_t) are expressed inline without separate schema definitions. - **Two exponent notations.** Unicode superscript (`m²`, `s⁻²`) and ASCII caret (`m^2`, `s^-2`) are accepted equivalently. - **Currency as a first-class unit.** ISO 4217 and cryptocurrency codes participate in all unit composition rules — prefixes, compound expressions, and the `value_unit_t` representation — with no special-case parsing. - **Dimensionless values.** The keyword `no_unit` is the canonical representation of a dimensionless quantity. --- ## 2. Syntax — Annotation as a Type Parameter The unit or currency code occupies the **third positional parameter class** of a type annotation, after the optional bit-width and optional base. Parameter classes are identified by their content, not by position: ``` type-spec = param-type [ ":" type-param-list ] type-param-list = type-param { "," type-param } type-param = width-param (* plain decimal integer, e.g. 32 *) | base-param (* "_" + decimal integer, e.g. _16 *) | unit-param (* everything else, e.g. m/s *) ``` ### 2.1 Parameter Ordering Flexibility ```bovnar .val = 42; # — identical (parameter order is free) # — identical ``` ### 2.2 Inline Unit Suffix A unit may be written directly after a scalar value literal, between the value and the terminating `;`: ```bovnar .distance = 1500 m; # inline physical unit .speed = 9.81 m/s; # compound inline unit .price = 19.99 $USD; # inline currency (mandatory $ sigil) .gold_rate = 2351.40 $USD/oz_t; # inline compound currency/unit .ratio = 3.14 no_unit; # explicit dimensionless ``` The inline unit uses the **same character set** and **same semantic parser** (`bvn_parse_unit`) as the type-annotation unit parameter. It is terminated by ASCII whitespace, `#` (comment), or `;`. #### Constraints | Situation | Result | |-----------|--------| | No annotation unit; inline unit present | Inline unit becomes the effective unit | | Annotation has no unit; inline unit present | Inline unit becomes the effective unit | | Annotation unit present; no inline unit | Annotation unit is the effective unit | | Annotation unit **equals** inline unit | Valid; the common unit is used | | Annotation unit **differs** from inline unit | `error_unit_mismatch` | | Inline unit inside an array element | `error_unexpected_input_byte` | When both are present, equality is checked after parsing via `bvn_unit_equal`, a structural comparison of the parsed `value_unit_t` values: the two units must have the same number of components and the same *set* of components (matching base, exponent, and prefix). The comparison is **order-insensitive** — unit multiplication is commutative, so components are matched as multisets and reordered spellings such as `N·m` and `m·N` (or `m·s⁻¹` and `m/s`) compare as equal. (It is *not* a raw `memcmp`, which would wrongly reject reordered components.) ```bovnar .v = 9.81 m·s⁻¹; # OK: both parse to m/s .v = 1.0 s; # ERROR: error_unit_mismatch ``` ### Applicable Type Families | Type family | Unit / currency parameter | |-------------|--------------------------| | `uint` | Supported | | `sint` | Supported | | `float` | Supported (binary floating-point; discouraged for monetary amounts — see §9.7) | | `float_fix` | Supported (wrong for monetary values — see §9.7) | | `float_dec` | Supported; **recommended** for monetary amounts | | `utf8` | Parameterless: a unit (or any other parameter) is `error_illegal_value_type` | --- ## 3. Physical Base Units Bovnar supports 163 named physical base units. Currency codes are a separate namespace and are covered in §9. > **Reading this section:** The *Symbol* column gives the canonical serialized form. *Long forms* are accepted on input but never produced on output. *Enum value* is the `value_base_unit_t` constant used in the C API. ### 3.1 SI Base Units | Symbol | Long forms | Name | Enum value | Notes | |--------|-----------|------|------------|-------| | `s` | `sec`, `second`, `seconds` | second | `bu_second` | SI base unit of time | | `m` | `meter`, `metre`, `meters`, `metres` | meter | `bu_meter` | SI base unit of length | | `g` | `gram`, `grams` | gram | `bu_gram` | SI base unit of mass is kg; `g` carries the prefix | | `A` | `amp`, `amps`, `ampere`, `amperes` | ampere | `bu_ampere` | SI base unit of electric current | | `K` | `kelvin`, `kelvins` | kelvin | `bu_kelvin` | SI base unit of thermodynamic temperature | | `mol` | `mole`, `moles` | mole | `bu_mol` | SI base unit of amount of substance | | `cd` | `candela`, `candelas` | candela | `bu_candela` | SI base unit of luminous intensity | > **Note on the kilogram:** Bovnar uses `g` (gram) as the base unit symbol so that the `k~` (kilo) prefix can be attached explicitly: `k~g` = kilogram. This is consistent with how the SI formally defines the kilogram as a prefixed gram. ### 3.2 Named SI-Derived Units | Symbol | Long forms | Name | Enum value | SI Definition | |--------|-----------|------|------------|---------------| | `Hz` | `hertz` | hertz | `bu_hertz` | s⁻¹ | | `N` | `newton`, `newtons` | newton | `bu_newton` | kg·m·s⁻² | | `Pa` | `pascal`, `pascals` | pascal | `bu_pascal` | kg·m⁻¹·s⁻² | | `J` | `joule`, `joules` | joule | `bu_joule` | kg·m²·s⁻² | | `W` | `watt`, `watts` | watt | `bu_watt` | kg·m²·s⁻³ | | `V` | `volt`, `volts` | volt | `bu_volt` | kg·m²·A⁻¹·s⁻³ | | `Ω` | `ohm`, `ohms`, `Ohm` | ohm | `bu_ohm` | kg·m²·A⁻²·s⁻³ — U+2126 OHM SIGN, UTF-8: `0xE2 0x84 0xA6`; U+03A9 (Greek capital omega) also accepted on input, canonical output is always U+2126 | | `F` | `farad`, `farads` | farad | `bu_farad` | kg⁻¹·m⁻²·A²·s⁴ | | `C` | `coulomb`, `coulombs` | coulomb | `bu_coulomb` | A·s | | `S` | `siemens` | siemens | `bu_siemens` | kg⁻¹·m⁻²·A²·s³ | | `Wb` | `weber`, `webers` | weber | `bu_weber` | kg·m²·A⁻¹·s⁻² | | `T` | `tesla`, `teslas` | tesla | `bu_tesla` | kg·A⁻¹·s⁻² | | `H` | `henry`, `henrys`, `henries` | henry | `bu_henry` | kg·m²·A⁻²·s⁻² | | `°C` | `degC`, `degrC`, `degreeC`, `degreesC`, `celsius` | degree Celsius | `bu_celsius` | K = °C + 273.15 (affine); BIPM Table 4 entry 14 | | `lm` | `lumen`, `lumens` | lumen | `bu_lumen` | cd·sr | | `lx` | `lux` | lux | `bu_lux` | cd·sr·m⁻² | | `Bq` | `becquerel`, `becquerels` | becquerel | `bu_becquerel` | s⁻¹ | | `Gy` | `gray`, `grays` | gray | `bu_gray` | m²·s⁻² | | `Sv` | `sievert`, `sieverts` | sievert | `bu_sievert` | m²·s⁻² | | `kat` | `katal`, `katals` | katal | `bu_katal` | mol·s⁻¹ | | `rad` | `radian`, `radians` | radian | `bu_radian` | dimensionless (plane angle; m/m) | | `sr` | `steradian`, `steradians` | steradian | `bu_steradian` | dimensionless (solid angle; m²/m²) | ### 3.3 Non-SI Units Accepted for Use with SI | Symbol | Long forms | Name | Enum value | Notes | |--------|-----------|------|------------|-------| | `L`, `l` | `liter`, `litre`, `liters`, `litres` | liter | `bu_liter` | 10⁻³ m³ | | `min` | `minute`, `minutes` | minute | `bu_minute` | 60 s | | `h` | `hour`, `hours` | hour | `bu_hour` | 3600 s | | `d` | `day`, `days` | day | `bu_day` | 86400 s | | `wk` | `week`, `weeks` | week | `bu_week` | 604800 s | | `yr` | `year`, `years` | year | `bu_year` | 31557600 s (Julian year) | | `mo` | `month`, `months` | month (Julian) | `bu_month` | 2629800 s (= 365.25 d / 12) | | `fn` | `fortnight`, `fortnights` | fortnight | `bu_fortnight` | 1209600 s (= 14 d) | | `°`, `deg` | `degr`, `degree`, `degrees` | degree (angle) | `bu_degree` | π/180 rad — U+00B0 | | `t` | `tonne` | tonne | `bu_tonne` | 10³ kg | | `bar` | — | bar | `bu_bar` | 10⁵ Pa | | `eV` | `electronvolt` | electronvolt | `bu_electronvolt` | 1.602176634×10⁻¹⁹ J | | `Da` | `dalton`, `amu`, `u` | dalton | `bu_dalton` | 1.66053906660×10⁻²⁷ kg | | `au` | — | astronomical unit | `bu_astronomical_unit` | 1.495978707×10¹¹ m | | `ha` | `hectare` | hectare | `bu_hectare` | 10⁴ m² | ### 3.4 Imperial and US Customary Units #### Length | Symbol | Long forms | Name | Enum value | Factor | |--------|-----------|------|------------|--------| | `in` | `inch`, `inches` | inch | `bu_inch` | 0.0254 m (exact) | | `ft` | `foot`, `feet` | foot | `bu_foot` | 0.3048 m (exact) | | `yd` | `yard`, `yards` | yard | `bu_yard` | 0.9144 m (exact) | | `mi` | `mile`, `miles` | statute mile | `bu_mile` | 1609.344 m (exact) | | `nmi` | `nautical_mile`, `nautical_miles` | nautical mile | `bu_nautical_mile` | 1852 m (exact) | | `Å` (U+212B) | `angstrom`, `angstroms`, Å (U+00C5) | ångström | `bu_angstrom` | 10⁻¹⁰ m | | `ly` | `light_year`, `light_years` | light-year | `bu_light_year` | 9.4607304725808×10¹⁵ m | | `pc` | `parsec`, `parsecs` | parsec | `bu_parsec` | 3.085677581491367×10¹⁶ m | | `fur` | `furlong`, `furlongs` | furlong | `bu_furlong` | 201.168 m (exact) | | `fath` | `fathom`, `fathoms` | fathom | `bu_fathom` | 1.8288 m (exact) | | `thou` | `thou`, `mil`, `mils` | thou | `bu_thou` | 25.4×10⁻⁶ m (exact) | | `ch` | `chain`, `chains` | chain (Gunter's) | `bu_chain` | 20.1168 m (exact) | | `rd` | `rod`, `rods` | rod (pole, perch) | `bu_rod` | 5.0292 m (exact) | > **Thou vs mil:** Both `thou` and `mil` are accepted for 1/1000 of an inch (25.4 µm). The canonical output form is `thou`. Note that `mil` does **not** mean milliradian; milliradians are written `m~rad`. #### Mass | Symbol | Long forms | Name | Enum value | Factor | |--------|-----------|------|------------|--------| | `lb` | `lbs`, `pound`, `pounds` | pound (avoirdupois) | `bu_pound` | 0.45359237 kg (exact) | | `oz` | `ounce`, `ounces` | ounce (avoirdupois) | `bu_ounce` | 0.028349523125 kg (exact) | | `gr` | `grain`, `grains` | grain | `bu_grain` | 6.479891×10⁻⁵ kg (exact) | | `st` | `stone`, `stones` | stone | `bu_stone` | 6.35029318 kg (exact) | | `tn_sh`| `short_ton`, `short_tons` | short ton (US ton) | `bu_short_ton` | 907.18474 kg (exact) | | `tn_l` | `long_ton`, `long_tons` | long ton (UK ton) | `bu_long_ton` | 1016.0469088 kg (exact) | | `oz_t` | `troy_ounce`, `troy_ounces` | troy ounce | `bu_troy_ounce` | 0.0311034768 kg (exact) | | `ct` | `carat`, `carats` | metric carat | `bu_carat` | 2×10⁻⁴ kg (exact) | | `slug` | `slugs` | slug | `bu_slug` | 14.593902937 kg | | `dr` | `dram`, `drams` | dram (avoirdupois) | `bu_dram` | 1.7718451953125×10⁻³ kg (exact) | | `dwt` | `pennyweight`, `pennyweights` | pennyweight (troy) | `bu_pennyweight` | 1.55517384×10⁻³ kg (exact) | #### Temperature | Symbol | Long forms | Name | Enum value | Conversion | |--------|-----------|------|------------|------------| | `°C`, `degC` | `degrC`, `degreeC`, `degreesC`, `celsius` | degree Celsius | `bu_celsius` | K = °C + 273.15 (affine) — also §3.2 (BIPM named derived unit) | | `°F`, `degF` | `degrF`, `degreeF`, `degreesF`, `fahrenheit` | degree Fahrenheit | `bu_fahrenheit` | K = (°F + 459.67) × 5/9 (affine) | | `°Ra`, `degRa` | `degrRa`, `degreeRa`, `degreesRa`, `rankine` | degree Rankine | `bu_rankine` | K = °Ra × 5/9 (linear) | | `°De`, `degDe` | `degrDe`, `degreeDe`, `degreesDe`, `delisle` | degree Delisle | `bu_delisle` | K = 373.15 − °De × 2/3 (affine) | | `°N`, `degN` | `degrN`, `degreeN`, `degreesN`, `newton_temperature` | degree Newton | `bu_newton_temp` | K = °N × 100/33 + 273.15 (affine) | | `°Re`, `degRe` | `degrRe`, `degreeRe`, `degreesRe`, `reaumur` | degree Réaumur | `bu_reaumur` | K = °Re × 5/4 + 273.15 (affine) | | `°Ro`, `degRo` | `degrRo`, `degreeRo`, `degreesRo`, `romer` | degree Rømer | `bu_romer` | K = (°Ro − 7.5) × 40/21 + 273.15 (affine) | > Kelvin (`K`) is the SI base unit (§3.1). `Ra` not `R` — `R` is reserved for the röntgen (`bu_roentgen`). ### 3.5 Pressure Units | Symbol | Long forms | Name | Enum value | Factor | |--------|-----------|------|------------|--------| | `atm` | `atmosphere`, `atmospheres` | standard atmosphere | `bu_atmosphere` | 101325 Pa (exact) | | `at` | `atmosphere_technical` | atmosphere technical | `bu_atmosphere_technical` | 98066.5 Pa (= 1 kgf/cm²) | | `mmHg` | — | millimetre of mercury | `bu_mmhg` | 133.322387415 Pa | | `Torr` | `torr` | torr | `bu_torr` | 101325/760 Pa | | `psi` | — | pound-force per square inch | `bu_psi` | 6894.757293168362 Pa | | `inHg` | `inch_hg`, `inch_mercury` | inch of mercury | `bu_inch_hg` | 3386.388645 Pa | ### 3.6 Energy Units | Symbol | Long forms | Name | Enum value | Factor | |--------|-----------|------|------------|--------| | `cal` | `calorie`, `calories` | thermochemical calorie | `bu_calorie` | 4.184 J (exact) | | `Btu` | `btu` | International Table BTU | `bu_btu` | 1055.05585262 J | | `erg` | `ergs` | erg | `bu_erg` | 10⁻⁷ J (exact) | | `thm` | `therm`, `therms` | US therm | `bu_therm` | 1.05480400×10⁸ J (exact) | | `ft_lb` | `foot_pound`, `foot_pounds` | foot-pound | `bu_foot_pound` | 1.3558179483 J | > **`BTU` alias note:** `BTU` (all uppercase, three characters) is a valid alias for `bu_btu`. Because currencies require the `$` sigil, the bare token `BTU` is a physical-unit lookup and resolves to `bu_btu`; `Btu` and `btu` are also accepted. See §10.2 for the complete look-alike table. ### 3.7 Power Units | Symbol | Long forms | Name | Enum value | Factor | |--------|-----------|------|------------|--------| | `hp` | `horsepower` | mechanical horsepower | `bu_horsepower` | 745.69987158227 W | | `PS` | `CV`, `metric_horsepower` | metric horsepower | `bu_metric_horsepower` | 735.49875 W (exact) | ### 3.8 Force Units | Symbol | Long forms | Name | Enum value | Factor | |--------|-----------|------|------------|--------| | `lbf` | `pound_force` | pound-force | `bu_pound_force` | 4.4482216152605 N | | `dyn` | `dyne`, `dynes` | dyne | `bu_dyne` | 10⁻⁵ N (exact) | | `kip` | `kips` | kip (kilopound-force) | `bu_kip` | 4448.2216152605 N | | `kgf` | `kilogram_force` | kilogram-force | `bu_kilogram_force` | 9.80665 N (exact) | ### 3.9 Speed and Rotational Frequency Units | Symbol | Long forms | Name | Enum value | Factor | |--------|-----------|------|------------|--------| | `kn` | `knot`, `knots` | knot | `bu_knot` | 1852/3600 m/s | | `rpm` | — | revolutions per minute | `bu_rpm` | 1/60 s⁻¹ | > `kn` has dimension m·s⁻¹. `rpm` has dimension s⁻¹ (rotational frequency, not linear speed); it is grouped here by convention. ### 3.10 Volume Units #### US Liquid Volume | Symbol | Long forms | Name | Enum value | Factor | |--------|-----------|------|------------|--------| | `gal` | `gallon`, `gallons` | US liquid gallon | `bu_gallon` | 3.785411784×10⁻³ m³ (exact) | | `qt` | `quart`, `quarts` | US liquid quart | `bu_quart` | 9.46352946×10⁻⁴ m³ | | `pt` | `pint`, `pints` | US liquid pint | `bu_pint` | 4.73176473×10⁻⁴ m³ | | `cup` | `cups` | US cup | `bu_cup` | 2.365882365×10⁻⁴ m³ | | `gi` | `gill`, `gills` | US gill | `bu_gill` | 1.18294118250×10⁻⁴ m³ | | `fl_oz`| `fluid_ounce`, `fluid_ounces` | US fluid ounce | `bu_fluid_ounce` | 2.95735295625×10⁻⁵ m³ | | `tbsp` | `tablespoon`, `tablespoons` | US tablespoon | `bu_tablespoon` | 1.47867648×10⁻⁵ m³ | | `tsp` | `teaspoon`, `teaspoons` | US teaspoon | `bu_teaspoon` | 4.92892159375×10⁻⁶ m³ | | `bbl` | `barrel`, `barrels` | petroleum barrel | `bu_barrel` | 0.158987294928 m³ | > **`cup` disambiguation:** The canonical symbol for the US cup volume unit is `cup` (all lowercase). The Cuban Peso (ISO 4217 code 192) is written with the mandatory currency sigil as `$CUP`; the bare uppercase token `CUP` is `error_unit_illegal`. The two cannot be confused. See §10.3 for full details. #### UK Imperial Volume | Symbol | Long forms | Name | Enum value | Factor | |--------|-----------|------|------------|--------| | `gal_uk` | `gallon_uk`, `gallons_uk` | imperial gallon | `bu_gallon_uk` | 4.54609×10⁻³ m³ (exact) | | `qt_uk` | `quart_uk`, `quarts_uk` | imperial quart | `bu_quart_uk` | 1136.5225×10⁻⁶ m³ | | `pt_uk` | `pint_uk`, `pints_uk` | imperial pint | `bu_pint_uk` | 568.26125×10⁻⁶ m³ | | `gi_uk` | `gill_uk`, `gills_uk` | imperial gill | `bu_gill_uk` | 1.420653125×10⁻⁴ m³ (exact) | | `fl_oz_uk` | `fluid_ounce_uk`, `fluid_ounces_uk` | imperial fluid ounce | `bu_fluid_ounce_uk` | 28.4130625×10⁻⁶ m³ | ### 3.11 Area Units | Symbol | Long forms | Name | Enum value | Factor | |--------|-----------|------|------------|--------| | `ac` | `acre`, `acres` | acre | `bu_acre` | 4046.8564224 m² (exact) | | `barn` | `barns` | barn | `bu_barn` | 10⁻²⁸ m² (exact) | ### 3.12 Angle Units | Symbol | Long forms | Name | Enum value | Factor | |--------|-----------|------|------------|--------| | `arcmin` | `arcminute`, `arcminutes` | arcminute | `bu_arcminute` | π/10800 rad | | `arcsec` | `arcsecond`, `arcseconds` | arcsecond | `bu_arcsecond` | π/648000 rad | | `grad` | `gradian`, `gradians`, `gon` | gradian | `bu_grad` | π/200 rad | | `rev` | `turn`, `revolution`, `revolutions`, `turns` | revolution | `bu_revolution` | 2π rad | ### 3.13 CGS Units | Symbol | Long forms | Name | Enum value | SI equivalent | |--------|-----------|------|------------|---------------| | `P` | `poise`, `poises` | poise (dynamic viscosity) | `bu_poise` | 0.1 Pa·s | | `St` | `stokes`, `stoke` | stokes (kinematic viscosity) | `bu_stokes` | 10⁻⁴ m²·s⁻¹ | | `G` | `gauss` | gauss (magnetic flux density) | `bu_gauss` | 10⁻⁴ T | | `Mx` | `maxwell`, `maxwells` | maxwell (magnetic flux) | `bu_maxwell` | 10⁻⁸ Wb | | `Oe` | `oersted`, `oersteds` | oersted (magnetic field strength) | `bu_oersted` | 1000/(4π) A/m | | `sb` | `stilb`, `stilbs` | stilb (luminance) | `bu_stilb` | 10⁴ cd/m² | | `ph` | `phot`, `phots` | phot (illuminance) | `bu_phot` | 10⁴ lx | | `Gal` | `galileo`, `galileos` | galileo (acceleration) | `bu_galileo` | 10⁻² m/s² | ### 3.14 Radiation Units | Symbol | Long forms | Name | Enum value | SI equivalent | |--------|-----------|------|------------|---------------| | `Ci` | `curie`, `curies` | curie (radioactivity) | `bu_curie` | 3.7×10¹⁰ Bq | | `R` | `roentgen`, `roentgens` | röntgen (radiation exposure) | `bu_roentgen` | 2.58×10⁻⁴ C/kg | | `rem` | `rems` | rem (dose equivalent) | `bu_rem` | 10⁻² Sv | ### 3.15 Logarithmic Units | Symbol | Long forms | Name | Enum value | Notes | |--------|-----------|------|------------|-------| | `Np` | `neper`, `nepers` | neper | `bu_neper` | dimensionless logarithmic ratio | | `dB` | `decibel`, `decibels` | decibel | `bu_decibel` | 1 Np = 20/ln(10) dB ≈ 8.686 dB | ### 3.16 Electrical Power Units | Symbol | Long forms | Name | Enum value | Notes | |--------|-----------|------|------------|-------| | `var` | `vars` | var (volt-ampere reactive) | `bu_var` | reactive power; same SI dimension as W | | `VA` | `volt_ampere`, `volt_amperes` | volt-ampere | `bu_volt_ampere` | apparent power; same SI dimension as W | > **Watt, var, and VA:** All three carry the same SI dimensional signature (kg·m²·s⁻³). `bvn_units_compatible` returns `true` when comparing them. They are kept as distinct base units because they represent distinct AC power interpretations. ### 3.17 Digital Units | Symbol | Long forms | Name | Enum value | |--------|-----------|------|------------| | `b` | `bit`, `bits` | bit | `bu_bit` | | `B` | `byte`, `bytes`, `Byte`, `Bytes` | byte | `bu_byte` | ### 3.18 Textile Linear Density | Symbol | Long forms | Name | Enum value | Factor | |--------|-----------|------|------------|--------| | `tex` | — | tex | `bu_tex` | 1×10⁻⁶ kg/m (ISO 1144) | | `den` | `denier`, `deniers` | denier | `bu_denier` | 1/9000000 kg/m | ### 3.19 US Apothecary / Dry Volume | Symbol | Long forms | Name | Enum value | Factor | |--------|-----------|------|------------|--------| | `fl_dr` | `fluid_dram`, `fluid_drams` | US fluid dram | `bu_fluid_dram` | 3.6966911953125×10⁻⁶ m³ | | `minim` | `minims` | US minim | `bu_minim` | 6.16115199218750×10⁻⁸ m³ | | `pk` | `peck`, `pecks` | US dry peck | `bu_peck` | 8.80976754172×10⁻³ m³ | | `bsh` | `bushel`, `bushels` | US bushel | `bu_bushel` | 3.523907016688×10⁻² m³ | > `minim` (not `min`, which is the minute) avoids ambiguity. ### 3.20 Old German Units Old German units fall into metric-compatible units (still in use in DACH regions) and historical pre-metric Prussian units. The prefix `pr` is reserved for Prussian symbols. No German unit accepts any non-trivial SI or IEC prefix; `bvn_prefix_unit_valid` rejects any non-`si_none`/`iec_none` prefix for every unit from `bu_pfund` through `bu_scheffel`. #### Metric-Compatible German Units — Mass | Symbol | Long forms | Name | Enum value | Factor | |--------|-----------|------|------------|--------| | `Pfd` | `pfund`, `pfunds` | Pfund | `bu_pfund` | 0.5 kg (exact) | | `Ztr` | `zentner` | Zentner | `bu_zentner` | 50 kg (exact) | | `dz` | `doppelzentner` | Doppelzentner | `bu_doppelzentner` | 100 kg (exact) | | `lot` | `lots` | Lot | `bu_lot` | 15.625×10⁻³ kg (exact) | #### Historical German Units — Length (Prussian) | Symbol | Long forms | Name | Enum value | Factor | |--------|-----------|------|------------|--------| | `prln` | `prussian_line`, `linie` | Prussian line | `bu_prussian_line` | 2.17953×10⁻³ m | | `prz` | `prussian_zoll`, `zoll` | Prussian Zoll | `bu_prussian_zoll` | 2.61544×10⁻² m | | `prf` | `prussian_fuss`, `preussischer_fuss` | Prussian Fuß | `bu_prussian_fuss` | 3.13853×10⁻¹ m | | `elle` | `prussian_elle`, `preussische_elle` | Prussian Elle | `bu_prussian_elle` | 6.67160×10⁻¹ m | | `rute` | `prussian_rute`, `preussische_rute` | Prussian Rute | `bu_prussian_rute` | 3.76624 m | | `klafter` | `prussian_klafter` | Klafter | `bu_klafter` | 1.88312 m | | `dt_mi` | `deutsche_meile`, `german_mile` | Geographische Meile | `bu_german_mile` | 7420.44 m | #### Historical German Units — Area (Prussian) | Symbol | Long forms | Name | Enum value | Factor | |--------|-----------|------|------------|--------| | `morgen` | `prussian_morgen` | Morgen (Prussian) | `bu_morgen` | 2553.22 m² | #### Historical German Units — Volume (Prussian) | Symbol | Long forms | Name | Enum value | Factor | |--------|-----------|------|------------|--------| | `schffl` | `scheffel`, `prussian_scheffel` | Scheffel (Prussian) | `bu_scheffel` | 54.961×10⁻³ m³ | > The enum values for German units occupy positions **348–360**, placed after the entire currency range (134–347). Additional physical units (survey foot, league, cable, hand, quintal, scruple, baud) occupy positions **361–367**. Historical temperature scales (Delisle, Newton, Réaumur, Rømer) occupy positions **368–371**, and the dimensionless ratio units (`bu_percent` … `bu_ppb`) occupy positions **372–377**. The ABI-stable currency extension segment (`bu_zwg`, `bu_xcg`) occupies positions **378–379**, appended after the unit block so adding a currency never shifts an existing enum value. `BVN_VALUE_BASE_UNIT_COUNT` is a `#define` equal to **380** (verified by static assert `bu_xcg + 1 == 380`). Currencies begin at 134, immediately after the last non-German physical unit. ### 3.21 Additional Length Units | Symbol | Long forms | Name | Enum value | Factor | |--------|-----------|------|------------|--------| | `ftUS` | `survey_foot` | US survey foot | `bu_survey_foot` | 1200/3937 m ≈ 0.304800609… m | | `lea` | `league`, `leagues` | statute league | `bu_league` | 4828.032 m (= 3 statute miles) | | `cbl` | `cable`, `cables` | cable length | `bu_cable` | 185.2 m | | `hand` | `hands` | hand | `bu_hand` | 0.1016 m (= 4 in, exact) | > `ftUS` (US survey foot) differs from `ft` (international foot, 0.3048 m exactly) by about 2 ppm. Used in US geodetic surveying. ### 3.22 Additional Mass Units | Symbol | Long forms | Name | Enum value | Factor | |--------|-----------|------|------------|--------| | `qntl` | `quintal`, `quintals` | quintal | `bu_quintal` | 100 kg (exact) | | `sc` | `scruple`, `scruples` | apothecary scruple | `bu_scruple` | 1.2959782×10⁻³ kg (= 20 grains) | ### 3.23 Acceleration | Symbol | Long forms | Name | Enum value | Factor | |--------|-----------|------|------------|--------| | `gn` | `standard_gravity` | standard gravity | `bu_standard_gravity` | 9.80665 m·s⁻² (exact, CGPM 1901) | ### 3.24 Signal Rate | Symbol | Long forms | Name | Enum value | Notes | |--------|-----------|------|------------|-------| | `Bd` | `baud`, `bauds` | baud | `bu_baud` | 1 symbol/s = 1 s⁻¹ (ITU-T V.662) | ### 3.25 Ratio and Proportion Units Dimensionless scaling factors. A value carrying one of these units reduces to the numeric value multiplied by the factor in the canonical (dimensionless) base representation — e.g. `5 %` ≡ `0.05`, `250 ppm` ≡ `0.00025`. Like the other dimensionless ratios they carry an empty dimension vector, but unlike `rad`/`sr` they do **not** accept SI or IEC prefixes (a prefixed `%` is meaningless). | Symbol | Long forms | Name | Enum value | Factor | |--------|-----------|------|------------|--------| | `%` | `percent` | per cent | `bu_percent` | 10⁻² | | `‰` | `per_mille` | per mille | `bu_per_mille` | 10⁻³ | | `‱` | `per_myriad` | per myriad | `bu_per_myriad` | 10⁻⁴ | | `pcm` | `per_cent_mille` | per cent mille | `bu_per_cent_mille` | 10⁻⁵ | | `ppm` | — | parts per million | `bu_ppm` | 10⁻⁶ | | `ppb` | — | parts per billion | `bu_ppb` | 10⁻⁹ | ### 3.26 Sentinel Value `bu_none` (value `0`) is the internal representation of "no base unit", used for the `no_unit` keyword and as the default when no unit annotation is present. --- ## 4. Prefixes Prefixes are attached to a base unit symbol with a mandatory `~` separator: `prefix~baseunit`. ### 4.1 SI Prefixes All 24 current SI prefixes are supported, from quecto (10⁻³⁰) to quetta (10³⁰). | Prefix | Symbol | Factor | Enum value | |--------|--------|--------|------------| | quetta | `Q` | 10³⁰ | `si_quetta` | | ronna | `R` | 10²⁷ | `si_ronna` | | yotta | `Y` | 10²⁴ | `si_yotta` | | zetta | `Z` | 10²¹ | `si_zetta` | | exa | `E` | 10¹⁸ | `si_exa` | | peta | `P` | 10¹⁵ | `si_peta` | | tera | `T` | 10¹² | `si_tera` | | giga | `G` | 10⁹ | `si_giga` | | mega | `M` | 10⁶ | `si_mega` | | kilo | `k` | 10³ | `si_kilo` | | hecto | `h` | 10² | `si_hecto` | | deca | `da` | 10¹ | `si_deca` | | *(none)* | — | 10⁰ | `si_none` | | deci | `d` | 10⁻¹ | `si_deci` | | centi | `c` | 10⁻² | `si_centi` | | milli | `m` | 10⁻³ | `si_milli` | | micro | `µ` *(or `u`)* | 10⁻⁶ | `si_micro` | | nano | `n` | 10⁻⁹ | `si_nano` | | pico | `p` | 10⁻¹² | `si_pico` | | femto | `f` | 10⁻¹⁵ | `si_femto` | | atto | `a` | 10⁻¹⁸ | `si_atto` | | zepto | `z` | 10⁻²¹ | `si_zepto` | | yocto | `y` | 10⁻²⁴ | `si_yocto` | | ronto | `r` | 10⁻²⁷ | `si_ronto` | | quecto | `q` | 10⁻³⁰ | `si_quecto` | > **Encoding note:** `µ` is U+00B5 (MICRO SIGN), UTF-8: `0xC2 0xB5`. U+03BC (GREEK SMALL LETTER MU) is a distinct code point but is also accepted on input; the canonical output form is always U+00B5. ASCII `u` is accepted as an input-only alias for `µ` (e.g. `u~m` parses as `µ~m`); the canonical output form is always `µ`. Note `u` is also the bare symbol for the dalton, but the two never collide — a prefix only appears before `~`, a base only bare or after `~`. #### Prefix–Symbol Ambiguities Several prefix symbols overlap with base unit symbols. The `~` separator is the disambiguator: `m~` introduces the milli prefix; bare `m` is the meter. | Symbol | As prefix | As base unit | |--------|-----------|--------------| | `m` | milli | meter | | `d` | deci | day | | `h` | hecto | hour | | `T` | tera | tesla | | `f` | femto | farad | | `a` | atto | *(none)* | | `u` | micro (ASCII alias for `µ`) | dalton | | `S` | *(none)* | siemens | `d~s` = decisecond; `d` alone = day. ### 4.2 IEC Binary Prefixes IEC 80000-13 binary prefixes are used for digital quantities (`b` and `B` only). The table below also lists `Ri` and `Qi`; the standard stops at `Yi` (2⁸⁰) and those two are an unratified extension. | Prefix | Symbol | Factor | Enum value | |--------|--------|--------|------------| | kibi | `Ki` | 2¹⁰ | `iec_kibi` | | mebi | `Mi` | 2²⁰ | `iec_mebi` | | gibi | `Gi` | 2³⁰ | `iec_gibi` | | tebi | `Ti` | 2⁴⁰ | `iec_tebi` | | pebi | `Pi` | 2⁵⁰ | `iec_pebi` | | exbi | `Ei` | 2⁶⁰ | `iec_exbi` | | zebi | `Zi` | 2⁷⁰ | `iec_zebi` | | yobi | `Yi` | 2⁸⁰ | `iec_yobi` | | robi | `Ri` | 2⁹⁰ | `iec_robi` | | quebi | `Qi` | 2¹⁰⁰ | `iec_quebi` | #### Prefix–Unit Validity Constraints - **IEC prefixes** (`Ki`…`Qi`) are only permitted on `b` and `B`. `Ki~m` → `error_unit_illegal`. - **SI sub-kilo prefixes** (`d`, `c`, `m`, `µ`, `n`, `p`, `f`, `a`, `z`, `y`, `r`, `q`, `da`, `h`) are forbidden on `b` and `B`. - **German units** (`bu_pfund` through `bu_scheffel`) accept only `si_none`/`iec_none`. - **Currency units** accept SI prefixes of any magnitude (see §9.4). IEC prefixes are forbidden on all currency codes. ```bovnar .valid1 = 8; # OK: IEC prefix on byte .valid2 = 100; # OK: SI mega on bit .valid3 = 250.0; # OK: kilo on currency .invalid1 = 1; # ERROR: IEC prefix on meter .invalid2 = 512; # ERROR: SI milli on byte .invalid3 = 1; # ERROR: IEC prefix on currency ``` --- ## 5. Unit Notation Grammar ### 5.1 Simple Units ``` unit-component = [ prefix "~" ] base-unit [ unit-exponent ] ``` ```bovnar .temperature = 300.0; # kelvin .distance = 1.5; # kilometer .frequency = 2400; # megahertz .storage = 1024; # kibibytes .fund_nav = 250.0; # $250,000 ``` ### 5.2 Compound Units ```ebnf compound-unit = "no_unit" | unit-component { unit-sep unit-component } unit-sep = "*" | "/" | "·" (* · = U+00B7 MIDDLE DOT *) unit-component = [ prefix "~" ] base-unit [ unit-exponent ] unit-exponent = [ exp-sign ] exp-digit | "^" [ "-" | "+" ] ASCII-digit exp-sign = "⁺" | "⁻" exp-digit = "¹" | "²" | "³" | "⁴" | "⁵" | "⁶" | "⁷" | "⁸" | "⁹" ``` Currency codes participate in compound expressions using the same separators: ```bovnar .gold_spot = 2351.40; # $/troy oz .rent = 12.50; # €/m² .billing_rate = 150.00; # €/h .eur_usd = 1.0842; # exchange rate ``` > The sub-grammar is **semantic**, not lexical. The outer lexer captures the type-annotation body as a raw byte sequence; `bvn_parse_unit` parses the unit string portion after the lexer finishes. ### 5.3 Separators | Character | Code point | UTF-8 bytes | Meaning | |-----------|-----------|-------------|---------| | `*` | U+002A | `0x2A` | Multiplication | | `·` | U+00B7 | `0xC2 0xB7` | Multiplication (preferred visual form) | | `/` | U+002F | `0x2F` | Division | `·` and `*` are semantically identical and may be mixed freely. ### 5.4 Denominator Semantics The first `/` sets a latching "in-denominator" flag to `true` for all subsequent components. Additional `/` separators do not toggle back to the numerator. `k~g·m/s²` stores: ``` [0]: gram, exp_linear, si_kilo ← numerator [1]: meter, exp_linear, si_none ← numerator [2]: second, exp_neg_square, si_none ← denominator (exponent negated) ``` Both `k~g·m/s²` and `k~g·m·s⁻²` parse to identical `value_unit_t` representations. #### Parenthesised grouping A `(…)` group is a sub-expression evaluated independently. Like any factor it obeys the latching denominator, so a `/` before a group negates the group's net component exponents as a whole. This makes the readable denominator form work and compose correctly: ```bovnar .pressure = 101325; # kg·m⁻¹·s⁻² — same as k~g/m·s² .areal = 1.0; # kg·m⁻¹·s² — grouping changes the s sign .rate = 1.0; # m·s⁻² ``` `k~g/(m·s²)` and `k~g/m·s²` therefore parse to the **same** `value_unit_t` (the canonical, parenless form is what the writer emits). Rules: an explicit separator is required before a group (`m·(s)`, not `m(s)`); a group is not followed by its own exponent (`(m·s)²` is rejected — write `m²·s²`); parentheses must balance; empty groups (`()`) are rejected; nesting is bounded at 16. Unmatched or malformed groups raise `error_unit_illegal`. --- ## 6. Exponents Exponents are limited to integer values in the range **−9 … +9**. ### 6.1 Unicode Superscript Form | Glyph | Code point | UTF-8 bytes | Maps to | |-------|-----------|------------------|---------| | `¹` | U+00B9 | `0xC2 0xB9` | `exp_linear` | | `²` | U+00B2 | `0xC2 0xB2` | `exp_square` | | `³` | U+00B3 | `0xC2 0xB3` | `exp_cubic` | | `⁴` | U+2074 | `0xE2 0x81 0xB4` | `exp_quartic` | | `⁵` | U+2075 | `0xE2 0x81 0xB5` | `exp_quintic` | | `⁶` | U+2076 | `0xE2 0x81 0xB6` | `exp_sextic` | | `⁷` | U+2077 | `0xE2 0x81 0xB7` | `exp_septic` | | `⁸` | U+2078 | `0xE2 0x81 0xB8` | `exp_octic` | | `⁹` | U+2079 | `0xE2 0x81 0xB9` | `exp_nonic` | | `⁺` | U+207A | `0xE2 0x81 0xBA` | positive sign (no-op) | | `⁻` | U+207B | `0xE2 0x81 0xBB` | negate exponent | ### 6.2 ASCII Caret Form | ASCII form | Equivalent Unicode | Parsed as | |------------|--------------------|-----------| | `m^2` | `m²` | `exp_square` | | `s^-2` | `s⁻²` | `exp_neg_square` | | `m^+2` | `m²` | `exp_square` | | `kg^1` | `kg¹` | `exp_linear` | Only a **single ASCII digit** is permitted after the caret. ### 6.3 Exponent Edge Cases - **`exp_invalid` (value 0):** Zero-initialized sentinel. API functions that have an error output path reject it and signal an error. The two prefix query functions (`bvn_unit_prefix_factor`, `bvn_unit_prefix_exponent`) silently skip components with `exp_invalid`. - **`exp_linear` (value 1):** Stored for both an explicit `¹`/`^1` and any component written without an exponent suffix. --- ## 7. The `no_unit` Keyword The literal `no_unit` declares a value as **explicitly dimensionless**: ```bovnar .ratio = 0.95; .count = 1000; ``` `bvn_parse_unit` detects `no_unit` via `memcmp` and returns `BVN_UNIT_NONE` (`num_components = 0`). **Omitting the unit parameter** (e.g. ``) yields `BVN_UNIT_NO_PREFIX(bu_none)` (`num_components = 1`, `base == bu_none`). Both forms are semantically equivalent — they compare as compatible via `bvn_units_compatible` and both serialize to `"no_unit"` — but they are structurally distinct internal states. --- ## 8. Constraints and Limits | Constraint | Value | Error on violation | |------------|-------|--------------------| | Maximum components per compound unit | 8 (`BVNR_MAX_UNIT_COMPONENTS`) | `error_unit_illegal` | | Empty component between separators (e.g. `m//s`) | Not allowed | `error_unit_illegal` | | Maximum raw unit string length | Enforced by type-buffer limit | `error_unit_too_long` | | Null or empty unit string | Rejected by `bvn_parse_unit` | `ok = false` | `a/b/c` parses as `a / (b·c)` → components `[a, b⁻¹, c⁻¹]`. The "no toggle back" rule means `/` always adds to the denominator; it never re-enters the numerator. --- ## 9. Currency Codes Currency amounts are dimensional quantities in financial computing. `$19.99 USD` carries a denomination dimension just as `9.81 m/s²` carries an acceleration dimension. Bovnar extends the unit system with 216 currency and cryptocurrency codes so that monetary data can be annotated and round-tripped with the same precision guarantees as physical measurements. ### 9.1 The `$` Sigil Rule As of spec 1.0 a currency is recognised **only** in its `$`-sigil form (`$USD`, `$BTC`, or prefixed `k~$EUR`). The sigil — and nothing else — dispatches a component to the currency table; see §10.4 for the full rules and rationale. Classification happens at the lookup stage, per unit component: 1. A component introduced by `$` (after any SI/IEC prefix and its `~`) is looked up in the **currency table**. If the code is not found there, `error_unit_illegal` is raised — a `$` introduces a currency and nothing else. 2. A component **without** a `$` is looked up only in the **physical unit table**. A bare code such as `USD` or `CUP` is therefore never a currency; if it is not a physical unit it is `error_unit_illegal`. Because the sigil is mandatory, a bare uppercase code can never collide with a physical-unit symbol — present or future — so the two namespaces are disjoint by construction. The collision cases this resolves are catalogued for reference in §10. ### 9.2 ISO 4217 Fiat Currencies and Precious Metals 166 ISO 4217 alphabetic codes are supported, including precious-metal X-codes. The original 164 occupy the `value_base_unit_t` slot range **134 … 297** in alphabetical order (AED first, ZWL last) with one exception — `SSP` precedes `SRD` at slots 260/261. Nothing depends on the ordering (`bvn_parse_currency_str` scans linearly) and the slots are ABI-frozen, so the pair stays as it is, and — unlike physical units — have **no named `bu_*` enumerators**: such a currency is resolved from its `$`-sigil code by `bvn_parse_currency_str` and carried as the numeric `base` value (the currency catalogue in `bovnar_currency.c` is index-aligned to these slots). Currencies added after that range was frozen are **appended past the unit block** at slots **378 … 379** — an *extension segment* (`bu_zwg`, `bu_xcg`) reached via `bvn_currency_index` rather than the direct 134-based indexing — so that adding a currency never shifts an existing enum value (ABI stability). Four codes are historical and retained for compatibility: `HRK` (Croatian Kuna, retired 2023-01-01 when Croatia adopted the Euro), `SLL` (Sierra Leonean Leone (old), replaced by `SLE` in 2022), `ZWL` (Zimbabwean Dollar, superseded by `ZWG` Zimbabwe Gold in 2024), and `BGN` (Bulgarian Lev, retired 2026-01-01 when Bulgaria adopted the Euro); `ANG` (Netherlands Antillean Guilder) likewise coexists with its successor `XCG` (Caribbean Guilder, which inherits ANG's numeric code 532). The `minor_unit` field carries the exponent N such that 1 major unit = 10^N minor units (e.g. 1 USD = 100 cents, N=2). Applications reading integer-annotated values (e.g. ``) should call `bvn_currency_minor_unit` to determine the correct decimal shift. Minor units are **bold** below when they differ from 2. `Num` is the ISO 4217 numeric identifier. | Code | Num | Min | Name | |------|----:|----:|------| | `AED` | 784 | 2 | UAE Dirham | | `AFN` | 971 | 2 | Afghan Afghani | | `ALL` | 8 | 2 | Albanian Lek | | `AMD` | 51 | 2 | Armenian Dram | | `ANG` | 532 | 2 | Netherlands Antillean Guilder | | `AOA` | 973 | 2 | Angolan Kwanza | | `ARS` | 32 | 2 | Argentine Peso | | `AUD` | 36 | 2 | Australian Dollar | | `AWG` | 533 | 2 | Aruban Florin | | `AZN` | 944 | 2 | Azerbaijani Manat | | `BAM` | 977 | 2 | Bosnia-Herzegovina Convertible Mark | | `BBD` | 52 | 2 | Barbados Dollar | | `BDT` | 50 | 2 | Bangladeshi Taka | | `BGN` | 975 | 2 | Bulgarian Lev *(historical; retired 2026-01-01)* | | `BHD` | 48 | **3** | Bahraini Dinar | | `BIF` | 108 | **0** | Burundian Franc | | `BMD` | 60 | 2 | Bermudian Dollar | | `BND` | 96 | 2 | Brunei Dollar | | `BOB` | 68 | 2 | Boliviano | | `BRL` | 986 | 2 | Brazilian Real | | `BSD` | 44 | 2 | Bahamian Dollar | | `BTN` | 64 | 2 | Bhutanese Ngultrum | | `BWP` | 72 | 2 | Botswana Pula | | `BYN` | 933 | 2 | Belarusian Ruble | | `BZD` | 84 | 2 | Belize Dollar | | `CAD` | 124 | 2 | Canadian Dollar | | `CDF` | 976 | 2 | Congolese Franc | | `CHF` | 756 | 2 | Swiss Franc | | `CLF` | 990 | **4** | Unidad de Fomento | | `CLP` | 152 | **0** | Chilean Peso | | `CNY` | 156 | 2 | Chinese Yuan | | `COP` | 170 | 2 | Colombian Peso | | `CRC` | 188 | 2 | Costa Rican Colon | | `CUP` | 192 | 2 | Cuban Peso | | `CVE` | 132 | 2 | Cape Verdean Escudo | | `CZK` | 203 | 2 | Czech Koruna | | `DJF` | 262 | **0** | Djiboutian Franc | | `DKK` | 208 | 2 | Danish Krone | | `DOP` | 214 | 2 | Dominican Peso | | `DZD` | 12 | 2 | Algerian Dinar | | `EGP` | 818 | 2 | Egyptian Pound | | `ERN` | 232 | 2 | Eritrean Nakfa | | `ETB` | 230 | 2 | Ethiopian Birr | | `EUR` | 978 | 2 | Euro | | `FJD` | 242 | 2 | Fijian Dollar | | `FKP` | 238 | 2 | Falkland Islands Pound | | `GBP` | 826 | 2 | Pound Sterling | | `GEL` | 981 | 2 | Georgian Lari | | `GHS` | 936 | 2 | Ghanaian Cedi | | `GIP` | 292 | 2 | Gibraltar Pound | | `GMD` | 270 | 2 | Gambian Dalasi | | `GNF` | 324 | **0** | Guinean Franc | | `GTQ` | 320 | 2 | Guatemalan Quetzal | | `GYD` | 328 | 2 | Guyanese Dollar | | `HKD` | 344 | 2 | Hong Kong Dollar | | `HNL` | 340 | 2 | Honduran Lempira | | `HRK` | 191 | 2 | Croatian Kuna *(historical; retired 2023-01-01)* | | `HTG` | 332 | 2 | Haitian Gourde | | `HUF` | 348 | 2 | Hungarian Forint | | `IDR` | 360 | 2 | Indonesian Rupiah | | `ILS` | 376 | 2 | Israeli New Shekel | | `INR` | 356 | 2 | Indian Rupee | | `IQD` | 368 | **3** | Iraqi Dinar | | `IRR` | 364 | 2 | Iranian Rial | | `ISK` | 352 | **0** | Icelandic Krona | | `JMD` | 388 | 2 | Jamaican Dollar | | `JOD` | 400 | **3** | Jordanian Dinar | | `JPY` | 392 | **0** | Japanese Yen | | `KES` | 404 | 2 | Kenyan Shilling | | `KGS` | 417 | 2 | Kyrgyzstani Som | | `KHR` | 116 | 2 | Cambodian Riel | | `KMF` | 174 | **0** | Comorian Franc | | `KPW` | 408 | 2 | North Korean Won | | `KRW` | 410 | **0** | South Korean Won | | `KWD` | 414 | **3** | Kuwaiti Dinar | | `KYD` | 136 | 2 | Cayman Islands Dollar | | `KZT` | 398 | 2 | Kazakhstani Tenge | | `LAK` | 418 | 2 | Laotian Kip | | `LBP` | 422 | 2 | Lebanese Pound | | `LKR` | 144 | 2 | Sri Lankan Rupee | | `LRD` | 430 | 2 | Liberian Dollar | | `LSL` | 426 | 2 | Lesotho Loti | | `LYD` | 434 | **3** | Libyan Dinar | | `MAD` | 504 | 2 | Moroccan Dirham | | `MDL` | 498 | 2 | Moldovan Leu | | `MGA` | 969 | 2 | Malagasy Ariary | | `MKD` | 807 | 2 | Macedonian Denar | | `MMK` | 104 | 2 | Myanmar Kyat | | `MNT` | 496 | 2 | Mongolian Togrog | | `MOP` | 446 | 2 | Macanese Pataca | | `MRU` | 929 | 2 | Mauritanian Ouguiya | | `MUR` | 480 | 2 | Mauritian Rupee | | `MVR` | 462 | 2 | Maldivian Rufiyaa | | `MWK` | 454 | 2 | Malawian Kwacha | | `MXN` | 484 | 2 | Mexican Peso | | `MYR` | 458 | 2 | Malaysian Ringgit | | `MZN` | 943 | 2 | Mozambican Metical | | `NAD` | 516 | 2 | Namibian Dollar | | `NGN` | 566 | 2 | Nigerian Naira | | `NIO` | 558 | 2 | Nicaraguan Cordoba | | `NOK` | 578 | 2 | Norwegian Krone | | `NPR` | 524 | 2 | Nepalese Rupee | | `NZD` | 554 | 2 | New Zealand Dollar | | `OMR` | 512 | **3** | Omani Rial | | `PAB` | 590 | 2 | Panamanian Balboa | | `PEN` | 604 | 2 | Peruvian Sol | | `PGK` | 598 | 2 | Papua New Guinean Kina | | `PHP` | 608 | 2 | Philippine Peso | | `PKR` | 586 | 2 | Pakistani Rupee | | `PLN` | 985 | 2 | Polish Zloty | | `PYG` | 600 | **0** | Paraguayan Guarani | | `QAR` | 634 | 2 | Qatari Riyal | | `RON` | 946 | 2 | Romanian Leu | | `RSD` | 941 | 2 | Serbian Dinar | | `RUB` | 643 | 2 | Russian Ruble | | `RWF` | 646 | **0** | Rwandan Franc | | `SAR` | 682 | 2 | Saudi Riyal | | `SBD` | 90 | 2 | Solomon Islands Dollar | | `SCR` | 690 | 2 | Seychellois Rupee | | `SDG` | 938 | 2 | Sudanese Pound | | `SEK` | 752 | 2 | Swedish Krona | | `SGD` | 702 | 2 | Singapore Dollar | | `SHP` | 654 | 2 | Saint Helena Pound | | `SLE` | 925 | 2 | Sierra Leonean Leone | | `SLL` | 694 | 2 | Sierra Leonean Leone (old) *(historical; replaced by SLE 2022)* | | `SOS` | 706 | 2 | Somali Shilling | | `SSP` | 728 | 2 | South Sudanese Pound | | `SRD` | 968 | 2 | Surinamese Dollar | | `STN` | 930 | 2 | Sao Tome and Principe Dobra | | `SVC` | 222 | 2 | Salvadoran Colon | | `SYP` | 760 | 2 | Syrian Pound | | `SZL` | 748 | 2 | Swazi Lilangeni | | `THB` | 764 | 2 | Thai Baht | | `TJS` | 972 | 2 | Tajikistani Somoni | | `TMT` | 934 | 2 | Turkmenistan Manat | | `TND` | 788 | **3** | Tunisian Dinar | | `TOP` | 776 | 2 | Tongan Pa'anga | | `TRY` | 949 | 2 | Turkish Lira | | `TTD` | 780 | 2 | Trinidad and Tobago Dollar | | `TWD` | 901 | 2 | New Taiwan Dollar | | `TZS` | 834 | 2 | Tanzanian Shilling | | `UAH` | 980 | 2 | Ukrainian Hryvnia | | `UGX` | 800 | **0** | Ugandan Shilling | | `USD` | 840 | 2 | US Dollar | | `UYU` | 858 | 2 | Uruguayan Peso | | `UZS` | 860 | 2 | Uzbekistani Som | | `VES` | 928 | 2 | Venezuelan Bolivar Soberano | | `VND` | 704 | **0** | Vietnamese Dong | | `VUV` | 548 | **0** | Vanuatu Vatu | | `WST` | 882 | 2 | Samoan Tala | | `XAF` | 950 | **0** | CFA Franc BEAC | | `XAG` | 961 | **0** | Silver | | `XAU` | 959 | **0** | Gold | | `XCD` | 951 | 2 | East Caribbean Dollar | | `XCG` | 532 | 2 | Caribbean Guilder | | `XDR` | 960 | **0** | Special Drawing Rights | | `XOF` | 952 | **0** | CFA Franc BCEAO | | `XPD` | 964 | **0** | Palladium | | `XPF` | 953 | **0** | CFP Franc | | `XPT` | 962 | **0** | Platinum | | `XTS` | 963 | **0** | Test currency (ISO 4217 reserved; do not use in production) | | `YER` | 886 | 2 | Yemeni Rial | | `ZAR` | 710 | 2 | South African Rand | | `ZMW` | 967 | 2 | Zambian Kwacha | | `ZWG` | 924 | 2 | Zimbabwe Gold | | `ZWL` | 932 | 2 | Zimbabwean Dollar *(historical; superseded by ZWG 2024)* | > `CLF` (Unidad de Fomento) is the only currency with 4 minor units **in this catalogue**. ISO 4217 also defines `UYW` with 4, but the catalogue omits the fund and bond codes (`BOV CHE CHW COU MXV USN UYI UYW`, `XBA`–`XBD`, `XSU`, `XUA`) and the no-currency code `XXX`: they denote accounting units and placeholders rather than money a value can be denominated in. The four historical codes `HRK`, `SLL`, `ZWL`, and `BGN` are retained for compatibility but should not be used for new data. ### 9.3 Cryptocurrencies 50 cryptocurrencies are supported, with 3- or 4-letter uppercase tickers. They occupy `value_base_unit_t` slots **298 … 347** and, like the fiat codes, have **no named `bu_*` enumerators** — they are resolved by `bvn_parse_currency_str` and carried as the numeric `base` value. The `minor_unit` field holds the canonical on-chain decimal places. `numeric_code = 0` for all cryptocurrencies. > **Min** = `minor_unit` = on-chain decimal places. E.g. `` stores satoshis; divide by 10⁸ to obtain whole BTC. | Code | Min | Subunit | Name | |--------|----:|---------|------| | `BTC` | 8 | satoshi | Bitcoin | | `ETH` | 18 | wei | Ethereum | | `SOL` | 9 | lamport | Solana | | `XRP` | 6 | drop | XRP | | `BNB` | 18 | — | BNB | | `ADA` | 6 | lovelace | Cardano | | `LTC` | 8 | — | Litecoin | | `DOT` | 10 | planck | Polkadot | | `XMR` | 12 | piconero | Monero | | `ETC` | 18 | — | Ethereum Classic | | `BCH` | 8 | — | Bitcoin Cash | | `XLM` | 7 | stroop | Stellar | | `FIL` | 18 | — | Filecoin | | `ICP` | 8 | — | Internet Computer | | `TRX` | 6 | — | TRON | | `EOS` | 4 | — | EOS | | `VET` | 18 | — | VeChain | | `NEO` | 0 | — | Neo | | `ZEC` | 8 | — | Zcash | | `UNI` | 18 | — | Uniswap | | `ARB` | 18 | — | Arbitrum | | `SUI` | 9 | — | Sui | | `TON` | 9 | — | Toncoin | | `INJ` | 18 | — | Injective | | `SEI` | 6 | — | Sei | | `APT` | 8 | — | Aptos | | `TAO` | 9 | — | Bittensor | | `WIF` | 6 | — | dogwifhat | | `DOGE` | 8 | koinu | Dogecoin | | `LINK` | 18 | — | Chainlink | | `USDT` | 6 | — | Tether | | `USDC` | 6 | — | USD Coin | | `AVAX` | 18 | — | Avalanche | | `ATOM` | 6 | — | Cosmos | | `POL` | 18 | — | Polygon | | `NEAR` | 24 | — | NEAR Protocol | | `ALGO` | 6 | — | Algorand | | `HBAR` | 8 | — | Hedera | | `AAVE` | 18 | — | Aave | | `MKR` | 18 | — | Maker | | `DAI` | 18 | — | Dai | | `STX` | 6 | — | Stacks | | `GRT` | 18 | — | The Graph | | `LDO` | 18 | — | Lido DAO | | `BONK` | 5 | — | Bonk | | `PEPE` | 18 | — | Pepe | | `SHIB` | 18 | — | Shiba Inu | | `JUP` | 6 | — | Jupiter | | `PYTH` | 6 | — | Pyth Network | | `RUNE` | 8 | — | THORChain | ### 9.4 Prefix Rules for Currency Units **All SI prefixes** are permitted on all currency units. `k~USD` denotes "values in thousands of USD" — a common scale annotation in financial reporting. ```bovnar .fund_nav = 250.0; # $250,000 .gdp = 42800.0; # €42.8 billion .eth_gwei = 35.0; # 35 Gwei gas price ``` **IEC binary prefixes** (`Ki~`, `Mi~`, …) are **forbidden** on all currency units. `bvn_currency_prefix_valid()` returns `false` for any IEC prefix; the parser raises `error_unit_illegal`. ### 9.5 Compound Currency Expressions Currency codes participate in compound unit expressions using the existing separators: ```bovnar .gold_spot = 2351.40; # $/troy oz .wheat = 5.82; # $/bushel .rent = 12.50; # €/m² .billing_rate = 150.00; # €/h .eur_usd = 1.0842; # exchange rate .eth_btc = 0.05610; # cross-crypto rate ``` Currency × currency compounds (`USD·EUR`) are syntactically valid and produce no error. Their financial interpretation is the application's responsibility. #### Exchange Rate Timestamps Bovnar annotates denomination; it does not store exchange rates or timestamps. The timestamp belongs in a separate field: ```bovnar .snapshot = { .epoch = 1716400000; .eur_usd = 1.0842; }; ``` ### 9.6 Compatibility Rules `bvn_units_compatible()` requires no modification for currencies. Two currency expressions are structurally compatible only if they have identical component sequences including base enum values. Since `bu_usd ≠ bu_eur`, `USD` and `EUR` are already structurally incompatible under the existing comparison logic. ### 9.7 Type Pairing Recommendations | Use case | Recommended annotation | Rationale | |----------|----------------------|-----------| | Decimal monetary amount | `` | Exact decimal; 16 significant digits | | High-precision / actuarial | `` | 34 significant digits | | Integer minor-unit storage | `` | Value in cents; app reads `minor_unit()` | | Negative balances in minor units | `` | | | Zero-minor-unit currency | `` | Integer is the only correct representation | | 3-minor-unit currency | `` | Value in fils | | Commodity price | `` | $/troy oz | | Exchange rate | `` | USD per EUR | | On-chain satoshi balance | `` | Integer satoshis | | Human-readable BTC amount | `` | | > **`float` (binary floating-point) is discouraged** for monetary amounts. Binary fractions cannot represent 0.10 USD exactly. Use `float_dec` for decimal-exact storage. > > **`float_fix` is wrong** for monetary values. Q-format stores values as `integer × 2^(-N)` — a binary fractional resolution. No power of 2 equals a power of 10 (except 2⁰ = 10⁰ = 1), so no Q value exactly represents cents. --- ## 10. Symbol Disambiguation This section documents how a physical-unit token and a currency token are kept apart. The mandatory `$` currency sigil (§10.4) is the normative rule and resolves every potential collision; the look-alike tables that follow are retained for reference. ### 10.1 The Namespace Rule as Disambiguator The mandatory `$` sigil (§9.1, normative rule in §10.4) makes the two namespaces disjoint by construction: the sigil — and nothing else — selects the currency table, so a bare token is **always** a physical-unit lookup and can never be mistaken for a currency. | Written token | Looked up in | Result | |---|---|---| | `$USD`, `$BTC`, `k~$EUR` | currency table (sigil present) | currency, or `error_unit_illegal` if the code is unknown | | `m`, `Hz`, `cup`, `BTU`, `k~g` (no `$`) | physical unit table only | physical unit, or `error_unit_illegal` if unknown | | `USD`, `CUP`, `XYZ` (no `$`) | physical unit table only | `error_unit_illegal` — not physical units, and a bare code is never a currency | Because the lookup is sigil-driven rather than spelling-driven, **case is no longer load-bearing for disambiguation**: `cup` and `CUP` are both physical-unit lookups (the first matches `bu_cup`, the second is `error_unit_illegal`), and the Cuban Peso is written `$CUP`. ### 10.2 Exhaustive Conflict Table Before the sigil, several uppercase tokens *looked* like they could be either a physical unit or a currency. The sigil removes the ambiguity outright; the table below is retained as a reference for the codes that previously needed disambiguation. In every row, the bare form is a physical-unit lookup and the currency is only ever the `$`-prefixed form. | Token | Bare form (no `$`) | `$`-sigil form | Notes | |-------|--------------------|----------------|-------| | `cup` / `CUP` | `cup` → US cup (`bu_cup`); `CUP` → `error_unit_illegal` | `$CUP` → Cuban Peso (ISO 4217:192) | the classic look-alike, now fully separated | | `BTU` | `BTU` → International Table BTU (`bu_btu`); `Btu`, `btu` also accepted | *(not ISO 4217)* | uppercase `BTU` is a physical alias, not a currency | | `SOL` | `SOL` → `error_unit_illegal` (no physical unit) | `$SOL` → Solana (crypto) | | | `BAR` | `BAR` → `error_unit_illegal`; use lowercase `bar` | *(not ISO 4217)* | | | `ERG` | `ERG` → `error_unit_illegal`; use lowercase `erg` | *(not ISO 4217)* | | | `CAD`, `AUD`, `GBP`, `XAU` | `error_unit_illegal` (no physical unit) | `$CAD`, `$AUD`, `$GBP`, `$XAU` → the respective currencies | | **Key finding:** with the sigil, no token is simultaneously a valid bare physical-unit symbol and a valid currency — currencies live entirely under `$`, physical units entirely without it. ### 10.3 The CUP Case in Detail `CUP` is the classic example because the ISO 4217 code for the Cuban Peso shares its letters with the English word for the culinary measure. Under the sigil rule the two are unambiguous: | Written in BVNR | Resolved as | Enum value | SI factor | |-----------------|-------------|------------|-----------| | `cup` | US cup | `bu_cup` | 2.365882365×10⁻⁴ m³ | | `cups` | US cup (long form) | `bu_cup` | 2.365882365×10⁻⁴ m³ | | `CUP` | *(error)* — bare uppercase is not a physical unit | — | `error_unit_illegal` | | `$CUP` | Cuban Peso | `CUP_` *(currency enum, ISO 4217:192)* | — (monetary, no SI factor) | ```bovnar .recipe_volume = 2.0; # 2 US cups (volume) .balance = 15.00; # 15 Cuban Pesos (currency) # .bad = 15.00; # error_unit_illegal: bare 'CUP' is not a unit ``` Calling `bvn_unit_to_si_factor` on a `$CUP` unit returns `*ok = false` because `bvn_find_si_conv` skips currency enum values. Calling `bvn_unit_is_currency` on a `cup` unit returns `false`. The two are completely disjoint in both parsing and the conversion API — and, because the peso *must* be written `$CUP`, a user who writes the bare `CUP` intending the culinary cup gets an immediate `error_unit_illegal` (the correct spelling is lowercase `cup`) rather than a silently-accepted currency. ### 10.4 The Mandatory Currency Sigil A currency code carries a **mandatory `$` sigil** as of spec 1.0. This is the resolution of the `CUP` (Cuban Peso) vs `cup` (the physical cup) namespace collision: a currency is *only* recognised in its sigil form, so a bare code can never collide with a physical-unit symbol — present or future. ```bovnar # Currencies — the '$' sigil is required: .price = 19.99; # US Dollar .btc = 54782000; # Bitcoin (satoshis) .fund = 250.0; # kilo-Euro (prefix before the sigil) .spot = 2351.40; # currency / physical-unit compound # A bare code is no longer a currency: .volume = 2.0; # the physical 'cup', unambiguously # .bad = 1.0; # error_unit_illegal: bare 'USD' is not a unit (needs '$') ``` The sigil attaches directly before the currency code, after any SI/IEC prefix and its `~` (`k~$EUR`, `M~$USDT`). It is accepted in inline units and type annotations alike, and the writer emits it on output so values round-trip. Because this **breaks** any document that used bare currency codes, it was made before the 1.0 freeze — afterwards it would be an incompatible change (see the Versioning & Stability section of the specification). --- ## 11. C Data Model ### 11.1 Enumerations #### `prefix_system_t` ```c typedef enum prefix_system_e { prefix_si, /* SI decimal prefixes (or no prefix) */ prefix_iec /* IEC binary prefixes */ } prefix_system_t; ``` #### `si_prefix_id_t` ```c typedef enum si_prefix_id_e { si_none = 0, si_quecto, si_ronto, si_yocto, si_zepto, si_atto, si_femto, si_pico, si_nano, si_micro, si_milli, si_centi, si_deci, si_deca, si_hecto, si_kilo, si_mega, si_giga, si_tera, si_peta, si_exa, si_zetta, si_yotta, si_ronna, si_quetta } si_prefix_id_t; ``` #### `iec_prefix_id_t` ```c typedef enum iec_prefix_id_e { iec_none = 0, iec_kibi, iec_mebi, iec_gibi, iec_tebi, iec_pebi, iec_exbi, iec_zebi, iec_yobi, iec_robi, iec_quebi } iec_prefix_id_t; ``` #### `value_base_unit_t` Non-German physical units occupy positions 1–133 (`bu_bit` … `bu_bushel`). Currency codes occupy positions 134–347 — an unnamed slot range (no `bu_*` enumerators; see §9.2/§9.3). German physical units are appended after the entire currency range at positions 348–360 (`bu_pfund` … `bu_scheffel`). Additional physical units occupy positions 361–367 (`bu_survey_foot` … `bu_baud`), historical temperature scales 368–371 (`bu_delisle` … `bu_romer`), and dimensionless ratio units 372–377 (`bu_percent` … `bu_ppb`). `bvn_unit_is_currency(base)` returns `true` for any value in the range 134–347 or the extension slots 378–379 (`bu_zwg`, `bu_xcg`). ```c typedef enum value_base_unit_e { bu_none = 0, /* dimensionless / no unit */ /* Digital */ bu_bit, bu_byte, /* SI base */ bu_second, bu_meter, bu_gram, bu_ampere, bu_kelvin, bu_mol, bu_candela, /* Named SI-derived */ bu_hertz, bu_newton, bu_pascal, bu_joule, bu_watt, bu_volt, bu_ohm, bu_farad, bu_coulomb, bu_siemens, bu_weber, bu_tesla, bu_henry, bu_lumen, bu_lux, bu_becquerel, bu_gray, bu_sievert, bu_katal, bu_liter, bu_minute, bu_hour, bu_day, bu_degree, bu_celsius, bu_radian, bu_steradian, bu_tonne, bu_bar, bu_electronvolt, bu_dalton, bu_astronomical_unit, bu_hectare, bu_week, bu_year, /* Imperial/US — length */ bu_inch, bu_foot, bu_yard, bu_mile, bu_nautical_mile, bu_angstrom, bu_light_year, bu_parsec, bu_furlong, bu_fathom, /* Imperial/US — mass */ bu_pound, bu_ounce, bu_grain, bu_stone, bu_short_ton, bu_long_ton, bu_troy_ounce, bu_carat, /* Temperature */ bu_fahrenheit, /* Pressure */ bu_atmosphere, bu_mmhg, bu_torr, bu_psi, /* Energy */ bu_calorie, bu_btu, bu_erg, bu_therm, /* Power */ bu_horsepower, /* Force */ bu_pound_force, bu_dyne, bu_kip, /* Speed */ bu_knot, /* Volume — US */ bu_gallon, bu_gallon_uk, bu_quart, bu_pint, bu_cup, bu_fluid_ounce, bu_tablespoon, bu_teaspoon, bu_barrel, /* Area */ bu_acre, bu_barn, /* Angle */ bu_arcminute, bu_arcsecond, bu_grad, /* CGS */ bu_poise, bu_stokes, bu_gauss, bu_maxwell, bu_oersted, bu_stilb, bu_phot, bu_galileo, /* Radiation */ bu_curie, bu_roentgen, bu_rem, /* Logarithmic */ bu_neper, bu_decibel, bu_rankine, /* Temperature — absolute Fahrenheit scale */ bu_slug, /* Imperial mass */ bu_thou, /* Imperial length */ /* Volume — UK imperial */ bu_pint_uk, bu_fluid_ounce_uk, bu_quart_uk, /* Electrical power */ bu_var, bu_volt_ampere, /* Force (additional) */ bu_kilogram_force, /* Pressure (additional) */ bu_inch_hg, /* Rotational frequency */ bu_rpm, /* Energy (additional) */ bu_foot_pound, /* Mass (additional) */ bu_dram, bu_pennyweight, /* Length (additional) */ bu_chain, bu_rod, /* Volume (additional) */ bu_gill, bu_gill_uk, /* Acceleration / gravity */ bu_standard_gravity, /* Power (additional) */ bu_metric_horsepower, /* Angle (additional) */ bu_revolution, /* Time (additional) */ bu_month, bu_fortnight, /* Pressure (additional) */ bu_atmosphere_technical, /* Textile */ bu_tex, bu_denier, /* Apothecary / dry volume */ bu_fluid_dram, bu_minim, bu_peck, bu_bushel, /* Slots 134–347 are the ISO 4217 fiat (134–297) and cryptocurrency * (298–347) ranges. These have NO named enumerators: the enum jumps * straight from bu_bushel (133) to bu_pfund (348). Currencies are * resolved by string via bvn_parse_currency_str and carried as the * numeric base value; the catalogue in bovnar_currency.c is index- * aligned to slots 134–347 (BVN_CURRENCY_FIAT_FIRST … CRYPTO_LAST). */ /* Old German — placed after the currency range */ bu_pfund = 348, bu_zentner, bu_doppelzentner, bu_lot, bu_prussian_line, bu_prussian_zoll, bu_prussian_fuss, bu_prussian_elle, bu_prussian_rute, bu_klafter, bu_german_mile, bu_morgen, bu_scheffel, /* = 360 */ /* Additional physical units */ bu_survey_foot, bu_league, bu_cable, bu_hand, bu_quintal, bu_scruple, bu_baud, /* = 367 */ /* Historical temperature scales */ bu_delisle, bu_newton_temp, bu_reaumur, bu_romer, /* = 371 */ /* Dimensionless ratio units */ bu_percent, bu_per_mille, bu_per_myriad, bu_per_cent_mille, bu_ppm, bu_ppb, /* = 377 */ /* ABI-stable currency extension segment, appended after the unit block. */ bu_zwg = 378, bu_xcg, /* = 378, 379 */ } value_base_unit_t; /* Total slot count — defined separately, not an enum member: */ /* #define BVN_VALUE_BASE_UNIT_COUNT 380 (bu_xcg + 1) */ ``` #### `unit_exponent_t` ```c typedef enum unit_exponent_e { exp_invalid = 0, exp_linear = 1, exp_square = 2, exp_cubic = 3, exp_quartic = 4, exp_quintic = 5, exp_sextic = 6, exp_septic = 7, exp_octic = 8, exp_nonic = 9, exp_neg_linear = -1, exp_neg_square= -2, exp_neg_cubic = -3, exp_neg_quartic= -4, exp_neg_quintic=-5, exp_neg_sextic= -6, exp_neg_septic = -7, exp_neg_octic = -8, exp_neg_nonic = -9, } unit_exponent_t; ``` `exp_invalid` (value `0`) is the zero-initialization sentinel. Always initialize components before use. ### 11.2 Structures #### `value_unit_component_t` ```c typedef struct value_unit_component_s { value_base_unit_t base; unit_exponent_t exponent; value_unit_prefix_t prefix; } value_unit_component_t; ``` where: ```c typedef struct value_unit_prefix_s { prefix_system_t system; union { si_prefix_id_t si; iec_prefix_id_t iec; } id; } value_unit_prefix_t; ``` #### `value_unit_t` ```c #define BVNR_MAX_UNIT_COMPONENTS 8 typedef struct value_unit_s { uint32_t num_components; value_unit_component_t components[BVNR_MAX_UNIT_COMPONENTS]; } value_unit_t; ``` For an explicit `no_unit` annotation, `num_components == 0` (`BVN_UNIT_NONE`). For an absent unit parameter in an explicit annotation (e.g. ``), `num_components == 1` with `base == bu_none`. #### `bvnr_data_t` (unit field) ```c typedef struct bvnr_data_s { token_type_t type; value_type_spec_t value_type; value_unit_t value_unit; /* parsed physical unit or currency */ const void* data; uint32_t length; const void* frac_data; /* spec 1.1 — ISO datetime sub-second digits, else NULL */ uint32_t frac_length; /* spec 1.1 — length of frac_data, else 0 */ } bvnr_data_t; ``` ### 11.3 Convenience Macros ```c /* Dimensionless or single-component without prefix */ #define BVN_UNIT_NO_PREFIX(b) \ ((value_unit_t){ .num_components = 1, .components = {{ \ .base=(b), .exponent=exp_linear, \ .prefix.system=prefix_si, .prefix.id.si=si_none }}}) /* Single-component with SI prefix */ #define BVN_UNIT_SI(b, p) \ ((value_unit_t){ .num_components = 1, .components = {{ \ .base=(b), .exponent=exp_linear, \ .prefix.system=prefix_si, .prefix.id.si=(p) }}}) /* Single-component with IEC prefix */ #define BVN_UNIT_IEC(b, p) \ ((value_unit_t){ .num_components = 1, .components = {{ \ .base=(b), .exponent=exp_linear, \ .prefix.system=prefix_iec, .prefix.id.iec=(p) }}}) /* Single-component with SI prefix and explicit exponent */ #define BVN_UNIT_SI_EXP(b, p, e) \ ((value_unit_t){ .num_components = 1, .components = {{ \ .base=(b), .exponent=(e), \ .prefix.system=prefix_si, .prefix.id.si=(p) }}}) /* Empty unit — explicit no_unit / internal sentinel */ #define BVN_UNIT_NONE ((value_unit_t){ .num_components = 0 }) /* Two-component compound unit, both SI-prefixed */ #define BVN_UNIT_COMPOUND2(b1,p1,e1, b2,p2,e2) \ ((value_unit_t){ .num_components = 2, .components = { \ { .base=(b1), .exponent=(e1), \ .prefix.system=prefix_si, .prefix.id.si=(p1) }, \ { .base=(b2), .exponent=(e2), \ .prefix.system=prefix_si, .prefix.id.si=(p2) }}}) ``` Usage: ```c value_unit_t kg = BVN_UNIT_SI(bu_gram, si_kilo); value_unit_t gib = BVN_UNIT_IEC(bu_byte, iec_gibi); value_unit_t m2 = BVN_UNIT_SI_EXP(bu_meter, si_none, exp_square); value_unit_t usd = BVN_UNIT_NO_PREFIX(bu_usd); /* single currency unit */ value_unit_t none = BVN_UNIT_NONE; ``` --- ## 12. C API Functions ### 12.1 Parsing a Unit String ```c value_unit_t bvn_parse_unit (const uint8_t* unit, bool* ok); value_unit_t bvn_parse_unit_n(const uint8_t* unit, uint32_t len, bool* ok); ``` `bvn_parse_unit` parses a NUL-terminated UTF-8 unit string. `bvn_parse_unit_n` is the length-bounded variant used internally when the unit string is a slice of a larger type-annotation buffer. Both set `*ok = false` on any parse error (unknown prefix, unknown base unit, unknown currency code, too many components, empty component, or NULL input). Single-pass parsing algorithm: 1. `memcmp` against `"no_unit"` → return `BVN_UNIT_NONE` immediately on match. 2. Scan for separator characters to distinguish simple vs. compound paths. 3. For compound units, split on `0x2A` (`*`), `0x2F` (`/`), `0xC2 0xB7` (`·`); parse each slice as a component; negate denominator exponents. 4. For each component, if it is introduced by the `$` sigil (after any prefix and its `~`), look the code up in the currency table; otherwise look it up in the physical unit table only. A bare code is never a currency. ```c bool ok; value_unit_t u = bvn_parse_unit((const uint8_t *)"k~g·m/s²", &ok); /* u.num_components == 3: [0]: bu_gram, exp_linear, si_kilo [1]: bu_meter, exp_linear, si_none [2]: bu_second, exp_neg_square, si_none */ value_unit_t c = bvn_parse_unit((const uint8_t *)"USD/oz_t", &ok); /* c.num_components == 2: [0]: bu_usd, exp_linear, si_none (currency) [1]: bu_troy_ounce, exp_neg_linear, si_none */ ``` ### 12.2 Serializing a Unit ```c int32_t bvn_unit_to_string (value_unit_t u, char* buf, size_t bufsize); int32_t bvn_unit_to_string_ex(value_unit_t u, char* buf, size_t bufsize, bvn_unit_flags_t flags); ``` Serializes `u` to a canonical UTF-8 string. Returns the number of bytes written (excluding NUL) or `-1` on buffer overflow or invalid component. | Flag | Effect | |------|--------| | `BVN_UNIT_FLAGS_NONE` | Unicode superscript exponents, no reduction | | `BVN_UNIT_ASCII_EXP` | ASCII caret form (`^N`) for all exponents | | `BVN_UNIT_REDUCE` | Reduce via `bvn_unit_reduce` before serializing | ```c char buf[64]; value_unit_t u = /* k~g·m/s² */; bvn_unit_to_string(u, buf, sizeof(buf)); /* buf == "k~g·m/s²" */ bvn_unit_to_string_ex(u, buf, sizeof(buf), BVN_UNIT_ASCII_EXP); /* buf == "k~g*m/s^2" */ ``` #### Validation Predicate ```c bool bvn_unit_valid(value_unit_t u); ``` Returns `true` if every component has a valid exponent, known base unit (physical or currency), and a legal prefix for that base. Both serialization functions call this before writing. ### 12.3 Prefix Factor and Exponent Queries ```c double bvn_unit_prefix_factor (value_unit_t u); int32_t bvn_unit_prefix_exponent(value_unit_t u); ``` `bvn_unit_prefix_factor` returns the multiplicative scale contributed by the prefixes, ignoring base-unit identity. For `si_none`/`iec_none` prefixes the factor is 1.0. `bvn_unit_prefix_exponent` returns the sum of `(prefix_base_exponent × |unit_exponent|)` across all components. ### 12.4 SI Conversion API Functions in `bovnar_si_units.h` provide dimensional analysis, compatibility checking, and value conversion between compatible physical units. **These functions reject currency units** — `bvn_find_si_conv` returns `NULL` for any `value_base_unit_t` for which `bvn_unit_is_currency` is true, causing `*ok = false`. ```c /* Full SI factor (physical units only) */ double bvn_unit_to_si_factor(value_unit_t u, bool *is_affine, double *affine_offset, bool *ok); /* SI dimension vector */ bool bvn_unit_dimension_vector(value_unit_t u, int32_t dims[bvn_si_dim_count]); /* Dimensional compatibility check */ bool bvn_units_compatible(value_unit_t a, value_unit_t b); /* Conversion factor: value_in_b = value_in_a × k */ double bvn_unit_convert_factor(value_unit_t a, value_unit_t b, bool *ok, bool *requires_affine); /* Unit reduction */ value_unit_t bvn_unit_reduce(value_unit_t u, double *scale, bool *overflow); /* Prefix validity */ bool bvn_prefix_unit_valid(value_unit_prefix_t prefix, value_base_unit_t base); /* Exponent integer conversion */ int32_t bvn_exponent_to_int (unit_exponent_t e); unit_exponent_t bvn_int_to_exponent(int32_t n); ``` For affine units (`bu_celsius`, `bu_fahrenheit`), `*is_affine` is set to `true` and `*affine_offset` receives the additive offset applied after multiplying by the returned factor. An affine unit is valid at exponent 1 only. ```c bool ok, affine; double offset; value_unit_t u = bvn_parse_unit((const uint8_t *)"°C", &ok); double f = bvn_unit_to_si_factor(u, &affine, &offset, &ok); /* f == 1.0, affine == true, offset == 273.15 */ ``` ### 12.5 Currency API ```c #include "bovnar_currency.h" /* Classification */ bool bvn_unit_is_currency(int base); bool bvn_unit_is_fiat (int base); bool bvn_unit_is_crypto (int base); /* Minor-unit exponent: 1 major unit = 10^N minor units */ uint8_t bvn_currency_minor_unit(int base, bool *ok); /* Full currency metadata */ const bvn_currency_info_t *bvn_currency_info(int base); /* Look up by 3–4 char code string; returns 0 (bu_none) on failure */ int bvn_parse_currency_str(const uint8_t *s, uint32_t len); /* Prefix validity for a specific currency */ bool bvn_currency_prefix_valid(int base, int prefix_system); ``` The `bvn_currency_info_t` structure: ```c typedef struct { char code[5]; /* "USD", "BTC", etc. */ uint16_t numeric_code; /* ISO 4217 numeric code (0 for crypto) */ uint8_t minor_unit; /* decimal places */ bool is_crypto; /* true for cryptocurrencies */ char name[48]; /* "US Dollar", "Bitcoin", etc. */ } bvn_currency_info_t; ``` Examples: ```c bool ok; uint8_t n = bvn_currency_minor_unit(bu_kwd, &ok); /* n=3, ok=true */ uint8_t m = bvn_currency_minor_unit(bu_jpy, &ok); /* m=0, ok=true */ uint8_t x = bvn_currency_minor_unit(bu_meter, &ok);/* x=0, ok=false */ const bvn_currency_info_t *ci = bvn_currency_info(bu_usd); /* ci->code="USD", ci->numeric_code=840, ci->minor_unit=2, ci->is_crypto=false, ci->name="US Dollar" */ int cv = bvn_parse_currency_str((const uint8_t *)"EUR", 3); /* cv=177 */ int cc = bvn_parse_currency_str((const uint8_t *)"DOGE", 4); /* cc=326 */ int cx = bvn_parse_currency_str((const uint8_t *)"xyz", 3); /* cx=0 */ /* Distinguishing cup (volume) from CUP (currency) in code: */ value_unit_t volume = bvn_parse_unit((const uint8_t *)"cup", &ok); value_unit_t currency = bvn_parse_unit((const uint8_t *)"CUP", &ok); assert(!bvn_unit_is_currency(volume.components[0].base)); /* true */ assert( bvn_unit_is_currency(currency.components[0].base)); /* true */ ``` ### 12.6 Python API ```python from bovnar.enums import BaseUnit from bovnar.currency import ( is_currency, is_fiat, is_crypto, minor_unit, currency_info, currency_name, from_code, all_fiat, all_crypto, ) assert is_currency(BaseUnit.USD) # True assert is_fiat(BaseUnit.XAU) # True (gold is ISO 4217 X-code) assert is_crypto(BaseUnit.ETH) # True assert not is_fiat(BaseUnit.BTC) # True (BTC is crypto, not fiat) assert minor_unit(BaseUnit.USD) == 2 # cents assert minor_unit(BaseUnit.JPY) == 0 # indivisible assert minor_unit(BaseUnit.KWD) == 3 # fils assert minor_unit(BaseUnit.BTC) == 8 # satoshis assert minor_unit(BaseUnit.ETH) == 18 # wei info = currency_info(BaseUnit.EUR) assert info.code == "EUR" assert info.numeric_code == 978 assert info.minor_unit == 2 btc = from_code("BTC") assert btc == BaseUnit.BTC fiat_count = sum(1 for _ in all_fiat()) # 166 crypto_count = sum(1 for _ in all_crypto()) # 50 ``` --- ## 13. Integration with the Parser Event Stream Unit information flows into the application through two paths: 1. **Type-annotation unit** — parsed from `` by the lexer, validated by the validator, delivered in the `ev_type_annotation_type_family_parameter` unit event. 2. **Inline unit suffix** — parsed from the suffix following a scalar value literal before the terminating `;`. In both cases the effective unit is reported in the `bvnr_data_t.value_unit` field of the `ev_data` event. #### Full event sequence — physical unit ``` Input: .force = 9.81; ev_assignment_start data = "force" ev_type_annotation_start data = "float:64,k~g·m/s²" ev_type_annotation_type_family data = "float" ev_type_annotation_type_family_parameter ← width=64 ev_type_annotation_type_family_parameter ← unit: value_unit = { num_components=3, [0] bu_gram, exp_linear, {prefix_si, si_kilo} [1] bu_meter, exp_linear, {prefix_si, si_none} [2] bu_second, exp_neg_square, {prefix_si, si_none} } ev_type_annotation_end ev_data data="9.81" ``` #### Full event sequence — currency unit ``` Input: .price = 19.99; ev_assignment_start data = "price" ev_type_annotation_start data = "float_dec:64,$USD" ev_type_annotation_type_family data = "float_dec" ev_type_annotation_type_family_parameter ← width=64 ev_type_annotation_type_family_parameter ← unit: value_unit = { num_components=1, [0] USD, exp_linear, {prefix_si, si_none} } ev_type_annotation_end ev_data data="19.99" ``` For events with an explicit type annotation but no unit parameter, the unit event is **not emitted**. For `no_unit`, the unit event IS emitted with `BVN_UNIT_NONE` (`num_components=0`). For synthesised (default) type annotations, the unit event IS emitted with `BVN_UNIT_NO_PREFIX(bu_none)`. #### Inline unit suffix — event stream view ``` Input: .distance = 1500 m; ev_assignment_start data = "distance" ev_type_annotation_start data = "uint" ← synthesised ev_type_annotation_type_family data = "uint" ev_type_annotation_type_family_parameter ← width=64 (synthesised) ev_type_annotation_type_family_parameter ← base=10 (synthesised) ev_type_annotation_type_family_parameter ← unit: BVN_UNIT_NO_PREFIX(bu_none) ev_type_annotation_end ev_data data="1500" value_unit = { num_components=1, [0] bu_meter, exp_linear, {prefix_si, si_none} } ``` The `value_unit` field of `ev_data` always reflects the final, reconciled unit. #### Practical callback ```c bool my_verified_handler(void* userdata, bvnr_event_t ev, bvnr_data_t* d) { if (ev != ev_type_annotation_type_family_parameter) return true; value_unit_t u = d->value_unit; if (u.num_components == 0 || (u.num_components == 1 && u.components[0].base == bu_none)) return true; if (bvn_unit_is_currency(u.components[0].base)) { const bvn_currency_info_t *ci = bvn_currency_info(u.components[0].base); printf("currency: %s minor_unit=%u\n", ci->code, ci->minor_unit); return true; } char unit_str[128]; bvn_unit_to_string(u, unit_str, sizeof(unit_str)); printf("unit: %s (prefix_factor: %g)\n", unit_str, bvn_unit_prefix_factor(u)); return true; } ``` --- ## 14. Validation Errors The validator raises the following unit-specific errors: | Error code | Value | Trigger condition | |------------|-------|-------------------| | `error_unit_illegal` | 32 | Unparseable unit string: unknown prefix, unknown base unit, unknown currency code after `$`, a bare token in neither the physical-unit table nor (lacking the `$` sigil) recognised as a currency (e.g. `XYZ`, or bare `USD`), invalid prefix–unit combination (e.g. IEC prefix on a currency, sub-kilo SI prefix on byte), empty component between separators (e.g. `m//s`), or more than 8 components | | `error_unit_too_long` | 22 | Unit string exceeds the internal type-buffer size limit | | `error_unit_mismatch` | 38 | An inline unit suffix and an explicit type-annotation unit are both present, but parse to different `value_unit_t` representations | | `error_unexpected_input_byte` | 15 | An inline unit suffix appears inside an array element | All four errors are raised during the `on_unverified` → validator phase. In `continue_on_error` mode the parser invokes `on_error` and enters the resync state machine, which skips to the next `;` at the current nesting depth. `bvnr_reader_get_error`, `…_line`, `…_column`, `…_byte`, and `…_offset` all report the location of the offending token. --- ## 15. Annotated Examples ### 15.1 Physical Quantities ```bovnar # Thermodynamic temperature .ambient_temp = 293.15; # Temperature in Celsius (affine: K = °C + 273.15) .room_temp = 20.0; # Velocity and acceleration .wind_speed = 12.5; .gravity = 9.80665; .gravity_asc = 9.80665; # ASCII caret — identical # Pressure and energy .tire_pressure = 250.0; .heat_energy = 5400.0; # Flow rate (liters per minute) .pump_flow = 15.0; # Angles .bearing = 270.0; .phase = 1.5708; # Volume — US culinary .recipe_water = 2.0; # "cup" (lowercase) = US cup .recipe_flour = 3.0; # Duration .shelf_life = 52; .service_life = 10.0; ``` ### 15.2 Digital Storage ```bovnar .packet_size = 1500; .cache_size = 512; .ram_size = 4096; .disk_size = 500; .link_rate = 1000; .nic_speed = 10.0; ``` ### 15.3 Compound SI Quantities ```bovnar # Force: Newton = k~g·m/s² .force = 9.81; .force_alt = 9.81; # identical internal form # Energy: Joule = k~g·m²/s² .kinetic_energy = 1000.0; # Electric field .field_strength = 150.0; # Torque .torque = 25.0; ``` ### 15.4 Currency Amounts and Rates ```bovnar # ── Fiat scalar amounts ──────────────────────────────────────────────────── .price_usd = 19.99; .balance_eur = 342.00; .yen_fee = 500; # zero minor unit — integer only .kwd_invoice = 3500; # 3.500 KWD in fils # "CUP" (uppercase) is the Cuban Peso, NOT the US cup volume unit: .cup_balance = 25.00; # 25 Cuban Pesos # ── Crypto scalar amounts ────────────────────────────────────────────────── .btc_sat = 54782000; # on-chain satoshis .eth_readable = 2.5; .doge_bag = 42000.0; .usdt_stable = 5000.00; # ── Compound units ───────────────────────────────────────────────────────── .gold_price = 2351.40; # $/troy oz .wheat = 5.82; # $/bushel .rent = 12.50; # €/m² .billing_rate = 150.00; # €/h .eur_usd = 1.0842; # exchange rate # ── Reporting scale ──────────────────────────────────────────────────────── .fund_nav = 250.0; # $250,000 .gdp = 42800.0; # €42.8 billion # ── Exchange rate with timestamp ─────────────────────────────────────────── .snapshot = { .epoch = 1716400000; .eur_usd = 1.0842; }; # ── Array of prices ──────────────────────────────────────────────────────── .tier_prices = [9.99, 19.99, 49.99, 99.99]; ``` ### 15.5 Error Cases ```bovnar # Empty component → error_unit_illegal .bad1 = 1.0; # Too many components (9 > 8) → error_unit_illegal .bad2 = 1.0; # Unknown base unit → error_unit_illegal .bad3 = 1.0; # IEC prefix on a currency → error_unit_illegal .bad4 = 1.0; # Annotation unit differs from inline unit → error_unit_mismatch .bad5 = 1.0 s; # Inline unit inside an array → error_unexpected_input_byte .bad6 = [1.0 m, 2.0 m]; # ERROR: suffix inside array # Correct: dimensionless explicit .ok1 = 42; # Correct: omitted unit (same behaviour as no_unit) .ok2 = 42; # Correct: BTU is a valid alias for bu_btu (currency lookup returns 0, physical table matches) .ok3 = 1.0; # valid: same as Btu or btu # Correct: sub-kilo SI prefix on currency is accepted .ok4 = 0.001; # valid: milli-dollar (one tenth of a cent) # Correct: cup (volume) vs CUP (currency) — both valid, different meaning .vol = 2.0; # US cup (236.6 mL) .bal = 25.00; # Cuban Peso ``` --- *End of Bovnar Quantity Annotation System — Unit and Currency Reference, v1.1.* # Bovnar — Read & Write API > **Spec version:** 1.1 This document covers every function you need to read and write Bovnar streams, in the order you call them. Nothing else is included. The writer uses the same event/data model as the reader — `bvnr_event_t` and `bvnr_data_t` — so the two APIs are deliberately symmetric. Learn one, the other follows. --- ## Contents **Reader** 1. [`bvnr_reader_create` / `bvnr_reader_destroy`](#1-bvnr_reader_create--bvnr_reader_destroy) 2. [`bvnr_source_from_fd`](#2-bvnr_source_from_fd) 3. [`bvnr_source_from_mem`](#3-bvnr_source_from_mem) 4. [`bvnr_open_read_source`](#4-bvnr_open_read_source) 5. [`bvnr_open_read_mem`](#5-bvnr_open_read_mem) 6. [`bvnr_read`](#6-bvnr_read) 7. [`bvnr_reader_get_error` and friends](#7-bvnr_reader_get_error-and-friends) 7a. [Version directive (spec 1.1)](#7a-version-directive-spec-11) 7b. [Datetime family (spec 1.1)](#7b-datetime-family-spec-11) 7c. [Read-time lossless unit / base conversion (`want_unit`)](#7c-read-time-lossless-unit--base-conversion-want_unit) 8. [`bvn_parse_uint64` / `bvn_parse_int64` / `bvn_parse_double`](#8-bvn_parse_uint64--bvn_parse_int64--bvn_parse_double) **Writer** 9. [`bvnr_writer_create` / `bvnr_writer_destroy`](#9-bvnr_writer_create--bvnr_writer_destroy) 10. [`bvnr_sink_to_fd`](#10-bvnr_sink_to_fd) 11. [`bvnr_sink_to_mem`](#11-bvnr_sink_to_mem) 12. [`bvnr_sink_bytes_written`](#12-bvnr_sink_bytes_written) 13. [`bvnr_open_write_sink`](#13-bvnr_open_write_sink) 14. [`bvnr_open_write_mem`](#14-bvnr_open_write_mem) 15. [`bvnr_write_event`](#15-bvnr_write_event) 16. [`bvnr_write_finish`](#16-bvnr_write_finish) 17. [`bvnr_writer_get_error` and friends](#17-bvnr_writer_get_error-and-friends) 18. [`bvn_format_uint64` / `bvn_format_int64` / `bvn_format_double`](#18-bvn_format_uint64--bvn_format_int64--bvn_format_double) 19. [`bvnr_write_bvnf_base` / `bvnr_write_bvnf_base_unit`](#19-bvnr_write_bvnf_base--bvnr_write_bvnf_base_unit) 20. [`bvnr_write_bvni` / `bvnr_write_bvni_unit`](#20-bvnr_write_bvni--bvnr_write_bvni_unit) 21. [`BVN_TYPE_FLOAT_BASE`](#21-bvn_type_float_base) **Shared** 22. [`bvnr_write_type_annotation`](#22-bvnr_write_type_annotation) 23. [`bvn_parse_unit`](#23-bvn_parse_unit--bvn_parse_unit_n) 24. [`bvn_unit_to_string`](#24-bvn_unit_to_string--bvn_unit_to_string_ex) 25. [`bvn_error_to_string`](#25-bvn_error_to_string) **DOM** 26. [DOM API (`bovnar_dom.h`)](#dom-api-bovnar_domh) --- ## Reader --- ### 1. `bvnr_reader_create` / `bvnr_reader_destroy` ```c bvnr_reader_t *bvnr_reader_create(void); void bvnr_reader_destroy(bvnr_reader_t *r); ``` Allocate and free a reader on the heap. `bvnr_reader_create` returns `NULL` if allocation fails. Always pair with `bvnr_reader_destroy` when done. ```c bvnr_reader_t *r = bvnr_reader_create(); if (!r) { perror("alloc"); exit(1); } /* ... open, read ... */ bvnr_reader_destroy(r); ``` --- ### 2. `bvnr_source_from_fd` ```c void bvnr_source_from_fd(bvnr_source_t *s, int fd); ``` Initialise the source `s` to read from an open, readable POSIX file descriptor. The caller retains ownership of `fd`; the library will not close it. - `s` — pointer to a caller-allocated `bvnr_source_t` (stack is fine). - `fd` — any readable descriptor: a file, a pipe, a socket. ```c bvnr_source_t src; int fd = open("config.bvnr", O_RDONLY); if (fd < 0) { perror("open"); exit(1); } bvnr_source_from_fd(&src, fd); /* pass &src to bvnr_open_read_source */ ``` --- ### 3. `bvnr_source_from_mem` ```c void bvnr_source_from_mem(bvnr_source_t *s, const void *buf, uint64_t len); ``` Initialise the source `s` to read from an in-memory buffer. The buffer must remain valid for the duration of `bvnr_read`. No copy is made. - `buf` — pointer to the Bovnar data. - `len` — number of bytes in the buffer. ```c static const char payload[] = ".host = \"localhost\";\n" ".port = 8080;\n"; bvnr_source_t src; bvnr_source_from_mem(&src, payload, sizeof(payload) - 1); ``` --- ### 4. `bvnr_open_read_source` ```c bool bvnr_open_read_source(bvnr_reader_t *r, const bvnr_source_t *src, const bvnr_sink_t *src_mirror, bvnr_read_flags_t *options); ``` Attach the source to the reader and configure it. Must be called before `bvnr_read`. Returns `false` on invalid arguments (`error_invalid_argument`). - `r` — reader obtained from `bvnr_reader_create`. - `src` — source initialised with one of the `bvnr_source_from_*` functions. - `src_mirror` — optional sink that mirrors the raw input bytes as they are consumed (useful for debugging; pass `NULL` in production). - `options` — configuration struct. Zero-initialise to get all defaults. The most important fields are: ```c typedef struct bvnr_read_flags_s { void *userdata; bool (*on_unverified)(void *userdata, bvnr_event_t, bvnr_data_t *); bool (*on_verified) (void *userdata, bvnr_event_t, bvnr_data_t *); bool continue_on_error; bvnr_on_error_fn on_error; uint64_t max_file_size; /* 0 → unlimited / endless (default); set positive to cap */ uint8_t max_struct_nesting; /* 0 → 64 internal default; hard cap 255 */ uint8_t max_array_nesting; /* 0 → 64 internal default; hard cap 255 */ /* ... other size limits ... */ } bvnr_read_flags_t; ``` `on_verified` is the callback you will implement almost always. `on_unverified` fires before semantic validation — use it only for diagnostics or partial inspection. The one exception is read-time unit conversion: `want_unit` (§7c) runs ahead of **both** callbacks, so with it installed an `on_unverified` consumer also sees a populated `converted`/`conv` on `ev_data`. Both callbacks must return `true` to continue parsing, `false` to abort (sets `error_scanner_callback_failed`). The `options` pointer is not stored; the struct is read during `bvnr_open_read_source` only, so it may live on the stack. **`max_text_bytes` is not a total-size cap.** It counts the bytes the *text* scanner consumes; an octet stream's binary payload does not count towards it. A 212-byte document whose body is a 200-byte octet stream is accepted at `max_text_bytes = 8`. If you are bounding memory or guarding against a hostile input, `max_file_size` is the one that counts every byte. Both are exact at their boundary: a document needing N is accepted at N and refused at N-1. **Reader default limits.** When a `bvnr_read_flags_t` field is set to `0`, the reader substitutes an internal default. For the nesting fields, the default is **64** (not 255); the hard maximum is 255. For `max_array_items` and `max_text_bytes`, the default is **2 147 483 647** — permissive but finite. **`max_file_size` differs: `0` means unlimited / endless** (no byte-count cap accumulated), which is the default so endless streaming works out of the box. Setting `max_file_size` explicitly to `16777216` (16 MiB) is recommended for production. ```c static bool on_event(void *ud, bvnr_event_t ev, bvnr_data_t *d) { /* handle ev / d ... */ return true; } bvnr_read_flags_t opts = { .on_verified = on_event, .userdata = &my_ctx, .max_file_size = 16777216, /* 16 MiB cap */ /* max_array_nesting: 0 → 64 internal default; hard cap 255 */ }; if (!bvnr_open_read_source(r, &src, NULL, &opts)) { fprintf(stderr, "open failed\n"); return -1; } ``` --- ### 5. `bvnr_open_read_mem` ```c bool bvnr_open_read_mem(bvnr_reader_t *r, const void *buf, uint64_t len, void *mirror_buf, uint64_t mirror_cap, bvnr_read_flags_t *options); ``` Convenience wrapper that constructs a memory source (and optionally a memory mirror sink) internally. Equivalent to calling `bvnr_source_from_mem` followed by `bvnr_open_read_source`. Pass `NULL` / `0` for `mirror_buf` / `mirror_cap` to skip mirroring. ```c bvnr_read_flags_t opts = { .on_verified = on_event, .userdata = &ctx }; if (!bvnr_open_read_mem(r, payload, payload_len, NULL, 0, &opts)) return -1; ``` --- ### 6. `bvnr_read` ```c bool bvnr_read(bvnr_reader_t *r); ``` Drive the parser until EOF or a fatal error. Fires the registered callbacks for every event. Returns `true` on clean completion, `false` on any error. This is the only call needed after `bvnr_open_read_*`. The reader does not allocate during this call; all buffering is internal to the reader struct. ```c if (!bvnr_read(r)) { fprintf(stderr, "parse error: %s at line %" PRIu64 " col %" PRIu64 "\n", bvn_error_to_string(bvnr_reader_get_error(r)), bvnr_reader_get_error_line(r), bvnr_reader_get_error_column(r)); bvnr_reader_destroy(r); return -1; } ``` --- ### 7. `bvnr_reader_get_error` and friends ```c error_code_t bvnr_reader_get_error (const bvnr_reader_t *r); uint64_t bvnr_reader_get_error_line (const bvnr_reader_t *r); uint64_t bvnr_reader_get_error_column (const bvnr_reader_t *r); uint64_t bvnr_reader_get_error_offset (const bvnr_reader_t *r); uint32_t bvnr_reader_get_error_byte (const bvnr_reader_t *r); uint64_t bvnr_reader_get_recovery_count(const bvnr_reader_t *r); ``` The three position counters measure **different things**, and a tool placing a caret needs the right one: | Getter | Unit | Base | Note | |---|---|---|---| | `..._line` | lines | 1 | | | `..._column` | **characters** | 1 | a multi-byte UTF-8 sequence advances it by one; a tab advances to the next multiple of 4 | | `..._offset` | **bytes** | 0 | use this to index the input buffer | In `.x = "café"; .y = @;` the `@` is byte 20 but column 19. This matters here more than in most formats: unit symbols are routinely non-ASCII (`µ~m`, `°C`, `Ω`), so the two disagree often. All five error/location getters above (everything except `bvnr_reader_get_recovery_count`) are only meaningful when `bvnr_read` returned `false` (or after a recoverable error when `continue_on_error` is set). `bvnr_reader_get_error_byte` returns the raw byte value that triggered the error. `bvnr_reader_get_recovery_count` is the exception: it returns how many errors triggered entry into resync mode in `continue_on_error` mode (and so is meaningful even when `bvnr_read` ultimately returned `true`). This count is incremented at error entry, not when resync completes at `";". ```c if (!bvnr_read(r)) { error_code_t ec = bvnr_reader_get_error(r); fprintf(stderr, "%s at line %" PRIu64 ", col %" PRIu64 ", offset %" PRIu64 ", byte 0x%02X\n", bvn_error_to_string(ec), bvnr_reader_get_error_line(r), bvnr_reader_get_error_column(r), bvnr_reader_get_error_offset(r), bvnr_reader_get_error_byte(r)); } ``` --- ### 7a. Version directive (spec 1.1) ```c bool bvnr_reader_get_declared_version( const bvnr_reader_t *r, uint16_t *major, uint16_t *minor); bool bvnr_peek_version( const void *buf, uint64_t len, uint16_t *major, uint16_t *minor); uint32_t bvnr_version(void); const char *bvnr_version_string(void); void bvnr_spec_version(uint16_t *major, uint16_t *minor); ``` A document may begin with a `#!bovnar .` directive (see spec §3.4). After `bvnr_read`, `bvnr_reader_get_declared_version` returns `true` and fills `major`/`minor` when the document carried one (either out pointer may be NULL). Set `bvnr_read_flags_t.strict_version` to reject a version newer than this build supports with `error_unsupported_spec_version`; by default such a version is recorded but accepted. A malformed directive is always `error_invalid_spec_version`. `bvnr_peek_version` scans a raw buffer for the directive without a full parse (handy before opening a writer to round-trip it). `bvnr_version` / `bvnr_version_string` return the library version; `bvnr_spec_version` returns the highest spec version this build understands (`BVNR_SPEC_VERSION_*`). ```c uint16_t maj, min; if (bvnr_reader_get_declared_version(r, &maj, &min)) printf("document declares bovnar %u.%u\n", maj, min); ``` To emit a directive, call `bvnr_write_version` right after opening the writer (see §13), or set `bvnr_write_flags_t.emit_version` to stamp the current spec version automatically. --- ### 7b. Datetime family (spec 1.1) ```c const char *bvnr_datetime_epoch_name (value_type_spec_t vt); int32_t bvnr_datetime_epoch_mjd (value_type_spec_t vt); int32_t bvnr_datetime_epoch_index(const char *name); bool bvnr_write_datetime(bvnr_writer_t *w, const char *key, uint32_t width, const char *epoch, int64_t value); ``` A `` value (family `vt_datetime`) is a **signed integer count of seconds since a named epoch** — a timestamp, distinct from a *duration* (a number with a time unit, e.g. ``). The carrier is validated like `sint`; the epoch is stored as a small dense index in `value_type_spec_t.base` (not a numeric base — the carrier is always decimal). These two helpers recover the epoch from a spec: `bvnr_datetime_epoch_name` returns its lowercase name (`"unix"` — the default — `"tai"`, `"gps"`, `"mjd"`, `"ntp"`, `"galileo"`, `"glonass"`, `"y2000"`, `"beidou"`), and `bvnr_datetime_epoch_mjd` returns its Modified Julian Day (the `bvn_epoch_t` value from `bvn_datetime.h`). Pass that to `bvn_dt_epoch_seconds_to_datetime()` to convert to a civil date/time. ```c /* on a datetime data event: */ int64_t secs; bvn_parse_int64((const char *)d->data, d->value_type, &secs); bvn_datetime_t civil; bvn_dt_epoch_seconds_to_datetime(&civil, (bvn_epoch_t)bvnr_datetime_epoch_mjd(d->value_type), secs); ``` To **write** a datetime, use the typed helper `bvnr_write_datetime` — `epoch` is an epoch name (or `NULL` for unix; an unknown name is `error_invalid_argument`), `value` is signed seconds. `bvnr_datetime_epoch_index` maps a name to the index stored in `value_type_spec_t.base` (the inverse of `bvnr_datetime_epoch_name`), for building a spec by hand. The document must declare `#!bovnar 1.1` to be re-read (emit the directive with `bvnr_write_version`). ```c bvnr_write_version(w, 1, 1); bvnr_write_datetime(w, "created", 64, "gps", 1750000000); /* */ ``` The family is spec 1.1: it requires a `#!bovnar 1.1` declaration, and in a 1.0/unversioned document a `datetime` annotation is `error_illegal_value_type`. **ISO-8601 literals and fractional seconds.** A datetime may be written as an ISO-8601 literal (`2026-06-15T12:00:00.5Z`) instead of a raw integer; the reader converts it to the whole-second carrier you receive in `d->data`. When the literal carries a fractional second, the verbatim digits (no leading `.`) are delivered alongside the carrier in two `bvnr_data_t` fields, `frac_data` and `frac_length` — `NULL`/`0` for every other value. The carrier is unchanged (the value floors to the written second), so the fraction is informational, but it lets a consumer see sub-second precision the integer cannot hold, and the writer re-emits it: a datetime data event whose `frac_data` is set is serialised back as an ISO literal so the value round-trips. Like `d->data`, `frac_data` is **not NUL-terminated** — bound the read by `frac_length`. ```c /* on a datetime data event: */ if (d->frac_data && d->frac_length) { /* d->frac_data[0 .. frac_length) are the sub-second digits, e.g. "5" */ } ``` --- ### 7c. Read-time lossless unit / base conversion (`want_unit`) ```c /* bvnr_read_flags_t */ bool (*want_unit)(void *userdata, const bvnr_data_t *data, value_unit_t *want, uint32_t *want_base); /* bvnr_data_t */ bool converted; /* true when this value was converted */ bvnr_converted_t conv; /* the exact converted value (see below) */ typedef struct bvnr_converted_s { value_unit_t unit; /* the target unit */ const char* text; /* exact value in `base`, NUL-terminated; * NULL if it does not terminate in `base` */ uint32_t length; /* strlen(text), 0 when text is NULL */ uint32_t base; /* base text is rendered in (2..62, 64, 85) */ const struct bvn_int_s* num; /* exact numerator (signed) */ const struct bvn_int_s* den; /* exact denominator (> 0) */ } bvnr_converted_t; /* bvnr_read_flags_t */ bool want_unit_allow_nonterminating; /* opt in to rational-only results */ uint32_t max_conversion_length; /* work limit; 0 = 1024 */ ``` By default the reader hands you every numeric value exactly as written and you convert it yourself (§24a, `bvn_unit_convert_rational`). Set `want_unit` to have the reader do it for you — **losslessly**, in exact arbitrary-precision arithmetic, for a value of any width and any base. When `want_unit` is non-NULL, the reader calls it for every numeric value (with or without a unit) after validation and before **either** value callback — it can abort the parse, and the two views of one value must not disagree. An `on_unverified` consumer therefore also sees a populated `converted`/`conv` on `ev_data`, even though everything else about that event is still the pre-validation view. Inspect `data->value_unit` (native unit), `data->value_type`, or `data->data` and either: - fill `*want` with the target unit and `*want_base` with the output base and **return `true`**, or - **return `false`** (or leave `want_unit` NULL) to receive the value untouched. `*want_base` accepts any base bvnr can write — `2..62` plus `64` (Base64) and `85` (Ascii85) — or `0` to keep the value's own. Anything else is an error, never a quiet substitution. Note that `64` and `85` have no sign character, so a negative result in those is rejected too. Requesting `*want` equal to the native unit with a different `*want_base` performs a pure **base conversion** (e.g. a hex integer delivered in decimal). The choice is usually key-specific; track "the current key" (from the earlier `ev_assignment_start` event) in your `userdata`. On a valid request the value arrives with `data->converted == true` and `data->conv` holding the **exact** result — the value in the requested unit and base as both a positional string (`conv.text`) and a reduced rational (`conv.num`/`conv.den`, `bvn_int.h`). Everything in `conv` is reader-owned and valid only for the callback — copy `text` (or the rational) to retain it. `data->data` / `data->value_unit` keep the original digits and unit. **The conversion is lossless.** A 1056-bit binary float or a 512-bit integer converts with no loss beyond the library's own declared factor — the result widens as needed rather than rounding into a `double`. Every numeric family is eligible: `uint`/`sint` of any width and base (including a multiprecision integer or a non-decimal base written as a string literal like ` "FF"`), `float`, `float_dec`, `float_fix`. A datetime is never offered. Once the hook has asked for a conversion, the value either arrives converted or the parse **stops**. Nothing approximate is delivered, and nothing is silently skipped: | Condition | Error | |-----------|-------| | `*want` dimensionally incompatible with the value's unit (seconds for a length; one currency for another) | `error_unit_mismatch` | | the true factor is irrational (a π-based angle, e.g. degree → radian) | `error_unit_inexact` | | the exact result has no terminating expansion in `*want_base` (e.g. `1 m → mile` in base 10), and `want_unit_allow_nonterminating` is off | `error_unit_inexact` | | the literal is finite but too extreme to build an exact rational from (e.g. `1e1000000`) | `error_value_out_of_range` | | `*want_base` is not a base bvnr writes, or the result is negative in base 64/85, or out of memory | `error_invalid_argument` | | the exact result would be longer than `max_conversion_length` characters | `error_value_out_of_range` | Only `nan`/`inf`/`ninf` are handed over untouched (`converted == false`, no error): they carry no finite value, so no conversion was possible or promised. #### Bounding the work Rendering an exact expansion is **quadratic in its digit count**, and the count follows the *magnitude of the value's exponent*, not the literal's length: `1e-9800` is seven characters and expands to 9800 digits. Left unbounded, a one-kilobyte document of such values costs minutes of CPU while looking trivial. `max_conversion_length` caps the characters a conversion may produce; `0` selects `BVNR_DEFAULT_MAX_CONVERSION_LENGTH` (1024), generous next to any real measurement. Anything longer is `error_value_out_of_range`, rejected before the digits are generated. Raise it if you genuinely need thousands of exact digits. Like `max_number_length` and `max_array_items`, it is here so a consumer of untrusted input is not at the mercy of the input's shape. #### Exact-but-not-writable results Plenty of everyday conversions are exact as a rational yet have no finite positional expansion in the output base — `km/h → m/s` is `5/18`, `°F → °C` and `m → km` in base 2 are the same story. By default those abort with `error_unit_inexact` rather than round. Set `want_unit_allow_nonterminating` when your consumer can take a rational. The value then arrives normally with `conv.num`/`conv.den` **exact** and `conv.text == NULL` (`conv.length == 0`) — always NULL-check `conv.text` before printing it. An irrational factor still aborts even with the flag set: there is no exact rational to hand over in the first place. ```c static bool want_unit(void *ud, const bvnr_data_t *d, value_unit_t *want, uint32_t *want_base) { /* deliver every length in metres, base 10; leave everything else */ bool ok; value_unit_t metre = bvn_parse_unit((const uint8_t *)"m", &ok); if (bvn_units_compatible(d->value_unit, metre)) { *want = metre; *want_base = 10; return true; } return false; } static bool on_event(void *ud, bvnr_event_t ev, bvnr_data_t *d) { if (ev == ev_data && d->converted && d->conv.text) printf("= %s m\n", d->conv.text); /* e.g. "5 k~m" -> "5000" */ return true; } bvnr_read_flags_t opts = { .on_verified = on_event, .want_unit = want_unit }; ``` --- ### 8. `bvn_parse_uint64` / `bvn_parse_int64` / `bvn_parse_double` ```c bool bvn_parse_uint64(const char *s, value_type_spec_t vt, uint64_t *out); bool bvn_parse_int64 (const char *s, value_type_spec_t vt, int64_t *out); bool bvn_parse_double(const char *s, value_type_spec_t vt, double *out); ``` Convert the raw token string received in `ev_data` into a C numeric type. The `vt` argument — taken directly from `d->value_type` — supplies the base and bit-width for range checking. The `data` pointer inside `bvnr_data_t` is **not NUL-terminated**, and for null/empty values `d->length` may be `0`. Always guard the copy by `d->length` and NUL-terminate manually. - Returns `true` and writes `*out` on success. - Returns `false` if the string is not representable in the declared type — this includes exceeding `vt.width`, so `` rejects `"256"`. A width above 64 is not checked: the 64-bit out parameter is the binding limit there. - Returns `false` for anything a value token cannot look like: an empty string, leading whitespace, a leading `+`, or a sign on an unsigned carrier. (The C standard library's `strtoull` accepts all four — the last by wrapping `-1` into `18446744073709551615` — which is why these helpers do not simply forward to it.) - Acceptance matches the reader exactly: anything these accept, a document can carry, and vice versa. ```c static bool on_event(void *ud, bvnr_event_t ev, bvnr_data_t *d) { if (ev != ev_data) return true; char buf[256]; if (d->length >= sizeof(buf)) return false; if (d->length) memcpy(buf, d->data, d->length); buf[d->length] = '\0'; switch (d->value_type.family) { case vt_uint: { uint64_t v; if (bvn_parse_uint64(buf, d->value_type, &v)) printf("uint = %" PRIu64 "\n", v); break; } case vt_sint: { int64_t v; if (bvn_parse_int64(buf, d->value_type, &v)) printf("sint = %" PRId64 "\n", v); break; } case vt_float: { double v; if (bvn_parse_double(buf, d->value_type, &v)) printf("float = %g\n", v); break; } default: break; } return true; } ``` --- ## Writer --- ### 9. `bvnr_writer_create` / `bvnr_writer_destroy` ```c bvnr_writer_t *bvnr_writer_create(void); void bvnr_writer_destroy(bvnr_writer_t *w); ``` Allocate and free a writer on the heap. Mirrors the reader lifecycle exactly. Returns `NULL` on allocation failure. ```c bvnr_writer_t *w = bvnr_writer_create(); if (!w) { perror("alloc"); exit(1); } /* ... open, write events, finish ... */ bvnr_writer_destroy(w); ``` --- ### 10. `bvnr_sink_to_fd` ```c void bvnr_sink_to_fd(bvnr_sink_t *s, int fd); ``` Initialise sink `s` to write serialised bytes to an open, writable POSIX file descriptor. The caller retains ownership of `fd`. ```c bvnr_sink_t sink; bvnr_sink_to_fd(&sink, STDOUT_FILENO); /* or: int fd = open("out.bvnr", O_WRONLY|O_CREAT|O_TRUNC, 0644); */ ``` --- ### 11. `bvnr_sink_to_mem` ```c void bvnr_sink_to_mem(bvnr_sink_t *s, void *buf, uint64_t cap); ``` Initialise sink `s` to write into a caller-provided memory buffer of `cap` bytes. Writing beyond `cap` produces `error_sink_buffer_exhausted`. Use `bvnr_sink_bytes_written` to query how many bytes were actually written — but see the note there: for output produced by a *writer*, `bvnr_writer_bytes_written` is the one that works. ```c char out[4096]; bvnr_sink_t sink; bvnr_sink_to_mem(&sink, out, sizeof(out)); ``` --- ### 12. `bvnr_sink_bytes_written` ```c uint64_t bvnr_sink_bytes_written(const bvnr_sink_t *s); ``` Return the number of bytes pushed into a memory sink so far. Only meaningful for sinks created with `bvnr_sink_to_mem`. **Not for writer output.** `bvnr_open_write_sink` takes a *copy* of the sink, so everything a writer emits advances the copy and never the caller's struct — this counter stays 0 for the whole lifetime of the writer. Use it only when you push into the sink yourself. To size writer output, ask the writer: ```c bvnr_write_finish(w); uint64_t n = bvnr_writer_bytes_written(w); /* NOT bvnr_sink_bytes_written(&sink) */ fwrite(out, 1, n, stdout); bvnr_writer_destroy(w); /* read the count before destroying */ ``` --- ### 13. `bvnr_open_write_sink` ```c bool bvnr_open_write_sink(bvnr_writer_t *w, const bvnr_sink_t *sink, bool pretty, bvnr_write_flags_t *options); ``` Attach `sink` to the writer and configure it. Must be called before any `bvnr_write_event`. Returns `false` on invalid arguments. - `pretty` — when `true`, the serialiser emits newlines and indentation. When `false`, output is compact (single line per assignment, no extra whitespace). - `options` — configuration struct. Zero-initialise for defaults. ```c typedef struct bvnr_write_flags_s { /* ── Writer enforces these ─────────────────────────────────── */ uint8_t max_struct_nesting; /* 0 → 64 internal default; hard cap 255 */ uint8_t max_array_nesting; /* 0 → 64 internal default; hard cap 255 */ void *userdata; bool (*on_event)(void *userdata, bvnr_event_t, bvnr_data_t *); bvn_unit_flags_t unit_flags; /* controls unit annotation format */ bool emit_version; /* emit a leading "#!bovnar M.N" on open */ /* ── Present for API symmetry with bvnr_read_flags_t; ─ the writer does not read or enforce these fields. Set to 0. */ uint16_t max_identifier_length; uint16_t max_string_length; uint16_t max_number_length; uint16_t max_symbol_length; uint16_t max_reference_length; uint64_t max_array_items; uint64_t max_text_bytes; uint64_t max_file_size; bool continue_on_error; /* no-op in the writer */ bvnr_on_error_fn on_error; /* no-op in the writer */ } bvnr_write_flags_t; ``` > **Writer limits.** Only `max_struct_nesting`, `max_array_nesting`, `on_event`, `userdata`, > and `unit_flags` have any effect on the writer. All other fields are present solely to keep > `bvnr_write_flags_t` structurally parallel to `bvnr_read_flags_t`; they are silently > ignored. In particular, `continue_on_error`, `on_error`, and all per-token-length fields > have no effect. The writer never internally limits array items, text bytes, or file size. `on_event` in the write flags fires for each event as it is serialised — useful for logging or auditing. Pass `NULL` if not needed. `unit_flags` controls how unit annotations are serialised by the writer. The valid flags are: | Flag | Value | Effect | |------|-------|--------| | `BVN_UNIT_FLAGS_NONE` | `0` | Default: Unicode superscript exponents, no reduction | | `BVN_UNIT_REDUCE` | `1 << 0` | Reduce compound units to canonical form before serialising | | `BVN_UNIT_ASCII_EXP` | `1 << 1` | Use `^N` ASCII caret notation instead of Unicode superscripts | These flags can be OR-combined: `BVN_UNIT_REDUCE | BVN_UNIT_ASCII_EXP`. The flags are fixed at open time. To change serialisation behaviour, destroy the writer and open a new one with the updated `unit_flags`. The getter `bvnr_writer_unit_flags(w)` is used internally by the Python FFI layer to retrieve the live flags before each unit serialisation call; there is no public setter. ```c bvnr_sink_t sink; bvnr_sink_to_fd(&sink, fd); bvnr_write_flags_t opts = { 0 }; /* all defaults */ if (!bvnr_open_write_sink(w, &sink, /*pretty=*/true, &opts)) return -1; ``` --- ### 14. `bvnr_open_write_mem` ```c bool bvnr_open_write_mem(bvnr_writer_t *w, void *buf, uint64_t cap, bool pretty, bvnr_write_flags_t *options); ``` Convenience wrapper that constructs a memory sink internally and calls `bvnr_open_write_sink`. Equivalent to `bvnr_sink_to_mem` + `bvnr_open_write_sink`. To retrieve the written byte count after finishing, call `bvnr_writer_bytes_written`. ```c char out[4096]; bvnr_write_flags_t opts = { 0 }; if (!bvnr_open_write_mem(w, out, sizeof(out), false, &opts)) return -1; ``` --- ### 15. `bvnr_write_event` ```c bool bvnr_write_event(bvnr_writer_t *w, bvnr_event_t ev, bvnr_data_t *data); ``` Emit one event to the writer. This is the only function used to produce output. It serialises the event and the data it describes directly into the configured sink. Returns `true` on success, `false` on any serialisation error. The event sequence you must emit for every assignment mirrors exactly what the reader delivers to `on_verified`. At minimum, for a typed scalar value: ``` ev_assignment_start (data->data = key, data->length = key length) ev_type_annotation_start (data->data = annotation text, or NULL for no annotation) ev_type_annotation_type_family ev_type_annotation_type_family_parameter (for each parameter) ev_type_annotation_end ev_data (data->data = value string, data->value_type/value_unit set) ``` The `BVN_TYPE_*` macros build `value_type_spec_t` literals conveniently: ```c #define BVN_TYPE_PLAIN ((value_type_spec_t){ .family = vt_plain }) #define BVN_TYPE_UTF8 ((value_type_spec_t){ .family = vt_utf8 }) #define BVN_TYPE_BOOL ((value_type_spec_t){ .family = vt_bool }) #define BVN_TYPE_UINT(w) ((value_type_spec_t){ .family = vt_uint, .width = (w) }) #define BVN_TYPE_SINT(w) ((value_type_spec_t){ .family = vt_sint, .width = (w) }) #define BVN_TYPE_FLOAT(w) ((value_type_spec_t){ .family = vt_float, .width = (w) }) /* float_fix: .base is repurposed to store Q (fractional bits). */ #define BVN_TYPE_FLOAT_FIX(w,q) ((value_type_spec_t){ .family = vt_float_fix, .width = (w), .base = (q) }) /* float_dec: base field is unused (always 0). */ #define BVN_TYPE_FLOAT_DEC(w) ((value_type_spec_t){ .family = vt_float_dec, .width = (w) }) /* float with explicit numeral base (for base-16 output): */ #define BVN_TYPE_FLOAT_BASE(w,b) ((value_type_spec_t){ .family = vt_float, .width = (w), .base = (b) }) /* uint/sint with explicit numeral base: */ #define BVN_TYPE_UINT_BASE(w,b) ((value_type_spec_t){ .family = vt_uint, .width = (w), .base = (b) }) #define BVN_TYPE_SINT_BASE(w,b) ((value_type_spec_t){ .family = vt_sint, .width = (w), .base = (b) }) ``` The maximum bit-width accepted for `uint` and `sint` is `BVN_MAX_INT_WIDTH` (defined as `32768u` in `bovnar.h`). The validator and writer reject any declared `uint`/`sint` width exceeding this limit with `error_illegal_value_type`. > **Critical:** The writer dispatches `ev_type_annotation_type_family_parameter` events on `d->type`, not on `d->value_type`. For each parameter event, `d.type` must be set to the appropriate `token_type_t` value: `token_is_type_width` for the width parameter, `token_is_type_base` for the base parameter, `token_is_type_q` for the Q (fractional bits) parameter of `float_fix`, and `token_is_unit` for the unit parameter. An unrecognised `d.type` causes the writer to emit nothing for that event — the annotation will be silently incomplete. **Use `bvnr_write_type_annotation` (see §22) to avoid this complexity entirely.** **Example: write `.port = 8080;`** ```c /* Helper: emit a fully-typed uint16 assignment */ static bool write_uint16(bvnr_writer_t *w, const char *key, uint16_t value) { char valbuf[16]; snprintf(valbuf, sizeof(valbuf), "%" PRIu16, value); value_type_spec_t vt = BVN_TYPE_UINT(16); value_unit_t vu = BVN_UNIT_NONE; bvnr_data_t d; /* 1. Assignment start — key */ d = (bvnr_data_t){ .data = (void*)key, .length = (uint32_t)strlen(key) }; if (!bvnr_write_event(w, ev_assignment_start, &d)) return false; /* 2. Type annotation — use bvnr_write_type_annotation for the full sequence */ if (!bvnr_write_type_annotation(w, vt, vu)) return false; /* 3. Value */ d = (bvnr_data_t){ .type = token_is_number, .value_type = vt, .value_unit = vu, .data = valbuf, .length = (uint32_t)strlen(valbuf), }; return bvnr_write_event(w, ev_data, &d); } ``` **Example: write a string assignment `.host = "localhost";`** ```c static bool write_string(bvnr_writer_t *w, const char *key, const char *value) { value_type_spec_t vt = BVN_TYPE_UTF8; value_unit_t vu = BVN_UNIT_NONE; bvnr_data_t d; d = (bvnr_data_t){ .data = (void*)key, .length = (uint32_t)strlen(key) }; if (!bvnr_write_event(w, ev_assignment_start, &d)) return false; d = (bvnr_data_t){ .value_type = vt }; if (!bvnr_write_event(w, ev_type_annotation_start, &d)) return false; if (!bvnr_write_event(w, ev_type_annotation_type_family, &d)) return false; if (!bvnr_write_event(w, ev_type_annotation_end, &d)) return false; d = (bvnr_data_t){ .value_type = vt, .data = (void*)value, .length = (uint32_t)strlen(value), }; return bvnr_write_event(w, ev_data, &d); } ``` **Example: open and close a struct** ```c static bool write_struct_start(bvnr_writer_t *w, const char *key) { bvnr_data_t d = { .data = (void*)key, .length = (uint32_t)strlen(key) }; if (!bvnr_write_event(w, ev_assignment_start, &d)) return false; return bvnr_write_event(w, ev_struct_start, &(bvnr_data_t){0}); } static bool write_struct_end(bvnr_writer_t *w) { return bvnr_write_event(w, ev_struct_end, &(bvnr_data_t){0}); } ``` --- ### 15a. `bvnr_write_version` ```c bool bvnr_write_version(bvnr_writer_t *w, uint16_t major, uint16_t minor); ``` Emit a leading `#!bovnar .` version directive (spec §3.4). Must be called immediately after `bvnr_open_write_*` and before any value; calling it once output has begun is `error_invalid_argument`. Use it to round-trip a directive read from a source document, or pass `BVNR_SPEC_VERSION_MAJOR` / `BVNR_SPEC_VERSION_MINOR` to stamp the current spec version. Setting `bvnr_write_flags_t.emit_version` is equivalent to calling it with the current spec version right after open. ```c bvnr_open_write_sink(w, &sink, true, NULL); bvnr_write_version(w, 1, 1); /* "#!bovnar 1.1\n" */ bvnr_write_uint(w, "port", 16, 443); bvnr_write_finish(w); ``` --- ### 16. `bvnr_write_finish` ```c bool bvnr_write_finish(bvnr_writer_t *w); ``` Flush any buffered output and finalise the stream. Must be called after all `bvnr_write_event` calls and before `bvnr_writer_destroy`. Returns `false` if any struct is still open (`error_got_incomplete_bvnr_stream`), if writing the trailing semicolon fails, or if flushing the write buffer to the sink fails. **64 KiB write buffer.** The writer accumulates output into an internal 64 KiB buffer. Individual `bvnr_write_event` calls do not push bytes to the sink immediately; instead bytes accumulate in the buffer and are forwarded to the sink only when the buffer is full or when `bvnr_write_finish` is called. This means the sink receives large contiguous writes rather than one tiny push per token, which is important for fd-based sinks. `bvnr_writer_bytes_written` always reflects the true total of bytes handed off to the sink plus bytes still in the buffer, so the count is accurate at any point during writing. ```c if (!bvnr_write_finish(w)) { fprintf(stderr, "write finish failed: %s\n", bvn_error_to_string(bvnr_writer_get_error(w))); } bvnr_writer_destroy(w); ``` --- ### 17. `bvnr_writer_get_error` and friends ```c error_code_t bvnr_writer_get_error (const bvnr_writer_t *w); uint64_t bvnr_writer_get_error_offset(const bvnr_writer_t *w); uint64_t bvnr_writer_bytes_written (const bvnr_writer_t *w); bvn_unit_flags_t bvnr_writer_unit_flags (const bvnr_writer_t *w); ``` The writer error API is smaller than the reader's: there are **no** `bvnr_writer_get_error_line` or `bvnr_writer_get_error_column` functions. The writer has no lexer and therefore cannot track source positions. Use `bvnr_writer_get_error_offset` (byte count into the output stream) and `bvnr_writer_get_error` (error code) for diagnostics. `bvnr_writer_bytes_written` returns the total bytes emitted to the sink so far — available at any point, not only after errors. `bvnr_writer_unit_flags` returns the `bvn_unit_flags_t` bitmask currently stored in the writer object (set via `bvnr_write_flags_t.unit_flags` at open time). The writer uses these flags whenever it serialises a unit annotation string (via `bvn_unit_to_string_ex`). This function is primarily used by the Python bindings FFI layer to retrieve the live flags before each unit serialisation call. ```c if (!bvnr_write_event(w, ev_data, &d)) { fprintf(stderr, "write error: %s at offset %" PRIu64 "\n", bvn_error_to_string(bvnr_writer_get_error(w)), bvnr_writer_get_error_offset(w)); return -1; } ``` --- ### 18. `bvn_format_uint64` / `bvn_format_int64` / `bvn_format_double` ```c int32_t bvn_format_uint64(char *buf, size_t bufsize, uint64_t value, uint32_t base, uint32_t min_digits); int32_t bvn_format_int64(char *buf, size_t bufsize, int64_t value, uint32_t base, uint32_t min_digits); int32_t bvn_format_double(char *buf, size_t bufsize, double value, value_type_spec_t vt); ``` Produce the value string that goes into `bvnr_data_t.data` when writing numeric values. All three return the number of bytes written (excluding NUL terminator), or `-1` on buffer overflow. - `base` — numeric base (2–62, 64, 85). Use `10` for the common case. - `min_digits` — zero-pad to at least this many digits. Pass `0` for no padding. - For `bvn_format_double`, the type spec `vt` controls the output precision according to `vt.width`. Because the input is a C `double`, the effective precision is capped at the 64-bit format (a wider `vt.width` yields no extra digits); render the full precision of a 128/256-bit value with the arbitrary-precision writer `bvnr_write_bvnf_base` instead. ```c char buf[32]; /* Format 255 as a base-16 value, minimum 2 digits → "ff" */ int32_t n = bvn_format_uint64(buf, sizeof(buf), 255, 16, 2); /* buf = "ff", n = 2 */ /* Format 9.81 as a 64-bit float */ value_type_spec_t vt = BVN_TYPE_FLOAT(64); n = bvn_format_double(buf, sizeof(buf), 9.81, vt); /* buf = "9.81e+0", n = 7 — the writer emits an exponent for every non-zero value (zero is written as "0.0" / "-0.0"), so a float always re-reads as a float and never as an integer carrier */ ``` These strings are then placed into `bvnr_data_t.data` / `.length` before calling `bvnr_write_event(w, ev_data, &d)`. --- ### 19. `bvnr_write_bvnf_base` / `bvnr_write_bvnf_base_unit` ```c bool bvnr_write_bvnf_base(bvnr_writer_t *w, const char *key, const bvn_float_t *f, uint32_t width, uint32_t base); bool bvnr_write_bvnf_base_unit(bvnr_writer_t *w, const char *key, const bvn_float_t *f, uint32_t width, uint32_t base, value_unit_t unit); ``` Write an arbitrary-precision `bvn_float_t` value at any valid `float` width (0, 16, or any multiple of 32 up to 32768) and in either base 10 or base 16. The float string is generated internally by `bvn_float_to_str` and then validated by the writer before being sent to the sink — the call fails and sets an error if the generated string does not satisfy the writer's type constraints. `bvnr_write_bvnf_base` is the no-unit variant; `bvnr_write_bvnf_base_unit` attaches a physical unit to the type annotation. **Base 10** — the value string uses the same decimal format as `bvn_format_double` and is emitted as a bare number token (`token_is_number`). The type annotation is `` or ``. **Base 16** — the value string uses a binary-exponent hexadecimal format: `[−]D.DDDDp[+|−]EEE` where `D.DDDD` are lowercase hex mantissa digits and `EEE` is the decimal binary exponent. The string is emitted as a quoted string token (`token_is_string`) because non-decimal literals must be quoted in BVNR. The type annotation is ``. ```c #include "bvn_float.h" bvn_float_t *f = bvn_float_alloc(1024u); bvn_float_from_str(f, "3.14159265358979323846", 10); /* .pi_dec = 3.14... ; */ bvnr_write_bvnf_base(w, "pi_dec", f, 1024u, 10u); /* .pi_hex = "1.921fb54442d18p+1"; */ bvn_float_t *g = bvn_float_alloc(256u); bvn_float_from_double(g, 3.14159265358979323846); bvnr_write_bvnf_base(w, "pi_hex", g, 256u, 16u); bvn_float_free(f); bvn_float_free(g); ``` `width` 0 uses the precision stored in the `bvn_float_t` itself. An invalid width (not 0, not 16, not a positive multiple of 32, or greater than `BVN_FLOAT_MAX_PREC`) causes the call to return `false` with `error_illegal_value_type`. These functions supersede calling `bvnr_write_bvnf` / `bvnr_write_bvnf_unit` when base 16 output or widths greater than 128 are needed. `bvnr_write_bvnf` and `bvnr_write_bvnf_unit` remain available as convenience wrappers that always use base 10. --- ### 20. `bvnr_write_bvni` / `bvnr_write_bvni_unit` ```c bool bvnr_write_bvni(bvnr_writer_t *w, const char *key, const bvn_int_t *n, uint32_t width, uint32_t base); bool bvnr_write_bvni_unit(bvnr_writer_t *w, const char *key, const bvn_int_t *n, uint32_t width, uint32_t base, value_unit_t unit); ``` Write an arbitrary bit-width integer. The type family (`uint` or `sint`) is determined by the `negative` flag of the `bvn_int_t`: negative values produce ``, non-negative values produce ``. Any numeric base supported by the writer (2–62, 64, 85) may be specified. The integer string is generated by `bvn_int_to_str` and then validated before reaching the sink. For non-decimal bases the string is emitted as a quoted token (`token_is_string`); for base 10 it is emitted as a bare number token (`token_is_number`). ```c #include "bvn_int.h" bvn_int_t *n = bvn_int_alloc(); /* 128-bit unsigned max in decimal */ bvn_int_from_str(n, "340282366920938463463374607431768211455", 10); bvnr_write_bvni(w, "u128max", n, 128u, 10u); /* 256-bit value in hex — emitted as "deadbeef..."; */ bvn_int_from_str(n, "deadbeefcafebabe0011223344556677" "8899aabbccddeeff0011223344556677", 16); bvnr_write_bvni(w, "wide_hex", n, 256u, 16u); /* signed negative in hex */ bvn_int_from_str(n, "-7fffffff", 16); bvnr_write_bvni(w, "neg_hex", n, 32u, 16u); bvn_int_free(n); ``` `width` 0 defaults to 64 bits for range validation. A `width` that cannot hold the value causes the call to return `false` with `error_value_out_of_range`. --- ### 21. `BVN_TYPE_FLOAT_BASE` ```c #define BVN_TYPE_FLOAT_BASE(w, b) /* value_type_spec_t */ ``` Convenience macro that constructs a `value_type_spec_t` for a `float` with explicit width `w` and base `b`. The existing `BVN_TYPE_FLOAT(w)` macro always yields base 0 (equivalent to base 10); `BVN_TYPE_FLOAT_BASE` is needed when base 16 must be encoded in the type spec before being passed to `bvnr_write_type_annotation` or `bvnr_write_event` directly. ```c value_type_spec_t vt16 = BVN_TYPE_FLOAT_BASE(256u, 16u); /* → { .family = vt_float, .width = 256, .base = 16 } */ value_type_spec_t vt10 = BVN_TYPE_FLOAT_BASE(64u, 10u); /* equivalent to BVN_TYPE_FLOAT(64) */ ``` --- ## Shared --- ### 22. `bvnr_write_type_annotation` ```c bool bvnr_write_type_annotation(bvnr_writer_t *w, value_type_spec_t vt, value_unit_t vu); ``` Emit a complete type-annotation event sequence (`ev_type_annotation_start`, `ev_type_annotation_type_family`, zero or more `ev_type_annotation_type_family_parameter` events, `ev_type_annotation_end`) in a single call. Returns `false` on any serialisation error. This is the **preferred** way to write type annotations. Using `bvnr_write_event` directly for parameter events requires setting `d.type` to the appropriate `token_type_t` constant for each parameter; `bvnr_write_type_annotation` handles this correctly and automatically. The function emits parameters as follows: - **Width** — emitted for numeric families when `vt.width != 0`. A width of `0` is **not** written to the stream (the absence implies the default width of 64 on the reader side via `bvn_effective_width`). Also emitted for `datetime` when `vt.width != 0`. It is **not** emitted for `utf8` or `bool`, which are parameterless — a width on them is rejected by the type-spec validator (`error_illegal_value_type`). - **Base** — emitted for `float` when `vt.base` is non-zero and not `10`; emitted for `uint`/`sint` when `vt.base` is non-zero and not `10`. - **Q** — emitted for `float_fix` when `vt.base` (which stores Q) is non-zero. A Q value of `0` is therefore not written explicitly. - **Unit** — emitted when `vu.num_components > 0`. `BVN_UNIT_NONE` (num_components == 0) produces no unit parameter; a lone `bu_none` component — `BVN_UNIT_NO_PREFIX(bu_none)`, num_components == 1 — is normalised to `BVN_UNIT_NONE` on entry and likewise produces **no** unit parameter. ```c value_type_spec_t vt = BVN_TYPE_FLOAT(64); value_unit_t vu = BVN_UNIT_COMPOUND2( bu_meter, si_none, exp_linear, bu_second, si_none, exp_neg_square); /* Emits: */ if (!bvnr_write_type_annotation(w, vt, vu)) return false; ``` --- ### 23. `bvn_parse_unit` / `bvn_parse_unit_n` ```c value_unit_t bvn_parse_unit (const uint8_t *unit, bool *ok); value_unit_t bvn_parse_unit_n(const uint8_t *unit, uint32_t len, bool *ok); ``` Parse a compound unit string (e.g. `"k~g·m/s²"`) into a `value_unit_t`. Both set `*ok` to `false` and return a zeroed unit on any error. `bvn_parse_unit` requires a NUL-terminated string. `bvn_parse_unit_n` accepts a length `len` and does **not** require a NUL terminator — use this variant when the unit string is a substring of a larger buffer (as is the case inside the parser itself). This is useful when reading: after `ev_type_annotation_type_family_parameter`, the unit is already parsed for you in `d->value_unit`. You only need `bvn_parse_unit` / `bvn_parse_unit_n` if you are constructing a unit from a string yourself (e.g. from a config or CLI argument). The validator also calls `bvn_parse_unit_n` internally when processing an **inline unit suffix** (the optional unit token that may follow a scalar value before its terminating `;`). You do not need to call either function yourself to consume inline units; the parsed result is automatically placed in `d->value_unit` of the `ev_data` event, exactly as for annotation-specified units. ```c bool ok; value_unit_t u = bvn_parse_unit((const uint8_t *)"k~g·m/s²", &ok); if (!ok) { fprintf(stderr, "bad unit\n"); return; } /* u now holds { num_components=3, [{bu_gram,exp_linear,si_kilo}, {bu_meter,exp_linear,si_none}, {bu_second,exp_neg_square,si_none}] } */ /* Length-bounded variant — no NUL needed */ const uint8_t *annotation = (const uint8_t *)"float:64,m/s"; value_unit_t u2 = bvn_parse_unit_n(annotation + 9, 3, &ok); /* "m/s" */ ``` --- ### 24. `bvn_unit_to_string` / `bvn_unit_to_string_ex` ```c int32_t bvn_unit_to_string(value_unit_t u, char *buf, size_t bufsize); int32_t bvn_unit_to_string_ex(value_unit_t u, char *buf, size_t bufsize, bvn_unit_flags_t flags); ``` Serialise a `value_unit_t` back into its canonical string form. Numerator components are joined by `·`, followed by `/` and denominator components joined by `·`. Returns bytes written (excluding NUL), or `-1` on buffer overflow. `bvn_unit_to_string` is equivalent to calling `bvn_unit_to_string_ex` with `flags = BVN_UNIT_FLAGS_NONE`. `bvn_unit_to_string_ex` accepts a `bvn_unit_flags_t` bitmask that controls output format: | Flag | Effect | |------|--------| | `BVN_UNIT_FLAGS_NONE` | Default: Unicode superscript exponents, no reduction | | `BVN_UNIT_REDUCE` | Reduce compound unit to canonical named SI unit before serialising | | `BVN_UNIT_ASCII_EXP` | Use `^N` ASCII caret notation instead of Unicode superscripts | These flags can be OR-combined. The writer uses `bvn_unit_to_string_ex` internally, passing the flags from `bvnr_writer_unit_flags(w)`. > **Note on writer usage:** When driving the writer manually via `bvnr_write_event`, do **not** pass a unit string in `bvnr_data_t.data` for the `ev_type_annotation_start` event — the serialiser ignores that field and derives the annotation from `data->value_type` and the subsequent parameter events. Use `bvnr_write_type_annotation` (§22) to emit a complete, correct type annotation in one call. ```c value_unit_t u = BVN_UNIT_COMPOUND2( bu_gram, si_kilo, exp_linear, bu_second, si_none, exp_neg_square); char buf[64]; int32_t n = bvn_unit_to_string(u, buf, sizeof(buf)); /* buf = "k~g/s²", n = 7 */ n = bvn_unit_to_string_ex(u, buf, sizeof(buf), BVN_UNIT_ASCII_EXP); /* buf = "k~g/s^2", n = 7 */ ``` --- ### 24a. `bvn_unit_convert_value` *(bovnar_si_units.h)* ```c bool bvn_unit_convert_value(double value, value_unit_t from, value_unit_t to, double *out); ``` Convert one numeric quantity from unit `from` into unit `to`, writing the result to `*out`. Handles both the simple multiplicative case (`5 k~m → 5000 m`) and the affine case (`25 °C → 298.15 K`), routing the latter through SI base units. This is the same routine the reader's `want_unit` hook (§7c) uses, and the C equivalent of the Python `convert_value`. Returns `false` — leaving `*out` untouched — when the two units are dimensionally **incompatible** or have no SI mapping. That boolean is the "validly convert only" guard: the reader turns a `false` here into `error_unit_mismatch`. ```c double m; if (bvn_unit_convert_value(5.0, bvn_parse_unit((const uint8_t *)"k~m", &ok), bvn_parse_unit((const uint8_t *)"m", &ok), &m)) printf("%.0f m\n", m); /* 5000 m */ ``` For the standalone factor (without applying it) or to detect the affine case, see `bvn_unit_convert_factor` in `bovnar_si_units.h`. `bvn_unit_convert_value` works in `double`, so it is lossy for wide values. For a **lossless** conversion — the engine behind the reader's `want_unit` hook (§7c) — use the exact-rational pair, also in `bovnar_si_units.h`: ```c bool bvn_unit_convert_rational(const bvn_int_t *vnum, const bvn_int_t *vden, value_unit_t from, value_unit_t to, bvn_int_t *out_num, bvn_int_t *out_den, bool *exact); int32_t bvn_rational_to_str(const bvn_int_t *num, const bvn_int_t *den, uint32_t base, char *buf, size_t bufsize, bool *exact); size_t bvn_rational_str_bufsize(const bvn_int_t *num, const bvn_int_t *den, uint32_t base); bool bvn_rational_base_valid(uint32_t base); /* static inline */ ``` `bvn_unit_convert_rational` converts the exact rational `vnum/vden` (parse a wire value into one with `bvn_float_parse_rational` for floats, or `bvn_int_from_str` for integers) from `from` into `to`, writing the exact reduced result to `out_num`/`out_den`. It returns `false` for dimensionally incompatible units, and sets `*exact = false` when the true factor is irrational (π-based angles) — the result is then only an approximation and a lossless consumer must reject it. `bvn_rational_to_str` renders an exact rational in any base bvnr can write — `2..62` plus `64` and `85` (`bvn_rational_base_valid`) — setting `*exact = true` and writing the full expansion when it terminates. It distinguishes two failures: | Return | Meaning | |--------|---------| | `BVN_RATIONAL_NONTERMINATING` (`-2`) | the expansion is infinite in this base. The rational itself is still exact, so use `num`/`den`. | | `-1` | bad arguments, an unsupported base, a negative value in the sign-less bases 64/85, out of memory, or a `bufsize` too small. | The buffer is **never truncated** — half an exact expansion is simply a different number — so size it with `bvn_rational_str_bufsize`, which upper-bounds the result from the operands' bit lengths. Both handle any value width — a 1056-bit float, a 512-bit integer — with no precision loss. The exactness they promise is bounded by the unit table's own declared factors: every non-irrational unit carries an exact rational `to_si` factor (see `src/gendata/units.bvnr`), and `gen_units.py` refuses to generate a table in which a rounded decimal is passed off as exact. --- ### 25. `bvn_error_to_string` ```c const char *bvn_error_to_string(error_code_t code); ``` Return a short, static, human-readable description of an error code. The returned pointer is valid for the lifetime of the program; do not free it. ```c fprintf(stderr, "error: %s\n", bvn_error_to_string(bvnr_reader_get_error(r))); /* e.g. "error: value_out_of_range" */ ``` **Unit-related error codes** (for reference): | Code | Value | String | Trigger | |------|-------|--------|---------| | `error_unit_illegal` | 32 | `"unit_illegal"` | Unparseable unit string (unknown base, bad prefix, empty component, >8 components) | | `error_unit_too_long` | 22 | `"unit_too_long"` | Unit string exceeds internal buffer | | `error_unit_mismatch` | 38 | `"unit_mismatch"` | Inline unit suffix present, type-annotation unit also present, and the two differ; or a `want_unit` target dimensionally incompatible with the value's unit (§7c) | | `error_unit_inexact` | 47 | `"unit_inexact"` | A `want_unit` conversion could not be delivered exactly: irrational factor, or a non-terminating expansion in the output base without `want_unit_allow_nonterminating` (§7c) | --- ## DOM API (`bovnar_dom.h`) The SAX reader above streams events. When you instead want the whole document in memory for random-access queries — without writing a callback — use the DOM API in `include/bovnar_dom.h`. It parses a document into a tree of `bvn_dom_node_t` owned by a `bvn_dom_doc_t`, which you navigate and read with typed accessors. ### Parsing and lifetime ```c bvn_dom_doc_t *bvn_dom_doc_create(void); void bvn_dom_doc_destroy(bvn_dom_doc_t *doc); /* frees the whole tree; NULL-safe */ bvn_dom_doc_t *bvn_dom_parse(const void *data, uint32_t len); bvn_dom_doc_t *bvn_dom_parse_fd(int fd); bvn_dom_doc_t *bvn_dom_parse_fd_ex(int fd, uint64_t max_bytes); error_code_t bvn_dom_doc_get_parse_error(const bvn_dom_doc_t *doc); ``` `bvn_dom_parse` returns NULL **only** on allocation failure — a *malformed* document still returns a non-NULL doc, so check `bvn_dom_doc_get_parse_error()` (`error_none` means it parsed cleanly), not the pointer, to detect a parse error. The `bvn_dom_parse_fd*` variants instead return NULL on **any** failure (allocation, I/O, or exceeding the size cap). `bvn_dom_parse_fd_ex` caps the accumulated input at `max_bytes`, but only *downward*: `0` or any value above the built-in hard cap `BVN_DOM_FD_MAX_BYTES` (256 MiB) is clamped to that cap — there is no unlimited mode. Free every returned doc with `bvn_dom_doc_destroy`. ### Navigation ```c bvn_dom_node_t *bvn_dom_lookup(const bvn_dom_doc_t *doc, const char *path); /* dot path, e.g. "server.tls.cert" */ bvn_dom_node_t *bvn_dom_struct_get(const bvn_dom_node_t *node, const char *key); bvn_dom_node_t *bvn_dom_array_at(const bvn_dom_node_t *node, uint32_t index); uint32_t bvn_dom_struct_count(const bvn_dom_node_t *node); uint32_t bvn_dom_array_count(const bvn_dom_node_t *node); uint32_t bvn_dom_array_dims(const bvn_dom_node_t *node); /* number of `/`-separated dimensions */ const bvn_dom_entry_t *bvn_dom_struct_entries(const bvn_dom_node_t *node); const bvn_dom_entry_t *bvn_dom_doc_entries(const bvn_dom_doc_t *doc); uint32_t bvn_dom_doc_count(const bvn_dom_doc_t *doc); ``` Each `bvn_dom_entry_t` is `{ char *key; bvn_dom_node_t *value; }`. A missing key or out-of-range index returns NULL. ### Type inspection ```c typedef enum bvn_dom_type_e { BVN_DOM_NULL, BVN_DOM_INT, BVN_DOM_FLOAT, BVN_DOM_STRING, BVN_DOM_SYMBOL, BVN_DOM_REFERENCE, BVN_DOM_STRUCT, BVN_DOM_ARRAY, BVN_DOM_OCTET_STREAM, BVN_DOM_BOOL } bvn_dom_type_t; bvn_dom_type_t bvn_dom_node_type(const bvn_dom_node_t *node); bool bvn_dom_is_null(const bvn_dom_node_t *node); value_type_spec_t bvn_dom_get_value_type(const bvn_dom_node_t *node); value_unit_t bvn_dom_get_unit(const bvn_dom_node_t *node); int32_t bvn_dom_get_unit_string(const bvn_dom_node_t *node, char *buf, size_t bufsize); double bvn_dom_get_value_in_base_units(const bvn_dom_node_t *node); ``` ### Typed value accessors Each accessor returns `false` (leaving the out-param **unchanged** — no clamping or truncation) when the node is NULL, not of the requested kind, or does not fit the target type. Pointer results are **borrowed** — valid only until the owning document is destroyed; do not free them. ```c bool bvn_dom_get_bool (const bvn_dom_node_t *node, bool *out); bool bvn_dom_get_float (const bvn_dom_node_t *node, double *out); bool bvn_dom_get_i64/u64/i32/u32/i16/u16/i8/u8(const bvn_dom_node_t *node, /* int type */ *out); bool bvn_dom_get_string (const bvn_dom_node_t *node, const char **out, uint32_t *len); /* NUL-terminated; *len excludes NUL */ bool bvn_dom_get_symbol (const bvn_dom_node_t *node, const char **out, uint32_t *len); bool bvn_dom_get_reference(const bvn_dom_node_t *node, const char **out, uint32_t *len); bool bvn_dom_get_octets (const bvn_dom_node_t *node, const uint8_t **out, uint32_t *len); /* raw bytes, NOT NUL-terminated */ ``` Wide integers (> 64 bits) are not stored inline; read them as a borrowed bigint or render them to a string: ```c const bvn_int_t *bvn_dom_get_bigint(const bvn_dom_node_t *node); /* NULL unless the int is wider than 64 bits */ char *bvn_dom_int_to_str(const bvn_dom_node_t *node, uint32_t base); /* caller owns; free with bvn_dom_free_string */ void bvn_dom_free_string(char *s); ``` For a `datetime` node written as a literal with a fractional second (spec 1.1), the verbatim sub-second digits are available separately; the node's integer value is still the whole-second epoch count read via `bvn_dom_get_i64`: ```c const char *bvn_dom_get_datetime_fraction(const bvn_dom_node_t *node, uint32_t *len_out); ``` ### Building a tree The DOM is also writable, e.g. to construct a document programmatically and hand it to a serialiser. `bvn_dom_node_alloc` / the `bvn_dom_node_from_*` constructors make nodes; the `*_add`/`*_append` functions attach them. ```c bvn_dom_node_t *bvn_dom_node_alloc(bvn_dom_type_t t); void bvn_dom_node_destroy(bvn_dom_node_t *n); bvn_dom_node_t *bvn_dom_node_from_i64/u64/i32/u32/i16/u16/i8/u8(/* value */); bvn_dom_node_t *bvn_dom_node_from_bigint(bvn_int_t *bigint, value_type_spec_t vt, value_unit_t vu); bool bvn_dom_struct_add (bvn_dom_node_t *s, const char *key, uint32_t klen, bvn_dom_node_t *val); bool bvn_dom_doc_add (bvn_dom_doc_t *d, const char *key, uint32_t klen, bvn_dom_node_t *val); bool bvn_dom_array_append (bvn_dom_node_t *a, bvn_dom_node_t *elem); char *bvn_dom_strdup(const char *s, uint32_t len); ``` **Ownership.** `bvn_dom_struct_add` / `bvn_dom_doc_add` / `bvn_dom_array_append` **always** take ownership of the value node: on success the container owns it, and on **every** failure path the node is destroyed internally — so never destroy it yourself after the call (doing so double-frees). `bvn_dom_node_from_bigint` is the one exception with *asymmetric* ownership: on success it takes ownership of the `bvn_int_t`; on failure (NULL return) it does not, and you still own it. ### Minimal example ```c bvn_dom_doc_t *doc = bvn_dom_parse(buf, (uint32_t)len); if (!doc) { /* out of memory */ } if (bvn_dom_doc_get_parse_error(doc) != error_none) { /* malformed input — inspect the code */ } else { bvn_dom_node_t *port = bvn_dom_lookup(doc, "server.port"); uint16_t p; if (port && bvn_dom_get_u16(port, &p)) { /* use p */ } } bvn_dom_doc_destroy(doc); ``` --- ## Complete Read Example ```c #include #include #include #include #include #include "bovnar.h" typedef struct { char key[256]; } ctx_t; static bool on_verified(void *ud, bvnr_event_t ev, bvnr_data_t *d) { ctx_t *ctx = ud; if (ev == ev_assignment_start) { size_t n = d->length < 255 ? d->length : 255; memcpy(ctx->key, d->data, n); ctx->key[n] = '\0'; return true; } if (ev != ev_data || d->length == 0) return true; char vbuf[256]; size_t n = d->length < 255 ? d->length : 255; memcpy(vbuf, d->data, n); vbuf[n] = '\0'; switch (d->value_type.family) { case vt_uint: { uint64_t v; bvn_parse_uint64(vbuf, d->value_type, &v); printf(".%-20s = %" PRIu64 " (uint%u)\n", ctx->key, v, bvn_effective_width(d->value_type)); break; } case vt_float: { double v; bvn_parse_double(vbuf, d->value_type, &v); printf(".%-20s = %g (float%u)\n", ctx->key, v, bvn_effective_width(d->value_type)); break; } case vt_utf8: printf(".%-20s = \"%s\"\n", ctx->key, vbuf); break; default: printf(".%-20s = %s\n", ctx->key, vbuf); break; } return true; } int main(int argc, char **argv) { if (argc < 2) { fprintf(stderr, "usage: %s \n", argv[0]); return 1; } int fd = open(argv[1], O_RDONLY); if (fd < 0) { perror(argv[1]); return 1; } bvnr_reader_t *r = bvnr_reader_create(); ctx_t ctx = {0}; bvnr_source_t src; bvnr_source_from_fd(&src, fd); bvnr_read_flags_t opts = { .on_verified = on_verified, .userdata = &ctx, .max_file_size = 16777216, }; if (!bvnr_open_read_source(r, &src, NULL, &opts)) { fputs("failed to open reader\n", stderr); bvnr_reader_destroy(r); close(fd); return 1; } int ret = 0; if (!bvnr_read(r)) { fprintf(stderr, "%s at line %" PRIu64 " col %" PRIu64 "\n", bvn_error_to_string(bvnr_reader_get_error(r)), bvnr_reader_get_error_line(r), bvnr_reader_get_error_column(r)); ret = 1; } bvnr_reader_destroy(r); close(fd); return ret; } ``` --- ## Inline Unit Suffix — Reading A scalar value may carry an **inline unit suffix** directly after the literal, before its terminating `;`: ```bovnar .speed = 9.81 m/s; # no annotation; inline unit .mass = 70.5 k~g; # annotation without unit; inline unit adopted .dist = 1.5 m; # annotation and inline agree — valid ``` From the application's perspective, inline units and annotation units are transparent: both end up in `d->value_unit` of the `ev_data` event. No special handling is needed. The only behavioral difference occurs when **both** are present and **disagree**: the validator raises `error_unit_mismatch` (38) and parsing fails: ```bovnar .bad = 1.0 s; /* annotation says m, inline says s → error */ ``` Inline unit suffixes are **illegal inside array elements**. The lexer rejects them with `error_unexpected_input_byte`. ### Reading inline unit values ```c /* The callback below works identically whether the unit came from a * type annotation or from an inline suffix — no change needed. */ static bool on_verified(void *ud, bvnr_event_t ev, bvnr_data_t *d) { if (ev != ev_data) return true; char unit_str[128] = "no_unit"; /* dimensionless: num_components==0 (BVN_UNIT_NONE) or num_components==1 with base==bu_none (BVN_UNIT_NO_PREFIX(bu_none)) */ bool is_dim = (d->value_unit.num_components == 0) || (d->value_unit.num_components == 1 && d->value_unit.components[0].base == bu_none); if (!is_dim) bvn_unit_to_string(d->value_unit, unit_str, sizeof(unit_str)); char val[256]; size_t n = d->length < 255 ? d->length : 255; memcpy(val, d->data, n); val[n] = '\0'; printf("value=%s unit=%s\n", val, unit_str); return true; } ``` --- ## Complete Write Example ```c #include #include #include #include #include "bovnar.h" /* Emit: .velocity = 9.81; */ static bool write_velocity(bvnr_writer_t *w) { value_type_spec_t vt = BVN_TYPE_FLOAT(64); value_unit_t vu = BVN_UNIT_COMPOUND2( bu_meter, si_none, exp_linear, bu_second, si_none, exp_neg_square); char valbuf[32]; bvn_format_double(valbuf, sizeof(valbuf), 9.81, vt); bvnr_data_t d; d = (bvnr_data_t){ .data = "velocity", .length = 8 }; if (!bvnr_write_event(w, ev_assignment_start, &d)) return false; if (!bvnr_write_type_annotation(w, vt, vu)) return false; d = (bvnr_data_t){ .type = token_is_number, .value_type = vt, .value_unit = vu, .data = valbuf, .length = (uint32_t)strlen(valbuf), }; return bvnr_write_event(w, ev_data, &d); } int main(void) { bvnr_writer_t *w = bvnr_writer_create(); bvnr_sink_t sink; bvnr_sink_to_fd(&sink, STDOUT_FILENO); bvnr_write_flags_t opts = { 0 }; if (!bvnr_open_write_sink(w, &sink, /*pretty=*/true, &opts)) { fputs("failed to open writer\n", stderr); bvnr_writer_destroy(w); return 1; } int ret = 0; if (!write_velocity(w) || !bvnr_write_finish(w)) { fprintf(stderr, "write error: %s\n", bvn_error_to_string(bvnr_writer_get_error(w))); ret = 1; } bvnr_writer_destroy(w); return ret; } /* Output: .velocity = 9.81; */ ``` --- *End of Bovnar Read & Write API Reference (v1.1)* # Bovnar Python Bindings > **Version:** 1.1 Pure-`ctypes` Python bindings for the **Bovnar (BVNR)** typed serialisation library (spec v1.1). No compiled extension module is needed — the bindings load `libbvnr.so` at import time via the standard `ctypes.CDLL` machinery. --- ## Requirements | Requirement | Notes | |---|---| | Python ≥ 3.10 | `dataclasses`, `enum.IntEnum`, union-type annotations (`X \| Y`) | | `libbvnr.so` | Runtime only; see *Library discovery* below | | `numpy` ≥ 1.24 | **Optional** — only for the NumPy bridge (`pip install bovnar[numpy]`) | | `pint` ≥ 0.22 | **Optional** — only for the pint bridge (`pip install bovnar[pint]`) | | pytest ≥ 7 | Test suite only (`pip install bovnar[dev]`) | `numpy` and `pint` are imported **lazily, on first use** of their respective bridge functions — importing `bovnar` never requires either to be installed. --- ## Installation ```bash # Editable install from source (recommended during development) pip install -e ".[dev]" ``` Optional extras pull in the dependencies for the bridges: ```bash pip install "bovnar[numpy]" # NumPy bridge pip install "bovnar[pint]" # pint bridge pip install "bovnar[all]" # both numpy and pint ``` --- ## Library discovery The bindings search for `libbvnr.so` in this order: 1. **`LIBBOVNAR_PATH`** — absolute path to the `.so` file, e.g. ```bash export LIBBOVNAR_PATH=/opt/bovnar/lib/libbvnr.so ``` 2. **`LIBBOVNAR_DIR`** — directory that *contains* the `.so`, e.g. ```bash export LIBBOVNAR_DIR=/opt/bovnar/lib ``` 3. **`ctypes.util.find_library('bvnr')`** — standard `ldconfig` / `LD_LIBRARY_PATH` search. 4. **In-tree build paths** — `../../build/`, `../../build/release/`, `../../`, `.` (resolved relative to the `_ffi.py` file; useful when building Bovnar alongside the bindings from a mono-repo). If the library cannot be found a `BovnarLibraryNotFound` exception is raised with the list of searched paths. --- ## Quick-start ### High-level API (`loads` / `dumps`) ```python import bovnar # Serialise a Python dict to BVNR bytes data = { "sensor_id": 42, "temperature": -3.7, "label": "outdoor", "active": True, "payload": None, } bvnr_bytes = bovnar.dumps(data) # Deserialise back to a Python dict recovered = bovnar.loads(bvnr_bytes) assert recovered["sensor_id"] == 42 ``` `loads` accepts an optional `typed` flag that wraps typed values in `Quantity` objects instead of decoding them to native Python scalars. This preserves the original text representation, exact type width, numeral base, and physical unit for lossless round-trips: ```python bvnr = b".pressure = 101325.0;" doc = bovnar.loads(bvnr, typed=True) q = doc["pressure"] # Quantity('101325.0', FLOAT [Pa]) print(q.raw) # '101325.0' print(q.unit_str()) # 'Pa' # dumps() accepts Quantity values — annotation and raw text are re-emitted as-is out = bovnar.dumps(doc) assert bovnar.loads(out, typed=True) == doc ``` `dumps()` starts with a 4 MiB write buffer and doubles it automatically on overflow, up to 256 MiB. The `cap` keyword argument that existed in earlier versions is no longer accepted. ### SAX-style streaming reader The verified callback receives exactly **two** positional arguments: the event code and the data payload. There is no userdata/context argument — capture external state via closure instead. ```python from bovnar.reader import Reader from bovnar import Event def on_event(ev, data): if ev == Event.ASSIGNMENT_START: print("key:", data.raw_str()) elif ev == Event.DATA: print("value bytes:", data.raw_bytes()) return True # returning False stops parsing (raises BovnarParseError) with Reader() as r: r.read_mem(bvnr_bytes, on_verified=on_event) ``` The `BvnrData` payload passed to a callback exposes: | Method | Returns | Description | |---|---|---| | `data.raw_str(encoding='utf-8')` | `str` | The token bytes decoded as text | | `data.raw_bytes()` | `bytes` | The raw token bytes | | `data.converted_str()` | `str \| None` | Exact `want_unit` conversion result as a positional string, or `None` if no conversion / it does not terminate in its base | | `data.converted_in_base(base)` | `str \| None` | The exact conversion re-rendered in `base` | | `data.converted_rational()` | `tuple[int, int] \| None` | The exact conversion as `(numerator, denominator)` | | `data.frac_str()` | `str \| None` | Verbatim ISO sub-second digits of a `datetime` literal (spec 1.1), or `None` | It also carries the `converted` (bool) and `value_type` / `value_unit` fields directly. ### Generator / iterator interface ```python from bovnar.reader import Reader with Reader() as r: for payload in r.iter_mem(bvnr_bytes): print(payload.event, payload.text) ``` ### DOM (random-access) API ```python import bovnar doc = bovnar.dom_parse(bvnr_bytes) # Top-level key lookup node = doc["sensor_id"] print(node.as_i64()) # → 42 print(node.value_type) # ValueTypeSpec(family=UINT, width=64, base=0) # Struct traversal cfg = doc["config"] host = cfg["host"].as_str() # Array access arr = doc["values"] for i in range(len(arr)): print(arr[i].as_float()) # Convert entire document to a plain Python dict (drops type/unit info) d = doc.to_dict() ``` ### Low-level writer ```python from bovnar.writer import Writer from bovnar.enums import BaseUnit, SIPrefix with Writer.to_mem() as w: w.write_float("velocity", 9.81, width=64, unit_si_base=BaseUnit.METER, unit_si_prefix=SIPrefix.NONE) w.write_uint("count", 1024, width=32) output: bytes = w.get_output() ``` ### Streaming / framing (`bovnar.stream`) Bindings for the streaming layer (see [Streaming, Framing & Multiplexing](9_bovnar_streaming.md) for the full treatment): ```python from bovnar import stream # Multi-document record framing blob = stream.dump_documents([{"id": 1}, {"id": 2}]) # list[dict] -> bytes docs = stream.load_documents(blob) # bytes -> list[dict] docs = stream.load_documents(blob, continue_past_failed=True) # bad docs -> None # Octet multiplexing: (channel, payload) of any size, interleaved & reassembled multiplexed = stream.mux_dump([(1, b"hello"), (42, b"world")]) messages = stream.mux_load(multiplexed) # [(1, b"hello"), (42, b"world")] # Document-in-document outer = stream.embed_document(bovnar.dumps({"v": 1}), key="payload") inner = stream.parse_embedded(bovnar.loads(outer)["payload"]) # {"v": 1} ``` --- ## Unit helpers ```python import bovnar # Parse a unit string into a ValueUnit struct vu = bovnar.parse_unit("k~g·m/s²") # Convert a ValueUnit back to its canonical string s = bovnar.unit_to_str(vu) # → "k~g·m/s²" # Scalar SI/IEC prefix factor for a unit string f = bovnar.unit_factor("M~Hz") # → 1_000_000.0 ``` ### Extended unit functions The following functions operate on `ValueUnit` objects and are available both from the top-level `bovnar` namespace and from `bovnar.units`. ```python from bovnar import ( unit_valid, unit_prefix_factor, unit_prefix_exponent, prefix_unit_valid, unit_to_si_factor, units_compatible, unit_convert_factor, unit_dimension_vector, unit_reduce, unit_to_str_ex, exponent_to_int, int_to_exponent, convert_value, UnitFlags, ) # Validate a ValueUnit struct ok = unit_valid(vu) # True when vu is structurally valid # Prefix scale factor (SI or IEC) for a ValueUnit f = unit_prefix_factor(vu) # e.g. 1000.0 for k~m, 2**30 for Gi~B # Prefix exponent (base-10 for SI, base-2 for IEC) e = unit_prefix_exponent(vu) # e.g. 3 for kilo, -3 for milli, 30 for gibi # Validate a prefix for a base unit (IEC prefixes are only valid on bit/byte) from bovnar import ValueUnitPrefix, IECPrefix, BaseUnit p = ValueUnitPrefix.make_iec(IECPrefix.GIBI) ok = prefix_unit_valid(p, BaseUnit.BYTE) # True ok = prefix_unit_valid(p, BaseUnit.METER) # False # Full SI conversion including affine terms (e.g. Celsius → Kelvin) conv = unit_to_si_factor(vu) # conv.factor, conv.is_affine, conv.affine_offset # Check dimensional compatibility ok = units_compatible(vu_a, vu_b) # True if same SI dimension vector # Conversion factor between two compatible units c = unit_convert_factor(vu_from, vu_to) # c.factor, c.requires_affine # 7-element SI dimension exponent vector [m, kg, s, A, K, mol, cd] dims = unit_dimension_vector(vu) # e.g. [1, 0, -1, 0, 0, 0, 0] for m/s # Reduce a compound unit to its canonical named SI unit r = unit_reduce(vu) # r.unit, r.scale # Convert a scalar value between units (handles affine conversions) kelvin = convert_value(25.0, vu_celsius, vu_kelvin) # Serialise with formatting options (see UnitFlags below) s = unit_to_str_ex(vu, UnitFlags.ASCII_EXP) # use ^N instead of Unicode superscripts s = unit_to_str_ex(vu, UnitFlags.REDUCE) # reduce to canonical named unit first s = unit_to_str_ex(vu, UnitFlags.REDUCE | UnitFlags.ASCII_EXP) # Exponent enum ↔ integer conversions n = exponent_to_int(Exponent.NEG_SQUARE) # → -2 exp = int_to_exponent(-2) # → Exponent.NEG_SQUARE ``` `SI_DIM_NAMES` is the ordered tuple `('m', 'kg', 's', 'A', 'K', 'mol', 'cd')` — the index positions used by `unit_dimension_vector`. ### `UnitFlags` ```python from bovnar import UnitFlags # also from bovnar.units UnitFlags.NONE # 0 — no special formatting UnitFlags.REDUCE # reduce to a canonical named SI unit before serialising UnitFlags.ASCII_EXP # use ^N exponent notation instead of Unicode superscripts ``` `UnitFlags` is an `IntFlag` and its values may be OR-combined: ```python s = unit_to_str_ex(vu, UnitFlags.REDUCE | UnitFlags.ASCII_EXP) ``` ### `ValueUnitPrefix` `ValueUnitPrefix` is the public mirror of the C `value_unit_prefix_t` struct. It can be constructed with class methods or extracted from a `ValueUnitComponent`: ```python from bovnar import ValueUnitPrefix, SIPrefix, IECPrefix p_si = ValueUnitPrefix.make_si(SIPrefix.KILO) p_iec = ValueUnitPrefix.make_iec(IECPrefix.GIBI) vu = bovnar.parse_unit("Gi~B") comp = vu.components[0] p = comp.prefix # ValueUnitPrefix extracted from a component ``` ### Inline unit suffix In addition to the unit embedded in a type annotation (``), the Bovnar format supports an **inline unit suffix** placed directly after a scalar value, before the terminating `;`: ```bovnar .speed = 9.81 m/s; # inline suffix, no annotation .mass = 70.5 k~g;# annotation without unit, inline adopted .dist = 1.5 m; # annotation and inline both say 'm' — valid ``` From the Python layer, inline and annotation units are **identical**: both reach the application as `data.value_unit` inside the `Event.DATA` payload. No extra code is required to consume inline units. The validator raises `ErrorCode.UNIT_MISMATCH` (38 / `BovnarParseError`) when an inline suffix is present and a type-annotation unit is also present but the two do not resolve to the same `value_unit_t`. Inline unit suffixes inside array elements always raise `ErrorCode.UNEXPECTED_INPUT_BYTE`. --- ## `Quantity` `Quantity` is a typed, unit-annotated scalar value that preserves the original text representation, type width, numeral base, and physical unit across a `loads` / `dumps` round-trip. ```python from bovnar import Quantity, ValueTypeSpec, ValueUnit from bovnar.enums import ValueTypeFamily ``` ### Construction `Quantity` is normally produced by `loads(..., typed=True)` rather than constructed by hand, but direct construction is supported: ```python from bovnar.structs import make_type_spec from bovnar.enums import ValueTypeFamily vt = make_type_spec(ValueTypeFamily.FLOAT, 32) q = Quantity('101325.0', vt) # dimensionless q2 = Quantity('9.81', vt, vu) # vu is a ValueUnit, e.g. from parse_unit ``` For exact numeric input there is also the `Quantity.from_number` constructor, which stores the value as an exact decimal literal (so writing it with `dumps` is lossless to the format's precision): ```python from decimal import Decimal from fractions import Fraction Quantity.from_number(Decimal('19.99')) # float_dec:64 Quantity.from_number(Decimal('1.1'), family=ValueTypeFamily.FLOAT, width=128) Quantity.from_number(Fraction(837, 256), family=ValueTypeFamily.FLOAT_FIX, width=32, frac=8) ``` It accepts a `Decimal`, `Fraction` (must be a terminating decimal), `int`, `str` (a verbatim literal), or `float` (only as precise as the double), and validates the width for the chosen family. ### Properties and methods | Name | Type | Description | |---|---|---| | `q.raw` | `str \| None` | Original text token as it appeared in the BVNR stream | | `q.vtype` | `ValueTypeSpec` | Type family, bit width, and numeral base | | `q.unit` | `ValueUnit` | Physical unit (`BVN_UNIT_NONE` when dimensionless) | | `q.value` | property | Decode `raw` to the closest native Python scalar (`int`, `float`, `str`, `bool`) — **lossy** for `float_dec` / `float_fix` / `float:128`+ (goes through a C `double`) | | `q.unit_str()` | `str` | Canonical unit string (e.g. `'m/s²'`), or `''` when dimensionless | | `q.decimal()` | `Decimal` | **Exact** value as `decimal.Decimal` from the verbatim literal — lossless at any width; raises for non-numeric families | | `q.fraction()` | `Fraction` | Exact value as `fractions.Fraction` (for `float_fix`, the exact `mantissa / 2**frac`) | | `q.fixed_point()` | `(int, int)` | `(mantissa, frac_bits)` of a `float_fix` value; the value is `mantissa / 2**frac_bits` | | `q.stored_value()` | `Decimal` | The value materialised into the declared IEEE/fixed format (round-to-nearest-even) — differs from `decimal()` only when the literal carries more precision than the format holds | | `q.ieee_bits()` | `bytes` | IEEE-754 interchange bytes (binary16…256 for `float`, decimal16…256 for `float_dec`), little-endian word order | | `q.epoch_name` | `str \| None` | For a `datetime`, the epoch name (`"unix"`, `"tai"`, …); `None` otherwise | | `q.epoch_mjd` | `int \| None` | For a `datetime`, the epoch's Modified Julian Day; `None` otherwise | | `q.datetime_fraction` | `str \| None` | For a `datetime` written as a literal with a fractional second, the verbatim sub-second digits (spec 1.1); `None` otherwise | ### Lossless numeric access (`float_dec`, `float_fix`, `float:128`/`256`) `q.value` decodes through a C `double`, which loses precision for the decimal-float, fixed-point, and wide binary-float families. The accessors above instead use the verbatim literal text (and bovnar's arbitrary-precision `bvn_float`), so the full precision the format carries is reachable from Python: ```python q = bovnar.loads(b'.p= 3.141592653589793238462643383279503;', typed=True)['p'] q.value # 3.141592653589793 (lossy C double) q.decimal() # Decimal('3.141592653589793238462643383279503') (exact literal) q.stored_value() # Decimal('3.141592653589793') (the decimal64-rounded value) f = bovnar.loads(b'.x= 3.27;', typed=True)['x'] f.fraction() # Fraction(837, 256) f.fixed_point() # (837, 8) ``` These materialise over the **full** representable range, including exponents the C parser cannot otherwise reach (beyond ~1e9865). `decimal()` / `fraction()` work at every binary width bovnar allows (16, or a multiple of 32 up to 32768); the bit-exact `stored_value()` / `ieee_bits()` apply to the IEEE encodings (`float:16/32/64/128/256`), which are also exposed directly as `bovnar.BvnFloat`. ### `dumps()` integration `_emit_value` dispatches on `Quantity` before the plain `int` / `float` path, so any dict that came from `loads(..., typed=True)` can be passed directly to `dumps()` and will produce identical output: ```python bvnr = b".speed = 9.81;" doc = bovnar.loads(bvnr, typed=True) out = bovnar.dumps(doc) assert out.strip() == bvnr.strip() ``` The annotation is suppressed when `_needs_annotation` returns `False` — i.e. when the type is `FLOAT:64` with no unit and no non-decimal base (matching the C library's own default-annotation omission rules). `dumps()` also accepts bare `decimal.Decimal` (written as an exact `float_dec:64` literal) and terminating `fractions.Fraction` values directly — a non-terminating fraction (e.g. `Fraction(1, 3)`) raises `BovnarArgumentError`. --- ## `write_array` helper `write_array` is a high-level helper exported from the top-level `bovnar` namespace. It handles both flat and multi-dimensional arrays. ```python from bovnar import write_array, Writer from bovnar.structs import make_type_spec, make_unit_si from bovnar.enums import ValueTypeFamily, BaseUnit with Writer.to_mem() as w: # Single-row array write_array(w, "primes", [2, 3, 5, 7]) # Multi-row array (rows separated by /) write_array(w, "matrix", [[1, 2, 3], [4, 5, 6]]) # Typed array: whole-array type annotation vt = make_type_spec(ValueTypeFamily.UINT, 16) write_array(w, "ports", [80, 443, 8080], vt=vt) ``` Element types accepted per element: `int`, `float`, `str`, `bool`, `None`, `dict` (written as a struct), or nested `list`/`tuple` (written as a nested array). When *rows* is a flat sequence it is treated as a single-row array; when all top-level elements are themselves `list` or `tuple` it is treated as a multi-row array. --- ## pint bridge bovnar units interoperate with [pint](https://pint.readthedocs.io/) through a hand-verified translation table (`bovnar._pint_units`). pint is an **optional** dependency, imported lazily on first use; importing `bovnar` never requires it. Install with `pip install "bovnar[pint]"` (or `bovnar[all]`). The four bridge functions are exported from the top-level `bovnar` namespace (and from `bovnar._pint_bridge`): | Function | Direction | Description | |---|---|---| | `to_pint(value, vu, *, ureg=None)` | bovnar → pint | Wrap a scalar/ndarray + `ValueUnit` in a pint `Quantity` | | `to_pint_unit(vu, *, ureg=None)` | bovnar → pint | `ValueUnit` → pint `Unit` (dimensionless when no real unit) | | `from_pint(qty, *, ureg=None, validate=True)` | pint → bovnar | `Quantity` → `(magnitude, ValueUnit)` | | `from_pint_unit(unit, *, ureg=None, validate=True)` | pint → bovnar | `Unit` / `Quantity` / `str` → `ValueUnit` | ```python import bovnar vu = bovnar.parse_unit("k~m") # kilometre qty = bovnar.to_pint(5.0, vu) # mag, vu2 = bovnar.from_pint(qty) # (5.0, ValueUnit for km) vu3 = bovnar.from_pint_unit("newton") # str/Unit/Quantity all accepted ``` ### Prefixes ride in the unit, never the magnitude bovnar's `k~m` maps to pint `kilometer` — the prefix is kept inside the unit *name*, never folded into the magnitude. A wrapped value is therefore returned unscaled, so a wrapped numpy array is never silently rescaled. ### Affine temperature units Offset/affine scales (`°C`, `°F`, Réaumur, Delisle, Newton, Rømer) cannot carry a prefix or exponent — pint forbids it and bovnar never emits it. A prefixed or exponentiated affine unit raises `BovnarArgumentError` rather than a cryptic pint error. ### Validation `from_pint` / `from_pint_unit` validate the resulting `ValueUnit` by default (`validate=True`); a pint unit that maps to a structurally invalid bovnar unit (e.g. a prefix not permitted on that base) raises `BovnarArgumentError`. pint units with more than 8 components, non-integer exponents, or exponents outside `[-9, 9]` also raise. ### Registry control: `build_registry` A module-level default `pint.UnitRegistry` is built on first use. To share a registry across calls — or to register bovnar's custom units onto your own — pass `ureg=`: ```python from bovnar._pint_units import build_registry, is_currency_unit ureg = build_registry() # fresh registry with bovnar units ureg = build_registry(my_existing_registry) # extend an existing one ureg = build_registry(with_currencies=False) # skip the currency dimensions qty = bovnar.to_pint(5.0, vu, ureg=ureg) ``` `build_registry` registers bovnar's custom physical-unit definitions — units where pint's own definition differs or is missing (e.g. `bvnr_gauss`, `bvnr_var`, the historical German units) — plus, by default, the ISO 4217 + crypto currencies as custom dimensions. `is_currency_unit(unit)` reports whether a pint unit involves a currency dimension (holds for products such as `USD/year`). ### Semantic caveats A few bovnar units map to a pint native unit whose *semantics* differ slightly; these are recorded in `bovnar._pint_units.SEMANTIC_CAVEATS` (e.g. `BYTE`, `DECIBEL`, `NEPER`). Consult that dict when exact round-trip semantics matter. The mapping is **not 1:1** by construction (bovnar `b`=bit vs pint barn, `R`=roentgen vs pint's gas constant). The translation table is locked against silent drift by `TestUnitTableIntegrity`, which re-derives every physical unit's dimension and magnitude from bovnar and asserts the pint bridge reproduces both. --- ## NumPy bridge The NumPy bridge converts between bovnar arrays and `numpy.ndarray`. numpy is an **optional** dependency, imported lazily on first use. Install with `pip install "bovnar[numpy]"` (or `bovnar[all]`). All five functions are exported from the top-level `bovnar` namespace (and from `bovnar._numpy`): | Function | Direction | Description | |---|---|---| | `to_numpy(src, *, dtype=None, return_unit=False)` | bovnar → numpy | Array → `ndarray` (optionally `(ndarray, unit_str)`) | | `to_pint_array(src, *, dtype=None, ureg=None)` | bovnar → pint | Array → pint `Quantity` (ndarray data + unit) | | `from_numpy(writer, key, arr, *, unit=None, float_format=None)` | numpy → bovnar | Write an `ndarray` into a `Writer` | | `from_pint_array(writer, key, qty)` | pint → bovnar | Write a pint `Quantity` (magnitude + unit) into a `Writer` | | `array_to_bvnr(key, arr, *, unit=None, pretty=True, float_format=None)` | numpy → bovnar | `ndarray` → bovnar bytes (convenience) | ### Reading: `to_numpy` *src* is either a `DomNode` for an ARRAY (random-access, from `dom_parse`) or the nested list/tuple that `loads(..., typed=True)` produces. Both `/`-separated rows and bracket nesting collapse to the same `ndarray` shape. ```python import bovnar # from a typed loads() result doc = bovnar.loads(b'.a=[1,2,3];', typed=True) arr = bovnar.to_numpy(doc['a']) # dtype uint8 # with the whole-array unit arr, unit = bovnar.to_numpy( bovnar.loads(b'.a=[1,2,3]/[4,5,6];', typed=True)['a'], return_unit=True) # unit == 'm/s', arr.shape == (2, 3) # from a DOM node arr = bovnar.to_numpy(bovnar.dom_parse(b'.a=[10,-20,30];')['a']) ``` The bovnar `(family, width)` maps directly to the numpy dtype (`uint:8` → `uint8`, `float:32` → `float32`, …). Pass `dtype=` to coerce. The families with no exact native numpy dtype — `float_dec`, `float_fix`, and the wide binary floats `float:128`/`256` (and any width above 64) — decode to an **object array of exact `decimal.Decimal`** on the typed (`loads(..., typed=True)`) path, which is lossless; pass `dtype='float64'` for the lossy native conversion. The DOM (`dom_parse`) path only has a C `double` for these, so it raises and points you at the typed path or `dtype='float64'`. Integers wider than 64 bits likewise come back as an object array of Python ints (`dtype=object`), which is exact. A **`datetime`** array on the **unix** epoch (spec 1.1) maps to/from `datetime64[s]` — the integer epoch-seconds carrier is unix-relative, so it round-trips losslessly. A non-unix epoch (`tai`, `gps`, …) is *not* unix-relative, so `to_numpy` refuses it (mapping it to `datetime64` would mis-date every value) and points you at `dtype='int64'` for the raw seconds. On the write side a `datetime64[*]` array is coarsened to whole seconds and written as ``; `NaT` has no bovnar representation and is rejected, and `array_to_bvnr` prepends the `#!bovnar 1.1` directive so its output re-parses. **Strict by default:** * an array that mixes numeric encodings (e.g. `uint:8` and `float:64`) raises unless you pass `dtype=` to coerce to one; * bovnar 1.0 arrays are rectangular and homogeneous, so the result is always a regular ndarray — there is no ragged case to handle; * a `null` element cannot fill a dtype with no null slot — integer, `bool`, or string (which would otherwise silently become `0`/`False`/`'None'`); pass `dtype=float` (→ `NaN`) or `dtype=object` (→ `None`); * the unit is a whole-array property (numpy has one dtype per array) — mixed units raise, as does mixing a dimensioned element with a dimensionless one; the unit is returned alongside the data, never baked into elements. ### Writing: `from_numpy` / `array_to_bvnr` ```python import numpy as np import bovnar from bovnar import Writer with Writer.to_mem() as w: bovnar.from_numpy(w, "velocity", np.array([1.5, 2.5], dtype=np.float32), unit="m/s") out = w.get_output() # one-shot convenience raw = bovnar.array_to_bvnr("matrix", np.arange(8).reshape(2, 2, 2)) # exact decimal/fixed/wide-binary float arrays via float_format from decimal import Decimal prices = np.array([Decimal("19.99"), Decimal("0.01")], dtype=object) bovnar.array_to_bvnr("p", prices, unit="$USD") # -> bovnar.array_to_bvnr("a", prices, float_format=("float", 128)) # binary128 ``` * `from_numpy` requires a 1-D-or-higher array; write a 0-D scalar with the scalar `Writer` API instead. * *unit* may be a bovnar unit string, a `ValueUnit`, or a pint `Unit`/`Quantity` (anything else raises `BovnarArgumentError`). * Units apply to **numeric** arrays only — a unit on a `bool` or string array raises `BovnarArgumentError`. * An **object array of `Decimal`/`Fraction`** is written as an exact decimal/fixed/wide-binary float. `float_format=(family, width[, frac])` — family `'float'`, `'float_dec'` (the default for a Decimal object array), or `'float_fix'` — selects the target; it is required to disambiguate an object array that is not purely `Decimal`/`Fraction`. * A **masked array** with any masked entry is rejected (it would otherwise serialise the underlying value of a masked cell); fill or unmask it first. * `array_to_bvnr` grows its write buffer like `dumps()` (4 MiB, doubling up to 256 MiB). ### pint arrays `to_pint_array` and `from_pint_array` bridge straight through to pint Quantities backed by ndarrays, reusing the unit translation above: ```python q = bovnar.to_pint_array( bovnar.loads(b'.a=[1,2,3];', typed=True)['a']) # q is a pint Quantity: magnitude ndarray [1, 2, 3], unit 'kilometer' with Writer.to_mem() as w: bovnar.from_pint_array(w, "dist", q) ``` A `datetime` array has no pint unit, so `to_pint_array` rejects it rather than wrap it as a meaningless dimensionless quantity — use `to_numpy` for the `datetime64` array (or `dtype='int64'` for the raw epoch seconds). --- ## Currency helpers The `bovnar.currency` submodule (exposed as `bovnar.currency`) provides metadata for the ISO 4217 fiat and cryptocurrency `BaseUnit` members. It is pure Python — no library or optional dependency required. ```python from bovnar import currency, BaseUnit currency.is_currency(BaseUnit.USD) # True currency.is_fiat(BaseUnit.USD) # True currency.is_crypto(BaseUnit.BTC) # True info = currency.currency_info(BaseUnit.USD) info.code # 'USD' info.numeric_code # 840 (ISO 4217 numeric; 0 for crypto) info.minor_unit # 2 (decimal places: 1 major = 10^minor minor units) info.name # 'US Dollar' currency.minor_unit(BaseUnit.JPY) # 0 currency.currency_code(BaseUnit.EUR) # 'EUR' currency.from_code('GBP') # BaseUnit.GBP for ci in currency.all_fiat(): # all_crypto() / all_currencies() also exist ... ``` `CurrencyInfo` is a frozen dataclass; `currency_info`, `minor_unit`, `currency_name`, `currency_code`, and `from_code` raise for non-currency bases or unknown codes. --- ## `Reader` reference ### Construction ```python with Reader() as r: ... ``` `Reader.__init__` calls `bvnr_reader_create` immediately. Use as a context manager or call `r.close()` explicitly to release the C object. ### `read_mem(data, *, on_verified, on_unverified, max_file_size, continue_on_error, strict_version, want_unit, want_unit_allow_nonterminating, max_conversion_length)` Parse BVNR from a `bytes`, `bytearray`, or `memoryview` object. | Parameter | Type | Default | Description | |---|---|---|---| | `data` | `bytes \| bytearray \| memoryview` | — | Input buffer | | `on_verified` | `Callable[[Event, BvnrData \| None], bool] \| None` | `None` | Callback for validated events | | `on_unverified` | `Callable[[Event, BvnrData \| None], bool] \| None` | `None` | Callback for raw pre-validation events | | `max_file_size` | `int` | `0` (unlimited) | Hard limit on bytes consumed | | `continue_on_error` | `bool` | `False` | Enable resync mode | | `strict_version` | `bool` | `False` | Reject a declared spec version newer than this build (`error_unsupported_spec_version`) | | `want_unit` | `Callable[[BvnrData], tuple \| None] \| None` | `None` | Optional read-time lossless unit/base conversion hook. Called per numeric value; return `(unit, base)` to request a conversion (the exact result arrives on the payload as `converted`/`converted_text`), or `None` to leave the value untouched. An inexact conversion aborts the parse (`error_unit_inexact`). | | `want_unit_allow_nonterminating` | `bool` | `False` | Deliver an exact-rational-but-non-terminating conversion (e.g. km/h→m/s) as a rational with `converted_text` `None` instead of aborting with `error_unit_inexact` | | `max_conversion_length` | `int` | `0` (default 1024) | Longest converted text a conversion may produce, in characters; a longer exact result aborts with `error_value_out_of_range` | ### `read_fd(fd, *, on_verified, on_unverified, max_file_size, continue_on_error, strict_version, want_unit, want_unit_allow_nonterminating, max_conversion_length)` Parse BVNR from an open POSIX file descriptor. Parameters identical to `read_mem` except the first argument is a non-negative `int` fd. ### `read_file(path, *, on_verified, on_unverified, max_file_size, continue_on_error, strict_version, want_unit, want_unit_allow_nonterminating, max_conversion_length)` Convenience wrapper: opens `path` with `os.O_RDONLY`, calls `read_fd`, closes the fd in a `finally` block. The `max_file_size` default is `MAX_FILESIZE_BYTES` (16 MiB) rather than unlimited. ### `iter_mem(data, *, verified_only, max_file_size)` Generator that collects all events from `read_mem` and yields `EventPayload(event, raw, value_type, value_unit)` objects. | Parameter | Default | Description | |---|---|---| | `verified_only` | `True` | When `False`, both callbacks fire and events may appear twice | | `max_file_size` | `0` | Forwarded to `read_mem` | `EventPayload` fields: | Field | Type | Description | |---|---|---| | `event` | `Event` | The event code | | `raw` | `bytes` | Raw token bytes | | `value_type` | `ValueTypeSpec \| None` | Type annotation if present | | `value_unit` | `ValueUnit \| None` | Unit if present | | `text` | `str` (property) | `raw` decoded as UTF-8 (with `errors='replace'`) | | `converted` | `bool` | `True` when a `want_unit` conversion was applied to this value | | `converted_text` | `str \| None` | Exact converted value as a positional string; `None` if the conversion does not terminate in `converted_base` | | `converted_base` | `int` | Base the converted text is rendered in | ### Error-state properties These properties query the C reader object after a failed parse. | Property | Type | Description | |---|---|---| | `error_code` | `ErrorCode` | Most recent error code | | `error_line` | `int` | 1-based line number of the error | | `error_column` | `int` | 1-based column of the error | | `error_offset` | `int` | Byte offset of the error | | `recovery_count` | `int` | Number of times resync was entered (incremented at error entry, not at resync completion) | ### `MAX_FILESIZE_BYTES` ```python from bovnar import MAX_FILESIZE_BYTES # 16 * 1024 * 1024 (16 MiB) ``` Default `max_file_size` cap used by `Reader.read_file`. --- ## `Writer` reference ### Construction class methods | Method | Description | |---|---| | `Writer.to_mem(buf=None, cap=4194304, *, pretty=True)` | Write to an in-process buffer. `buf` may be a pre-allocated `bytearray`; when `None` an internal buffer of size `cap` is allocated. | | `Writer.to_fd(fd, *, pretty=True)` | Write to an open POSIX file descriptor. | | `Writer.to_file(path, *, pretty=True)` | Open `path` for writing (`O_WRONLY\|O_CREAT\|O_TRUNC`, mode `0o644`) and write to it; the fd is closed when the writer is finished or destroyed. | All three are used as context managers. On clean exit (`exc_type is None`) the context manager calls `finish()` automatically. ### Output retrieval | Method / property | Description | |---|---| | `w.get_output() -> bytes` | Return the bytes written so far (mem writers only). | | `w.bytes_written` | Number of bytes written (all writer modes). | | `w.finish()` | Flush and seal the output. Raises `BovnarWriteError` if any struct is still open. | | `w.destroy()` | Release the underlying C writer object immediately. | ### Scalar write methods All scalar writers accept keyword-only unit parameters. Unit resolution priority: `unit_str` (parsed via `bvn_parse_unit_n`) → `unit_iec_base` → `unit_si_base` → `BVN_UNIT_NONE` (no annotation emitted). #### `write_uint(key, value, *, width=64, base=10, unit_str=None, unit_si_base=None, unit_si_prefix=SIPrefix.NONE, unit_si_exp=Exponent.LINEAR, unit_iec_base=None, unit_iec_prefix=IECPrefix.NONE)` Write an unsigned integer. `base` selects the numeral system (10 or 16 for inline values; any Bovnar-supported base for `write_bvni`). #### `write_sint(key, value, *, width=64, base=10, unit_str=None, unit_si_base=None, unit_si_prefix=SIPrefix.NONE, unit_si_exp=Exponent.LINEAR)` Write a signed integer. #### `write_float(key, value, *, width=64, unit_str=None, unit_si_base=None, unit_si_prefix=SIPrefix.NONE, unit_si_exp=Exponent.LINEAR)` Write an IEEE 754 binary float. #### `write_float_fix(key, value, *, width=64, q=0, unit_str=None, unit_si_base=None, unit_si_prefix=SIPrefix.NONE, unit_si_exp=Exponent.LINEAR)` Write a Q-format fixed-point value. `q` is the number of fractional bits (`0 ≤ q < width`). #### `write_float_dec(key, value, *, width=64, unit_str=None, unit_si_base=None, unit_si_prefix=SIPrefix.NONE, unit_si_exp=Exponent.LINEAR)` Write an IEEE 754-2008 decimal float. #### `write_string(key, value)` Write a bare quoted UTF-8 string with no type annotation: ```bovnar .host = "localhost"; ``` To emit an explicit `` annotation, use the low-level `emit` API with `Event.TYPE_ANNOTATION_START` / `TYPE_ANNOTATION_END` before `Event.DATA`. #### `write_bool(key, value)` Write a `bool` value (`token_is_bool`) — serialized as the bare keyword `true` or `false`. On read-back it decodes to a Python `bool`. #### `write_null(key)` Write a null value (empty slot). ### Extended integer writers #### `write_bvni(key, value, *, width=64, base=10, signed=None, unit_str=None, unit_si_base=None, unit_si_prefix=SIPrefix.NONE, unit_si_exp=Exponent.LINEAR)` Arbitrary-width integer writer that supports all Bovnar numeral bases (**2–62, 64, and 85**). Non-decimal values are formatted using Python's own big-integer arithmetic and emitted as quoted strings. `signed` defaults to `True` when `value < 0`. #### `write_bvnf_base(key, value_str, *, width=0, base=10, unit_str=None, unit_si_base=None, unit_si_prefix=SIPrefix.NONE, unit_si_exp=Exponent.LINEAR)` Write a float from a pre-formatted string in base 10 or 16. `base` must be 10 or 16; any other value raises `BovnarArgumentError`. ### Struct helpers ```python w.begin_struct(key) # emit ASSIGNMENT_START + STRUCT_START, increment depth w.end_struct() # emit STRUCT_END, decrement depth ``` `finish()` verifies that the struct depth is zero; an unclosed struct raises `BovnarWriteError(GOT_INCOMPLETE_BVNR_STREAM)`. ### Version directive ```python w.write_version(major=1, minor=1) # emit a leading "#!bovnar 1.1" directive ``` Emits a leading `#!bovnar .` directive. Must be called before any value is written; calling it after output has begun raises `BovnarWriteError(INVALID_ARGUMENT)`. Use it to self-declare the spec version a document targets (spec-1.1 constructs such as datetime literals need it to re-read). ### Array helpers ```python w.begin_array_row() # emit ARRAY_ROW_START w.end_array_row() # emit ARRAY_ROW_END w.new_array_dim() # emit ARRAY_DIM_START (the / separator between rows) ``` ### Low-level `emit` ```python w.emit(event, *, key=None, value=None, vt=None, vu=None) ``` Send an arbitrary event to the C writer. `key` and `value` are encoded as UTF-8. When both `vt` and `value` are supplied the token type is inferred: `_TOKEN_IS_STRING` for `ValueTypeFamily.UTF8`, `_TOKEN_IS_NUMBER` otherwise. --- ## DOM API The DOM API parses a complete BVNR document into an in-memory tree for random-access queries without writing a SAX callback. ### `dom_parse(data) -> DomDoc` Top-level convenience function (mirrors `DomDoc.parse`). ```python import bovnar doc = bovnar.dom_parse(bvnr_bytes) ``` ### `DomDoc` Owning wrapper around `bvn_dom_doc_t`. Destroying the object frees the entire tree; any `DomNode` derived from it becomes invalid after that point. | Method / property | Description | |---|---| | `DomDoc.parse(data)` | Class method. Parse `bytes \| bytearray \| memoryview`. | | `DomDoc.parse_fd(fd)` | Class method. Parse from an open file descriptor. | > **File-size limits:** `DomDoc.parse_fd` and `DomDoc.parse_file` apply an internal > ceiling of `BVN_DOM_FD_MAX_BYTES` (256 MiB). This is 16× larger than > `Reader.read_file`'s `MAX_FILESIZE_BYTES` (16 MiB). Use `bvn_dom_parse_fd_ex` > directly if you need a different limit. | `DomDoc.parse_file(path)` | Class method. Open path and parse (fd closed in `finally`). Applies an internal hard cap of `BVN_DOM_FD_MAX_BYTES` (256 MiB) via the C function `bvn_dom_parse_fd`. This ceiling is distinct from `Reader.read_file`'s `MAX_FILESIZE_BYTES` (16 MiB). | | `doc.parse_error` | `ErrorCode` — `NONE` on success. | | `doc[key]` | Return top-level `DomNode` by key; raises `KeyError` when absent. | | `key in doc` | `True` when the top-level key exists. | | `len(doc)` | Number of top-level entries. | | `iter(doc)` | Iterate over `(key, DomNode)` pairs. | | `doc.entries()` | Return all top-level `(key, DomNode)` pairs as a list. | | `doc.lookup(path)` | Dot-separated path lookup, e.g. `'server.tls.cert'`. Returns `None` when absent. | | `doc.to_dict()` | Convert entire document to a plain Python dict, dropping type and unit info. | ### `DomNode` Non-owning view into a `bvn_dom_node_t`. The parent `DomDoc` must remain alive for as long as any derived `DomNode` is in use. | Property / method | Description | |---|---| | `node.dom_type` | `DomType` enum value | | `node.value_type` | `ValueTypeSpec` | | `node.unit` | `ValueUnit` | | `node.unit_str` | Unit as a string via `bvn_dom_get_unit_string`, or `''` | | `node.is_null()` | `True` for null values | | `node.value_in_base_units()` | Numeric value scaled to SI base units (`float`) | | `node.as_i64()` / `as_u64()` | Signed / unsigned 64-bit integer | | `node.as_i32()` / `as_u32()` | Signed / unsigned 32-bit integer | | `node.as_i16()` / `as_u16()` | Signed / unsigned 16-bit integer | | `node.as_i8()` / `as_u8()` | Signed / unsigned 8-bit integer | | `node.as_float()` | `float` (64-bit) | | `node.as_bool()` | `bool` for `BOOL` nodes | | `node.as_str()` | Python `str` for `STRING`, `SYMBOL`, or `REFERENCE` nodes | | `node.as_bytes()` | `bytes` for `OCTET_STREAM` nodes | | `node.as_int_str(base=10)` | Integer value as a string in the given base; result C string is freed before return | | `node.datetime_fraction` | `str` of the verbatim ISO sub-second digits for a `datetime` written as a literal with a fraction (spec 1.1), else `None`; the carrier value is unchanged and still read via `to_python()` | | `node[key]` | Child `DomNode` by string key (STRUCT) or integer index (ARRAY) | | `key in node` | Membership test for STRUCT nodes | | `len(node)` | Element count for STRUCT or ARRAY nodes | | `iter(node)` | For STRUCT: iterate `(key, DomNode)` pairs; for ARRAY: iterate elements | | `node.entries()` | List of `(key, DomNode)` pairs (STRUCT nodes only) | | `node.array_dims()` | Number of `/`-separated dimensions (ARRAY nodes only) | | `node.to_python()` | Recursively convert to a native Python value (drops type/unit info) | ### `DomType` ``` NULL=0 INT=1 FLOAT=2 STRING=3 SYMBOL=4 REFERENCE=5 STRUCT=6 ARRAY=7 OCTET_STREAM=8 BOOL=9 ``` --- ## Running the test suite ```bash # Run everything (library-dependent tests are skipped when libbvnr.so is absent) pytest # Run only the pure-Python tests (no library required) pytest -m "not needs_lib" # Run only integration tests (requires libbvnr.so) pytest -m needs_lib # Verbose with short tracebacks (already the default via pyproject.toml) pytest -v --tb=short ``` The NumPy- and pint-bridge tests `pytest.importorskip` their dependency, so they are skipped automatically when `numpy` / `pint` is not installed. Install `bovnar[all]` to run the full suite. --- ## Package layout ``` bovnar/ ├── __init__.py # loads() / dumps() / dom_parse() / unit helpers — public API ├── _ffi.py # ctypes FFI: library discovery + argtypes/restype ├── _bvnfloat.py # BvnFloat: arbitrary-precision float + IEEE binary/decimal/fixed encoders ├── _numpy.py # NumPy bridge: to_numpy/from_numpy/to_pint_array/array_to_bvnr (numpy optional) ├── _pint_bridge.py # pint bridge: to_pint/from_pint/to_pint_unit/from_pint_unit (pint optional) ├── _pint_units.py # verified bovnar↔pint unit table + build_registry() ├── currency.py # ISO 4217 / crypto currency metadata (pure Python) ├── dom.py # DomDoc, DomNode, DomType — random-access DOM ├── enums.py # Python IntEnum mirrors of C enums ├── exceptions.py # BovnarError hierarchy ├── quantity.py # Quantity — typed, unit-annotated scalar for lossless round-trips ├── reader.py # Reader class + EventPayload dataclass ├── structs.py # ctypes Structure/Union definitions + ValueUnitPrefix + make_* helpers ├── units.py # unit_to_si_factor, unit_convert_factor, etc. └── writer.py # Writer class tests/ ├── conftest.py # shared fixtures, needs_lib marker ├── test_analytics.py # analytic / benchmarking tests (needs_lib) ├── test_array_parser.py # array parsing integration tests (needs_lib) ├── test_currency_units.py # currency unit round-trip tests (needs_lib) ├── test_dom.py # DOM API integration tests (needs_lib) ├── test_enums.py # pure-Python enum tests ├── test_example_numpy_roundtrip.py # NumPy bridge end-to-end example (needs_lib + numpy) ├── test_lossless_floats.py # exact decimal/fixed/float128-256 round-trips (needs_lib) ├── test_numpy_bridge.py # NumPy bridge tests (needs_lib + numpy) ├── test_pint_bridge.py # pint bridge + unit-table integrity (needs_lib + pint) ├── test_reader.py # integration: Reader (needs_lib) ├── test_structs.py # pure-Python struct / helper tests ├── test_unit_physics.py # unit physics / conversion tests (needs_lib) ├── test_units.py # mixed: unit parsing / serialisation (needs_lib for FFI) ├── test_write_array.py # write_array integration tests (needs_lib) └── test_writer.py # integration: Writer (needs_lib) ``` --- ## Error handling All errors surface as subclasses of `BovnarError`: | Exception | When raised | |---|---| | `BovnarLibraryNotFound` | `libbvnr.so` not found at import | | `BovnarParseError` | Parse error in `Reader` (carries `code`, `line`, `column`, `offset`, `byte`) | | `BovnarWriteError` | Write error in `Writer` (carries `code`, `offset`) | | `BovnarArgumentError` | Invalid argument passed to a helper (e.g. bad unit string, closed reader/writer) | **`unit_convert_factor` error semantics:** The C function uses the `(ok, requires_affine)` pair to signal three distinct outcomes: | ok | requires_affine | Meaning | |---|---|---| | `True` | `False` | Multiplicative conversion; `factor` is ready to use | | `False` | `True` | Affine conversion required (e.g. °C ↔ K); `factor` is still valid but a plain multiply is insufficient | | `False` | `False` | Units are dimensionally incompatible → `BovnarArgumentError` | `BovnarArgumentError` is raised **only** when both `ok=False` and `requires_affine=False`. When `requires_affine=True`, call `convert_value` which handles the two-step affine path automatically, or call `unit_to_si_factor` on both units and perform the conversion manually. **Callbacks returning `False`:** When an `on_verified` or `on_unverified` callback returns `False`, the C parser stops and `bvnr_read` returns failure. The Python layer then calls `_raise_error()` and raises `BovnarParseError` with whatever error code the reader recorded. `BovnarCallbackAbort` is defined in `exceptions.py` but is not raised by the current implementation. **Callbacks raising exceptions:** If the callback itself raises a Python exception, that exception is captured, the C callback returns `False` to stop the parse, and the original exception is re-raised from `read_mem` / `read_fd` after the C call returns. --- ## FFI details ### `ON_ERROR_FUNC` signature The error callback type matches the C `bvnr_on_error_fn` signature exactly: ```c void (*bvnr_on_error_fn)( void* userdata, error_code_t err, uint64_t line, uint64_t column, uint32_t byte, uint64_t offset); ``` In Python this is declared as: ```python ON_ERROR_FUNC = ctypes.CFUNCTYPE( None, ctypes.c_void_p, # userdata ctypes.c_int, # error_code_t err ctypes.c_uint64, # line ctypes.c_uint64, # column ctypes.c_uint32, # byte ctypes.c_uint64, # offset ) ``` ### `BvnrWriteFlags` layout `BvnrWriteFlags` mirrors the C `bvnr_write_flags_s` struct in full, including the trailing `unit_flags` field (`bvn_unit_flags_t`, a `uint32_t`): | Flag constant (C) | Value | Effect | |---|---|---| | `BVN_UNIT_FLAGS_NONE` | `0` | Default: full Unicode exponent characters | | `BVN_UNIT_REDUCE` | `1 << 0` | Reduce compound units before serialising | | `BVN_UNIT_ASCII_EXP` | `1 << 1` | Use `^N` ASCII exponent notation instead of Unicode superscripts | The writer respects the `unit_flags` stored inside the C writer object when serialising unit annotations. `_emit_annotation` retrieves the live flags via `bvnr_writer_unit_flags(w)` and passes them to `bvn_unit_to_string_ex`. ### `write_string` behaviour `Writer.write_string` emits a bare quoted string with no type annotation, matching `bvnr_write_string` in the C library: ```bovnar .host = "localhost"; ``` To write a string with an explicit `` annotation, use the low-level `emit` API with `Event.TYPE_ANNOTATION_START` / `TYPE_ANNOTATION_END` events before the `Event.DATA` event. ### `_resolve_unit` default When no unit arguments are supplied to `write_uint`, `write_sint`, `write_float`, etc., the unit resolves to `BVN_UNIT_NONE` (zero components), matching the C convenience helpers. No unit annotation is emitted in this case. Passing `unit_si_base` or `unit_iec_base` produces a unit with one component; the `_SENTINEL`-only `no_unit` keyword is only produced when the caller explicitly constructs a dimensionless `ValueUnit` with `BaseUnit.NONE`. --- ## `BaseUnit` enum The `BaseUnit` enum mirrors the full C `value_base_unit_e`: | Range | Members | |---|---| | 0 | `NONE` | | 1–2 | `BIT`, `BYTE` | | 3–9 | SI base units (`SECOND` … `CANDELA`) | | 10–28 | Named SI derived units (`HERTZ` … `KATAL`) | | 29–44 | Non-SI accepted units (`LITER` … `YEAR`) | | 45–54 | Imperial/US length (`INCH` … `FATHOM`) | | 55–62 | Imperial/US mass (`POUND` … `CARAT`) | | 63 | `FAHRENHEIT` | | 64–67 | Pressure (`ATMOSPHERE` … `PSI`) | | 68–71 | Energy (`CALORIE` … `THERM`) | | 72 | `HORSEPOWER` | | 73–75 | Force (`POUND_FORCE`, `DYNE`, `KIP`) | | 76 | `KNOT` | | 77–85 | US volume (`GALLON` … `BARREL`) | | 86–87 | Area (`ACRE`, `BARN`) | | 88–90 | Angle (`ARCMINUTE`, `ARCSECOND`, `GRAD`) | | 91–98 | CGS units (`POISE` … `GALILEO`) | | 99–101 | Radiation (`CURIE`, `ROENTGEN`, `REM`) | | 102–103 | Logarithmic (`NEPER`, `DECIBEL`) | | 104 | `RANKINE` | | 105 | `SLUG` | | 106 | `THOU` | | 107–109 | UK imperial volume (`PINT_UK`, `FLUID_OUNCE_UK`, `QUART_UK`) | | 110–111 | Electrical power (`VAR`, `VOLT_AMPERE`) | | 112 | `KILOGRAM_FORCE` | | 113 | `INCH_HG` | | 114 | `RPM` | | 115 | `FOOT_POUND` | | 116–117 | Mass additional (`DRAM`, `PENNYWEIGHT`) | | 118–119 | Length additional (`CHAIN`, `ROD`) | | 120–121 | Volume additional (`GILL`, `GILL_UK`) | | 122 | `STANDARD_GRAVITY` | | 123 | `METRIC_HORSEPOWER` | | 124 | `REVOLUTION` | | 125–126 | Time additional (`MONTH`, `FORTNIGHT`) | | 127 | `ATMOSPHERE_TECHNICAL` | | 128–129 | Textile linear density (`TEX`, `DENIER`) | | 130–133 | Apothecary/dry volume (`FLUID_DRAM`, `MINIM`, `PECK`, `BUSHEL`) | | 134–297 | ISO 4217 fiat currencies (`AED` … `ZWL`) — see `CURRENCY_FIAT_FIRST` / `CURRENCY_FIAT_LAST` | | 298–347 | Cryptocurrencies (`BTC` … `RUNE`) — see `CURRENCY_CRYPTO_FIRST` / `CURRENCY_CRYPTO_LAST` | | 348–360 | Historical German units (`PFUND`, `ZENTNER`, `DOPPELZENTNER`, `LOT`, Prussian line/zoll/fuss/elle/rute, `KLAFTER`, `GERMAN_MILE`, `MORGEN`, `SCHEFFEL`) | | 361–367 | Additional physical units (`SURVEY_FOOT`, `LEAGUE`, `CABLE`, `HAND`, `QUINTAL`, `SCRUPLE`, `BAUD`) | | 368–371 | Temperature scales (`DELISLE`, `NEWTON_TEMP`, `REAUMUR`, `ROMER`) | | 372–377 | Ratio/proportion units (`PERCENT`, `PER_MILLE`, `PER_MYRIAD`, `PER_CENT_MILLE`, `PPM`, `PPB`) | | 378–379 | Appended fiat currencies (`ZWG`, `XCG`) — added past the unit block for ABI stability; see `CURRENCY_EXT_FIRST` / `CURRENCY_EXT_LAST` | | 380 | `_SENTINEL` (internal bound; do not use) | > **Note on `CUP`:** the Cuban-Peso currency is exposed as **`BaseUnit.CUP_`** (trailing > underscore), not `BaseUnit.CUP`. The plain name `CUP` is the US-cup volume unit > (enum value 81); since Python enum member names must be unique, the currency at > value 167 takes the suffixed name. This affects the Python member name only — the > `.bvnr` wire token is still the bare uppercase `CUP`, and case stays load-bearing > (`cup` = volume, `CUP` = currency). --- ## Spec 1.1 additions These bindings target the **Bovnar spec (v1.1)**; spec 1.0 remains the frozen baseline. The 1.1 features (all gated on a `#!bovnar 1.1` directive — an unversioned document is treated as 1.0) are exposed as: - **Version:** `bovnar.version()` (library version string), `bovnar.spec_version()` → `(major, minor)` of the highest spec understood, and `bovnar.peek_version(data)` → the `(major, minor)` a document declares, or `None`. `Reader.declared_version` gives the same after a read; `read_mem`/ `read_fd`/`read_file` accept `strict_version=True` to reject a too-new version (`ErrorCode.UNSUPPORTED_SPEC_VERSION`). `loads` does not take `strict_version`; use a `Reader` when you need it. - **Richer escapes:** `\u{…}` and `\xHH` in string literals are decoded by the C reader, so `loads` returns the resulting text transparently. A surrogate / out-of-range `\u` is `ErrorCode.INVALID_CODEPOINT`. - **Datetime family:** `loads` decodes a `` value to a plain `int` (signed epoch seconds). With `typed=True` it is a `Quantity` whose `.epoch_name` (`"unix"`, `"tai"`, …) and `.epoch_mjd` recover the epoch. `dumps()` emits the `` annotation and prepends `#!bovnar 1.1` automatically when the object contains a datetime. With `typed=True` the sub-second fraction of an ISO-8601 literal (spec 1.1) is preserved on the `Quantity` and re-emitted, so a `loads(typed=True)`→`dumps()` round-trip is lossless (`…T12:00:00.5Z` survives verbatim). The plain (non-typed) value is only the whole-second `int` carrier, so a non-typed `loads`→`dumps` drops the fraction; to read it explicitly use the DOM tier (`DomNode.datetime_fraction`) or the streaming reader (`bvnr_data_t.frac_data` via a callback). `ValueTypeFamily.DATETIME` is the family enum member. - **Reference array indexing:** `&.matrix[0][1]` paths are stored verbatim and resolved by `DomDoc.lookup("matrix[0][1]")` at the DOM layer. `ErrorCode` adds `INVALID_SPEC_VERSION` (42), `UNSUPPORTED_SPEC_VERSION` (43), `INVALID_CODEPOINT` (44), `INVALID_DATETIME_LITERAL` (45), and `DATETIME_LITERAL_UNSUPPORTED_EPOCH` (46). (* ================================================================== *) (* BOVNAR – EBNF derived from parser/lexer/validator implementation *) (* Version 1.1 Notation: ISO/IEC 14977:1996 *) (* { } = zero or more [ ] = optional *) (* ( ) = grouping | = alternation *) (* , = concatenation ; = rule terminator *) (* "…" = literal terminal (*…*) = comment *) (* Updated: 2026-06-17 — drop the residual "1.1 (draft)" label (spec *) (* 1.1 is released); note datetime also accepts a width parameter; *) (* reframe base-unit as a lookup into the full 163-unit / 491-spelling *) (* table (NOT the closed SI subset it previously claimed). *) (* 2026-06-16 — consistency pass vs the implementation: *) (* allow ws/comments before the assignment ";" and the "/" array *) (* separator; move the inline unit to a value-only scalar-with-unit *) (* rule so array elements are correctly excluded; bound \u{…} to 1–6 *) (* hex digits and version-int to no-leading-zero; inline type-spec; *) (* split the array-homogeneity / duplicate-key constraints into the *) (* LEXER/VALIDATOR tier (the streaming reader) vs the DOM-BUILDER *) (* tier (only a DOM parse rejects ragged siblings, mixed kinds, and *) (* duplicate keys; the streaming reader accepts them). *) (* 2026-06-15 — spec 1.1 (additive over the frozen 1.0): *) (* version directive (#!bovnar M.N), datetime type family, *) (* \xHH / \u{…} string escapes, reference array indexing. *) (* Earlier (2026-05-29): null/true/false/on/off keywords, bool *) (* type family, special numbers respelled nan / inf / ninf *) (* ================================================================== *) (* ───────────────────────────────────────────────────────────────── *) (* 1. Top-level stream *) (* ───────────────────────────────────────────────────────────────── *) (* Valid end-of-stream states (lexer states where EOF is accepted): *) (* undefined – nothing has been read yet (empty stream) *) (* first_comment_intro – inside a leading comment that runs to EOF *) (* with no trailing newline (comment-only stream) *) (* first_bom – BOM or first-line comment (its newline seen), *) (* no assignment yet *) (* value_outro – the semicolon of the last assignment consumed *) (* comment_intro / ignore_comment_byte – a trailing comment *) (* opened after the last ";", ended by EOF *) (* In all these cases struct_nesting_level must be zero. *) (* Events emitted to the validator callbacks (bvnr_event_t): *) (* *) (* ev_stream_start – once at the very start of bvn_lex_run *) (* ev_stream_end – once at the very end of bvn_lex_run, *) (* after the last value's terminator is consumed *) (* ev_assignment_start – on the identifier token of an assignment *) (* ev_struct_start – on "{" *) (* ev_struct_end – on "}" *) (* ev_array_row_start – on "[" opening a bracket row *) (* ev_array_row_end – on "]" closing a bracket row *) (* ev_array_dim_start – on "/" between rows; fired immediately *) (* when "/" is consumed, before the next *) (* "[" opens the next row *) (* ev_octet_stream_start – on the NUL byte that enters binary mode *) (* ev_octet_stream_end – on the 0x00 tag ending an octet stream *) (* *) (* For every typed value (including values whose type annotation is *) (* synthesised by the validator – see §4) the following sequence *) (* is emitted before ev_data: *) (* *) (* ev_type_annotation_start – once, before params *) (* ev_type_annotation_type_family – the family name *) (* ev_type_annotation_type_family_parameter – zero to three *) (* times, once per present parameter (width, *) (* base or q, unit), in that order *) (* ev_type_annotation_end – once, after params *) (* ev_data – the value token *) (* *) (* ev_type_annotation_start is emitted to on_unverified first *) (* (raw bytes only), then to on_verified (with value_type and *) (* value_unit filled in). All other type-annotation events and *) (* ev_data are emitted to both callbacks simultaneously. *) (* For bool keywords (true/false/on/off) the same default type- *) (* annotation sequence is emitted, synthesising when no *) (* explicit annotation was given (no width/base/unit params). *) (* *) (* ev_data is also emitted for null values, symbols, references, *) (* and octet-stream chunks without a preceding type-annotation *) (* sequence. Struct values do NOT produce ev_data; they are *) (* signaled by ev_struct_start and ev_struct_end. *) (* Only blank characters — NOT comments — may precede the version *) (* directive: it is recognised solely as the very first comment, so an *) (* earlier comment would demote it to an ordinary one (see below). *) stream = [utf8-bom] , {ws-char} , [version-directive] , ws , {assignment , ws} ; (* UTF-8 BOM: legal only as the very first three bytes of the stream. *) utf8-bom = "\xEF\xBB\xBF" ; (* Version directive (spec 1.1): an OPTIONAL leading comment of the *) (* form "#!bovnar ." declaring the spec version the *) (* document targets. It is recognised only as the very first comment *) (* (after the optional BOM and whitespace); a "#!bovnar …" on any *) (* later line is an ordinary comment. Lexically it IS a comment, so a *) (* spec-1.0 reader skips it transparently — the declaration is purely *) (* additive. A 1.1+ reader parses it, exposes the version, and (in *) (* strict mode) rejects a version it does not support. Each component *) (* is a decimal integer with no leading zero (a bare "0" excepted). *) (* A malformed version after the "!bovnar" prefix is *) (* error_invalid_spec_version. *) (* The directive accepts ONLY horizontal whitespace (HT or SP) around *) (* its components — not the LF/CR/VT/FF that "ws-char" also covers. *) version-directive = "#" , "!bovnar" , hspace , {hspace} , version-int , "." , version-int , {hspace} , ("\x0D" | "\x0A" | (* eof *)) ; hspace = "\x09" (* HT *) | "\x20" (* SP *) ; (* version-int: a decimal integer with no leading zero (a bare "0" is the *) (* only form starting with "0"); each component must fit in a uint16. *) version-int = "0" | ( ("1"|"2"|"3"|"4"|"5"|"6"|"7"|"8"|"9") , {DIGIT} ) ; (* Whitespace: blank characters and comments, freely intermixed. *) ws = {ws-char | comment} ; (* At least one whitespace character, followed by optional further *) (* whitespace and comments. Used as the mandatory separator before *) (* an inline unit suffix. *) ws-mandatory = ws-char , {ws-char | comment} ; ws-char = "\x09" (* HT *) | "\x0A" (* LF *) | "\x0B" (* VT *) | "\x0C" (* FF *) | "\x0D" (* CR *) | "\x20" ; (* SP *) (* Comment: "#" through the next LF, CR, or end of input. *) (* Control bytes 0x00–0x08, 0x0E–0x1F, and 0x7F are hard errors *) (* inside a comment body. *) comment = "#" , {comment-char} , ("\x0D" | "\x0A" | (* eof *)) ; comment-char = "\x09" (* HT *) | "\x0B" | "\x0C" (* VT, FF *) | printable-byte (* 0x20–0x7E *) | high-byte ; (* 0x80–0xFF *) (* ───────────────────────────────────────────────────────────────── *) (* 2. Assignments *) (* ───────────────────────────────────────────────────────────────── *) (* The leading "." is a syntactic sigil; it is not stored in the key. *) (* At least one id-start character after "." is required; *) (* ".=" is error_empty_identifier. *) (* Comments are permitted between the last key character and "=". *) assignment = "." , key , ws , "=" , ws , value , ws , ";" ; key = id-start , {id-body-char} ; (* First character of an identifier, symbol, or reference segment: *) (* A–Z, a–z, "_", and UTF-8 leader bytes 0xC3–0xF4. *) (* 0xC2 is explicitly rejected (ACT_NONE) at all token-start *) (* positions, so code points U+0080–U+00BF cannot begin these tokens.*) id-start = ALPHA | "_" | utf8-leader ; (* Body characters of an identifier: *) (* Accepted: "+", "-", digits, alpha, "_", *) (* UTF-8 leader bytes 0xC3–0xF4, UTF-8 continuation bytes 0x80–0xBF.*) (* Hard-error bytes: 0x00–0x08, 0x0E–0x1F, 0x7F, 0xC2, punct: *) (* " # , . / ; < > [ ] { } = ! $ % & ' ( ) * : ? @ \ ^ ` | ~ *) (* "=" terminates the body (ACT_value_intro). *) (* Whitespace terminates the body (ACT_to_identifier_outro), after *) (* which only "=" is accepted (with comments permitted between). *) id-body-char = "+" | "-" | DIGIT | ALPHA | "_" | utf8-leader (* 0xC3–0xF4 *) | utf8-continuation ; (* 0x80–0xBF *) (* ───────────────────────────────────────────────────────────────── *) (* 3. Values *) (* ───────────────────────────────────────────────────────────────── *) (* A value is the right-hand side of an assignment or an element *) (* inside an array row; the optional type annotation applies in both *) (* contexts. *) (* *) (* Typed null elements: in array context, a type annotation not *) (* followed by any raw value token (i.e. the next byte is "," "]" *) (* or ";") is a null value carrying the annotated type. This is *) (* handled by ACT_type_null_then_new_array_value, *) (* ACT_type_null_then_array_outro, and ACT_type_null_then_value_outro *) (* from the type_outro state. *) value = [type-annotation , ws] , ( scalar-with-unit | raw-value ) ; (* A bare number or string scalar may carry an inline unit suffix — but ONLY *) (* at value position, never inside an array element (array-elem uses raw- *) (* value, which has no unit option). The lexer reaches the inline-unit state *) (* only from number_outro / string_outro, so no other value form takes one. *) scalar-with-unit = ( number | string ) , ws-mandatory , inline-unit ; raw-value = null-value | bool-value | special-number | datetime-literal (* spec 1.1 *) | number | string | symbol | reference | array | struct | octet-stream ; (* datetime-literal (spec 1.1): an ISO-8601 UTC timestamp accepted in *) (* place of the integer epoch-seconds carrier. The lexer accumulates the *) (* shape (dtlit_* states, entered from a digit run followed by "-"); the *) (* validator strictly range-checks the fields and converts the UTC civil *) (* instant to the integer carrier on the declared epoch (unix when a bare *) (* literal infers ). Whole-second carrier: a frac- *) (* tional part (any digit count) does not change the value but is kept *) (* verbatim and re-emitted so the literal round-trips. A "+HH:MM" / *) (* "-HH:MM" offset is folded to true UTC before conversion. *) (* The atomic GNSS epochs (gps/galileo/glonass/beidou) reject a literal. *) datetime-literal = year , "-" , month , "-" , day , [ "T" , hour , ":" , minute , ":" , second , [ "." , dt-fraction ] , [ dt-zone ] ] ; dt-fraction = digit , { digit } ; (* kept verbatim; carrier is whole seconds *) dt-zone = "Z" | ( "+" | "-" ) , hour , ":" , minute ; (* tz offset *) year = digit , digit , digit , digit ; month = digit , digit ; (* 01..12 *) day = digit , digit ; (* 01..28/29/30/31 per month *) hour = digit , digit ; (* 00..23 *) minute = digit , digit ; (* 00..59 *) second = digit , digit ; (* 00..60 (60 = UTC leap second; on the *) (* tai epoch it is its own instant, on *) (* the civil ones it folds onto the *) (* following second -- see the spec) *) (* null-value: either the reserved keyword "null" or the empty *) (* production. *) (* At assignment level: ".foo = ;" and ".foo = null;" are equivalent. *) (* In array context: a leading or trailing comma, two consecutive *) (* commas, a bare "null", or a type annotation immediately followed *) (* by "," / "]" all denote a null element. *) null-value = "null" | (* empty *) ; (* bool-value: a reserved keyword in value position. "on" is an *) (* alias for "true" and "off" for "false"; the validator collapses *) (* all four spellings to a vt_bool value (canonical text "true" / *) (* "false") and synthesises a type annotation when none is *) (* given (see §4). An explicit annotation accepts only these *) (* keywords as its value. *) (* *) (* Lexically these keywords — together with "null" — are ordinary *) (* symbol tokens (§7); the validator reserves the exact words and *) (* reclassifies them. A longer word that merely starts with one *) (* (e.g. "truthy", "nullable") remains a plain symbol. *) bool-value = "true" | "false" | "on" | "off" ; (* ───────────────────────────────────────────────────────────────── *) (* 4. Type annotations *) (* ───────────────────────────────────────────────────────────────── *) (* Lexer: a keyword state machine (tf_* states) recognises the eight *) (* family names via six keyword paths (float_fix / float_dec share *) (* the "float" prefix; see §4 below). After the family keyword the *) (* raw bytes of the ":…" *) (* parameter string are accumulated into type_data by copy_type_byte. *) (* *) (* Validator: bvn_parse_type_annotation parses the accumulated string *) (* and the validator emits the structured type-annotation event *) (* sequence described in §1. *) (* *) (* Default synthesis: when a number or string value carries no *) (* explicit type annotation (value_type is still vt_plain), the *) (* validator synthesises a default before emitting ev_data: *) (* strings / special-numbers → / *) (* floats (has "." or "e"/"E") → *) (* negative integers → *) (* plain integers → *) (* bool keywords (true/false/on/off) → *) (* The full type-annotation event sequence is always emitted for *) (* number and string values, whether annotated explicitly or not. *) type-annotation = "<" , ws , param-type , ws , ">" ; (* Types with an optional parameter list after ":". *) (* Parameters are identified by class and may appear in any order; *) (* at most one of each class (width, base, q, unit) is permitted. *) (* *) (* "utf8" goes to type_body_outro after the keyword; the lexer *) (* accepts ":" and the parameter bytes, but utf8 is a parameterless *) (* family — any width, base, q, or unit parameter is rejected by *) (* bvn_parse_type_annotation as error_illegal_value_type (see bool). *) (* *) (* "float_fix" and "float_dec" are not separate keyword paths in the *) (* lexer state table. The lexer fires ACT_tf_float_done after the *) (* shared "float" prefix (states tf_f → tf_fl → tf_flo → tf_floa *) (* → ACT_tf_float_done), which stores "float" in type_data and *) (* transitions to type_body_outro. The subsequent bytes "_fix" or *) (* "_dec" are accumulated via copy_type_byte. The final string *) (* ("float", "float_fix", or "float_dec") is dispatched in *) (* bvn_parse_type_annotation. *) (* "bool" and "utf8" are parameterless families: any width, base, q, *) (* or unit parameter is error_illegal_value_type. bool's only valid *) (* values are the bool keywords (see bool-value, §3). *) (* "datetime" (spec 1.1) is a timestamp: a signed integer seconds count *) (* validated like sint. It accepts an optional width parameter (as sint) *) (* plus one epoch-name parameter (unix|tai|gps|mjd|ntp|galileo|glonass| *) (* y2000|beidou; default unix), e.g. . A numeric base, *) (* q, or unit parameter is error_illegal_value_type. Available only when *) (* the document declares "#!bovnar 1.1" or newer. *) param-type = ("uint" | "sint" | "float" | "float_fix" | "float_dec" | "utf8" | "bool" | "datetime") , [ws , ":" , ws , type-param-list] ; (* The parameter list is accumulated byte-by-byte into type_data by *) (* the copy_type_byte / type_body_outro states, and later re-parsed *) (* by bvn_parse_type_annotation. *) (* *) (* Bytes accepted in the accumulated string: *) (* A–Z, a–z, 0–9, "$", "%", "(", ")", "*", "+", ",", ".", "/", *) (* "-", ":", "^", "_", "~" *) (* ("~" joins a prefix to its unit, "$" starts a currency, "%" is *) (* percent, "(" and ")" group a unit expression -- all required by *) (* inline-unit-char below) *) (* UTF-8 continuation bytes 0x80–0xBF *) (* UTF-8 leader bytes 0xC2–0xF4 (0xC2 IS accepted here, unlike *) (* identifier / symbol / reference token starts) *) (* Whitespace is accepted but not accumulated; it transitions to *) (* type_body_outro, which accepts the same bytes as copy_type_byte *) (* plus "#" (0x23, entering comment_intro), while treating whitespace *) (* as ignored rather than re-triggering the outro transition. *) (* ">" always terminates the annotation. *) type-param-list = type-param , {ws , "," , ws , type-param} ; type-param = width-param (* e.g. 32, 64 *) | base-param (* e.g. _16, _64, _85 *) | q-param (* e.g. q8, q16 – float_fix only *) | unit-param ; (* e.g. m~s, Gi~B, m^2 *) (* Width: one or more decimal digits. 0 means "no width restriction".*) (* float: 0, 16, or any multiple of 32 up to BVN_FLOAT_MAX_PREC.*) (* float_fix, *) (* float_dec: 0, 16, 32, 64, 128, 256. *) width-param = DIGIT , {DIGIT} ; (* Base: underscore followed by one or more decimal digits. *) (* Valid values: 2–62, 64 (standard Base64), 85 (Ascii85). *) (* Bases 64 and 85 are uint-only: their alphabets use '+'/'-' as *) (* digits, leaving no sign character, so sint+64/85 is illegal. *) (* "float" accepts only base 10 (or absent, → 10) or base 16; *) (* other bases → error_illegal_value_type. *) (* "float_fix" and "float_dec" reject the base parameter entirely: *) (* any base-param → error_illegal_value_type. *) (* A non-decimal base with uint/sint should use a quoted string value; *) (* a bare literal is not rejected by the validator but will be parsed *) (* as a symbol token, causing a type/value mismatch instead. *) base-param = "_" , DIGIT , {DIGIT} ; (* Q~parameter: lowercase "q" followed by one or more decimal digits. *) (* Specifies the number of fractional bits for the float_fix Q-format.*) (* Value = raw_signed_integer × 2^(-Q). *) (* Valid only for "float_fix"; rejected for all other families. *) (* Valid range: 0 ≤ Q < effective_width (width 0 → effective 64). *) (* Q is stored in value_type_spec_t.base and retrieved via *) (* bvn_effective_q; bvn_effective_base always returns 10 for *) (* float_fix and float_dec. *) q-param = "q" , DIGIT , {DIGIT} ; (* Unit parameter: everything up to the next "," or ">". *) (* Parsed against the unit table by bvn_parse_unit. *) (* "no_unit" is a reserved keyword meaning explicitly dimensionless. *) (* Valid for: uint, sint, float, float_fix, float_dec. *) unit-param = unit-char , {unit-char} ; unit-char = (* any byte accepted by copy_type_byte that is not *) (* "," (0x2C) or ">" (0x3E): *) (* A–Z, a–z, 0–9, "*", "+", ".", "/", "-", ":", "^", *) (* "_", "~", "$", "%", "(", ")", or a UTF-8 multi- *) (* byte byte (0x80–0xBF or 0xC2–0xF4) *) ; (* ── Unit sub-grammar (semantic, enforced by bvn_parse_unit) ─── *) (* *) (* resolved-unit = "no_unit" | unit-expr ; *) (* unit-expr = unit-factor , {unit-sep , unit-factor} ; *) (* unit-factor = unit-component | "(" , unit-expr , ")" ; *) (* *) (* unit-sep = "*" (* product – U+002A *) *) (* | "·" (* product – U+00B7 *) *) (* | "/" ; (* division *) *) (* "·" is the middle dot, encoded as 0xC2 0xB7 (UTF-8 for U+00B7). *) (* "*" and "·" are semantically equivalent (multiplication). *) (* *) (* Semantics of "/": the first "/" switches every subsequent factor *) (* into the denominator (its stored exponents are negated). A "(...)" *) (* group is a sub-expression evaluated independently; when it falls in *) (* the denominator its net exponents are negated as a whole, so *) (* "k~g/(m·s²)" = kg·m⁻¹·s⁻² and "(k~g/m)·s²" = kg·m⁻¹·s². Additional *) (* "/" separators at one level never toggle back to the numerator. *) (* An explicit separator is required before a group ("m·(s)", not *) (* "m(s)"); a group is not (yet) followed by its own exponent; paren *) (* nesting is bounded at 16. *) (* *) (* Empty components or groups (e.g. "m//s", "()", "m*·s"), unmatched *) (* parentheses, and more than BVNR_MAX_UNIT_COMPONENTS = 8 total *) (* components per unit string all produce error_unit_illegal. *) (* *) (* unit-component = [prefix "~"] base-unit [unit-exponent] ; *) (* *) (* When a prefix is present the separator "~" is mandatory. *) (* A bare base-unit with no prefix requires no separator. *) (* *) (* si-prefix = "Q"|"R"|"Y"|"Z"|"E"|"P"|"T"|"G"|"M"|"k"|"h"|"da" *) (* | "d"|"c"|"m"|"µ"|"n"|"p"|"f"|"a"|"z"|"y"|"r"|"q" ; *) (* (µ = U+00B5 0xC2 0xB5; Greek μ = U+03BC; ASCII "u" alias) *) (* *) (* iec-prefix = "Ki"|"Mi"|"Gi"|"Ti"|"Pi"|"Ei"|"Zi"|"Yi"|"Ri"|"Qi" ; *) (* Ri (2^90) and Qi (2^100) were added in IEC 80000-13:2022 ed. 2. *) (* *) (* base-unit = a symbol looked up in the fixed unit table by *) (* bvn_parse_unit (src/utils/bovnar_utils.c): 163 named units with *) (* 491 accepted spellings (canonical symbols plus long-form and plural *) (* aliases), spanning SI, imperial / US customary, CGS, radiation, *) (* surveying, culinary, Old German and digital-storage quantities. *) (* Doc 2 (the unit-system spec) is the authoritative registry; the SI *) (* coherent / common subset below is REPRESENTATIVE only, NOT a closed *) (* alternation: *) (* b B s m g A K mol cd Hz N Pa J W V Ω F C S Wb T H lm lx Bq Gy Sv *) (* kat rad sr L l min h d wk yr °C ° degC degrC degrees degree degr *) (* deg t bar eV Da au ha *) (* Symbols are case-sensitive and a few spellings collide across roles, *) (* resolved by context: "P" = poise (base unit) vs peta (SI prefix), *) (* "G" = gauss vs giga. (Ω = U+2126: 0xE2 0x84 0xA6; ° = U+00B0: 0xC2 *) (* 0xB0) *) (* *) (* unit-exponent = [exp-sign] exp-digit *) (* | "^" ["-"|"+"] ASCII-digit ; *) (* *) (* exp-sign = "⁺" (* U+207A: 0xE2 0x81 0xBA, positive / no-op *) *) (* | "⁻" ; (* U+207B: 0xE2 0x81 0xBB, negate exponent *) *) (* *) (* exp-digit = "¹"|"²"|"³"|"⁴"|"⁵"|"⁶"|"⁷"|"⁸"|"⁹" ; *) (* (U+00B9 / 00B2 / 00B3 / 2074–2079) *) (* *) (* "⁺" is accepted but has no effect (same as absent). *) (* The ASCII form "^[+-]?[1-9]" maps to the same internal constants *) (* as the Unicode superscript form. "^0" is not a valid exponent. *) (* ───────────────────────────────────────────────────────────────── *) (* 5. Numbers *) (* ───────────────────────────────────────────────────────────────── *) (* The lexer is digit-agnostic: any ASCII digit 0–9 is accepted in a *) (* bare number literal without base validation. Digit validity against*) (* a non-decimal base is checked later by bvn_acc_parse_number. *) (* Leading zeros are not rejected by the lexer ("007" is valid). *) (* Only "e"/"E" are accepted as exponent markers in bare literals. *) (* "p"/"P" (binary exponent) is recognised by the validator inside a *) (* quoted-string number value of a base-16 float, e.g. *) (* "1.8p+2"; there "e"/"E" are hex mantissa digits, so *) (* the binary exponent must use "p"/"P" (see spec §6.3). It is the *) (* only context in which "p"/"P" is reachable. *) number = ["-"] , (int-led | dot-led) , [dec-exponent] ; (* Integer-led: one or more digits with an optional fractional part. *) (* A trailing dot ("123.") without fractional digits is valid. *) int-led = DIGIT , {DIGIT} , ["." , {DIGIT}] ; (* Dot-led: at least one digit after the dot is required. *) (* ".5" and "-.5" are valid; a lone "." is a hard error. *) dot-led = "." , DIGIT , {DIGIT} ; dec-exponent = ("e" | "E") , ["+" | "-"] , DIGIT , {DIGIT} ; (* Special IEEE-754 values are bare reserved keywords: nan, inf, and *) (* ninf (negative infinity). Like null / true / false / on / off they *) (* are lexed as ordinary symbols and reclassified to numeric special *) (* values by the validator; a bare word that is not one of these exact *) (* spellings stays an ordinary symbol (e.g. "infinity", "nans"). *) (* There is no sigil and no inline-unit suffix — a unit is supplied *) (* via the type annotation, e.g. inf. *) special-number = "nan" | "inf" | "ninf" ; (* ───────────────────────────────────────────────────────────────── *) (* 6. Strings *) (* ───────────────────────────────────────────────────────────────── *) (* Two or more adjacent quoted literals with only whitespace/comments *) (* between them are concatenated into a single string token by the *) (* lexer (string_outro re-enters string_intro on '"'). The combined *) (* byte length is bounded by max_string_length (default UINT16_MAX). *) string = string-literal , {ws , string-literal} ; string-literal = '"' , {string-char} , '"' ; string-char = safe-string-byte | escape-seq ; (* safe-string-byte: whitespace 0x09–0x0D, 0x20–0x7E except '"' (0x22)*) (* and '\' (0x5C), plus high bytes 0x80–0xFF. *) (* Control bytes 0x00–0x08, 0x0E–0x1F, and DEL (0x7F) are hard *) (* errors even inside strings. *) safe-string-byte = (* byte in [0x09,0x0D] | [0x20,0x21] | [0x23,0x5B] *) (* | [0x5D,0x7E] | [0x80,0xFF] *) ; (* Recognised escape sequences (from bvn_escape_lut): *) escape-seq = "\" , ( "t" (* \t = 0x09 HT *) | "n" (* \n = 0x0A LF *) | "v" (* \v = 0x0B VT *) | "f" (* \f = 0x0C FF *) | "r" (* \r = 0x0D CR *) | '"' (* \" *) | "\" (* \\ *) | byte-escape (* spec 1.1, see below *) | unicode-escape ) ; (* spec 1.1, see below *) (* All other bytes after "\" are error_illegal_escape_sequence. *) (* spec 1.1 only — available when the document declares "#!bovnar 1.1" *) (* or newer; in a 1.0/unversioned document "x"/"u" after "\" stay *) (* error_illegal_escape_sequence. *) byte-escape = "x" , hex-digit , hex-digit ; (* one byte 0xHH *) unicode-escape = "u" , "{" , hex-digit , [hex-digit] , [hex-digit] , [hex-digit] , [hex-digit] , [hex-digit] , "}" ; (* 1 to 6 hex digits naming a Unicode scalar. A 7th hex digit before "}" *) (* (or an empty "{}") is error_illegal_escape_sequence — a structural *) (* check made before the value is examined. A value that is a surrogate *) (* (D800..DFFF) or above 10FFFF is error_invalid_codepoint; otherwise the *) (* scalar is UTF-8 encoded. A \x byte must leave the whole string valid *) (* UTF-8 (else error_invalid_utf8_byte). *) hex-digit = DIGIT | "a".."f" | "A".."F" ; (* ───────────────────────────────────────────────────────────────── *) (* 6.5 Inline unit suffix *) (* ───────────────────────────────────────────────────────────────── *) (* An inline-unit may follow a scalar number or string value (but not *) (* a special-number keyword — give its unit via the type annotation). *) (* At least one whitespace character (ws-mandatory) is the sole *) (* required separator between the value literal and the unit. *) (* Inline units attach only at value position: the value rule has a *) (* scalar-with-unit alternative, while an array element (array-elem) *) (* uses raw-value, which has no unit option. At the action level the *) (* lexer additionally rejects ACT_inline_unit_intro when the *) (* in_array_element flag is set (a redundant guard). *) (* *) (* INVALID (no whitespace before the unit): *) (* .bad = 9.81m/s; (* no separator at all — hard error *) *) (* *) (* Examples: *) (* .a = 9.81 m/s; (* space separator *) *) (* .b = 1.5 k~m; (* with type annotation *) *) (* .c = inf; (* special-number unit via annotation *) *) (* .d = 1.5e3 k~J; (* number with exponent *) *) (* .e = "FF" m; (* string literal with unit *) *) (* *) (* State-machine flow: *) (* 1. Whitespace from a number-body state fires ACT_to_number_outro *) (* → number_outro. Strings enter string_outro_nosp after the *) (* closing ""; whitespace transitions to string_outro. *) (* Special numbers (nan, inf, ninf) are lexed as symbols and *) (* take no inline unit (give the unit via the type annotation). *) (* 2. From number_outro / string_outro (whitespace already seen): *) (* alpha / "_" / "$" / "%" / "(" / UTF-8 leader (0xC2–0xF4) *) (* → ACT_inline_unit_intro *) (* 3. ACT_inline_unit_intro pushes the first byte and enters *) (* inline_unit_body directly. *) (* 4. inline_unit_body accumulates unit characters (including ":" *) (* as a valid mid-unit byte) until whitespace (→ *) (* inline_unit_outro) or ";" (→ value_outro via finalize). *) (* *) (* Inside an array element (in_array_element flag set), *) (* ACT_inline_unit_intro immediately rejects with *) (* error_unexpected_input_byte. *) (* *) (* Semantic rule (enforced by validator): *) (* If a type-annotation unit was also given, the inline unit must *) (* parse to the identical value_unit_t; mismatch → error_unit_mismatch. *) inline-unit = inline-unit-start , { inline-unit-char } ; (* A currency component carries a mandatory "$" sigil (spec 1.0): "$USD", *) (* "$BTC", or prefixed "~$EUR". Bare codes are no longer currencies, *) (* so a currency code can never collide with a physical-unit symbol. *) (* "(" lets an inline unit begin with a parenthesised group, e.g. "(m/s)/s", *) (* so the inline form accepts the same unit grammar as a type-annotation unit. *) inline-unit-start = ALPHA | "_" | "$" | "%" | "(" | unit-lead-byte ; inline-unit-char = ALPHA | DIGIT | "_" | "$" | "%" | "+" | "-" | "." | "/" | ":" | "^" | "*" | "~" | "(" | ")" | unit-lead-byte | utf8-continuation ; (* unit-lead-byte: the UTF-8 lead-byte range accepted inside a unit — the *) (* full 0xC2-0xF4, BROADER than utf8-leader (0xC3-0xF4). Unlike identifier, *) (* symbol, and reference token starts (where 0xC2 is rejected, see §12), a *) (* unit may begin with or contain a 0xC2-led code point: the µ prefix *) (* (U+00B5 = 0xC2 0xB5), the ° / °C units (U+00B0 = 0xC2 0xB0), and the *) (* "·" product separator (U+00B7 = 0xC2 0xB7). The lexer maps the whole *) (* 0xC2-0xF4 range to ACT_inline_unit_intro / ACT_copy_inline_unit_byte, *) (* matching copy_type_byte in a type-annotation unit body. *) unit-lead-byte = (* byte in [0xC2, 0xF4] *) ; (* ───────────────────────────────────────────────────────────────── *) (* 7. Symbols (unquoted bare-word values in value position) *) (* ───────────────────────────────────────────────────────────────── *) (* A symbol starts with id-start and continues with sym-body-char. *) (* Differences from the identifier body action table: *) (* "=" is a hard error (not a state transition) *) (* "," / "]" / ";" terminate the symbol cleanly (not errors) *) (* "." is a hard error (same as in identifier body) *) (* Reserved keywords: a symbol token whose text is exactly "null", *) (* "true", "false", "on", or "off" is reclassified by the validator *) (* (see null-value / bool-value, §3) and is NOT delivered as a symbol.*) (* Any other bare word — including one that merely begins with a *) (* keyword, like "ontology" or "nullish" — remains a symbol. *) symbol = id-start , {sym-body-char} ; (* sym-body-char spans the same byte range as id-body-char; the *) (* difference is purely in the termination actions fired for *) (* "," (new_array_value), "]" (array_outro), and ";" (value_outro). *) sym-body-char = id-body-char ; (* ───────────────────────────────────────────────────────────────── *) (* 8. References *) (* ───────────────────────────────────────────────────────────────── *) (* The "&" sigil introduces a dotted path to another key. *) (* The stored string starts with "." and includes all dots: *) (* "&.foo.bar" stores ".foo.bar". *) (* At least one segment is required; "&" alone or "&." without a *) (* following id-start character is an error. *) (* A reference is stored UNRESOLVED — the path is a string token; the *) (* library never dereferences it, so the target need not exist and *) (* cycles are not detected. Resolution is the application's job. *) reference = "&" , ref-segment , {ref-segment | ref-index} ; ref-segment = "." , id-start , {ref-body-char} ; (* ref-index (spec 1.1): array indexing in a reference path, e.g. *) (* &.matrix[0][1]. The bytes are captured verbatim into the path *) (* string (references are stored unresolved); bvn_dom_lookup *) (* interprets them at the DOM layer. Gated on a "#!bovnar 1.1" *) (* declaration — in a 1.0/unversioned document "[" in a reference is *) (* error_unexpected_input_byte. The reference_index sub-state *) (* distinguishes a "]" that closes an index from one that closes an *) (* enclosing array (which ends the reference). *) ref-index = "[" , DIGIT , {DIGIT} , "]" ; (* ref-body-char: same byte set as sym-body-char. A "." (0x2E) *) (* does NOT extend the body: it is ACT_copy_reference_dot, which *) (* starts a NEW ref-segment (advancing to reference_segment_intro, *) (* which REQUIRES an id-start next). A "." with no following *) (* id-start — a trailing or doubled dot — is *) (* error_unexpected_input_byte. *) ref-body-char = sym-body-char ; (* ───────────────────────────────────────────────────────────────── *) (* 9. Arrays *) (* ───────────────────────────────────────────────────────────────── *) (* An array is one or more bracket-enclosed rows separated by "/": *) (* .a = [1, 2, 3]/[4, 5, 6]; *) (* "/" emits ev_array_dim_start; the next "[" opens the next row. *) (* Whitespace/comments are permitted on both sides of "/" (between the *) (* closing "]" and "/", and between "/" and the next "["). *) (* *) (* The former adjacent-row syntax "[1,2][3,4]" and the comma-between- *) (* rows form "[1,2],[3,4]" are hard errors at the assignment level. *) (* array_outro no longer accepts "[" directly; the only continuation *) (* for a further row is "/". *) (* *) (* Nested arrays are valid element values inside a row: *) (* [[1,2],[3,4]] is a one-row outer array whose two elements are *) (* the inner arrays [1,2] and [3,4]. *) (* *) (* Leading and trailing commas produce null elements: *) (* [,1,2,] → four elements: null, 1, 2, null. *) (* *) (* Semantic constraints — two distinct tiers: *) (* *) (* a) Enforced by the LEXER/VALIDATOR (the streaming reader): the *) (* "/"-separated dimension rows of a SINGLE array must all have *) (* the same element count; a mismatch is *) (* error_array_row_size_mismatch (code 2). The lexer checks each *) (* row as it closes, so the error fires at the earliest byte (the *) (* overshooting element, or the closing "]" of a short row). *) (* The per-bracket-pair context save/restore mechanism keeps this *) (* check scoped to one array instance: a "/"-row width never *) (* leaks across a "," boundary or into a sibling branch. *) (* [1,2,3]/[4,5] → error (one array's /-rows: 3 vs 2) *) (* [[1,2]/[3,4,5]] → error (inner /-array's rows: 2 vs 3) *) (* *) (* b) Enforced by the DOM BUILDER ONLY (bvn_dom_parse), NOT by the *) (* streaming reader — `loads`/the SAX callbacks accept these and *) (* deliver every element; only a DOM parse rejects them. Array *) (* elements must be HOMOGENEOUS (spec 1.0): the same kind *) (* throughout, `,`-separated sibling sub-arrays must be *) (* rectangular (match in length), and sibling structs must share *) (* the same shape. A ragged sibling set is *) (* error_array_row_size_mismatch (code 2); a mix of element kinds *) (* (e.g. a scalar beside a sub-array) is *) (* error_array_element_type_mismatch (code 39); sibling structs *) (* with differing shapes is error_struct_shape_mismatch (code 40).*) (* [[1,2],[3,4,5]] → DOM error (ragged sibling sub-arrays) *) (* [1,[2,3]] → DOM error (scalar mixed with a sub-array) *) (* [{.x=1;},{.y=2;}] → DOM error (sibling structs, differing *) (* shapes) *) (* [[1,2]/[3,4], [5,6]/[7,8]] → accepted (same-shape blocks) *) (* *) (* c) Array nesting depth ≤ max_array_nesting (default 64, cap 255).*) array = array-row , {ws , "/" , ws , array-row} ; array-row = "[" , ws , [row-content] , ws , "]" ; (* row-content is OPTIONAL: "[]" (nothing between the brackets) is an *) (* EMPTY row — zero elements, no ev_data. It is distinct from "[null]" *) (* (one null element) and "[,]" (two null elements). When present, *) (* row-content has at least one element position; leading / trailing / *) (* consecutive commas each expand to a null-value element. An empty row *) (* has width 0, a valid row width for /-dimension consistency. *) row-content = array-elem , {ws , "," , ws , array-elem} ; (* array-elem has the same structure as a top-level value, except that an *) (* inline unit suffix is not permitted: it uses raw-value (no unit option), *) (* not the value rule's scalar-with-unit alternative (see §6.5). *) array-elem = [type-annotation , ws] , raw-value ; (* ───────────────────────────────────────────────────────────────── *) (* 10. Structs (nested key-value scopes) *) (* ───────────────────────────────────────────────────────────────── *) (* A struct "{" … "}" contains zero or more assignments. "{}" is an *) (* empty struct (zero members). After "}" the struct behaves like any *) (* completed value: followed by ";" at the top level, or "," / "]" in *) (* array context. *) (* *) (* Key uniqueness is a DOM-BUILDER constraint, NOT a grammar one: the *) (* lexer/validator (the streaming reader) accepts a repeated key and *) (* delivers BOTH assignments. A DOM parse (bvn_dom_parse) is stricter — *) (* a duplicate key within one scope (a struct, or the top-level *) (* document) is error_duplicate_struct_key. The same key in different *) (* scopes is always unrelated. *) (* Nesting depth: struct_nesting_level ≤ max_struct_nesting *) (* (default 64 when the flag is 0; hard cap 255). *) (* An unmatched "}" is error_illegal_struct_close. *) struct = "{" , ws , {assignment , ws} , "}" ; (* ───────────────────────────────────────────────────────────────── *) (* 11. Octet streams (binary-framed escape from the text layer) *) (* ───────────────────────────────────────────────────────────────── *) (* A NUL byte (0x00) appearing where a value is expected switches from*) (* text mode to binary chunk mode. The UTF-8 stream validator is *) (* suspended for the duration. *) (* *) (* Binary protocol (bvn_read_octet_stream): *) (* Tag 0x01 → chunk follows (2-byte LE length + data bytes) *) (* Tag 0x00 → end of stream *) (* Any other tag → error_octet_stream_out_of_sync *) (* *) (* After ev_octet_stream_end the lexer enters octet_stream_outro, *) (* which accepts the same terminators as any other completed value. *) octet-stream = "\x00" , {os-chunk} , os-end ; os-chunk = "\x01" , os-length , os-data ; os-end = "\x00" ; (* 16-bit little-endian length; 0x0000 encodes 65536 bytes. *) os-length = byte , byte ; (* little-endian uint16; 0x0000 → 65536 *) os-data = {byte} ; (* exactly os-length bytes; not CF *) (* ───────────────────────────────────────────────────────────────── *) (* 12. Lexical primitives *) (* ───────────────────────────────────────────────────────────────── *) DIGIT = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" ; ALPHA = "A"|"B"|"C"|"D"|"E"|"F"|"G"|"H"|"I"|"J"|"K"|"L"|"M" | "N"|"O"|"P"|"Q"|"R"|"S"|"T"|"U"|"V"|"W"|"X"|"Y"|"Z" | "a"|"b"|"c"|"d"|"e"|"f"|"g"|"h"|"i"|"j"|"k"|"l"|"m" | "n"|"o"|"p"|"q"|"r"|"s"|"t"|"u"|"v"|"w"|"x"|"y"|"z" ; (* utf8-leader here denotes the range used at identifier, symbol, and *) (* reference token-start and -body positions: 0xC3–0xF4. *) (* 0xC2 is the leader for U+0080–U+00BF. It is explicitly overridden *) (* to ACT_NONE at every identifier, symbol, and reference boundary, *) (* so code points U+0080–U+00BF cannot start or appear in those *) (* tokens. 0xC2 IS accepted inside quoted strings and type parameter *) (* bodies (copy_type_byte), where the full range 0xC2–0xF4 applies. *) utf8-leader = (* byte in [0xC3, 0xDF] | [0xE0, 0xEF] | [0xF0, 0xF4] *) ; utf8-continuation = (* byte in [0x80, 0xBF] *) ; (* Printable ASCII: 0x20–0x7E *) printable-byte = (* byte in [0x20, 0x7E] *) ; (* High byte: any byte 0x80–0xFF. 0xC0, 0xC1, 0xF5–0xFF are *) (* structurally in this range but rejected by the parallel UTF-8 *) (* validator (error_invalid_utf8_byte). *) high-byte = utf8-continuation | utf8-leader | (* 0xC0 | 0xC1 | 0xF5–0xFF – rejected by UTF-8 check *) ; byte = (* any single byte 0x00–0xFF *) ; (* ───────────────────────────────────────────────────────────────── *) (* 13. Constraints not expressible in context-free EBNF *) (* ───────────────────────────────────────────────────────────────── *) (* a) UTF-8 validity Every byte in the text layer (outside octet-stream regions) must form valid UTF-8 when fed through bvn_utf8_feed. The UTF-8 validator runs in parallel with the action state machine; a non-UTF-8 byte causes error_invalid_utf8_byte regardless of which lexer state the parser is in. Overlong encodings and surrogate halves are rejected. b) BOM uniqueness The UTF-8 BOM (EF BB BF) is legal only at byte offset 0. If the stream begins with "#" instead (a first-line comment), the special first_comment_* state machine watches the comment body for the exact byte sequence 0xEF 0xBB 0xBF and rejects it as error_invalid_byte_order_mark. This detection applies only while the first_comment_* states are active (i.e. until the first newline); subsequent regular comments (comment_intro state) have no BOM detection, so U+FEFF encoded as 0xEF 0xBB 0xBF appearing in any later comment body or string literal is valid UTF-8 and is accepted. c) 0xC2 at token boundaries The byte 0xC2 (UTF-8 leader for U+0080–U+00BF) is mapped to ACT_NONE at identifier, symbol, and reference token-start and token-body positions, forbidding code points U+0080–U+00BF in those tokens. 0xC2 is accepted inside quoted strings and in type parameter bodies (copy_type_byte / type_body_outro). d) Special-number keywords The special floats nan / inf / ninf are bare reserved keywords (no sigil). They are lexed as symbols and reclassified to numeric special values by the validator, like null / true / false / on / off; any other bare word (e.g. "infinity", "nans") stays a symbol. They take no inline unit — a unit is supplied via the annotation. e) String concatenation Adjacent string literals with only whitespace/comments between them are concatenated into a single token by the lexer. The combined byte length is bounded by max_string_length (default UINT16_MAX = 65535 bytes). f) Number exponent in bare literals vs. quoted strings Bare number literals only accept "e"/"E" as exponent markers (exp_intro state). g) Array row-size consistency (LEXER/VALIDATOR — the streaming reader) The "/"-separated dimension rows of a single array must all have the same element count; the lexer checks each row as it closes, so the violation is reported at the earliest offending byte. The per-bracket-pair context save/restore mechanism keeps the /-row check scoped to one array instance. Violation: error_array_row_size_mismatch (e.g. [1,2,3]/[4,5], or [[1,2]/[3,4,5]]). Sibling homogeneity (DOM BUILDER ONLY — bvn_dom_parse; the streaming reader accepts these). Array elements must be the same kind throughout, `,`-separated sibling sub-arrays must be rectangular (match in length, so a bare array/matrix is rectangular), and sibling structs must share the same shape. A ragged sibling set is error_array_row_size_mismatch (e.g. [[1,2],[3,4,5]]); a mix of element kinds is error_array_element_type_mismatch (code 39, e.g. [1,[2,3]]); sibling structs with differing shapes is error_struct_shape_mismatch (code 40, e.g. [{.x=1;},{.y=2;}]). loads()/the SAX callbacks deliver such arrays unchanged; only a DOM parse rejects them. h) Array and struct nesting limits Array nesting (bracket depth): max_array_nesting (default 64 when the flag is 0; hard cap 255). Struct nesting: max_struct_nesting (default 64 when the flag is 0; hard cap 255). i) Type-annotation / value compatibility (enforced by validator) "utf8" requires a string value. "uint", "sint", "float", "float_fix", "float_dec" accept a number or string value; a quoted string allows special float notation. "utf8" and "bool" are parameterless: the lexer accepts the ":" and parameter bytes, but bvn_parse_type_annotation rejects any width, base, q, or unit parameter as error_illegal_value_type (e.g. "" and "" are both errors). "uint" rejects negative number literals (error_value_out_of_range). Digit characters in the value must be valid for the declared base (error_digit_not_in_base). The numeric value must fit in the declared bit-width (error_value_out_of_range). For "float": accepted widths are 0, 16, or any multiple of 32 up to BVN_FLOAT_MAX_PREC (32768); accepted bases are 10 (or absent) and 16; any other base → error_illegal_value_type. For "float_fix": accepted widths are 0, 16, 32, 64, 128, 256; the base parameter (_N) is forbidden (error_illegal_value_type); the Q parameter (qN) must satisfy 0 ≤ Q < effective_width (error_illegal_value_type if violated); Q stored in value_type_spec_t.base; retrieved via bvn_effective_q. For "float_dec": accepted widths are 0, 16, 32, 64, 128, 256; the base parameter (_N) is forbidden (error_illegal_value_type); the Q parameter is not valid for float_dec. Neither "float_fix" nor "float_dec" can be synthesised by the validator's default type synthesis; both require explicit annotation. j) Identifier (key) non-empty The grammar requires at least one id-start character after ".". An empty key (".=") triggers error_empty_identifier. k) Struct closing brace without matching open A "}" seen when struct_nesting_level == 0 is error_illegal_struct_close. l) Comma in non-array context "," triggers ACT_new_array_value, which checks array_nesting_level > 0 and returns error_unexpected_input_byte if the condition is not met. A stray "," at the top level is therefore an error. m) Octet stream out-of-sync Any byte other than 0x01 (chunk) or 0x00 (end) as the tag byte inside an octet stream causes error_octet_stream_out_of_sync. n) Error recovery (continue_on_error mode) When bvnr_read_flags_t.continue_on_error is set, any parse error invokes the on_error callback and then enters the resync state machine (states: resync, resync_string, resync_string_escape, resync_comment). The resync machine skips bytes while tracking bracket and brace nesting depths, and attempts to re-synchronise at the next ";" at the current nesting depth. recovery_count (bvnr_reader_get_recovery_count) is incremented immediately when an error triggers entry into resync mode, not when resync completes at ";". EOF inside any resync state raises a final error_got_incomplete_bvnr_stream (delivered to on_error and returned by bvnr_reader_get_error); the original error that triggered resync was already delivered to on_error earlier, at the point of fault. o) Inline unit suffix An inline-unit follows a scalar number or string in non-array context only. A special-number keyword (nan, inf, ninf) takes NO inline unit — it is lexed as a symbol, and symbols accept no inline suffix; "inf m/s" is error_unexpected_input_byte. Supply a unit for a special value through the type annotation ( inf). At least one whitespace character MUST separate the value literal from the unit. The only accepted form is "value unit"; any other form (no separator or colon) is a hard error. State-machine flow: 1. Whitespace from number-body states (zero_intro, copy_number_byte, fraction_intro, copy_fraction_byte, copy_exp_byte) fires ACT_to_number_outro → number_outro. Strings enter string_outro_nosp after the closing quote; whitespace transitions to string_outro. A comment's terminating newline also counts as whitespace (comment_outro promotes string_outro_nosp → string_outro). 2. From number_outro / string_outro (at least one ws consumed): alpha / "_" / "$" / "%" / "(" / UTF-8-leader (0xC2-0xF4) → ACT_inline_unit_intro 3. ACT_inline_unit_intro pushes the first byte and enters inline_unit_body directly. 4. inline_unit_body accumulates unit characters (including ":" as a valid mid-unit byte) until whitespace (→ inline_unit_outro) or ";" (→ value_outro via finalize). Inside an array element (in_array_element flag set), ACT_inline_unit_intro immediately rejects with error_unexpected_input_byte. The validator's bvn_val_receive calls bvn_parse_unit on the inline buffer and compares the result to v->parsed_unit using memcmp on the full value_unit_t structure (valid because both structs are zero-initialised before parsing and all padding is zeroed). When has_annotation_unit is true and the comparison fails, the validator returns error_unit_mismatch. When no annotation unit was present, the inline unit is silently adopted as the effective unit. *) # Bovnar (BVNR) — Frequently Asked Questions > **Applies to:** Bovnar specification v1.1 --- ## Table of Contents 1. [General](#1-general) 2. [File Format and Syntax](#2-file-format-and-syntax) 3. [Type System and Annotations](#3-type-system-and-annotations) 4. [Units](#4-units) 5. [Numbers and Special Values](#5-numbers-and-special-values) 6. [Strings](#6-strings) 7. [Arrays](#7-arrays) 8. [Structs](#8-structs) 9. [Null, Symbols, and References](#9-null-symbols-and-references) 10. [Octet Streams](#10-octet-streams) 11. [Error Handling and Debugging](#11-error-handling-and-debugging) 12. [C API](#12-c-api) 13. [Python Bindings](#13-python-bindings) 14. [Limits and Performance](#14-limits-and-performance) 15. [Why C99 and Not C23?](#15-why-c99-and-not-c23) --- ## 1. General **What problem does Bovnar solve?** A bare `9.81` tells you nothing about whether the value is a float or an integer, 32-bit or 64-bit, and it carries no physical meaning. The type and the unit live outside the data — in an external schema, a naming convention, or documentation — and that gap is where unit-confusion failures hide. Bovnar is self-describing at the per-value level: every value in a `.bvnr` stream carries its type family, bit-width, numeric base, and physical unit inside the same byte stream. No external schema is required. You can hand a `.bvnr` file to anyone and they have everything they need to interpret it exactly as the author intended. --- **Is Bovnar a binary or a text format?** It is primarily a text format. The syntax is UTF-8 prose that is readable and writable by hand. The one binary escape mechanism — the octet stream (`0x00` framing) — lets you embed opaque binary payloads without Base64 overhead. Outside of octet-stream regions the parser enforces strict UTF-8 validity. --- **Does Bovnar perform unit conversion or dimensional analysis?** No. The unit annotation is parsed, validated, and delivered to the application through the event API, but Bovnar itself never converts between compatible units. That is deliberately left to the application. The Python bindings expose helper functions (`unit_convert_factor`, `convert_value`, `units_compatible`) that applications can use to implement dimensional checking and conversion on top of the parsed unit data. --- **Is there an official file extension?** Yes. The canonical extension is `.bvnr`. --- **Which version of the specification does the reference implementation target?** Spec 1.1 — the additive features documented here (the `#!bovnar` version directive, `\u`/`\x` escapes, the `datetime` family, and reference array indexing). Spec 1.0 remains the frozen, stable baseline: a document that declares no `#!bovnar` directive is treated as 1.0, and every 1.0 document parses unchanged. `bvnr_version_string()` reports the library version (`1.1.0`) and `bvnr_spec_version()` the highest spec it understands. --- ## 2. File Format and Syntax **What is the minimum valid Bovnar file?** An empty file (zero bytes) is valid. A file with a single assignment is the practical minimum: ```bovnar .key = value; ``` --- **Why do keys start with a dot?** The leading dot is a hard syntactic marker. It unambiguously separates key tokens from value tokens anywhere in the stream, including inside structs and arrays-of-structs. The parser uses it as the entry point for the identifier state machine. --- **Can I use Unicode characters in key names?** Yes, with restrictions. UTF-8 leader bytes in the range `0xC3–0xF4` are valid at both the start and body of an identifier, covering most non-ASCII letters. The one excluded range is `0xC2` (U+0080–U+00BF), which is explicitly rejected everywhere in identifiers. The following ASCII punctuation characters are also hard errors inside identifier bodies: ``` ! " # $ % & ' ( ) * , . / : ; < = > ? @ [ \ ] ^ ` { | } ~ ``` The characters `+`, `-`, and `_` are allowed in identifier bodies (but not as the first character, except for `_`). --- **Can a key start with a digit?** No. Identifiers must begin with a letter (`A–Z`, `a–z`), an underscore, or a valid UTF-8 leader byte. A leading digit is a hard parse error. --- **Are comments supported?** Yes. A `#` character starts a comment that runs to the end of the line. Comments are legal anywhere whitespace is legal — before the first assignment, between tokens, and at the end of a value line: ```bovnar # full-line comment .port = 5432; # inline comment ``` --- **Is whitespace significant?** No, except as a token separator and (required) separator between a scalar value and an inline unit suffix. Indentation and blank lines are purely cosmetic and are freely interleaved between all tokens. --- **Is a UTF-8 BOM accepted?** A UTF-8 BOM (`EF BB BF`) is accepted only at byte offset 0 of the stream. A BOM appearing later in a string is valid UTF-8 and passes through without error. For BOMs in other positions the error code depends on where the BOM appears: - A BOM found **inside the first comment line** (the dedicated `first_comment_*` states) produces `error_invalid_byte_order_mark`. - A BOM byte (`0xEF`) found **after the first comment line** but before the first `.` identifier (the `first_bom` state) is not handled in the state table and produces `error_unexpected_input_byte`. A BOM appearing inside any *subsequent* comment is valid UTF-8 and accepted without error. --- **Can a document declare which bovnar version it uses?** Yes, since spec 1.1. Put a directive on the first line: ```bovnar #!bovnar 1.1 .x = 42; ``` A document with **no** directive is treated as spec **1.0** — the frozen baseline grammar — so existing files need no change. The directive only opts in to a newer version. It is recognised only as the very first comment (after an optional BOM and whitespace). A reader records the declared version (`bvnr_reader_get_declared_version`, `bovnar validate` reports it, and the Python `Reader.declared_version` / `bovnar.peek_version()` expose it), and a reader opened with `strict_version` rejects a version it does not support with `error_unsupported_spec_version`. A malformed directive is `error_invalid_spec_version`. --- **Wait — isn't a comment supposed to be meaningless? How can `#!bovnar …` matter?** The version directive is the single, deliberate exception. Everywhere else a comment is semantically inert. The directive is *shaped* like a comment on purpose: that is exactly what makes it backward compatible — a spec-1.0 reader (which knows nothing about versions) skips it as a plain comment and parses the rest of the document normally, while a 1.1+ reader additionally recognises the `#!bovnar` prefix on the first line and acts on it. Nothing else about comment semantics changes, and a `#!bovnar …` on any line other than the first is still just a comment. --- ## 3. Type System and Annotations **What are the seven type families?** | Keyword | Description | |---|---| | `uint` | Unsigned integer | | `sint` | Signed integer | | `float` | IEEE 754 binary floating-point | | `float_fix` | Q-format signed fixed-point | | `float_dec` | IEEE 754-2008 decimal floating-point | | `utf8` | UTF-8 string | | `bool` | Boolean (`true`/`false`); takes no parameters | Spec 1.1 adds an eighth, `datetime` (see below). --- **Is there a date/time type?** Yes, since spec 1.1: the `datetime` family. A `datetime` value is a **signed integer count of seconds since a named epoch** — a timestamp: ```bovnar #!bovnar 1.1 .created = 1750000000; ``` The epoch parameter is one of `unix` (default), `tai`, `gps`, `mjd`, `ntp`, `galileo`, `glonass`, `y2000`, `beidou`. The value validates like `sint` (negative = before the epoch); a numeric base/unit/`q` is `error_illegal_value_type` and a fractional value is `error_type_value_mismatch`. Being a 1.1 feature it requires a `#!bovnar 1.1` declaration. Convert to civil time with the `bvn_datetime.h` helpers. You can also write the value as an **ISO-8601 literal** instead of a raw integer — `2026-06-15`, `2026-06-15T12:00:00`, a trailing `Z`, or a numeric `±HH:MM` offset, with an optional fractional second: ```bovnar #!bovnar 1.1 .created = 2026-06-15T12:00:00Z; # bare literal infers .local = 2026-06-15T12:00:00+02:00; # offset folds to 10:00:00Z .logline = 2026-06-15T12:00:00.123Z; # fraction preserved as a string; round-trips ``` It is converted to the epoch-seconds carrier at parse time (the integer is what is stored, so round-trips are idempotent). A bare literal with no annotation infers ``. A `±HH:MM` offset shifts the time to true UTC before the conversion. A fractional part (any number of digits) does not change the whole-second carrier, but the digits are kept verbatim — consumers read them as a string (`bvnr_data_t.frac_data`, or `bvn_dom_get_datetime_fraction()`), and a value carrying a fraction is pretty-printed back as an ISO literal so it round-trips. For sub-second values you compute on, use a finer integer carrier (e.g. milliseconds). The UTC→epoch conversion is leap-second correct for the civil epochs and `tai` (the offset is folded before `tai`'s leap-second lookup). A second of `60` is an inserted leap second: on the civil epochs it folds onto the following second (POSIX time has none to spend on it), but on `tai` it keeps its own value one below the boundary, which is what makes UTC⇄TAI injective there. The atomic GNSS epochs (`gps`/`galileo`/`glonass`/`beidou`) reject a literal (`error_datetime_literal_unsupported_epoch`) — give those an integer carrier. A malformed or out-of-range literal is `error_invalid_datetime_literal`. **Timestamp vs. duration — don't confuse them.** A *timestamp* (an instant) is a `datetime`; a *duration* (an elapsed amount) is a plain number with a time unit, e.g. ` 2.5` (2.5 seconds) or ` 8` (8 hours). The time *units* (`s`, `min`, `h`, `d`, …) measure spans; the `datetime` *family* names a point on a timeline. --- **What is default type synthesis?** When no explicit type annotation is present, the parser synthesises one based on the literal: - Positive integer literal → `uint:64` - Negative integer literal → `sint:64` - Literal containing `.` or `e` → `float:64` - Quoted string → `utf8` - Boolean keyword (`true`/`false`/`on`/`off`) → `bool` - ISO-8601 datetime literal (spec 1.1) → `datetime:64,unix` This is called default type synthesis and is convenient for configuration files and quick hand-authored data. For precision-sensitive use cases, always write an explicit annotation. --- **Where exactly does the type annotation go?** Between `=` and the value — never on the key, and never after the value: ```bovnar .port = 443; # correct .port = 443; # WRONG — hard error .port = 443 ; # WRONG — hard error ``` --- **Can annotation parameters appear in any order?** Yes. Width, base, and unit are distinguished by their syntactic form, not by position. The following three are equivalent: ```bovnar .x = "1F4"; # — identical (parameter order is free) # — identical ``` --- **What widths are valid for each type family?** | Family | Valid widths | |---|---| | `uint`, `sint` | Any positive integer up to `BVN_MAX_INT_WIDTH` (32768), e.g. `` is legal for a 12-bit ADC | | `float` | `0` (→64), `16`, and any multiple of `32` up to `32768` | | `float_fix` | `0` (→64), `16`, `32`, `64`, `128`, `256` | | `float_dec` | `0` (→64), `16`, `32`, `64`, `128`, `256` | | `datetime` | Same as `sint` — any positive integer up to `BVN_MAX_INT_WIDTH` (the carrier is a signed epoch-seconds integer); `0` →64 | Width `0` always means "use the default," which is 64 bits for all families. Width `8` is legal for `uint`/`sint`/`datetime` but is a hard error for all float families. A non-multiple-of-32 (other than 16) width for `float` is `error_illegal_value_type`. --- **What is the `float_fix` (Q-format) type and how is it annotated?** `float_fix` encodes a value as a signed integer with a declared number of fractional bits. The mathematical value is `raw_integer × 2⁻Q`. The annotation requires a `qN` parameter specifying fractional bits: ```bovnar .pid = -1.5; # 32-bit, 16 fractional bits .adc = 25.0; # 16-bit Q8 with a unit ``` Q must satisfy `0 ≤ Q < effective_width`. `` is illegal. The base parameter (`_N`) is forbidden for `float_fix`. --- **What is `float_dec` and when should I use it?** `float_dec` is IEEE 754-2008 decimal floating-point. Binary floats cannot represent `0.1` exactly; decimal floats can. Use `float_dec` in financial, metrological, or billing contexts where exact decimal representation matters. The base parameter is forbidden for `float_dec` as well. --- **Can I annotate a string value with a type?** Yes, with ``: ```bovnar .label = "hello"; ``` This is redundant — a bare quoted literal is already synthesised as `utf8` — but it makes the type explicit, which is useful for documentation and tooling. `utf8` is a parameterless family: a width, base, q, or unit parameter inside a `` annotation is `error_illegal_value_type`. --- ## 4. Units **What is the purpose of the unit annotation?** Physical units are a first-class part of the type system, not a comment field. Attaching a unit to a value makes the data self-documenting and enables the consuming application to perform dimensional verification and conversion. The parser fully validates every unit expression and exposes the structured result through the `value_unit_t` field of the `ev_data` event. --- **How do I write a simple unit?** Place the unit symbol inside the angle brackets after the other type parameters: ```bovnar .distance = 384400000; .voltage = 3.3; .frequency = 2400000000; ``` --- **How do SI prefixes work, and why is the `~` separator mandatory?** An SI prefix is written before the unit symbol with a mandatory `~` separator: `k~m` (kilometer), `m~V` (millivolt), `G~Hz` (gigahertz). The `~` is required because without it there is no general way to distinguish a two-character unit symbol from a one-character prefix followed by a one-character unit — `mV` would be ambiguous. The `~` resolves all such cases unambiguously: ```bovnar .k_ohm = 4.7; .micro = 50.0; .giga = 2.4; ``` IEC binary prefixes (`Ki`, `Mi`, `Gi`, `Ti`, …) follow the same rule: ```bovnar .ram = 8; .disk = 2; ``` --- **What is the difference between `M~B` (megabytes) and `Mi~B` (mebibytes)?** `M~` is the SI decimal prefix (mega = 10⁶). `Mi~` is the IEC binary prefix (mebi = 2²⁰ = 1,048,576). The difference matters for storage: ```bovnar .link_rate = 1000; # 1 000 × 10⁶ bits .cache = 512; # 512 × 2²⁰ bytes ``` --- **How do I write compound units like m/s² or kg·m/s²?** Use `*` or the middle-dot `·` (U+00B7) for multiplication and `/` for division: ```bovnar .velocity = 9.81; .acceleration = 9.81; .force = 9.81; .energy = 1000.0; ``` The `/` separator is a one-way switch: once written, every subsequent component is in the denominator. To place a component back in the numerator after a divisor, use a negative exponent instead: ```bovnar .force_alt = 9.81; # identical to k~g·m/s² ``` The maximum number of unit components in a compound unit is 8; exceeding that is `error_unit_illegal`. --- **Can exponents be written in ASCII instead of Unicode superscripts?** Yes. Both forms are accepted and produce identical internal representations: ```bovnar .area1 = 100.0; # Unicode superscript .area2 = 100.0; # ASCII caret form — same result .inv1 = 50.0; .inv2 = 50.0; ``` Only a single ASCII digit is supported after `^`; multi-digit exponents are a parse error. --- **What does `no_unit` mean, and how does it differ from omitting the unit?** Both mean "dimensionless," but they produce distinct internal states: - Omitting the unit parameter → `BVN_UNIT_NO_PREFIX(bu_none)`: `num_components == 1`, `base == bu_none`. - Writing `no_unit` explicitly → `BVN_UNIT_NONE`: `num_components == 0`. Both compare as compatible via `bvn_units_compatible` and both serialise to `"no_unit"` via `bvn_unit_to_string`. The distinction is expressive: explicit `no_unit` signals that the author actively chose dimensionless, whereas an omitted unit might mean the author simply did not think about units. For documentation-grade data, prefer the explicit form. --- **Can I write a unit as an inline suffix instead of inside the annotation?** Yes, for scalar values in non-array context. Write the unit directly after the value literal, separated by at least one whitespace character: ```bovnar .speed = 9.81 m/s; # no annotation; inline unit adopted .mass = 70.0 k~g; .storage = 65536 B; ``` The inline unit is forbidden inside array elements — any letter or underscore following an array-element value is `error_unexpected_input_byte`. If both an annotation unit and an inline unit are present, they must be identical; a mismatch is `error_unit_mismatch`. --- **How many base units does Bovnar support?** 163 named base units across the following categories: - **7 SI base units** — second, meter, gram, ampere, kelvin, mole, candela. - **21 named SI-derived units** — hertz through katal (Hz, N, Pa, J, W, V, Ω, F, C, S, Wb, T, H, lm, lx, Bq, Gy, Sv, kat, rad, sr). - **14 non-SI units accepted for use with SI** — liter, minute, hour, day, week, year, degree (angle), degree Celsius, tonne, bar, electronvolt, dalton, astronomical unit, hectare. - **2 digital units** — bit (`b`), byte (`B`), with IEC binary prefixes (kibi through quebi). - **13 Imperial/US customary length units** — inch, foot, yard, mile, nautical mile, ångström, light-year, parsec, furlong, fathom, **chain** (`ch`), **rod** (`rd`), and **thou** (thousandth of an inch, alias `mil`). - **11 Imperial/US customary mass units** — pound, ounce, grain, stone, short ton, long ton, troy ounce, carat, **slug**, **dram** (`dr`), **pennyweight** (`dwt`). - **6 temperature scales** — degree Fahrenheit (affine), **degree Rankine** (linear, absolute), and the historical **Delisle** (`°De`), **Newton** (`°N`), **Réaumur** (`°Re`), and **Rømer** (`°Ro`) scales (all affine). Degree Celsius is counted among the non-SI accepted units above. - **6 pressure units** — atmosphere, mmHg, Torr, psi, **inch of mercury** (`inHg`), and **atmosphere technical** (`at`, = 1 kgf/cm² = 98 066.5 Pa). - **5 energy units** — calorie, BTU, erg, therm, **foot-pound** (`ft_lb`). - **2 power units** — horsepower (`hp`), **metric horsepower** (`PS`, also `CV`; = 735.49875 W). - **4 force units** — pound-force, dyne, kip, **kilogram-force** (`kgf`). - **1 acceleration unit** — **standard gravity** (`gn`, = 9.80665 m/s², exact). - **3 speed/frequency/rotation units** — knot, **revolutions per minute** (`rpm`), **revolution** (`rev`, full angular turn = 2π rad). - **15 US and UK volume units** — gallon, gallon (UK), quart, pint, cup, fluid ounce, tablespoon, teaspoon, barrel, **US gill** (`gi`), **imperial gill** (`gi_uk`), **fluid dram** (`fl_dr`), **minim**, **peck** (`pk`), **bushel** (`bsh`). - **3 UK imperial volume units** — **pint_uk**, **fluid_ounce_uk**, **quart_uk**. - **2 area units** — acre, barn. - **3 angle units** — arcminute, arcsecond, gradian; plus **revolution** (see rotation above). - **8 CGS units** — poise, stokes, gauss, maxwell, oersted, stilb, phot, galileo. - **3 radiation units** — curie, röntgen, rem. - **2 logarithmic units** — neper, **decibel** (`dB`). - **2 electrical power units** — **var** (reactive power), **volt-ampere** (`VA`, apparent power). - **2 time extensions** — **month** (`mo`, Julian month = 2 629 800 s), **fortnight** (`fn` = 14 d = 1 209 600 s). - **2 textile linear density units** — **tex** (1 g/km = 10⁻⁶ kg/m, ISO 1144), **denier** (`den`, 1 g/9 000 m; 9 den = 1 tex). - **13 Old German units** — **Pfund** (`Pfd`), **Zentner** (`Ztr`), **Doppelzentner**, **Lot** (mass); **Prussian line, Zoll, Fuß, Elle, Rute**, **Klafter**, and **German (geographical) mile** (length); **Morgen** (area); **Scheffel** (volume). None accept an SI or IEC prefix. - **7 surveying & signalling units** — **US survey foot**, **league**, **cable length**, **hand** (length); **quintal**, **scruple** (mass); **baud** (`Bd`, signalling rate). - **6 ratio / proportion units** — **percent** (`%`), **per mille** (`‰`), **per myriad** (`‱`), **per cent mille** (`pcm`), **ppm**, **ppb** — dimensionless scaling factors that take no prefix. The `bu_gram` base unit is used for mass so that the `k~` prefix can carry the kilo: `k~g` = kilogram. The Rankine symbol is `°Ra` (also accepted as `Ra`); note `R` alone is reserved for röntgen. Thou accepts `mil` as an alternative spelling. `var` and `VA` share the same SI dimensional signature as watt but are kept distinct for physical clarity. `rpm` has the same SI dimension as `Hz` (s⁻¹) but a distinct base unit for semantic clarity in rotational contexts. `at` (atmosphere technical) must not be confused with `atm` (standard atmosphere): 1 at = 98 066.5 Pa; 1 atm = 101 325 Pa. --- ## 5. Numbers and Special Values **Do I need to quote non-decimal numbers?** Yes. Bovnar's lexer distinguishes bare words (symbols) from numbers by context. A bare `FF` in value position is a symbol, not a hex number, and will produce `error_type_value_mismatch`. Any non-decimal value must be a quoted string: ```bovnar .hex = "FF"; # correct — 255 .hex = FF; # WRONG — FF is a symbol .bin = "11010110"; # correct ``` For signed non-decimal values, the minus sign goes inside the string: ```bovnar .neg = "-7FFFFFFF"; ``` --- **What base systems are supported?** For `uint` and `sint`: every base from `_2` through `_62` (consecutive). Bases `_64` (standard Base64) and `_85` (Ascii85) are also supported, but **for `uint` only** — their alphabets claim the sign glyphs as digits (Base64 uses `+` and `/`; Ascii85 uses both `+` and `-`), leaving no unambiguous sign character, so a signed value in these bases is a hard error (`error_illegal_value_type`). For `float`: `_10` (decimal, the default) and `_16` (hexadecimal) only. `float_fix` and `float_dec` do not accept a base parameter at all — specifying one is a hard error. --- **What are the special number literals?** Three special values bypass normal numeric parsing. They are bare reserved keywords — no sigil — and, like `null`/`true`/`false`, are reclassified out of the symbol space by the validator. Only these exact spellings are reserved; any other bare word (e.g. `infinity`, `nans`) is an ordinary symbol: ```bovnar .nan = nan; .inf = inf; .neg = ninf; # negative infinity ``` They are valid in both typed and untyped context and may be combined with any float type annotation. A special-number keyword takes no inline unit suffix — give its unit via the annotation (` inf`). --- **Are leading zeros legal in decimal integers?** Yes. `007` parses as the integer `7`. There is no octal interpretation — the leading zeros are ignored. --- **Are there shorthand float forms like `-.5` or `123.`?** Yes. Both leading-dot (`.5` or `-.5`) and trailing-dot (`123.`) forms are valid for `float:64`: ```bovnar .a = -.5; # -0.5 as float:64 .b = 123.; # 123.0 as float:64 ``` --- ## 6. Strings **What escape sequences are defined?** Exactly seven: | Escape | Meaning | |---|---| | `\t` | Horizontal tab | | `\n` | Line feed | | `\v` | Vertical tab | | `\f` | Form feed | | `\r` | Carriage return | | `\"` | Double quote | | `\\` | Backslash | In **spec 1.0** that is all — any other character after a backslash is `error_illegal_escape_sequence`. A **spec 1.1** document (declaring `#!bovnar 1.1`, see §3.4) adds two more: | Escape | Meaning | |---|---| | `\xHH` | the single byte `HH` (two hex digits) | | `\u{H…}` | Unicode scalar `U+H…` (1–6 hex digits), UTF-8 encoded | `\u{…}` rejects surrogates and values above `U+10FFFF` (`error_invalid_codepoint`). `\x` writes a raw byte but the string must stay valid UTF-8, so `"\xC3\xA9"` is `"é"` while a lone `"\xFF"` is `error_invalid_utf8_byte` — for arbitrary binary data, still use an octet stream. Both `\x` and `\u` are **1.1-only**: in a 1.0/unversioned document they remain `error_illegal_escape_sequence`, so existing documents are unaffected. --- **Can strings span multiple lines?** Yes. Raw whitespace bytes (HT, LF, VT, FF, CR) are accepted unescaped inside string literals: ```bovnar .poem = "roses are red violets are blue"; ``` --- **How does string concatenation work?** Adjacent string literals separated only by whitespace or comments are concatenated at lex time. This is the idiomatic way to split a long string across lines: ```bovnar .url = "https://" "api.example.com" "/v1"; .guid = "deadbeef" "cafebabe" "01234567" "89abcdef"; ``` The combined byte length must not exceed `max_string_length` (default 65535). --- **Is the `` annotation ever required?** No. A bare quoted literal is always synthesised as `utf8`. The annotation is purely optional and expressive. --- ## 7. Arrays **What is the basic array syntax?** Comma-separated values inside square brackets: ```bovnar .primes = [2, 3, 5, 7, 11]; ``` --- **How are multi-dimensional arrays written?** Rows are separated by `/`: ```bovnar .matrix = [1, 2, 3]/[4, 5, 6]/[7, 8, 9]; ``` Each `/[…]` is a new row dimension. The parser emits `ev_array_row_start` and `ev_array_row_end` events for each row and `ev_array_dim_start` for each `/` separator. --- **Can array elements have different types?** Array elements must be **homogeneous** (spec 1.0): every non-null element shares the same kind, and a bare array of numbers shares the same physical dimension. The numeric *encodings* may still mix, since they all denote the same dimension: ```bovnar .mixed = [ 1, -1, 3.14]; # valid: all dimensionless ``` But genuinely different kinds (`[1, "two"]`) or dimensions (`[ 1.0, 2.0]`) are rejected with `error_array_element_type_mismatch`. Heterogeneous data goes in a **struct**, not an array. Homogeneity is a **materialised-document** rule: it is checked over the whole value once it is assembled, so it is enforced by the DOM parser (`bvn_dom_parse` / `bovnar.dom_parse`), not by the streaming SAX `Reader` (nor by `loads`, which is built on it) — the streaming layer surfaces per-value type/unit errors as the bytes arrive but cannot see sibling elements. Parse through the DOM tier (or the `bovnar` CLI's `convert`) if you need these rules enforced. A whole-array annotation (placed before `[`) is the default type for elements that do not carry their own annotation: ```bovnar .ports = [80, 443, 8080]; ``` --- **How do null elements work inside arrays?** Leading, trailing, or consecutive commas produce null elements: ```bovnar .sparse = [, 1, , 2, ]; # null, 1, null, 2, null — five elements ``` --- **Can I nest arrays?** Yes, inner arrays are just element values: ```bovnar .nested = [[1, 2], [3, 4]]; # valid; rectangular sub-arrays .ragged = [[1, 2], [3, 4, 5]]; # error_array_row_size_mismatch — sibling sub-arrays differ in length ``` Note: since spec 1.0 sibling sub-arrays must match in length (and element shape), so bare arrays and matrices are rectangular — `[[1, 2], [3, 4, 5]]` is rejected. `error_array_row_size_mismatch` fires both when the `/`-rows of one array disagree (`[1,2,3]/[4,5]`) and when ragged sibling sub-arrays disagree (`[[1,2],[3,4,5]]`). --- **Are inline unit suffixes allowed inside arrays?** No. The inline unit suffix is forbidden at the lexer level for all array elements. Any letter or underscore following an array-element value token is `error_unexpected_input_byte`. --- ## 8. Structs **What is the struct syntax?** A struct groups zero or more assignments inside `{…}`. Every field inside is a complete `.key = value;` assignment. The struct value at the parent level ends with `};`: ```bovnar .server = { .host = "db.internal"; .port = 5432; }; ``` --- **How deeply can structs nest?** Up to `max_struct_nesting` levels — the reference reader defaults to 64 and the field (a `uint8_t`) has a hard cap of 255. An empty struct (`{}`) is valid. An unmatched closing brace is `error_illegal_struct_close`. --- **Are structs valid as array elements?** Yes: ```bovnar .sensors = [ {.id = 0; .value = 27.3;}, {.id = 1; .value = 31.7;} ]; ``` --- ## 9. Null, Symbols, and References **What is a null value?** An explicit absence of value — nothing between `=` and `;`, or the reserved keyword `null`: ```bovnar .nothing = ; # null, no type annotation .also_null = null; # the null keyword — identical to the empty slot .typed_null = ; # null with an explicit type ``` --- **Does Bovnar have booleans?** Yes. `true`, `false`, `on`, and `off` are reserved keywords carrying the `bool` type family (`on` ≡ `true`, `off` ≡ `false`). A bare boolean synthesises a `` annotation; `` takes no parameters and accepts only those four keywords. They serialize canonically as `true` / `false`: ```bovnar .enabled = true; .debug = off; # == false .typed = on; # explicit; on == true ``` --- **What is a symbol?** A bare, unquoted word in value position — for application-defined enums and the like. The reserved keywords `null`, `true`, `false`, `on`, `off` and the special floats `nan`, `inf`, `ninf` are *not* symbols (a word that merely starts with one, like `Monday`, `ontology`, or `infinity`, is): ```bovnar .status = ok; .mode = read_only; .day = Monday; ``` Symbol bodies follow the same character rules as identifier bodies. The key distinction from an identifier is context: identifiers appear after the leading dot, symbols appear as values. --- **What is a reference?** A dotted path to another key in the document, introduced by `&`. The parser stores the path as a string token; it does not resolve it — resolution is left entirely to the application: ```bovnar .host = "db.internal"; .conn_host = &.host; # stored as ".host" .deep = &.config.db.host; # multi-segment path ``` --- **Can a reference index into an array?** Yes, since spec 1.1: a reference path may carry `[N]` index suffixes. ```bovnar #!bovnar 1.1 .matrix = [10, 20, 30]/[40, 50, 60]; .row0c1 = &.matrix[0][1]; # stored as ".matrix[0][1]" ``` As with the rest of a reference, the index is stored **verbatim and unresolved**; it is interpreted only when the application resolves the path against the materialised tree (`bvn_dom_lookup`, also used by the CLI `query` command), where `&.matrix[0][1]` resolves to `20`. A flat `/`-row matrix is addressed `[row][col]`, a 1-D array `[i]`, and nested arrays descend one index per level; a partial/out-of-range index simply doesn't resolve. Being a 1.1 feature, a `[` in a reference is `error_unexpected_input_byte` in a 1.0/unversioned document. --- ## 10. Octet Streams **When should I use an octet stream?** When you need to embed raw binary data without Base64 overhead. A NUL byte (`0x00`) in value position switches the parser into binary chunk mode. The UTF-8 validator is suspended for the duration of the binary region. In practice you will not hand-author octet streams. Use the writer API, which generates the correct framing automatically: ``` 0x00 ← stream open 0x01 ← chunk: tag 0x01, length as little-endian uint16, data 0x00 ← stream close ``` A length value of `0x0000` means exactly 65536 bytes. Any tag byte other than `0x01` (chunk) or `0x00` (close) inside a stream is `error_octet_stream_out_of_sync`. --- ## 11. Error Handling and Debugging **What information does a parse error carry?** Every error includes: - `error_code_t` — a named code (e.g. `error_value_out_of_range`) - Line number and column number in the input stream - Byte offset from stream start - The raw byte value that triggered the error Retrieve these after a failed `bvnr_read`: ```c fprintf(stderr, "%s at line %" PRIu64 " col %" PRIu64 " offset %" PRIu64 " byte 0x%02X\n", bvn_error_to_string(bvnr_reader_get_error(r)), bvnr_reader_get_error_line(r), bvnr_reader_get_error_column(r), bvnr_reader_get_error_offset(r), bvnr_reader_get_error_byte(r)); ``` --- **What is `continue_on_error` mode and when should I use it?** When `continue_on_error` is set in `bvnr_read_flags_t`, a parse error invokes the `on_error` callback and enters a resync state machine that skips bytes while tracking bracket and brace nesting. Once the resync state machine finds a `;` at the original nesting depth, normal parsing resumes. The recovery count (accessible via `bvnr_reader_get_recovery_count`) is incremented at error entry, not on resync completion. Use this mode for log streams and unreliable transports where a single malformed assignment should not discard the rest of the file. For configuration and data serialization, disable it and fail fast. --- **What does `error_type_value_mismatch` mean?** The value token is incompatible with the declared type annotation. The most common cause is a non-decimal number written as a bare word (symbol) instead of a quoted string: ```bovnar .x = FF; # FF is a symbol → error_type_value_mismatch .x = "FF"; # correct ``` --- **What causes `error_unit_mismatch`?** An inline unit suffix and a type-annotation unit are both present but resolve to different `value_unit_t` values: ```bovnar .bad = 1.0 s; # annotation says m, inline says s → error ``` Semantically equivalent notations that parse to the same internal representation (e.g. `m/s` and `m·s⁻¹`) compare as equal and do not trigger this error. --- **What are the most common mistakes?** | Mistake | Error produced | |---|---| | Annotation on the key side of `=` | `error_unexpected_input_byte` | | Non-decimal value not quoted | `error_type_value_mismatch` | | Signed value in an unsigned type | `error_value_out_of_range` | | Integer overflow (e.g. ` 256`) | `error_value_out_of_range` | | `` or a non-16, non-multiple-of-32 width for `float` | `error_illegal_value_type` | | Base parameter on `float_fix` or `float_dec` | `error_illegal_value_type` | | `q ≥ width` in `float_fix` | `error_illegal_value_type` | | Empty unit component (`m//s`) | `error_unit_illegal` | | More than 8 unit components | `error_unit_illegal` | | Inline unit suffix inside an array | `error_unexpected_input_byte` | | Unknown escape sequence (`\0`, `\uXXXX`; `\x`/`\u{}` outside spec 1.1) | `error_illegal_escape_sequence` | | `\u{…}` surrogate or `> U+10FFFF` (spec 1.1) | `error_invalid_codepoint` | | Unmatched `}` | `error_illegal_struct_close` | | Empty key (`.= value;`) | `error_empty_identifier` | --- ## 12. C API **What is the overall reader lifecycle?** ``` bvnr_reader_create() → bvnr_source_from_fd() or bvnr_source_from_mem() → bvnr_open_read_source() or bvnr_open_read_mem() → bvnr_read() → bvnr_reader_destroy() ``` The reader does not allocate during `bvnr_read`; all buffering is internal to the reader struct. The `bvnr_read_flags_t` struct is read only during `bvnr_open_read_source` and may live on the stack. --- **Which callback should I implement — `on_verified` or `on_unverified`?** Almost always `on_verified`. This callback receives fully validated events after semantic checking. `on_unverified` fires before validation and receives raw token events; use it only for diagnostics or partial pre-inspection. There is an important asymmetry for `ev_type_annotation_start`: - `on_unverified` receives it with raw annotation bytes but no `value_type` or `value_unit` populated. - `on_verified` receives it with `value_type` and `value_unit` filled in. All other type-annotation events and `ev_data` are emitted to both callbacks simultaneously. --- **Do callbacks need to return anything?** Yes. Both `on_verified` and `on_unverified` must return `bool`. Return `true` to continue parsing; return `false` to abort — the parser stops and `bvnr_read` returns `false` with `error_scanner_callback_failed`. --- **How do I read a value from the `ev_data` event?** The `bvnr_data_t` struct provides the raw bytes in `data`/`length`. Use the `bvn_parse_*` helpers to convert them to native C types: ```c uint64_t v; bvn_parse_uint64(vbuf, d->value_type, &v); double f; bvn_parse_double(vbuf, d->value_type, &f); ``` The `d->value_type` contains `family`, `width`, and `base` (which holds the numeric base, or the Q for `float_fix`, or the epoch index for `datetime`). Use `bvn_effective_width(d->value_type)` to get the resolved width (handles the `width == 0` → 64 default). --- **How is the writer API structured?** It is symmetric with the reader. You construct events and push them through `bvnr_write_event`, then call `bvnr_write_finish`: ``` bvnr_writer_create() → bvnr_sink_to_fd() or bvnr_sink_to_mem() → bvnr_open_write_sink() or bvnr_open_write_mem() → bvnr_write_event() × N → bvnr_write_finish() → bvnr_writer_destroy() ``` Use `bvnr_write_type_annotation(w, vt, vu)` as a convenience to emit the full annotation event sequence in one call. Use `bvnr_write_bvnf_base` and `bvnr_write_bvni` for the convenience integer/float helpers that generate the scalar key+value pair in a single call. --- **Does the library close file descriptors for me?** No. Both `bvnr_source_from_fd` and `bvnr_sink_to_fd` leave ownership of the file descriptor with the caller. Close it yourself after the reader or writer is destroyed. --- **What is the `max_array_nesting` limit and why is it capped at 255?** The lexer stores per-nesting-level state in a heap array sized to `max_array_nesting + 1` entries (so at most 256 at the 255 hard cap). `max_array_nesting` is a `uint8_t` field, so values above 255 cannot be represented; no runtime rejection is performed. Zero-initialising `bvnr_read_flags_t` is safe: a zero value causes the reader to substitute the internal default of **64**. The hard maximum is 255. The same default and cap apply to `max_struct_nesting`. --- ## 13. Python Bindings **Do the Python bindings require a compiled extension module?** No. The bindings are pure `ctypes` and load `libbvnr.so` at import time via `ctypes.CDLL`. No compilation step beyond building the C library is needed. --- **How does the library discovery order work?** 1. `LIBBOVNAR_PATH` environment variable — absolute path to the `.so`. 2. `LIBBOVNAR_DIR` environment variable — directory containing the `.so`. 3. `ctypes.util.find_library('bvnr')` — standard `ldconfig`/`LD_LIBRARY_PATH` search. 4. In-tree build paths relative to `_ffi.py` (`../../build/`, etc.). If none of these resolves the library, `BovnarLibraryNotFound` is raised with the list of searched paths. --- **What is the difference between `loads`/`dumps`, the SAX reader, and the DOM?** | Interface | When to use | |---|---| | `bovnar.loads` / `bovnar.dumps` | Simple dict serialization — loses type and unit metadata | | `Reader` (SAX) | Streaming; low memory; full access to type and unit data via callbacks | | `bovnar.dom_parse` / `DomDoc` | Random-access queries on a fully parsed in-memory tree | --- **How do I capture state in a SAX callback?** The Python `on_verified` callback receives exactly two positional arguments — the event code and the data payload — with no userdata parameter. Use a closure to capture external state: ```python state = {"current_key": ""} def on_event(ev, data): if ev == Event.ASSIGNMENT_START: state["current_key"] = data.raw_str() elif ev == Event.DATA: print(state["current_key"], "→", data.raw_bytes()) return True ``` --- **What happens when a callback raises a Python exception?** The exception is captured, the C callback returns `False` to stop parsing, the C call returns, and then the original Python exception is re-raised from `read_mem` / `read_fd`. The exception is not wrapped; it propagates as-is. --- **How do I access unit information in Python?** The `data.value_unit` field of an `Event.DATA` payload is a `ValueUnit` ctypes struct. Use the helper functions to work with it: ```python import bovnar vu = bovnar.parse_unit("k~g·m/s²") # parse string → ValueUnit s = bovnar.unit_to_str(vu) # → "k~g·m/s²" ok = bovnar.units_compatible(vu_a, vu_b) # dimensional compatibility c = bovnar.unit_convert_factor(vu_from, vu_to) kelvin = bovnar.convert_value(25.0, vu_celsius, vu_kelvin) ``` `unit_dimension_vector` returns a 7-element list of SI dimension exponents in the order `[m, kg, s, A, K, mol, cd]`. --- **What exception classes exist?** | Exception | Trigger | |---|---| | `BovnarLibraryNotFound` | `libbvnr.so` not found at import | | `BovnarParseError` | Parse error (carries `code`, `line`, `column`, `offset`, `byte`) | | `BovnarWriteError` | Write error (carries `code`, `offset`) | | `BovnarArgumentError` | Invalid argument to a helper function | All are subclasses of `BovnarError`. --- **What Python version is required?** Python ≥ 3.10. The bindings use `dataclasses`, `enum.IntEnum`, and union-type annotations (`X | Y`). --- ## 14. Limits and Performance **What are the default size limits and what should I set in production?** All limits are configurable via `bvnr_read_flags_t`. Defaults are permissive. | Field | Default | Suggested production cap | |---|---|---| | `max_identifier_length` | 255 | 255 | | `max_string_length` | 65535 | application-defined | | `max_number_length` | 65535 | 65535 | | `max_symbol_length` | 255 | 255 | | `max_reference_length` | 65535 | 65535 | | `max_array_items` | 0 (→ 2 147 483 647 internal default) | application-defined | | `max_text_bytes` | 0 (→ 2 147 483 647 internal default) | application-defined | | `max_file_size` | 0 (→ unlimited / endless) | `16777216` (16 MiB) | | `max_array_nesting` | 0 (→ 64 internal default; hard cap 255) | 32 or less | | `max_struct_nesting` | 0 (→ 64 internal default; hard cap 255) | 32 or less | Setting a field to `0` causes the reader to substitute a finite internal default — **2 147 483 647** for `max_array_items`/`max_text_bytes` and **64** for both nesting depths. **`max_file_size` is the exception: `0` means unlimited / endless** (no byte-count cap), which is the default so endless streams work out of the box. Production deployments should set `max_file_size` explicitly at minimum. For untrusted input, set `max_array_items`, `max_text_bytes`, and the nesting limits as well. --- **Does `bvnr_read` allocate heap memory?** No. The reader does not allocate during `bvnr_read`. All buffering is internal to the `bvnr_reader_t` struct, which is allocated once by `bvnr_reader_create`. Event data pointers (`d->data`) point into the reader's internal buffer and are valid only for the duration of the callback invocation. --- **What throughput can I expect from the parser?** Performance depends heavily on the input profile (scalars, typed values, structs, arrays, units) and hardware. The CLI's benchmark subcommand (`bovnar bench`) can measure MB/s, assignments/s, and events/s across configurable profiles and payload sizes. Run `bovnar bench --profile all --size 1024,65536 --iterations 200` for a representative baseline. Use `--min-overhead` to isolate raw lexer throughput by skipping the `on_verified` callback. --- **How many unit components can a compound unit have?** A maximum of 8 (`BVNR_MAX_UNIT_COMPONENTS`). Exceeding this limit during parsing is `error_unit_illegal`. --- ## 15. Why C99 and Not C23? **What is the stated language standard for the reference implementation?** Strict C99 (`-std=c99 -pedantic`). No C11, C17, or C23 extensions are used intentionally. --- **Why not C23, given that it was finalised in 2024?** Several compilers that are still in active use on embedded and cross-compilation targets do not yet ship a complete C23 implementation, and some may never receive one for cost or lifecycle reasons: | Toolchain / target | C23 status (as of 2026) | |---|---| | GCC < 14 | Partial; `_BitInt`, `typeof`, `nullptr` missing or gated behind `-std=c2x` | | Clang < 17 | Partial; several C23 headers and attributes absent | | IAR Embedded Workbench | Trails the standard by several years; C11 support only on recent versions | | Arm Compiler 5 (armcc) | Frozen at C99/C11 subset; no C23 roadmap | | Green Hills MULTI | Embedded-safety toolchain; C23 not yet qualified | | SDCC (small-device C compiler) | C11 subset; no C23 | | Keil MDK (ARMCC legacy) | C99 only | C99 has essentially universal compiler support across every architecture Bovnar is expected to run on — x86-64, Arm Cortex-M/R/A, RISC-V, MIPS, PowerPC, and various DSP families. C23 does not. --- **Which specific C23 features would have been attractive but were ruled out?** | Feature | C23 addition | Why skipped | |---|---|---| | `bool`, `true`, `false` keywords (no ``) | C23 | Requires C23-capable front-end; `` works everywhere | | `nullptr` | C23 | Cosmetic; `NULL` or `(void*)0` is unambiguous | | `typeof` / `typeof_unqual` | C23 | Useful for generic macros; GCC/Clang extensions exist but are non-standard pre-C23 | | `#embed` | C23 | Attractive for embedding binary tables at compile time; workaround via `xxd -i` is portable | | `[[attributes]]` | C23 | `__attribute__((…))` is already used where needed via compiler-specific guards | | `_BitInt(N)` | C23 | Would help for arbitrary-width integer types; current code uses `uint64_t` + masking | | Improved `constexpr` | C23 | Not applicable to a runtime library | None of these are blockers. Every C23 feature Bovnar would have used has a correct, readable C99 equivalent. --- **What about C11? It has `_Atomic` and `_Static_assert`.** C11 adds atomics, `_Generic`, `_Static_assert`, and ``. `_Static_assert` is useful and is approximated in the codebase with a macro. The atomics and threading additions are irrelevant, but for a more specific reason than "not thread-safe": * A **single** reader or writer object must not be used from two threads at once — it carries the whole parse state. Guard it with your own primitive. * **Distinct** objects on distinct threads need no synchronisation at all. The library keeps no mutable shared state: the lexer's run lookup tables are built from a pre-main constructor precisely so their init flag can never be raced on, and nothing else is written after startup. `tests/bovnar_concurrency_test.c` pins this (12 000 concurrent reader / DOM / writer operations across 8 threads, clean under ThreadSanitizer). So no atomics are needed — not because concurrency is unsupported, but because the only shared state is immutable by the time any thread exists. Pulling in C11 just for `_Static_assert` is not worth fragmenting compiler compatibility. --- **Does C99 impose any meaningful limitations on the implementation?** In practice, no. The limitations that do exist are handled cleanly: - **Variable-length arrays (VLAs):** Not used. All fixed-size buffers are declared with compile-time constants. VLAs are optional in C11 and later and are dangerous on stack-limited embedded targets regardless of standard version. - **`` and ``:** Both are C99 additions and are available everywhere the library targets. - **`//` comments:** C99. No issue. - **Designated initialisers:** C99. Used extensively for `bvnr_data_t` and flag struct setup. - **Flexible array members:** C99. Not currently used. - **`restrict`:** C99. Used on internal buffer pointers where beneficial. The one area where C99 imposes a real discipline is the absence of `_Static_assert` — the implementation works around this with the classic `typedef char static_assert_failed[condition ? 1 : -1]` pattern. --- **Is this likely to change in the future?** The minimum standard may be raised to C11 if a compelling use of `_Generic` or `_Static_assert` arises and the embedded toolchain matrix shifts accordingly. C23 as a baseline is not on the roadmap until the IAR, Keil, and Green Hills ecosystems ship production-qualified C23 front-ends, which historically lags the standard publication by three to five years. --- *End of Bovnar FAQ — Specification (v1.1)* # Bovnar Conformance Test Tool > **Version:** 1.1 > **Protocol:** bvnr-conformance-v1 > **Last updated:** 2026-06-01 --- ## Table of Contents 1. [Purpose](#1-purpose) 2. [Quick Start](#2-quick-start) 3. [Architecture](#3-architecture) 4. [Building](#4-building) 5. [Running](#5-running) 6. [IUT Protocol](#6-iut-protocol) 7. [Writing a Compliant IUT Adapter](#7-writing-a-compliant-iut-adapter) 8. [Test Case Corpus](#8-test-case-corpus) 9. [Output Format (TAP)](#9-output-format-tap) 10. [Extending the Corpus](#10-extending-the-corpus) 11. [CMake Integration](#11-cmake-integration) --- ## 1. Purpose The Bovnar Conformance Test Tool (`bvnr_conformance`) verifies that any implementation of the Bovnar serialization format produces correct, spec- compliant behaviour. The reference implementation (this repository) is used both as the test driver and as the oracle against which candidate implementations are judged. Two use modes are supported: | Mode | Description | |------|-------------| | **Self-test** (default) | Runs the corpus against the reference libbvnr directly | | **IUT test** (`--iut`) | Invokes an external binary and compares its output to the reference | --- ## 2. Quick Start ### Self-test (verify the reference implementation) ```sh cd build cmake --build . --target bvnr_conformance ctest -R bvnr_conformance_self --output-on-failure ``` ### Testing an external implementation ```sh # Build your adapter (see Section 7) cc -o my_impl_adapter my_adapter.c -lmy_bovnar # Run the conformance suite against it ./tests/bvnr_conformance --iut ./my_impl_adapter ``` ### Filtering by group ```sh ./tests/bvnr_conformance --filter units ./tests/bvnr_conformance --iut ./my_impl_adapter --filter arrays ``` --- ## 3. Architecture ``` ┌───────────────────────────────────────────────────────────────────┐ │ bvnr_conformance │ │ │ │ Test corpus (319 cases) ──→ for each test case: │ │ │ │ Self-test mode: IUT mode: │ │ ┌─────────────────────┐ ┌──────────────────────────┐ │ │ │ ref_parse() │ │ ref_parse() → event log │ │ │ │ (uses libbvnr) │ │ fork/exec IUT binary │ │ │ │ verify error codes │ │ compare IUT stdout to │ │ │ │ verify key presence │ │ reference event log │ │ │ └─────────────────────┘ └──────────────────────────┘ │ │ │ │ Output: TAP v14 stream │ └───────────────────────────────────────────────────────────────────┘ ``` The reference implementation is the single authoritative oracle. An implementation is conformant when its IUT adapter produces output byte-for-byte identical to the reference for every test case. ### Validation tiers Most cases exercise the **streaming reader** (`bvnr_read`): the lexer, validator, and the `on_verified` event stream the IUT protocol mirrors. A smaller set — the `homogeneity` group — exercises the **materialised-document (DOM) tier** (`bvn_dom_parse`), because the spec-1.0 array-homogeneity (§7.4), struct-shape, and duplicate-key (§8.1) rules are enforced *above* the lexer and are therefore unreachable through the streaming `on_verified` callback. These DOM-tier cases run in **self-test mode only**; under `--iut` they are reported as `# SKIP`, since IUT protocol v1 is streaming-only and cannot express a DOM-tier check. An implementation that targets full spec-1.0 conformance must still enforce these rules in its document/tree API; the self-test cases pin the reference behaviour and its frozen error codes (39, 40, 41). --- ## 4. Building The conformance tool is built automatically as part of the standard CMake build. Both the main driver and the reference IUT adapter are compiled: ```sh mkdir build && cd build cmake .. cmake --build . ``` Targets produced: | Target | Binary | Purpose | |--------|--------|---------| | `bvnr_conformance` | `tests/bvnr_conformance` | Main conformance driver | | `bvnr_conformance_iut` | `tests/bvnr_conformance_iut` | Reference IUT adapter | The conformance tool links against `bvnr_static`. No external dependencies beyond libc and POSIX are required. --- ## 5. Running ### Command-line options ``` bvnr_conformance [OPTIONS] Options: --iut Path to the IUT binary to test --filter Run only cases in the specified group --list List all test case IDs and descriptions --verbose Print additional diagnostic information --help Show this help and exit ``` ### Examples ```sh # Run self-test (reference vs. reference) ./tests/bvnr_conformance # Run with verbose output ./tests/bvnr_conformance --verbose # List all test cases ./tests/bvnr_conformance --list # Run only unit-related tests ./tests/bvnr_conformance --filter units # Test an external IUT adapter ./tests/bvnr_conformance --iut ./my_adapter # Test external adapter, units group only ./tests/bvnr_conformance --iut ./my_adapter --filter units ``` ### Test groups | Group | Description | |-------|-------------| | `encoding` | UTF-8, BOM handling, byte class enforcement, truncated streams | | `limits` | Lexer token-length caps | | `version` | `#!bovnar M.N` directive parsing and strictness | | `identifiers` | Key syntax, length limits | | `strings` | Escape sequences, concatenation, length limits | | `datetime` | Timestamp family: epochs, range, version gating, ISO-8601 literals (spec 1.1) | | `numbers` | Integer and float literals, scientific notation | | `types` | All type families, widths, bases | | `default_synthesis` | Automatic type annotation inference | | `symbols` | Bare-word values | | `references` | `&.path` syntax | | `null_values` | Null scalars and null array elements | | `structs` | Nesting, empty structs, struct arrays | | `arrays` | 1D, 2D, nested, typed, null elements | | `octet_streams` | Binary chunk protocol | | `units` | SI, IEC, compound, inline suffix, mismatch errors | | `special_numbers` | `nan`, `inf`, `ninf` | | `roundtrip` | Multi-assignment sequences | | `recovery` | Error-resync behaviour | | `comments` | Comment parsing | | `whitespace` | Whitespace tolerance | | `homogeneity` | DOM-tier: array homogeneity, struct shape, key uniqueness (self-test only) | --- ## 6. IUT Protocol The **IUT (Implementation Under Test) Protocol** version 1 (`bvnr-conformance-v1`) defines the interface between the conformance driver and a candidate implementation's adapter binary. ### Communication model ``` conformance driver IUT adapter │ │ │ fork, exec IUT binary │ │ ─────────────────────────────────→│ │ │ │ Bovnar text (stdin) │ │ ─────────────────────────────────→│ │ │ │ closes stdin (EOF) │ │ ─────────────────────────────────→│ │ │ │ event log or error (stdout) │ │ ←─────────────────────────────── │ │ │ │ exit(0) or exit(1) │ │ ←─────────────────────────────── │ ``` ### Success response The IUT must: 1. Exit with code **0**. 2. Write the conformance event log to stdout with **no trailing garbage**. The event log is a sequence of lines, one event per line, in the order the events were received from the `on_verified` callback. ### Error response The IUT must: 1. Exit with code **non-zero** (typically 1). 2. Write exactly one line to stdout: `ERROR ` followed by `\n`. Where `` is the value returned by `bvn_error_to_string()` for the first error encountered, e.g. `value_out_of_range`. --- ## 7. Writing a Compliant IUT Adapter The file `tests/bvnr_conformance_iut.c` is the reference IUT adapter. It uses the reference libbvnr and is intended both for self-testing and as a template for third-party implementors. A minimal conforming adapter must: 1. **Read all of stdin** into a buffer. 2. **Parse the buffer** using the implementation under test. 3. **Collect events** from the `on_verified` callback. 4. **Emit the event log** to stdout on success. 5. **Emit** `ERROR \n` to stdout and exit non-zero on failure. ### Event log format reference Each event maps to one line. Text fields use `\xNN` escaping for any byte outside the printable ASCII range `0x20–0x7E` or for the backslash character `0x5C`. ``` STREAM_START ASSIGNMENT_START TYPE_ANN_START TYPE_FAMILY TYPE_PARAM_WIDTH TYPE_PARAM_BASE TYPE_PARAM_Q TYPE_PARAM_UNIT TYPE_ANN_END DATA STRUCT_START STRUCT_END ARRAY_ROW_START ARRAY_ROW_END ARRAY_DIM_START OCTET_STREAM_START OCTET_STREAM_END ``` ### Field details | Line | Fields | Notes | |------|--------|-------| | `STREAM_START` | — | Always first | | `ASSIGNMENT_START ` | key: raw key bytes, safe-escaped | | | `TYPE_ANN_START ` | family: `uint`, `sint`, `float`, `float_fix`, `float_dec`, `utf8`, `bool`, `datetime` | | | `TYPE_FAMILY ` | Same as TYPE_ANN_START | | | `TYPE_PARAM_WIDTH ` | N: effective width (0 → 64) | Only for numeric types | | `TYPE_PARAM_BASE ` | N: effective base (0 → 10) | Only for numeric types | | `TYPE_PARAM_Q ` | N: Q parameter | Only for `float_fix` | | `TYPE_PARAM_UNIT ` | unit: unit string, safe-escaped | Only for numeric types | | `TYPE_ANN_END ` | Same as TYPE_ANN_START | | | `DATA ` | token_type: see below; value: safe-escaped | | | `STRUCT_START` | — | | | `STRUCT_END` | — | | | `ARRAY_ROW_START` | — | | | `ARRAY_ROW_END` | — | | | `ARRAY_DIM_START` | — | Emitted between `/`-separated rows | | `OCTET_STREAM_START` | — | | | `OCTET_STREAM_END` | — | | ### TOKEN_TYPE values for DATA lines | Token type | String | |------------|--------| | `token_is_number` | `number` | | `token_is_string` | `string` | | `token_is_symbol` | `symbol` | | `token_is_reference` | `reference` | | `token_is_array_number` | `array_number` | | `token_is_array_string` | `array_string` | | `token_is_null_value` | `null` | | `token_is_octet_stream` | `octets` | | `token_is_bool` | `bool` | For `octets` token type, the value field is ` bytes` (decimal byte count, then a space, then the literal string `bytes`), not the raw binary data. ### Effective width and base - **Effective width**: if the stored width is 0, emit `64`. Use `bvn_effective_width(value_type)`. - **Effective base**: if the stored base is 0, emit `10`. Use `bvn_effective_base(value_type)`. These rules ensure that untyped values synthesised to `uint:64,_10` produce `TYPE_PARAM_WIDTH 64` and `TYPE_PARAM_BASE 10`, matching the reference output exactly. ### Example traces **Input:** `.x = 42;` ``` STREAM_START ASSIGNMENT_START x TYPE_ANN_START uint TYPE_FAMILY uint TYPE_PARAM_WIDTH 64 TYPE_PARAM_BASE 10 TYPE_PARAM_UNIT no_unit TYPE_ANN_END uint DATA number 42 ``` **Input:** `.s = "hello";` ``` STREAM_START ASSIGNMENT_START s TYPE_ANN_START utf8 TYPE_FAMILY utf8 TYPE_ANN_END utf8 DATA string hello ``` **Input:** `.v = 9.81;` ``` STREAM_START ASSIGNMENT_START v TYPE_ANN_START float TYPE_FAMILY float TYPE_PARAM_WIDTH 64 TYPE_PARAM_BASE 10 TYPE_PARAM_UNIT m/s TYPE_ANN_END float DATA number 9.81 ``` **Input:** `.x = ok;` (symbol) ``` STREAM_START ASSIGNMENT_START x DATA symbol ok ``` **Input:** `.a = [1, 2];` ``` STREAM_START ASSIGNMENT_START a ARRAY_ROW_START TYPE_ANN_START uint TYPE_FAMILY uint TYPE_PARAM_WIDTH 64 TYPE_PARAM_BASE 10 TYPE_PARAM_UNIT no_unit TYPE_ANN_END uint DATA array_number 1 TYPE_ANN_START uint TYPE_FAMILY uint TYPE_PARAM_WIDTH 64 TYPE_PARAM_BASE 10 TYPE_PARAM_UNIT no_unit TYPE_ANN_END uint DATA array_number 2 ARRAY_ROW_END ``` **Input:** `.x = 999;` (error — value out of range) ``` ERROR value_out_of_range ``` (exit code 1) --- ## 8. Test Case Corpus The corpus is embedded in `tests/bvnr_conformance.c`. Each case specifies: | Field | Description | |-------|-------------| | `id` | Unique identifier, e.g. `TYP-019` | | `group` | Group name for `--filter` | | `description` | Human-readable description | | `input` | Bovnar text (or binary) to parse | | `expect` | `CF_VALID` or `CF_ERROR` | | `expected_error` | Error code for `CF_ERROR` cases | | `continue_on_error` | Whether the reader should resync | | `max_*` | Limit overrides (0 = use defaults) | | `expect_key` | Optional: key name expected in event log | ### Coverage summary | Group | Cases | What is tested | |-------|-------|---------------| | `encoding` | 14 | UTF-8 validity, BOM placement, byte classes, truncated streams | | `limits` | 4 | Lexer token-length caps at both sides of the boundary | | `version` | 13 | `#!bovnar M.N` directive: valid, malformed, strictness (spec 1.1) | | `identifiers` | 11 | Syntax, body characters, length limits | | `strings` | 33 | Escapes, concatenation, UTF-8, limits | | `datetime` | 54 | Timestamp family: epochs, signed range, gating, ISO-8601 literals (spec 1.1) | | `numbers` | 16 | Integer, float, scientific, special numbers | | `types` | 52 | All seven type families, widths, bases, errors | | `default_synthesis` | 8 | Auto-type inference rules | | `symbols` | 6 | Bare-word values and limits | | `references` | 10 | Dotted paths, array indexing (spec 1.1), limits | | `null_values` | 5 | Null in all positions | | `structs` | 7 | Nesting, empty, unmatched braces | | `arrays` | 19 | 1D, 2D, nested, typed, null, limits, /-row size consistency | | `octet_streams` | 4 | Single/multi-chunk, sync errors | | `units` | 25 | SI/IEC prefixes, compound, inline, errors | | `special_numbers` | 5 | `nan`, `inf`, `ninf` | | `roundtrip` | 5 | Multi-assignment correctness | | `recovery` | 2 | Error-resync: valid data after error | | `comments` | 6 | Comment styles | | `whitespace` | 4 | Whitespace tolerance | | `homogeneity` | 16 | DOM-tier: array homogeneity (§7.4), struct shape, key uniqueness (§8.1) — self-test only | | **Total** | **319** | | --- ## 9. Output Format (TAP) The tool emits **TAP version 14** (Test Anything Protocol), which is consumed natively by CTest and many CI systems. Each case **group** is a TAP 14 *subtest*: a 4-space-indented child stream of the individual cases, a trailing child plan, and a leading `# Subtest:` comment, rolled up into one parent test point. The parent plan therefore counts the groups (currently 22), not the 319 cases. ``` TAP version 14 1..21 # Subtest: encoding ok 1 - [ENC-001] empty stream ok 2 - [ENC-002] UTF-8 BOM at byte 0 not ok 3 - [ENC-003] UTF-8 BOM after first comment --- message: expected error invalid_byte_order_mark but got none ... 1..9 not ok 1 - encoding # Subtest: version ok 1 - [VER-001] directive declaring the current spec version ... 1..13 ok 2 - version ``` A group's parent point is `not ok` if any of its cases failed. Exit code is **0** when all tests pass, **1** when any test fails. (`--filter ` restricts the run to a single group; the parent plan is then `1..1`.) --- ## 10. Extending the Corpus To add a new conformance test, add an entry to the `g_cases[]` array in `tests/bvnr_conformance.c` using one of the provided macros: ```c /* Valid input — no expected error */ VALID("GRP-NNN", "group_name", "description of the test", ".input = value;"), /* Valid input — verify a specific key and value appear */ VALID_KEY("GRP-NNN", "group_name", "description", ".key = value;", "key", "value"), /* Error — no special limits */ ERROR_CASE("GRP-NNN", "group_name", "description", ".bad = 999;", error_value_out_of_range), /* Error — with continue_on_error (resync mode) */ ERROR_CONT("GRP-NNN", "group_name", "description", ".a = 1; .bad = 999; .b = 2;", error_value_out_of_range), /* Error — with custom limits (max_id, max_str, max_num, max_struct_nesting, max_array_nesting, max_array_items) */ ERROR_LIM("GRP-NNN", "group_name", "description", ".long_name = 1;", error_identifier_too_long, 4 /*max_id*/, 0, 0, 0, 0, 0), /* Materialised-document (DOM) tier — validated via bvn_dom_parse instead of the streaming reader; runs in self-test mode only (skipped under --iut). Use for the spec-1.0 homogeneity / struct-shape / duplicate-key rules. */ DOM_VALID("GRP-NNN", "group_name", "description", ".a = [1, 2.5, 3];"), DOM_ERROR("GRP-NNN", "group_name", "description", ".a = [1, \"two\"];", error_array_element_type_mismatch), ``` After editing, rebuild: ```sh cmake --build build --target bvnr_conformance ``` --- ## 11. CMake Integration Two CTest tests are registered: | CTest name | What it runs | |------------|-------------| | `bvnr_conformance_self` | Conformance suite against the reference implementation | | `bvnr_conformance_iut_self` | Conformance suite using the IUT adapter as the external binary | Both are in the `conformance` label group and can be run with: ```sh ctest -L conformance --output-on-failure ``` The `bvnr_conformance_iut_self` test validates the IUT adapter itself: it must produce bit-for-bit identical output to the internal reference path for every valid test case. To run all tests including conformance: ```sh ctest --output-on-failure ``` # Bovnar (BVNR) — Units & Currencies Reference > Spec version 1.1 · 163 physical units · 166 fiat currencies · 50 cryptocurrencies --- ## Contents 1. [SI Prefixes](#1-si-prefixes) 2. [IEC Binary Prefixes](#2-iec-binary-prefixes) 3. [Prefix Validity Rules](#3-prefix-validity-rules) 4. [Physical Units](#4-physical-units) - 4.1 [SI Base Units](#41-si-base-units) - 4.2 [Named SI-Derived Units](#42-named-si-derived-units) - 4.3 [Non-SI Units Accepted with SI](#43-non-si-units-accepted-with-si) - 4.4 [Imperial & US Customary — Length](#44-imperial--us-customary--length) - 4.5 [Imperial & US Customary — Mass](#45-imperial--us-customary--mass) - 4.6 [Temperature](#46-temperature) - 4.7 [Pressure](#47-pressure) - 4.8 [Energy](#48-energy) - 4.9 [Power](#49-power) - 4.10 [Force](#410-force) - 4.11 [Speed & Rotation](#411-speed--rotation) - 4.12 [Acceleration](#412-acceleration) - 4.13 [Volume — US Liquid](#413-volume--us-liquid) - 4.14 [Volume — UK Imperial](#414-volume--uk-imperial) - 4.15 [Volume — US Apothecary & Dry](#415-volume--us-apothecary--dry) - 4.16 [Area](#416-area) - 4.17 [Angle](#417-angle) - 4.18 [Digital](#418-digital) - 4.19 [CGS Units](#419-cgs-units) - 4.20 [Radiation](#420-radiation) - 4.21 [Logarithmic](#421-logarithmic) - 4.22 [Electrical Power](#422-electrical-power) - 4.23 [Textile Linear Density](#423-textile-linear-density) - 4.24 [Old German Units](#424-old-german-units) - 4.25 [Additional Physical Units](#425-additional-physical-units-361367) - 4.26 [Ratio and Proportion](#426-ratio-and-proportion-372377) 5. [Currencies](#5-currencies) - 5.1 [The Mandatory Currency Sigil](#51-the-mandatory-currency-sigil) - 5.2 [ISO 4217 Fiat Currencies](#52-iso-4217-fiat-currencies) - 5.3 [Cryptocurrencies](#53-cryptocurrencies) - 5.4 [Currency Prefix Rules](#54-currency-prefix-rules) 6. [Symbol Disambiguation](#6-symbol-disambiguation) --- ## 1. SI Prefixes Written as `prefix~base` (mandatory `~` separator). Example: `k~m` = kilometre. | Name | Symbol | Factor | Enum (`si_prefix_id_t`) | |--------|--------|--------|--------------------------| | quetta | `Q` | 10³⁰ | `si_quetta` | | ronna | `R` | 10²⁷ | `si_ronna` | | yotta | `Y` | 10²⁴ | `si_yotta` | | zetta | `Z` | 10²¹ | `si_zetta` | | exa | `E` | 10¹⁸ | `si_exa` | | peta | `P` | 10¹⁵ | `si_peta` | | tera | `T` | 10¹² | `si_tera` | | giga | `G` | 10⁹ | `si_giga` | | mega | `M` | 10⁶ | `si_mega` | | kilo | `k` | 10³ | `si_kilo` | | hecto | `h` | 10² | `si_hecto` | | deca | `da` | 10¹ | `si_deca` | | *(none)* | — | 10⁰ | `si_none` | | deci | `d` | 10⁻¹ | `si_deci` | | centi | `c` | 10⁻² | `si_centi` | | milli | `m` | 10⁻³ | `si_milli` | | micro | `µ` *(or `u`)* | 10⁻⁶ | `si_micro` | | nano | `n` | 10⁻⁹ | `si_nano` | | pico | `p` | 10⁻¹² | `si_pico` | | femto | `f` | 10⁻¹⁵ | `si_femto` | | atto | `a` | 10⁻¹⁸ | `si_atto` | | zepto | `z` | 10⁻²¹ | `si_zepto` | | yocto | `y` | 10⁻²⁴ | `si_yocto` | | ronto | `r` | 10⁻²⁷ | `si_ronto` | | quecto | `q` | 10⁻³⁰ | `si_quecto` | > `µ` is U+00B5 MICRO SIGN (UTF-8 `0xC2 0xB5`). U+03BC (Greek small letter mu) is also accepted on input; the canonical output is always U+00B5. ASCII `u` is accepted as an input-only alias for `µ` (e.g. `u~m` = `µ~m`); the canonical output is always `µ`. > `da` is a two-character prefix: `da~m` = decametre. **Prefix–base ambiguities** — resolved by the mandatory `~`: | Bare token | Is a base unit | With `~` becomes prefix | |------------|----------------|--------------------------| | `m` | meter (`bu_meter`) | milli | | `d` | day (`bu_day`) | deci | | `h` | hour (`bu_hour`) | hecto | | `T` | tesla (`bu_tesla`) | tera | | `G` | gauss (`bu_gauss`) | giga | | `P` | poise (`bu_poise`) | peta | | `R` | röntgen (`bu_roentgen`) | ronna | | `f` | farad (`bu_farad`) | femto | | `u` | dalton (`bu_dalton`) | micro (ASCII alias for `µ`) | | `S` | siemens (`bu_siemens`) | *(not a prefix — `S` has no prefix role)* | Examples: bare `m` = metre; `m~s` = millisecond. Bare `d` = day; `d~s` = decisecond. --- ## 2. IEC Binary Prefixes Used **only** on `b` (bit) and `B` (byte). Written as `prefix~base`: `Ki~B` = kibibyte. | Name | Symbol | Factor | Enum (`iec_prefix_id_t`) | |-------|--------|----------|--------------------------| | kibi | `Ki` | 2¹⁰ | `iec_kibi` | | mebi | `Mi` | 2²⁰ | `iec_mebi` | | gibi | `Gi` | 2³⁰ | `iec_gibi` | | tebi | `Ti` | 2⁴⁰ | `iec_tebi` | | pebi | `Pi` | 2⁵⁰ | `iec_pebi` | | exbi | `Ei` | 2⁶⁰ | `iec_exbi` | | zebi | `Zi` | 2⁷⁰ | `iec_zebi` | | yobi | `Yi` | 2⁸⁰ | `iec_yobi` | | robi | `Ri` | 2⁹⁰ | `iec_robi` | | quebi | `Qi` | 2¹⁰⁰ | `iec_quebi` | --- ## 3. Prefix Validity Rules | Unit category | SI prefixes | IEC prefixes | |---------------|-------------|--------------| | All physical units (default) | All 24 allowed | Forbidden | | `b` (bit) and `B` (byte) | Only ≥ kilo (`k`, `M`, `G`, …, `Q`) | All 10 allowed | | `b` and `B` with sub-kilo SI (`d`, `c`, `m`, `µ`, `n`, `p`, `f`, `a`, `z`, `y`, `r`, `q`, `da`, `h`) | **Forbidden** | — | | Currency codes | All 24 allowed | **Forbidden** | | Old German units (`bu_pfund` … `bu_scheffel`) | **None** (`si_none` only) | **Forbidden** | --- ## 4. Physical Units > **Symbol** = canonical serialized form (produced on output; accepted on input). > **Long forms** = accepted on input only; never produced on output. > **Factor** = conversion factor to SI base units unless noted. ### 4.1 SI Base Units | Symbol | Long forms | Name | Enum | |--------|-----------|------|------| | `s` | `sec`, `second`, `seconds` | second | `bu_second` | | `m` | `meter`, `metre`, `meters`, `metres` | metre | `bu_meter` | | `g` | `gram`, `grams` | gram | `bu_gram` | | `A` | `amp`, `amps`, `ampere`, `amperes` | ampere | `bu_ampere` | | `K` | `kelvin`, `kelvins` | kelvin | `bu_kelvin` | | `mol` | `mole`, `moles` | mole | `bu_mol` | | `cd` | `candela`, `candelas` | candela | `bu_candela` | > `g` (gram) is the base symbol; `k~g` = kilogram. ### 4.2 Named SI-Derived Units | Symbol | Long forms | Name | Enum | SI definition | |--------|-----------|------|------|---------------| | `Hz` | `hertz` | hertz | `bu_hertz` | s⁻¹ | | `N` | `newton`, `newtons` | newton | `bu_newton` | kg·m·s⁻² | | `Pa` | `pascal`, `pascals` | pascal | `bu_pascal` | kg·m⁻¹·s⁻² | | `J` | `joule`, `joules` | joule | `bu_joule` | kg·m²·s⁻² | | `W` | `watt`, `watts` | watt | `bu_watt` | kg·m²·s⁻³ | | `V` | `volt`, `volts` | volt | `bu_volt` | kg·m²·A⁻¹·s⁻³ | | `Ω` | `ohm`, `ohms`, `Ohm` | ohm | `bu_ohm` | kg·m²·A⁻²·s⁻³ — U+2126 OHM SIGN; U+03A9 (Greek capital omega) also accepted on input, canonical output is always U+2126 | | `F` | `farad`, `farads` | farad | `bu_farad` | kg⁻¹·m⁻²·A²·s⁴ | | `C` | `coulomb`, `coulombs` | coulomb | `bu_coulomb` | A·s | | `S` | `siemens` | siemens | `bu_siemens` | kg⁻¹·m⁻²·A²·s³ | | `Wb` | `weber`, `webers` | weber | `bu_weber` | kg·m²·A⁻¹·s⁻² | | `T` | `tesla`, `teslas` | tesla | `bu_tesla` | kg·A⁻¹·s⁻² | | `H` | `henry`, `henrys`, `henries` | henry | `bu_henry` | kg·m²·A⁻²·s⁻² | | `lm` | `lumen`, `lumens` | lumen | `bu_lumen` | cd·sr | | `lx` | `lux` | lux | `bu_lux` | cd·sr·m⁻² | | `Bq` | `becquerel`, `becquerels` | becquerel | `bu_becquerel` | s⁻¹ | | `Gy` | `gray`, `grays` | gray | `bu_gray` | m²·s⁻² | | `Sv` | `sievert`, `sieverts` | sievert | `bu_sievert` | m²·s⁻² | | `kat` | `katal`, `katals` | katal | `bu_katal` | mol·s⁻¹ | | `rad` | `radian`, `radians` | radian | `bu_radian` | dimensionless (m/m) | | `sr` | `steradian`, `steradians` | steradian | `bu_steradian` | dimensionless (m²/m²) | ### 4.3 Non-SI Units Accepted with SI | Symbol | Long forms | Name | Enum | Factor / notes | |--------|-----------|------|------|----------------| | `L`, `l` | `liter`, `litre`, `liters`, `litres` | litre | `bu_liter` | 10⁻³ m³ | | `min` | `minute`, `minutes` | minute | `bu_minute` | 60 s | | `h` | `hour`, `hours` | hour | `bu_hour` | 3600 s | | `d` | `day`, `days` | day | `bu_day` | 86400 s | | `wk` | `week`, `weeks` | week | `bu_week` | 604 800 s | | `mo` | `month`, `months` | month (Julian) | `bu_month` | 2 629 800 s (= 365.25 d / 12) | | `fn` | `fortnight`, `fortnights` | fortnight | `bu_fortnight` | 1 209 600 s (= 14 d) | | `yr` | `year`, `years` | year (Julian) | `bu_year` | 31 557 600 s | | `°`, `deg` | `degr`, `degree`, `degrees` | degree (angle) | `bu_degree` | π/180 rad — U+00B0 | | `t` | `tonne` | tonne | `bu_tonne` | 10³ kg | | `bar` | — | bar | `bu_bar` | 10⁵ Pa | | `eV` | `electronvolt` | electronvolt | `bu_electronvolt` | 1.602176634×10⁻¹⁹ J | | `Da` | `dalton`, `amu`, `u` | dalton | `bu_dalton` | 1.66053906660×10⁻²⁷ kg | | `au` | — | astronomical unit | `bu_astronomical_unit` | 1.495978707×10¹¹ m | | `ha` | `hectare` | hectare | `bu_hectare` | 10⁴ m² | ### 4.4 Imperial & US Customary — Length | Symbol | Long forms | Name | Enum | Factor | |--------|-----------|------|------|--------| | `in` | `inch`, `inches` | inch | `bu_inch` | 0.0254 m (exact) | | `ft` | `foot`, `feet` | foot | `bu_foot` | 0.3048 m (exact) | | `yd` | `yard`, `yards` | yard | `bu_yard` | 0.9144 m (exact) | | `mi` | `mile`, `miles` | statute mile | `bu_mile` | 1609.344 m (exact) | | `nmi` | `nautical_mile`, `nautical_miles` | nautical mile | `bu_nautical_mile` | 1852 m (exact) | | `Å` (U+212B) | `angstrom`, `angstroms`, Å (U+00C5) | ångström | `bu_angstrom` | 10⁻¹⁰ m | | `ly` | `light_year`, `light_years` | light-year | `bu_light_year` | 9.4607304725808×10¹⁵ m | | `pc` | `parsec`, `parsecs` | parsec | `bu_parsec` | 3.085677581491367×10¹⁶ m | | `fur` | `furlong`, `furlongs` | furlong | `bu_furlong` | 201.168 m (exact) | | `fath` | `fathom`, `fathoms` | fathom | `bu_fathom` | 1.8288 m (exact) | | `thou` | `thou`, `mil`, `mils` | thou | `bu_thou` | 25.4×10⁻⁶ m (exact) | | `ch` | `chain`, `chains` | chain (Gunter's) | `bu_chain` | 20.1168 m (exact) | | `rd` | `rod`, `rods` | rod (pole, perch) | `bu_rod` | 5.0292 m (exact) | > `thou` and `mil` are synonyms. Canonical output is `thou`. `mil` does **not** mean milliradian; milliradians are written `m~rad`. ### 4.5 Imperial & US Customary — Mass | Symbol | Long forms | Name | Enum | Factor | |---------|-----------|------|------|--------| | `lb` | `lbs`, `pound`, `pounds` | pound (avoirdupois) | `bu_pound` | 0.45359237 kg (exact) | | `oz` | `ounce`, `ounces` | ounce (avoirdupois) | `bu_ounce` | 0.028349523125 kg (exact) | | `gr` | `grain`, `grains` | grain | `bu_grain` | 6.479891×10⁻⁵ kg (exact) | | `st` | `stone`, `stones` | stone | `bu_stone` | 6.35029318 kg (exact) | | `tn_sh` | `short_ton`, `short_tons` | short ton (US) | `bu_short_ton` | 907.18474 kg (exact) | | `tn_l` | `long_ton`, `long_tons` | long ton (UK) | `bu_long_ton` | 1016.0469088 kg (exact) | | `oz_t` | `troy_ounce`, `troy_ounces` | troy ounce | `bu_troy_ounce` | 0.0311034768 kg (exact) | | `ct` | `carat`, `carats` | metric carat | `bu_carat` | 2×10⁻⁴ kg (exact) | | `slug` | `slugs` | slug | `bu_slug` | 14.593902937 kg | | `dr` | `dram`, `drams` | dram (avoirdupois) | `bu_dram` | 1.7718451953125×10⁻³ kg (exact) | | `dwt` | `pennyweight`, `pennyweights` | pennyweight (troy) | `bu_pennyweight` | 1.55517384×10⁻³ kg (exact) | ### 4.6 Temperature | Symbol | Long forms | Name | Enum | Conversion | |--------|-----------|------|------|------------| | `°C`, `degC` | `degrC`, `degreeC`, `degreesC`, `celsius` | degree Celsius | `bu_celsius` | K = °C + 273.15 **(affine)** | | `°F`, `degF` | `degrF`, `degreeF`, `degreesF`, `fahrenheit` | degree Fahrenheit | `bu_fahrenheit` | K = (°F + 459.67) × 5/9 **(affine)** | | `°Ra`, `degRa` | `degrRa`, `degreeRa`, `degreesRa`, `rankine` | degree Rankine | `bu_rankine` | K = °Ra × 5/9 (linear) | | `°De`, `degDe` | `degrDe`, `degreeDe`, `degreesDe`, `delisle` | degree Delisle | `bu_delisle` | K = 373.15 − °De × 2/3 **(affine)** | | `°N`, `degN` | `degrN`, `degreeN`, `degreesN`, `newton_temperature` | degree Newton | `bu_newton_temp` | K = °N × 100/33 + 273.15 **(affine)** | | `°Re`, `degRe` | `degrRe`, `degreeRe`, `degreesRe`, `reaumur` | degree Réaumur | `bu_reaumur` | K = °Re × 5/4 + 273.15 **(affine)** | | `°Ro`, `degRo` | `degrRo`, `degreeRo`, `degreesRo`, `romer` | degree Rømer | `bu_romer` | K = (°Ro − 7.5) × 40/21 + 273.15 **(affine)** | > Kelvin (`K`) is in §4.1. `R` is reserved for röntgen (§4.20). `N` alone is newton (§4.2); use `°N` or `degN` for Newton temperature. ### 4.7 Pressure | Symbol | Long forms | Name | Enum | Factor | |--------|-----------|------|------|--------| | `atm` | `atmosphere`, `atmospheres` | standard atmosphere | `bu_atmosphere` | 101 325 Pa (exact) | | `at` | `atmosphere_technical` | atmosphere technical | `bu_atmosphere_technical` | 98 066.5 Pa (= 1 kgf/cm²) | | `mmHg` | — | millimetre of mercury | `bu_mmhg` | 133.322387415 Pa | | `Torr` | `torr` | torr | `bu_torr` | 101 325/760 Pa | | `psi` | — | pound-force per square inch | `bu_psi` | 6894.757293168362 Pa | | `inHg` | `inch_hg`, `inch_mercury` | inch of mercury | `bu_inch_hg` | 3386.388645 Pa | > `at ≠ atm`: 1 at = 98 066.5 Pa; 1 atm = 101 325 Pa. ### 4.8 Energy | Symbol | Long forms | Name | Enum | Factor | |---------|-----------|------|------|--------| | `cal` | `calorie`, `calories` | thermochemical calorie | `bu_calorie` | 4.184 J (exact) | | `Btu` | `btu`, `BTU` | International Table BTU | `bu_btu` | 1055.05585262 J | | `erg` | `ergs` | erg | `bu_erg` | 10⁻⁷ J (exact) | | `thm` | `therm`, `therms` | US therm | `bu_therm` | 1.05480400×10⁸ J (exact) | | `ft_lb` | `foot_pound`, `foot_pounds` | foot-pound | `bu_foot_pound` | 1.3558179483 J | > `BTU` (all-caps) is a valid alias: with no `$` sigil the token is a physical-unit lookup (the currency table is only consulted for `$`-prefixed tokens), and it matches `bu_btu`. `Btu` and `btu` are also accepted. ### 4.9 Power | Symbol | Long forms | Name | Enum | Factor | |--------|-----------|------|------|--------| | `hp` | `horsepower` | mechanical horsepower | `bu_horsepower` | 745.69987158227 W | | `PS` | `CV`, `metric_horsepower` | metric horsepower | `bu_metric_horsepower` | 735.49875 W (exact) | ### 4.10 Force | Symbol | Long forms | Name | Enum | Factor | |--------|-----------|------|------|--------| | `lbf` | `pound_force` | pound-force | `bu_pound_force` | 4.4482216152605 N | | `dyn` | `dyne`, `dynes` | dyne | `bu_dyne` | 10⁻⁵ N (exact) | | `kip` | `kips` | kip (kilopound-force) | `bu_kip` | 4448.2216152605 N | | `kgf` | `kilogram_force` | kilogram-force | `bu_kilogram_force` | 9.80665 N (exact) | ### 4.11 Speed & Rotation | Symbol | Long forms | Name | Enum | Factor | |--------|-----------|------|------|--------| | `kn` | `knot`, `knots` | knot | `bu_knot` | 1852/3600 m/s | | `rpm` | — | revolutions per minute | `bu_rpm` | 1/60 s⁻¹ | ### 4.12 Acceleration | Symbol | Long forms | Name | Enum | Factor | |--------|-----------|------|------|--------| | `gn` | `standard_gravity` | standard gravity | `bu_standard_gravity` | 9.80665 m·s⁻² (exact) | ### 4.13 Volume — US Liquid | Symbol | Long forms | Name | Enum | Factor | |---------|-----------|------|------|--------| | `gal` | `gallon`, `gallons` | US liquid gallon | `bu_gallon` | 3.785411784×10⁻³ m³ (exact) | | `qt` | `quart`, `quarts` | US liquid quart | `bu_quart` | 9.46352946×10⁻⁴ m³ | | `pt` | `pint`, `pints` | US liquid pint | `bu_pint` | 4.73176473×10⁻⁴ m³ | | `cup` | `cups` | US cup | `bu_cup` | 2.365882365×10⁻⁴ m³ | | `gi` | `gill`, `gills` | US gill | `bu_gill` | 1.18294118250×10⁻⁴ m³ | | `fl_oz` | `fluid_ounce`, `fluid_ounces` | US fluid ounce | `bu_fluid_ounce` | 2.95735295625×10⁻⁵ m³ | | `tbsp` | `tablespoon`, `tablespoons` | US tablespoon | `bu_tablespoon` | 1.47867648×10⁻⁵ m³ | | `tsp` | `teaspoon`, `teaspoons` | US teaspoon | `bu_teaspoon` | 4.92892159375×10⁻⁶ m³ | | `bbl` | `barrel`, `barrels` | petroleum barrel | `bu_barrel` | 0.158987294928 m³ | > `cup` (lowercase) = US cup; `CUP` (uppercase) = Cuban Peso. See §6. ### 4.14 Volume — UK Imperial | Symbol | Long forms | Name | Enum | Factor | |------------|-----------|------|------|--------| | `gal_uk` | `gallon_uk`, `gallons_uk` | imperial gallon | `bu_gallon_uk` | 4.54609×10⁻³ m³ (exact) | | `qt_uk` | `quart_uk`, `quarts_uk` | imperial quart | `bu_quart_uk` | 1136.5225×10⁻⁶ m³ | | `pt_uk` | `pint_uk`, `pints_uk` | imperial pint | `bu_pint_uk` | 568.26125×10⁻⁶ m³ | | `gi_uk` | `gill_uk`, `gills_uk` | imperial gill | `bu_gill_uk` | 1.420653125×10⁻⁴ m³ (exact) | | `fl_oz_uk` | `fluid_ounce_uk`, `fluid_ounces_uk` | imperial fluid ounce | `bu_fluid_ounce_uk` | 28.4130625×10⁻⁶ m³ | ### 4.15 Volume — US Apothecary & Dry | Symbol | Long forms | Name | Enum | Factor | |---------|-----------|------|------|--------| | `fl_dr` | `fluid_dram`, `fluid_drams` | US fluid dram | `bu_fluid_dram` | 3.6966911953125×10⁻⁶ m³ | | `minim` | `minims` | US minim | `bu_minim` | 6.16115199218750×10⁻⁸ m³ | | `pk` | `peck`, `pecks` | US dry peck | `bu_peck` | 8.80976754172×10⁻³ m³ | | `bsh` | `bushel`, `bushels` | US bushel | `bu_bushel` | 3.523907016688×10⁻² m³ | > `minim` not `min` (which is the minute). ### 4.16 Area | Symbol | Long forms | Name | Enum | Factor | |--------|-----------|------|------|--------| | `ac` | `acre`, `acres` | acre | `bu_acre` | 4046.8564224 m² (exact) | | `barn` | `barns` | barn | `bu_barn` | 10⁻²⁸ m² (exact) | ### 4.17 Angle | Symbol | Long forms | Name | Enum | Factor | |----------|-----------|------|------|--------| | `arcmin` | `arcminute`, `arcminutes` | arcminute | `bu_arcminute` | π/10800 rad | | `arcsec` | `arcsecond`, `arcseconds` | arcsecond | `bu_arcsecond` | π/648000 rad | | `grad` | `gradian`, `gradians`, `gon` | gradian | `bu_grad` | π/200 rad | | `rev` | `turn`, `revolution`, `revolutions`, `turns` | revolution | `bu_revolution` | 2π rad | ### 4.18 Digital | Symbol | Long forms | Name | Enum | |--------|-----------|------|------| | `b` | `bit`, `bits` | bit | `bu_bit` | | `B` | `byte`, `bytes`, `Byte`, `Bytes` | byte | `bu_byte` | ### 4.19 CGS Units | Symbol | Long forms | Name | Enum | SI equivalent | |--------|-----------|------|------|---------------| | `P` | `poise`, `poises` | poise (dynamic viscosity) | `bu_poise` | 0.1 Pa·s | | `St` | `stokes`, `stoke` | stokes (kinematic viscosity) | `bu_stokes` | 10⁻⁴ m²·s⁻¹ | | `G` | `gauss` | gauss (magnetic flux density) | `bu_gauss` | 10⁻⁴ T | | `Mx` | `maxwell`, `maxwells` | maxwell (magnetic flux) | `bu_maxwell` | 10⁻⁸ Wb | | `Oe` | `oersted`, `oersteds` | oersted (magnetic field strength) | `bu_oersted` | 1000/(4π) A/m | | `sb` | `stilb`, `stilbs` | stilb (luminance) | `bu_stilb` | 10⁴ cd/m² | | `ph` | `phot`, `phots` | phot (illuminance) | `bu_phot` | 10⁴ lx | | `Gal` | `galileo`, `galileos` | galileo (acceleration) | `bu_galileo` | 10⁻² m/s² | ### 4.20 Radiation | Symbol | Long forms | Name | Enum | SI equivalent | |--------|-----------|------|------|---------------| | `Ci` | `curie`, `curies` | curie (radioactivity) | `bu_curie` | 3.7×10¹⁰ Bq | | `R` | `roentgen`, `roentgens` | röntgen (radiation exposure) | `bu_roentgen` | 2.58×10⁻⁴ C/kg | | `rem` | `rems` | rem (dose equivalent) | `bu_rem` | 10⁻² Sv | ### 4.21 Logarithmic | Symbol | Long forms | Name | Enum | Notes | |--------|-----------|------|------|-------| | `Np` | `neper`, `nepers` | neper | `bu_neper` | dimensionless; 1 Np = 20/ln(10) dB ≈ 8.686 dB | | `dB` | `decibel`, `decibels` | decibel | `bu_decibel` | dimensionless | ### 4.22 Electrical Power | Symbol | Long forms | Name | Enum | Notes | |--------|-----------|------|------|-------| | `var` | `vars` | var (volt-ampere reactive) | `bu_var` | reactive power; same SI dim as W | | `VA` | `volt_ampere`, `volt_amperes` | volt-ampere | `bu_volt_ampere` | apparent power; same SI dim as W | > `W`, `var`, and `VA` all have SI dimension kg·m²·s⁻³. `bvn_units_compatible` returns `true` across them; use `.components[0].base` to distinguish. ### 4.23 Textile Linear Density | Symbol | Long forms | Name | Enum | Factor | |--------|-----------|------|------|--------| | `tex` | — | tex | `bu_tex` | 1×10⁻⁶ kg/m (ISO 1144) | | `den` | `denier`, `deniers` | denier | `bu_denier` | 1/9 000 000 kg/m | > 9 den = 1 tex. ### 4.24 Old German Units No Old German unit accepts any SI or IEC prefix (`bvn_prefix_unit_valid` rejects all non-`si_none` prefixes for `bu_pfund` … `bu_scheffel`). Enum values 348–360. #### Mass (metric-compatible) | Symbol | Long forms | Name | Enum | Factor | |--------|-----------|------|------|--------| | `Pfd` | `pfund`, `pfunds` | Pfund | `bu_pfund` | 0.5 kg (exact) | | `Ztr` | `zentner` | Zentner | `bu_zentner` | 50 kg (exact) | | `dz` | `doppelzentner` | Doppelzentner | `bu_doppelzentner` | 100 kg (exact) | | `lot` | `lots` | Lot | `bu_lot` | 15.625×10⁻³ kg (exact) | #### Length (historical Prussian) | Symbol | Long forms | Name | Enum | Factor | |-----------|-----------|------|------|--------| | `prln` | `prussian_line`, `linie` | Prussian line | `bu_prussian_line` | 2.17953×10⁻³ m | | `prz` | `prussian_zoll`, `zoll` | Prussian Zoll | `bu_prussian_zoll` | 2.61544×10⁻² m | | `prf` | `prussian_fuss`, `preussischer_fuss` | Prussian Fuß | `bu_prussian_fuss` | 3.13853×10⁻¹ m | | `elle` | `prussian_elle`, `preussische_elle` | Prussian Elle | `bu_prussian_elle` | 6.67160×10⁻¹ m | | `rute` | `prussian_rute`, `preussische_rute` | Prussian Rute | `bu_prussian_rute` | 3.76624 m | | `klafter` | `prussian_klafter` | Klafter | `bu_klafter` | 1.88312 m | | `dt_mi` | `deutsche_meile`, `german_mile` | Geographische Meile | `bu_german_mile` | 7420.44 m | #### Area (historical Prussian) | Symbol | Long forms | Name | Enum | Factor | |----------|-----------|------|------|--------| | `morgen` | `prussian_morgen` | Morgen (Prussian) | `bu_morgen` | 2553.22 m² | #### Volume (historical Prussian) | Symbol | Long forms | Name | Enum | Factor | |----------|-----------|------|------|--------| | `schffl` | `scheffel`, `prussian_scheffel` | Scheffel (Prussian) | `bu_scheffel` | 54.961×10⁻³ m³ | ### 4.25 Additional Physical Units (361–367) #### Length | Symbol | Long forms | Name | Enum | Factor | |--------|-----------|------|------|--------| | `ftUS` | `survey_foot` | US survey foot | `bu_survey_foot` | 1200/3937 m ≈ 0.30480061 m | | `lea` | `league`, `leagues` | League (US statute, 3 mi) | `bu_league` | 4828.032 m | | `cbl` | `cable`, `cables` | Cable (international, ¹⁄₁₀ nmi) | `bu_cable` | 185.2 m | | `hand` | `hands` | Hand (4 in) | `bu_hand` | 0.1016 m | #### Mass | Symbol | Long forms | Name | Enum | Factor | |--------|-----------|------|------|--------| | `qntl` | `quintal`, `quintals` | Metric quintal | `bu_quintal` | 100 kg | | `sc` | `scruple`, `scruples` | Apothecary scruple (20 gr) | `bu_scruple` | 1.2959782×10⁻³ kg | #### Signal Rate | Symbol | Long forms | Name | Enum | SI dimension | |--------|-----------|------|------|--------------| | `Bd` | `baud`, `bauds` | Baud (symbol/s) | `bu_baud` | s⁻¹ | SI prefixes are accepted on all units in this section. IEC prefixes are rejected for all non-digital units. ### 4.26 Ratio and Proportion (372–377) Dimensionless scaling factors: `5 %` ≡ `0.05`, `250 ppm` ≡ `0.00025`. These do **not** accept SI or IEC prefixes. | Symbol | Long forms | Name | Enum | Factor | |--------|-----------|------|------|--------| | `%` | `percent` | per cent | `bu_percent` | 10⁻² | | `‰` | `per_mille` | per mille | `bu_per_mille` | 10⁻³ | | `‱` | `per_myriad` | per myriad | `bu_per_myriad` | 10⁻⁴ | | `pcm` | `per_cent_mille` | per cent mille | `bu_per_cent_mille` | 10⁻⁵ | | `ppm` | — | parts per million | `bu_ppm` | 10⁻⁶ | | `ppb` | — | parts per billion | `bu_ppb` | 10⁻⁹ | --- ## 5. Currencies ### 5.1 The Mandatory Currency Sigil As of spec 1.0 a currency code carries a **mandatory `$` sigil**: write `$USD`, `$BTC`, `k~$EUR`, `$USD/oz_t`. The codes listed in the tables below are the bare ISO 4217 / crypto identifiers — prefix each with `$` when you use it in a document. A bare code (no `$`) is **not** a currency; it is matched against the physical-unit table and raises `error_unit_illegal` if it is not a unit. This removes every currency/unit namespace collision (e.g. `$CUP` the Cuban Peso vs `cup` the unit). ### 5.2 ISO 4217 Fiat Currencies 166 codes: 164 occupying `value_base_unit_t` slots **134 … 297** (`BVN_CURRENCY_FIAT_FIRST … BVN_CURRENCY_FIAT_LAST`), plus `ZWG` and `XCG` appended past the unit block at slots **378 … 379** (`BVN_CURRENCY_EXT_FIRST … BVN_CURRENCY_EXT_LAST`) so that adding them shifted no existing enum value. The 134–297 codes have no named `bu_*` enumerators — they are resolved from the `$`-sigil code by `bvn_parse_currency_str` and carried as the numeric `base` value; query them with `bvn_unit_is_fiat` / `bvn_currency_info`. > **Min** = minor unit exponent N: 1 major unit = 10^N minor units (e.g. 1 USD = 100 cents, N=2). > Minor units are **bold** when they differ from 2. `numeric_code` is the ISO 4217 numeric identifier. | Code | Num | Min | Name | |------|----:|----:|------| | `AED` | 784 | 2 | UAE Dirham | | `AFN` | 971 | 2 | Afghan Afghani | | `ALL` | 8 | 2 | Albanian Lek | | `AMD` | 51 | 2 | Armenian Dram | | `ANG` | 532 | 2 | Netherlands Antillean Guilder | | `AOA` | 973 | 2 | Angolan Kwanza | | `ARS` | 32 | 2 | Argentine Peso | | `AUD` | 36 | 2 | Australian Dollar | | `AWG` | 533 | 2 | Aruban Florin | | `AZN` | 944 | 2 | Azerbaijani Manat | | `BAM` | 977 | 2 | Bosnia-Herzegovina Convertible Mark | | `BBD` | 52 | 2 | Barbados Dollar | | `BDT` | 50 | 2 | Bangladeshi Taka | | `BGN` | 975 | 2 | Bulgarian Lev *(historical; retired 2026-01-01)* | | `BHD` | 48 | **3** | Bahraini Dinar | | `BIF` | 108 | **0** | Burundian Franc | | `BMD` | 60 | 2 | Bermudian Dollar | | `BND` | 96 | 2 | Brunei Dollar | | `BOB` | 68 | 2 | Boliviano | | `BRL` | 986 | 2 | Brazilian Real | | `BSD` | 44 | 2 | Bahamian Dollar | | `BTN` | 64 | 2 | Bhutanese Ngultrum | | `BWP` | 72 | 2 | Botswana Pula | | `BYN` | 933 | 2 | Belarusian Ruble | | `BZD` | 84 | 2 | Belize Dollar | | `CAD` | 124 | 2 | Canadian Dollar | | `CDF` | 976 | 2 | Congolese Franc | | `CHF` | 756 | 2 | Swiss Franc | | `CLF` | 990 | **4** | Unidad de Fomento | | `CLP` | 152 | **0** | Chilean Peso | | `CNY` | 156 | 2 | Chinese Yuan | | `COP` | 170 | 2 | Colombian Peso | | `CRC` | 188 | 2 | Costa Rican Colon | | `CUP` | 192 | 2 | Cuban Peso | | `CVE` | 132 | 2 | Cape Verdean Escudo | | `CZK` | 203 | 2 | Czech Koruna | | `DJF` | 262 | **0** | Djiboutian Franc | | `DKK` | 208 | 2 | Danish Krone | | `DOP` | 214 | 2 | Dominican Peso | | `DZD` | 12 | 2 | Algerian Dinar | | `EGP` | 818 | 2 | Egyptian Pound | | `ERN` | 232 | 2 | Eritrean Nakfa | | `ETB` | 230 | 2 | Ethiopian Birr | | `EUR` | 978 | 2 | Euro | | `FJD` | 242 | 2 | Fijian Dollar | | `FKP` | 238 | 2 | Falkland Islands Pound | | `GBP` | 826 | 2 | Pound Sterling | | `GEL` | 981 | 2 | Georgian Lari | | `GHS` | 936 | 2 | Ghanaian Cedi | | `GIP` | 292 | 2 | Gibraltar Pound | | `GMD` | 270 | 2 | Gambian Dalasi | | `GNF` | 324 | **0** | Guinean Franc | | `GTQ` | 320 | 2 | Guatemalan Quetzal | | `GYD` | 328 | 2 | Guyanese Dollar | | `HKD` | 344 | 2 | Hong Kong Dollar | | `HNL` | 340 | 2 | Honduran Lempira | | `HRK` | 191 | 2 | Croatian Kuna *(historical; retired 2023-01-01)* | | `HTG` | 332 | 2 | Haitian Gourde | | `HUF` | 348 | 2 | Hungarian Forint | | `IDR` | 360 | 2 | Indonesian Rupiah | | `ILS` | 376 | 2 | Israeli New Shekel | | `INR` | 356 | 2 | Indian Rupee | | `IQD` | 368 | **3** | Iraqi Dinar | | `IRR` | 364 | 2 | Iranian Rial | | `ISK` | 352 | **0** | Icelandic Krona | | `JMD` | 388 | 2 | Jamaican Dollar | | `JOD` | 400 | **3** | Jordanian Dinar | | `JPY` | 392 | **0** | Japanese Yen | | `KES` | 404 | 2 | Kenyan Shilling | | `KGS` | 417 | 2 | Kyrgyzstani Som | | `KHR` | 116 | 2 | Cambodian Riel | | `KMF` | 174 | **0** | Comorian Franc | | `KPW` | 408 | 2 | North Korean Won | | `KRW` | 410 | **0** | South Korean Won | | `KWD` | 414 | **3** | Kuwaiti Dinar | | `KYD` | 136 | 2 | Cayman Islands Dollar | | `KZT` | 398 | 2 | Kazakhstani Tenge | | `LAK` | 418 | 2 | Laotian Kip | | `LBP` | 422 | 2 | Lebanese Pound | | `LKR` | 144 | 2 | Sri Lankan Rupee | | `LRD` | 430 | 2 | Liberian Dollar | | `LSL` | 426 | 2 | Lesotho Loti | | `LYD` | 434 | **3** | Libyan Dinar | | `MAD` | 504 | 2 | Moroccan Dirham | | `MDL` | 498 | 2 | Moldovan Leu | | `MGA` | 969 | 2 | Malagasy Ariary | | `MKD` | 807 | 2 | Macedonian Denar | | `MMK` | 104 | 2 | Myanmar Kyat | | `MNT` | 496 | 2 | Mongolian Togrog | | `MOP` | 446 | 2 | Macanese Pataca | | `MRU` | 929 | 2 | Mauritanian Ouguiya | | `MUR` | 480 | 2 | Mauritian Rupee | | `MVR` | 462 | 2 | Maldivian Rufiyaa | | `MWK` | 454 | 2 | Malawian Kwacha | | `MXN` | 484 | 2 | Mexican Peso | | `MYR` | 458 | 2 | Malaysian Ringgit | | `MZN` | 943 | 2 | Mozambican Metical | | `NAD` | 516 | 2 | Namibian Dollar | | `NGN` | 566 | 2 | Nigerian Naira | | `NIO` | 558 | 2 | Nicaraguan Cordoba | | `NOK` | 578 | 2 | Norwegian Krone | | `NPR` | 524 | 2 | Nepalese Rupee | | `NZD` | 554 | 2 | New Zealand Dollar | | `OMR` | 512 | **3** | Omani Rial | | `PAB` | 590 | 2 | Panamanian Balboa | | `PEN` | 604 | 2 | Peruvian Sol | | `PGK` | 598 | 2 | Papua New Guinean Kina | | `PHP` | 608 | 2 | Philippine Peso | | `PKR` | 586 | 2 | Pakistani Rupee | | `PLN` | 985 | 2 | Polish Zloty | | `PYG` | 600 | **0** | Paraguayan Guarani | | `QAR` | 634 | 2 | Qatari Riyal | | `RON` | 946 | 2 | Romanian Leu | | `RSD` | 941 | 2 | Serbian Dinar | | `RUB` | 643 | 2 | Russian Ruble | | `RWF` | 646 | **0** | Rwandan Franc | | `SAR` | 682 | 2 | Saudi Riyal | | `SBD` | 90 | 2 | Solomon Islands Dollar | | `SCR` | 690 | 2 | Seychellois Rupee | | `SDG` | 938 | 2 | Sudanese Pound | | `SEK` | 752 | 2 | Swedish Krona | | `SGD` | 702 | 2 | Singapore Dollar | | `SHP` | 654 | 2 | Saint Helena Pound | | `SLE` | 925 | 2 | Sierra Leonean Leone | | `SLL` | 694 | 2 | Sierra Leonean Leone (old) *(historical; replaced by SLE 2022)* | | `SOS` | 706 | 2 | Somali Shilling | | `SSP` | 728 | 2 | South Sudanese Pound | | `SRD` | 968 | 2 | Surinamese Dollar | | `STN` | 930 | 2 | Sao Tome and Principe Dobra | | `SVC` | 222 | 2 | Salvadoran Colon | | `SYP` | 760 | 2 | Syrian Pound | | `SZL` | 748 | 2 | Swazi Lilangeni | | `THB` | 764 | 2 | Thai Baht | | `TJS` | 972 | 2 | Tajikistani Somoni | | `TMT` | 934 | 2 | Turkmenistan Manat | | `TND` | 788 | **3** | Tunisian Dinar | | `TOP` | 776 | 2 | Tongan Pa'anga | | `TRY` | 949 | 2 | Turkish Lira | | `TTD` | 780 | 2 | Trinidad and Tobago Dollar | | `TWD` | 901 | 2 | New Taiwan Dollar | | `TZS` | 834 | 2 | Tanzanian Shilling | | `UAH` | 980 | 2 | Ukrainian Hryvnia | | `UGX` | 800 | **0** | Ugandan Shilling | | `USD` | 840 | 2 | US Dollar | | `UYU` | 858 | 2 | Uruguayan Peso | | `UZS` | 860 | 2 | Uzbekistani Som | | `VES` | 928 | 2 | Venezuelan Bolivar Soberano | | `VND` | 704 | **0** | Vietnamese Dong | | `VUV` | 548 | **0** | Vanuatu Vatu | | `WST` | 882 | 2 | Samoan Tala | | `XAF` | 950 | **0** | CFA Franc BEAC | | `XAG` | 961 | **0** | Silver | | `XAU` | 959 | **0** | Gold | | `XCD` | 951 | 2 | East Caribbean Dollar | | `XCG` | 532 | 2 | Caribbean Guilder | | `XDR` | 960 | **0** | Special Drawing Rights | | `XOF` | 952 | **0** | CFA Franc BCEAO | | `XPD` | 964 | **0** | Palladium | | `XPF` | 953 | **0** | CFP Franc | | `XPT` | 962 | **0** | Platinum | | `XTS` | 963 | **0** | Test (ISO 4217 reserved) | | `YER` | 886 | 2 | Yemeni Rial | | `ZAR` | 710 | 2 | South African Rand | | `ZMW` | 967 | 2 | Zambian Kwacha | | `ZWG` | 924 | 2 | Zimbabwe Gold | | `ZWL` | 932 | 2 | Zimbabwean Dollar *(historical; superseded by ZWG 2024)* | > `CLF` is the only currency with 4 minor units. > `XTS` is the ISO 4217 testing code; present in the table but should not appear in production data. ### 5.3 Cryptocurrencies 50 codes occupying `value_base_unit_t` slots **298 … 347** (`BVN_CURRENCY_CRYPTO_FIRST … BVN_CURRENCY_CRYPTO_LAST`). Like the fiat codes they have no named `bu_*` enumerators — resolved by `bvn_parse_currency_str`, queried with `bvn_unit_is_crypto` / `bvn_currency_info`. `numeric_code = 0` for all. > **Min** = `minor_unit` = on-chain decimal places. E.g. `` stores satoshis; divide by 10⁸ to obtain BTC. | Code | Min | Subunit | Name | |--------|----:|---------|------| | `BTC` | 8 | satoshi | Bitcoin | | `ETH` | 18 | wei | Ethereum | | `SOL` | 9 | lamport | Solana | | `XRP` | 6 | drop | XRP | | `BNB` | 18 | — | BNB | | `ADA` | 6 | lovelace | Cardano | | `LTC` | 8 | — | Litecoin | | `DOT` | 10 | planck | Polkadot | | `XMR` | 12 | piconero | Monero | | `ETC` | 18 | — | Ethereum Classic | | `BCH` | 8 | — | Bitcoin Cash | | `XLM` | 7 | stroop | Stellar | | `FIL` | 18 | — | Filecoin | | `ICP` | 8 | — | Internet Computer | | `TRX` | 6 | — | TRON | | `EOS` | 4 | — | EOS | | `VET` | 18 | — | VeChain | | `NEO` | 0 | — | Neo | | `ZEC` | 8 | — | Zcash | | `UNI` | 18 | — | Uniswap | | `ARB` | 18 | — | Arbitrum | | `SUI` | 9 | — | Sui | | `TON` | 9 | — | Toncoin | | `INJ` | 18 | — | Injective | | `SEI` | 6 | — | Sei | | `APT` | 8 | — | Aptos | | `TAO` | 9 | — | Bittensor | | `WIF` | 6 | — | dogwifhat | | `DOGE` | 8 | koinu | Dogecoin | | `LINK` | 18 | — | Chainlink | | `USDT` | 6 | — | Tether | | `USDC` | 6 | — | USD Coin | | `AVAX` | 18 | — | Avalanche | | `ATOM` | 6 | — | Cosmos | | `POL` | 18 | — | Polygon | | `NEAR` | 24 | — | NEAR Protocol | | `ALGO` | 6 | — | Algorand | | `HBAR` | 8 | — | Hedera | | `AAVE` | 18 | — | Aave | | `MKR` | 18 | — | Maker | | `DAI` | 18 | — | Dai | | `STX` | 6 | — | Stacks | | `GRT` | 18 | — | The Graph | | `LDO` | 18 | — | Lido DAO | | `BONK` | 5 | — | Bonk | | `PEPE` | 18 | — | Pepe | | `SHIB` | 18 | — | Shiba Inu | | `JUP` | 6 | — | Jupiter | | `PYTH` | 6 | — | Pyth Network | | `RUNE` | 8 | — | THORChain | ### 5.4 Currency Prefix Rules All 24 SI prefixes are allowed on all currency units. IEC binary prefixes are forbidden on all currencies (`error_unit_illegal`). | Example | Meaning | |---------|---------| | `k~USD` | thousands of USD (×10³) | | `M~EUR` | millions of EUR (×10⁶) | | `G~ETH` | giga-ETH = Gwei scale (×10⁹) | | `m~USD` | milli-USD = one tenth of a cent (×10⁻³) | --- ## 6. Symbol Disambiguation As of spec 1.0 a currency is written **only** with the `$` sigil (§5.1), so the bare form is always a physical-unit lookup and the namespaces never collide: | Token | Bare form (no `$`) | `$`-sigil form | |-------|--------------------|----------------| | `cup` / `CUP` | `cup` → US cup (`bu_cup`); `CUP` → `error_unit_illegal` | `$CUP` → Cuban Peso (ISO 4217:192) | | `BTU` | `BTU` → BTU (`bu_btu`); `Btu`, `btu` also accepted | *(not ISO 4217)* | | `SOL` | `SOL` → `error_unit_illegal` | `$SOL` → Solana (crypto) | | `BAR` | `BAR` → `error_unit_illegal`; use lowercase `bar` | *(not ISO 4217)* | | `ERG` | `ERG` → `error_unit_illegal`; use lowercase `erg` | *(not ISO 4217)* | | `CAD`, `XAU` | `error_unit_illegal` (no physical unit) | `$CAD` → Canadian Dollar; `$XAU` → Gold (X-code) | No bare token is simultaneously a valid physical unit and a currency: currencies live entirely under `$`, physical units entirely without it. --- *Physical unit enum range: 1–133, 348–367, 368–371, and 372–377 (163 total) · Fiat: 134–297 and 378–379 (166) · Crypto: 298–347 (50)* *`BVN_VALUE_BASE_UNIT_COUNT` = 380 (`bu_xcg + 1`)* # Bovnar Streaming, Framing & Multiplexing This document describes four capabilities layered **on top of** the Bovnar lexer/validator event API, *beside* the DOM. None of them change the frozen 1.0 grammar or wire format (see [doc/1_bovnar_spec.md](1_bovnar_spec.md)); they are applications built on the SAX-style event pipe and the octet-stream value form. | # | Capability | Where it lives | Header | |---|------------|----------------|--------| | 1 | Endless streaming | core policy knob | `bovnar.h` | | 2 | Multi-document framing | transport beside the lexer | `bovnar_stream.h` | | 3 | Octet multiplexing | convention inside one octet stream | `bovnar_stream.h` | | 4 | Document-in-document | a document embedded as octet payload | `bovnar_stream.h` | The mental model: the **reader** turns bytes into a `bvnr_event_t` stream and the **writer** turns events back into bytes. The DOM is one consumer of that pipe; everything here is another. Octet streams (spec §9) are the binary substrate — opaque, chunked, UTF-8-validation-suspended byte regions — and three of the four capabilities are sub-protocols carried in or around them. --- ## 1. Endless streaming (2⁶⁴−1) Every byte/offset/line/column counter in the core is 64-bit, so a single never-ending document — or a single octet stream of unbounded length — is limited only by one policy knob: `max_file_size` on `bvnr_read_flags_t` / `bvnr_write_flags_t`. ```c #define BVNR_FILESIZE_UNLIMITED 0 /* in bovnar.h */ ``` Semantics — **`0` is unlimited and the default**: | `max_file_size` value | Effect | |-----------------------|--------| | `0` (= `BVNR_FILESIZE_UNLIMITED`, the default) | **No cap** — endless, no accumulated size limit | | any positive N | Cap at N bytes (`error_file_too_long` beyond) | ```c bvnr_read_flags_t fl = { .on_verified = my_cb, .max_file_size = BVNR_FILESIZE_UNLIMITED, /* endless */ }; ``` A long-lived document is processed incrementally: process each value in the `on_verified`/`on_unverified` callback and retain nothing, and memory stays flat regardless of stream length. (The DOM builder, by contrast, retains a tree and is therefore not the tool for an endless stream.) --- ## 2. Multi-document record framing The grammar has no in-band document separator: a reader ends one document at EOF. To carry a **sequence of independent documents** over one byte stream (file, socket, pipe) without closing it, wrap each in a length-prefixed frame. The framing lives beside the lexer; each payload is a complete, standalone document parsed by an ordinary reader. ### Wire format ``` frame = "BVF1" ; 4-byte magic payload_len ; uint64, little-endian payload[payload_len] stream = { frame } ; terminated by EOF on a frame boundary ``` A clean EOF *before any header byte* ends the sequence successfully; an EOF part-way through a header or payload is a truncation error. ### API ```c /* Producer */ bool bvnr_frame_write(bvnr_sink_t* sink, const void* doc, uint64_t doc_len); /* Consumer */ typedef struct { bvnr_read_flags_t* flags; /* per-document reader callbacks/limits */ void* userdata; bool (*on_document)(void* ud, uint64_t index, bool ok, error_code_t err); bool continue_past_failed; /* keep going after a failed document */ uint64_t max_document_size; /* 0 => BVNR_DOC_DEFAULT_MAX (256 MiB) */ } bvnr_doc_stream_opts_t; bool bvnr_doc_stream_read(bvnr_source_t* src, const bvnr_doc_stream_opts_t* opts, uint64_t* out_count); ``` `bvnr_doc_stream_read` reads frames until EOF, parsing each payload with a fresh reader (reusing one handle internally) and invoking `on_document` after each. It returns false on a framing/IO error, or on the first per-document parse failure unless `continue_past_failed` is set. Each frame payload is buffered in RAM before parsing (the buffer grows with the bytes that actually arrive, so a crafted oversized length cannot force a large up-front allocation), bounded by `max_document_size`. Unlike `max_file_size`, this framing guard stays finite when `0` (it then selects `BVNR_DOC_DEFAULT_MAX`, 256 MiB); set an explicit huge cap (e.g. `UINT64_MAX`) to lift it. Two **orthogonal** error controls — because each document occupies its own length-delimited frame, the two are independent: | Control | Scope | Effect | |---------|-------|--------| | `flags->continue_on_error` | *within* a document | resync/recover from errors inside one document | | `continue_past_failed` | *across* documents | report a failed document and advance to the next frame instead of aborting | So you can parse each document **strictly** (`continue_on_error = false`) yet **resiliently** process every frame (`continue_past_failed = true`), reporting the bad ones via `on_document` — the natural mode for a log of independent records. ### CLI ```sh bovnar frames pack a.bvnr b.bvnr c.bvnr > docs.bvf # wrap each file in a frame bovnar frames list docs.bvf # list documents + status bovnar frames list - # read the frame stream from stdin ``` --- ## 3. Octet multiplexing (out-of-band channels, cross-chunk) Many logical channels interleaved inside a **single octet-stream value**. The channel id and message framing are a convention interpreted *above* `ev_data`, so to the lexer these are ordinary `0x01` data chunks and `bvnr_data_t` needs no channel field. The 1.0 wire format is untouched. ### Wire convention (two layers) ``` per octet chunk: varint(channel) fragment_bytes per channel: concat(fragments in order) = the channel's logical stream, a sequence of length-prefixed messages: varint(msg_len) msg_bytes ``` - `varint` is unsigned LEB128 (1 byte for channels/lengths < 128; up to 10 bytes). - A message is **length-prefixed**, so it may span any number of chunks, and channels may interleave freely at chunk granularity — the demultiplexer reassembles each channel independently. Both the message data **and** its length varint may straddle chunk boundaries (the demux accumulates a partial length varint per channel). - `BVNR_MUX_MAX_MESSAGE` is a conservative **single-chunk guarantee**, not an enforced cap: any message of at most that many bytes is emitted by `bvnr_mux_send` in one chunk regardless of channel id or length magnitude (it subtracts the maximum 10+10 varint bytes from the 64 KiB chunk). Larger messages are simply split; nothing rejects them. - **The one wire requirement:** the per-chunk **channel** varint — the routing prefix the demux needs to attribute a chunk — is whole within its chunk. `bvnr_mux_send` always satisfies this (the channel id is ≤ 10 bytes and leads every chunk). ### API ```c /* Producer */ bool bvnr_mux_begin(bvnr_writer_t* w, const char* key); bool bvnr_mux_send (bvnr_writer_t* w, uint64_t channel, const void* data, uint64_t len); /* split as needed */ bool bvnr_mux_end (bvnr_writer_t* w); /* Consumer */ typedef bool (*bvnr_mux_on_msg_fn)(void* ud, uint64_t channel, const uint8_t* data, uint64_t len); bvnr_demux_t* bvnr_demux_create(bvnr_mux_on_msg_fn on_message, void* userdata, uint64_t max_message); void bvnr_demux_destroy(bvnr_demux_t* dm); bool bvnr_demux_set_key (bvnr_demux_t* dm, const char* key); bool bvnr_demux_on_event(void* demux, bvnr_event_t ev, bvnr_data_t* d); error_code_t bvnr_demux_error(const bvnr_demux_t* dm); ``` Install `bvnr_demux_on_event` as `bvnr_read_flags_t.on_verified` with `userdata` set to the `bvnr_demux_t*`. It peels the channel varint off each chunk, reassembles per-channel messages (owning a buffer per active channel), and invokes `on_message` for each complete message. A malformed varint, an oversized declared length (> `max_message`, default 64 MiB), or an allocation failure latches `bvnr_demux_error` and aborts the read. If `on_message` returns false the read is aborted and `bvnr_demux_error` reports `error_scanner_callback_failed` (distinct from the desync code, so a deliberate stop is distinguishable from a framing error). A separate, non-fatal case: if the octet stream *ends* while a channel is still mid-message (or mid-length-varint), the octet framing itself was well-formed, so the read still **succeeds** — but the partial message is dropped and `bvnr_demux_error` latches `error_octet_stream_truncated`. This is deliberately a different code from the desync `error_octet_stream_out_of_sync`: a truncated message is a message-layer loss, not a framing error. Always check `bvnr_demux_error` after the read even when it returned true. **Key scoping.** By default the demux treats *every* octet stream in the document as multiplexed. Call `bvnr_demux_set_key(dm, "mux")` so it only demuxes streams opened under that key — a document can then mix one mux stream with ordinary binary octet payloads (those under other keys are ignored, not misread as framing). Pass `NULL` to clear the filter. **Memory.** The demux keeps one reassembly buffer per channel it has seen; each grows to that channel's largest message and is retained for reuse until `bvnr_demux_destroy`, so steady-state memory is the sum of per-channel high-water marks (bounded by 4096 channels). Channels are not reclaimed individually. ```c bvnr_demux_t* dm = bvnr_demux_create(on_message, ctx, 0 /* default cap */); bvnr_read_flags_t fl = { .on_verified = bvnr_demux_on_event, .userdata = dm }; /* ... open_read_* + bvnr_read ... */ if (bvnr_demux_error(dm) != error_none) { /* desync, oversize, alloc, or a truncated message */ } bvnr_demux_destroy(dm); ``` ### CLI ```sh bovnar mux pack 1:req.bvnr 42:log.bvnr > mux.bvnr # files onto channels 1 and 42 bovnar mux list mux.bvnr # channel : bytes bovnar mux pack 7:a.bvnr | bovnar mux list - # pipe producer to consumer ``` --- ## 4. Document-in-document Embed a complete Bovnar document as the octet-stream payload of a key. The outer parser treats the inner document as opaque octets; only an application that knows the key holds a nested document re-feeds those bytes to a sub-reader. This is the natural recursion primitive — no envelope type, just the octet-stream value form. ```c /* Producer: emits . = ; (64 KiB chunks) */ bool bvnr_embed_document(bvnr_writer_t* w, const char* key, const void* doc, uint64_t doc_len); /* Consumer: parse the embedded bytes with an ordinary reader (recursion). */ bool bvnr_parse_embedded(const void* doc, uint64_t doc_len, bvnr_read_flags_t* flags); ``` To recurse while reading the outer document, accumulate the octet chunks for the known key between `ev_octet_stream_start` and `ev_octet_stream_end`, then call `bvnr_parse_embedded` on the accumulated bytes with the nested callbacks: ```c case ev_octet_stream_end: if (in_payload) bvnr_parse_embedded(buf, len, &inner_flags); /* parses the nested doc */ break; ``` The inner callbacks may themselves embed/extract further documents, so nesting is arbitrarily deep (bounded by your own recursion budget). --- ## Python bindings All four capabilities are exposed through the `bovnar.stream` module (a thin ctypes layer over the C functions, so framing, varint reassembly and the strict-vs-resilient document policy behave identically): ```python import bovnar from bovnar import stream # Multi-document framing blob = stream.dump_documents([{"id": 1}, {"id": 2}]) # list[dict] -> bytes docs = stream.load_documents(blob) # bytes -> list[dict] docs = stream.load_documents(blob, continue_past_failed=True) # bad docs -> None # Octet multiplexing (channel, payload) — any size, interleaved m = stream.mux_dump([(1, b"hello"), (42, b"world"), (1, b"x" * 200000)]) out = stream.mux_load(m) # [(1, b"hello"), ...] # Document-in-document outer = stream.embed_document(bovnar.dumps({"v": 1}), key="payload") inner = stream.parse_embedded(bovnar.loads(outer)["payload"]) # {"v": 1} ``` `stream.BVNR_FILESIZE_UNLIMITED` (`== 0`) selects endless mode for **`max_file_size`** only. Note it does **not** lift the framing/mux guards: for `max_document_size` and `max_message`, `0` selects their finite defaults (`BVNR_DOC_DEFAULT_MAX` / `BVNR_MUX_DEFAULT_MAX`), so pass an explicit large value there to raise them. See [Python Bindings](4_bovnar_python_bindings.md) and `python/tests/test_stream.py`. ## Composition The four compose cleanly because they live at different layers: - A **frame stream** (2) can carry documents that each **embed** sub-documents (4). - A **multiplexed** octet stream (3) is itself just one value in a document, which can be one frame in a frame stream (2). - Any of the above is **endless** (1) by default (`max_file_size` 0 == `BVNR_FILESIZE_UNLIMITED`); set a positive `max_file_size` to impose a cap. Because each is a thin application over the public event API, you can also write your own consumer/producer at the same seam without touching the core. --- ## See also - [Specification §9 — Octet Streams](1_bovnar_spec.md#9-octet-streams-binary-mode) - [Read & Write API](3_bovnar_readwrite_api.md) — the underlying event interface - `include/bovnar_stream.h` — full declarations and per-function contracts - `tests/bovnar_stream_test.c` — round-trip tests for all four capabilities - `python/bovnar/stream.py` — Python bindings; `python/tests/test_stream.py`