unit-safe / spec-0.x draft / c99

Unit-safe serialization
for science, industry and finance.

Every measurement carries a validated physical unit — bit-width, numeric base, and dimension travel inline, in the same byte stream, with no external schema. A bare 9.81 is never ambiguous, and a mismatched unit is a parse error. Built for instrumentation, telemetry, and control systems where the wrong unit is a failure.

bovnar — telemetry.bvnr
telemetry.bvnr
examples telemetry.bvnr
1
2
3
4
5
6
7
8
# Every measurement carries a validated unit .host = "api.sensors.io"; .mass = 142.6 k~g; .sensor = { .pressure = <float:32,k~Pa> 101.325; .velocity = <float:64,m/s> 9.81; }; .matrix = [1,2,3]/[4,5,6];
◈ BOVNAR
UTF-8 Ln 8, Col 1 unit-safe
scroll

One format, end to endconfig in, decoded values out.

You saw the syntax — here's a spacecraft running on it. One format, the whole loop: config in, telemetry out. Edit the orbit; the craft encodes each measurement into a .bvnr frame and the ground station decodes it — the orbit you see is drawn only from those decoded values, a full round-trip through the format. Units, bit-widths and types ride inside the stream; no schema required. Hover a gauge to jump to the line it decoded.

Spacecraft config editable · .bvnr
Orbit visualizer ECI · XY projection
Earth (day / night) spacecraft (decoded pos)
Downlink frame .bvnr stream
160×
frame 0 MET 0 s orbits 0.00 dropped 0 lock ACQ


      
With resync on, a corrupt assignment is skipped and the rest of the frame still decodes — the reader's recovery mode. Off, one bad line drops the whole frame. Flip noisy channel to compare.

Numbers that
can't lose their units.

In scientific and industrial systems, the costly 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 parsed fine; the dimension was wrong.

"A bare 9.81 could be meters per second, volts, or a dimensionless ratio. Bovnar makes that difference explicit — and a mismatched unit a parse error."

Bovnar is not a replacement for JSON in simple REST APIs, nor for Protobuf in performance-critical RPC. It is built for the place where dimensional correctness is a requirement: scientific instrumentation and metrology, industrial telemetry and control, IoT sensor networks, long-term measurement archival, and mixed text-binary log streams.

01
Units travel with every value
Physical unit, type family, bit-width, and numeric base all ride in the same byte stream as the value. No schema file, no code generation, no naming convention to decode what a measurement means.
02
A mismatched unit is an error
Annotate a value as m/s and write an inline unit that doesn't match, and the parser rejects it. Dimensional intent is checked against a built-in unit table — not assumed, not left to the reader.
03
Human-readable, machine-precise
A .bvnr file is UTF-8 prose you can read in any editor. The parser validates type and unit annotations on the fly, with no external toolchain required.
04
Optional typing, well-defined defaults
Omit annotations and get sensible defaults (uint:64, float:64). Add them — including compound units like k~g·m/s² and Gi~B written inline — and every value is validated. Both modes are unambiguous.

Where Bovnar fits.

Every format is a set of trade-offs. Bovnar's are deliberate.

Format Human readable Type-precise Physical units Schema-free Binary embed Multi-dim arrays
JSON
YAML partial
TOML partial
CBOR
Protobuf
BOVNAR
Best forBovnar is the format to reach for when units must travel with the data and the receiver may not share a schema — scientific instrumentation & metrology, industrial telemetry & control, IoT sensor networks, and long-term measurement archival. Where a wrong unit is a failure, it is unit-safe by construction; for simple REST payloads reach for JSON, for minimal wire size reach for CBOR or Protobuf.

The grammar of precision.

Every assignment is a declaration. The type annotation is not metadata — it is part of the value itself.

Typed Values
type · width · unit
# Integers .port = <uint:16> 443; .offset = <sint:64> -2147483648; .flags = <uint:32,_2> 11001010; # Floats with SI units .temp = <float:32,°C> 36.6; .speed = <float:64,m/s> 9.81; .force = <float:64,k~g·m/s²> 1.0; .buffer = <uint:64,Mi~B> 16;
Structures & Arrays
nested · multidimensional
# Nested struct .sensor = { .id = <uint:16> 7; .name = "pressure_01"; .val = <float:32,k~Pa> 101.325; }; # 3×3 rotation matrix (rows separated by /) .rotation = [1.0, 0.0, 0.0]/ [0.0, 1.0, 0.0]/ [0.0, 0.0, 1.0];
<family:width,unit>
Type Annotation
Six families: uint sint float float_fix float_dec utf8. Width in bits. Unit in SI notation. All are optional.
k~g·m·s⁻²
Compound Units
SI base and derived units, IEC binary prefixes, compound expressions. Units are validated by the parser. No external schema needed.
\x00 … \x00
Octet Streams
Raw binary embedding without Base64 overhead. Framed by null bytes. Text and binary payloads coexist in the same document.

Built for exactness.

Type System
Strong, optional typing
Six type families with explicit bit-width and numeric base. Defaults are defined, not guessed. Annotate when precision matters; omit when it doesn't.
<uint:16> 443 <float:32,_16> "1.0p+0" <sint:64> -9223372036
SI Units
First-class physical units
All seven SI base units, 22 derived SI units, and IEC binary prefixes. Compound units, exponents, and prefix enforcement are built into the grammar.
<float:64,k~g·m·s⁻²> 1.0 <uint:64,Gi~B> 16 <float:32,m/s²> 9.807
Streaming
SAX-style incremental reader
Parse from memory, file descriptor, or socket via a symmetric on_unverified / on_verified callback pair. No heap required for the lexer itself.
bvnr_open_read_source( r, &src, NULL, &opts); bvnr_read(r);
Resilience
Error recovery & resync
Optional resync mode skips broken assignments and continues parsing. Suitable for log streams, unreliable transports, and long-running telemetry.
opts.continue_on_error = true; /* parser skips broken record, continues with the next */
DOM API
Tree-based access
Build a navigable DOM from any Bovnar stream, then traverse, query, and mutate it with a clean C API. Or use the Python dict-like interface.
bvn_dom_lookup(doc, ".sensor.val"); doc["sensor"]["val"] # Python
Python
Pure-ctypes + NumPy bridge
No compiled extension. No Cython. A full high-level / streaming API — plus a zero-glue NumPy bridge that loads typed arrays straight into an ndarray with the physical unit attached.
bovnar.to_numpy(arr, return_unit=True) # → (ndarray float64, 'm/s²')

Up in minutes.

01
Read from memory
Register callbacks through bvnr_read_flags_t. Both callbacks receive the event type and parsed data.
#include "bovnar.h" static bool on_event(void *ud, bvnr_event_t ev, bvnr_data_t *d) { if (ev == ev_data) printf("val=%.*s\n", (int)d->length, (const char *)d->data); return true; } int main(void) { const char *src = ".velocity = <float:64,m/s> 9.81;"; bvnr_read_flags_t opts = {0}; opts.on_verified = on_event; bvnr_reader_t *r = bvnr_reader_create(); bvnr_open_read_mem(r, src, strlen(src), NULL, 0, &opts); bvnr_read(r); bvnr_reader_destroy(r); }
02
Write typed values
High-level write helpers accept a key string and a typed value directly. The format is emitted with correct annotations.
#include "bovnar.h" int main(void) { char buf[256]; bvnr_writer_t *w = bvnr_writer_create(); bvnr_sink_t sink; bvnr_sink_to_mem(&sink, (uint8_t *)buf, sizeof(buf)); bvnr_open_write_sink(w, &sink, true, NULL); bool ok; value_unit_t u = bvn_parse_unit( (const uint8_t *)"m/s", &ok); bvnr_write_float_unit( w, "velocity", 64, 9.81, u); bvnr_write_finish(w); bvnr_writer_destroy(w); fwrite(buf, 1, (size_t)bvnr_sink_bytes_written(&sink), stdout); // → .velocity = <float:64,m/s> 9.81; }
01
High-level API
Dict-like loads / dumps interface. Pure-ctypes — no compiled extension required.
import bovnar data = { "sensor_id": 42, "temperature": 36.6, "unit": "celsius", } raw = bovnar.dumps(data) doc = bovnar.loads(raw) print(doc["sensor_id"]) # 42 print(doc["temperature"]) # 36.6
02
Streaming event API
Full control over parse events. Inspect units, type annotations, and raw string representations as they arrive.
from bovnar import Reader, Event, unit_to_str def on_event(ev, data): if ev == Event.DATA: u = data.value_unit if u.num_components: print(data.raw_str(), unit_to_str(u)) src = b".velocity = <float:64,m/s> 9.81;" Reader().read_mem( src, on_verified=on_event ) # → 9.81 m/s
03
Typed round-trips with Quantity
loads(typed=True) wraps each typed value in a Quantity that preserves its exact text, bit width, and unit. Pass the dict directly back to dumps() for a lossless round-trip.
import bovnar src = b".pressure = <float:32,Pa> 101325.0;" doc = bovnar.loads(src, typed=True) q = doc["pressure"] # Quantity('101325.0', FLOAT [Pa]) print(q.raw) # '101325.0' print(q.unit_str()) # 'Pa' # dumps() re-emits annotation + raw text unchanged out = bovnar.dumps(doc) assert bovnar.loads(out, typed=True) == doc
04
NumPy bridge
Typed arrays load straight into a numpy.ndarray — bovnar widths map to native dtypes (float:32 → float32) and the whole-array unit rides alongside. NumPy is an optional, lazily-imported extra (pip install "bovnar[numpy]").
import bovnar, numpy as np src = b".accel = <float:64,m/s^2> " \ b"[9.81,0.02,-9.79]/[0.05,9.80,0.01];" doc = bovnar.loads(src, typed=True) a, unit = bovnar.to_numpy( doc["accel"], return_unit=True) # a.shape == (2, 3) a.dtype == float64 unit == 'm/s²' g = np.linalg.norm(a, axis=1) # vectorized, per row out = bovnar.array_to_bvnr("g_mag", g, unit=unit) # → .g_mag = <float:64,m/s²> [9.810…, 9.800…];
01
Build the library
Requires CMake ≥ 3.21 and a C99-conforming compiler. libm is the only mandatory dependency.
# Clone and build cmake -B build . cmake --build build # Release build cmake -B build \ -DCMAKE_BUILD_TYPE=Release . cmake --build build # Outputs: # build/libbvnr_static.a # build/libbvnr_shared.so # build/bovnar (CLI)
02
Link & test
The test suite includes unit tests, socket-pair round-trip tests, fuzz harnesses, and a benchmark binary.
# Link your app gcc my_app.c \ -I include \ -L build \ -lbvnr_static -lm \ -o my_app # Run tests cd build && ctest \ --output-on-failure # Python tests export LIBBOVNAR_PATH=\ $(pwd)/build/libbvnr_shared.so cd python && pytest tests -v

Straight into NumPy — units and all.

A unit-tagged Bovnar array is already the shape of a tensor. The optional NumPy bridge reads it directly into an ndarray — native dtype, original shape, and the physical unit carried alongside — so the wire format and your analysis code never disagree about what a number means.

accel_log.bvnr — on the wire
.accel = <float:64,m/s²> [9.81, 0.02, -9.79]/ [0.05, 9.80, 0.01]; # self-describing: type, # width and unit travel # with the payload
bovnar.to_numpy(
  src, return_unit=True)
numpy.ndarray — in memory
shape (2, 3)  ·  dtype float64
9.81
0.02
-9.79
0.05
9.80
0.01
unit  m/s²
[ ]
Zero glue code
The wire array becomes a vectorized ndarray in a single call. /-rows and bracket nesting collapse to the same shape automatically.
Units never drift
The unit is returned beside the data — never baked into values. Mixed units or dtypes raise instead of silently corrupting the math.
Round-trips losslessly
Write results back with from_numpy / array_to_bvnr — dtype and unit preserved. A pint hand-off (to_pint_array) is one call further.

Where the wrong unit is a failure.

01
Scientific Instrumentation & Metrology
Measurements carry explicit, validated physical units with the payload. No schema, no naming convention, no lab notebook needed to interpret a reading — or to catch a unit mismatch before it reaches analysis.
.acceleration = <float:64,m/s²> 9.807;
02
Industrial Telemetry & IoT
The streaming parser has a small, allocation-friendly footprint — no heap required for the lexer. Sensor frames carry their units; resync mode handles unreliable field transports gracefully.
.temp = <float:32,°C> 23.5; .vcc = <float:32,V> 3.31;
03
Typed Configuration Files
Typed values eliminate range ambiguity. A <uint:16> port number cannot accidentally become a 64-bit float. Optional annotations; sensible defaults.
.port = <uint:16> 5432; .timeout = <float:64,s> 2.5;
04
Mixed Text + Binary Payloads
Log entries with attached raw memory dumps, firmware images, or sensor frames coexist in the same document. No Base64 overhead; octet streams are native.
.frame_data = \x00 … raw bytes … \x00;
05
Multi-dimensional & Tabular Data
Matrices, image frames, time-series batches — any tabular structure where rows are a natural unit. The / separator creates new rows; the result is a 3×3 matrix or any rectangular array — and the Python NumPy bridge loads it into an ndarray in one call.
.matrix = [1,2,3]/[4,5,6]/[7,8,9]; .rotation = [1,0,0]/[0,1,0]/[0,0,1];
06
Long-term Archival
Self-describing data stays interpretable without an out-of-band schema, even years after the original application is gone. The file itself is the documentation.
.spec_version = "bovnar-0.x"; # always readable
Formally Specified
Full EBNF grammar. Unambiguous.
180+ Conformance Tests
TAP output. CTest integrated.
Verifiable by Any Impl.
Open IUT protocol. Fork and test.
14 Test Suites
Unit, roundtrip, fuzz, benchmark.
Read the Conformance Spec →

Everything you need to ship.

Eight documents covering every layer of the stack — from a five-minute tutorial to the formal EBNF grammar and the independent conformance verification protocol.

Parse events, as you type.

Type any valid (or invalid) Bovnar below. The parser emits events in real time — every token, type annotation, and error is shown as it is recognised.

Editor
ready
Event Stream 0 events
Events appear here as you type.