Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 14 additions & 6 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -1183,15 +1183,23 @@ Known weaknesses, so you neither trip over them nor assume they are intentional:
**The fixed-size limits are the real boundary, not an oversight.** `sizeof(schema_t)` is
51 KB because every `field_def` carries `cases[16]` and `lookup[16]` unconditionally.
Raising `SCHEMA_MAX_FIELDS` to fit mla20's 67 fields would put it past 110 KB, which
defeats the point of a firmware-tier interpreter. `repeat` is the last construct it has
no field type for, and at 3 schemas it is worth less than widening the harness to cover
`transform` (26) and `bitfield_string` (24).
defeats the point of a firmware-tier interpreter. Its largest *capability* gaps are
`transform` (26 schemas, and with it `polynomial`, `sqrt`, `pow`, `log`, `floor`,
`clamp`) and `bitfield_string`/`version_string` (24); `repeat` is the last construct with
no field type at all, at 3. This said the first two were worth "widening the harness to
cover", which was backwards - they are the interpreter's gaps, not the harness's, and
CR-2026-035 corrected it here, in SESSION-NOTES.md and in the harness's own skip
reasons, which had said "not built by the struct API" and were read as harness limits.

Two things to keep straight when reading that report. **A skipped schema is not a passing
one**, and **a harness limitation is not a C gap** - inline `match`, `byte_group`,
`object`, `$ref` and ports are named separately because C supports them and the harness
does not. Adding a construct to C without extending the harness would produce a feature
with no cross-check, which is why the TLV work is CR-2026-033 and not CR-2026-032.
`object` and `$ref` are named separately because C supports them and the harness does
not. Ports are the other way round and were listed here in error: the header says
outright that it has no port selection, so a port-based schema is a C gap. When in
doubt, grep the header before writing down which side a limit sits on - every
misattribution in this file and in SESSION-NOTES.md would have been caught by that.
Adding a construct to C without extending the harness would produce a feature with no
cross-check, which is why the TLV work is CR-2026-033 and not CR-2026-032.

`src/test_comprehensive.c` is deliberately out of `make test-c`: 22 of its 160 assertions
encode the pre-CR-2026-009 lookup and enum behaviour that PS-105/PS-269 changed, and its
Expand Down
10 changes: 9 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ PROTO_C = $(patsubst proto/%.proto,src/%.pb.c,$(PROTO_SRCS))
CXX = g++
CXXFLAGS = -std=c++17 -Wall -Wextra -O3 -Iinclude

.PHONY: all clean test test-c selftest validate-devices ci docs-index docs-index-check score-check validate-examples hypothesis coverage proto help codec benchmark generate-codec pytest pytest-cov coverage-html coverage-all validate fuzz fuzz-quick fuzz-hypothesis fuzz-go fuzz-c test-go test-java test-dotnet test-languages
.PHONY: all clean test test-c selftest validate-devices ci docs-index docs-index-check score-check validate-examples hypothesis coverage proto help codec benchmark bench-c generate-codec pytest pytest-cov coverage-html coverage-all validate fuzz fuzz-quick fuzz-hypothesis fuzz-go fuzz-c test-go test-java test-dotnet test-languages

all: $(TEST_BIN)

Expand Down Expand Up @@ -259,6 +259,14 @@ $(BENCHMARK_BIN): src/benchmark.cpp include/env_sensor_codec.h | $(BUILD_DIR)
benchmark: $(BENCHMARK_BIN)
$(BENCHMARK_BIN)

# Benchmarks the runtime interpreter in include/schema_interpreter.h against the Python
# reference on one corpus schema. Distinct from `benchmark` above, which times a small
# interpreter defined inline in src/benchmark.cpp and never includes that header - the
# figures in docs/SPEC-IMPLEMENTATION-STATUS.md come from this target, not that one.
# Not in `ci`: it is a measurement, and its numbers depend on the machine.
bench-c: $(VENV)/bin/activate
$(PYTHON) tools/benchmark-c-interpreter.py

# Generate C codec from schema (old single-file generator)
generate-codec:
python3 tools/generate-c.py examples/env_sensor.yaml -o include/env_sensor_codec.h
Expand Down
17 changes: 12 additions & 5 deletions SESSION-NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,18 @@ Green as of the last run:
Corpus 1229 -> 1250. Decode floors 1193 -> 1239. Encode round-trip: reference 1131 ->
1163, Go 1144 -> 1173, Java 1143 -> 1163, C# 1144 -> 1164.

**The next work on C is widening the harness, not the interpreter.** `tlv` (CR-2026-033)
and `flagged` (CR-2026-034) are done, and `repeat` is the last construct C has no field type
for - 3 schemas, worth less than the two harness limits above it: `transform` (26 schemas)
and `bitfield_string` (24). Whether C supports either is unknown *because the harness cannot
build them*, which is exactly the question the harness exists to answer.
**The next work on C is the interpreter, and the two biggest items are `transform` (26
schemas) and `bitfield_string` (24).** CR-2026-035 corrected this paragraph, which said the
opposite: it called those two *harness* limits whose status was "unknown because the harness
cannot build them". They are the interpreter's own gaps, and the status was never unknown -
`grep -cw transform include/schema_interpreter.h` is 0, as are `polynomial`, `sqrt`, `pow`,
`log`, `floor`, `clamp`, `compute` and `version_string`. The harness cannot build them
because there is nothing in C to build them with.

The misreading came from the skip reasons themselves, which said "not built by the struct
API" - true, but it reads as a limit on the harness, and I took it that way and wrote it
down. They now say "the interpreter has no transform pipeline". After `transform` and
`bitfield_string`, `repeat` is the last construct with no field type at all (3 schemas).

Whatever is done next: **do not add a construct to C without extending
`tools/c-corpus-harness.py` in the same change**, or the new field type lands uncovered - and
Expand Down
3 changes: 2 additions & 1 deletion docs/INDEX.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ Generated inventory of this repository: what lives where, what each document cov
| [`SCHEMA-DEVELOPMENT-GUIDE.md`](SCHEMA-DEVELOPMENT-GUIDE.md) | Best practices for creating complete, validated payload schemas. | 194 |
| [`SCHEMA-LANGUAGE-REFERENCE.md`](SCHEMA-LANGUAGE-REFERENCE.md) | Complete reference for the LoRa Alliance Payload Schema specification (v0.5.0). | 1152 |
| [`SESSION-NOTES-2026-02-25.md`](SESSION-NOTES-2026-02-25.md) | The prototype tests were using a custom REQ-xxx-yyy numbering scheme that was inconsistent with the... | 78 |
| [`SPEC-IMPLEMENTATION-STATUS.md`](SPEC-IMPLEMENTATION-STATUS.md) | Feature support matrix across reference implementations. | 439 |
| [`SPEC-IMPLEMENTATION-STATUS.md`](SPEC-IMPLEMENTATION-STATUS.md) | Feature support matrix across reference implementations. | 457 |
| [`TTN-CODEC-CONVERSION-GUIDE.md`](TTN-CODEC-CONVERSION-GUIDE.md) | Complete guide for AI-assisted conversion of The Things Network device repository codecs to Payload... | 549 |
| [`WOT-REFERENCE.md`](WOT-REFERENCE.md) | Reference for mapping LoRaWAN payload schema fields to W3C WoT Thing Descriptions and SAREF... | 316 |

Expand Down Expand Up @@ -77,6 +77,7 @@ Generated inventory of this repository: what lives where, what each document cov
| `tools/analyze_codec.js` | Load and execute codec in sandbox |
| `tools/analyze_ttn_codec.py` | TTN Codec Analyzer |
| `tools/batch_analyze_codecs.py` | Batch analyze TTN Device Repository codecs. |
| `tools/benchmark-c-interpreter.py` | Benchmark the C interpreter against the Python reference on one corpus schema. |
| `tools/benchmark_all.py` | Comprehensive codec benchmark |
| `tools/benchmark_codecs.js` | Default test configuration |
| `tools/binary_schema.py` | Binary Schema Encoder/Decoder for OTA Schema Transfer |
Expand Down
70 changes: 44 additions & 26 deletions docs/SPEC-IMPLEMENTATION-STATUS.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,12 +79,12 @@ ordinary-field path, disagreeing with its own enum path.

| Feature | Python | Java | Go | C | JS |
|---------|--------|------|-----|---|-----|
| `sqrt` | ✓ | ✓ | ✓ | | ✓ |
| `abs` | ✓ | ✓ | ✓ | | ✓ |
| `pow` | ✓ | ✓ | ✓ | | ✓ |
| `log` / `log10` | ✓ | - | ✓ | | ✓ |
| `floor` / `ceiling` | ✓ | - | ✓ | | ✓ |
| `clamp` | ✓ | - | ✓ | | ✓ |
| `sqrt` | ✓ | ✓ | ✓ | - | ✓ |
| `abs` | ✓ | ✓ | ✓ | - | ✓ |
| `pow` | ✓ | ✓ | ✓ | - | ✓ |
| `log` / `log10` | ✓ | - | ✓ | - | ✓ |
| `floor` / `ceiling` | ✓ | - | ✓ | - | ✓ |
| `clamp` | ✓ | - | ✓ | - | ✓ |
| `round` | ✓ | - | ✓ | - | ✓ |

### Computed Fields
Expand All @@ -93,7 +93,7 @@ ordinary-field path, disagreeing with its own enum path.
|---------|--------|------|-----|---|-----|
| `type: number` | ✓ | ✓ | ✓ | ✓ | ✓ |
| `ref: $field` | ✓ | ✓ | ✓ | ✓ | ✓ |
| `polynomial` | ✓ | - | ✓ | | ✓ |
| `polynomial` | ✓ | - | ✓ | - | ✓ |
| `compute: {op, a, b}` | ✓ | - | ✓ | - | ✓ |
| `guard` conditions | ✓ | - | ✓ | - | ✓ |

Expand All @@ -113,12 +113,12 @@ ordinary-field path, disagreeing with its own enum path.
| Feature | Python | Java | Go | C | JS |
|---------|--------|------|-----|---|-----|
| `type: object` | ✓ | ✓ | ✓ | ✓ | ✓ |
| `type: repeat` (count) | ✓ | ✓ | ✓ | | ✓ |
| `repeat` (count_field) | ✓ | ✓ | ✓ | | ✓ |
| `repeat` (until: end) | ✓ | ✓ | ✓ | | ✓ |
| `type: repeat` (count) | ✓ | ✓ | ✓ | - | ✓ |
| `repeat` (count_field) | ✓ | ✓ | ✓ | - | ✓ |
| `repeat` (until: end) | ✓ | ✓ | ✓ | - | ✓ |
| `definitions` / `use` | ✓ | - | ✓ | - | ✓ |
| `ports` (fPort routing) | ✓ | ✓ | ✓ | | ✓ |
| `var` (variables) | ✓ | ✓ | ✓ | - | ✓ |
| `ports` (fPort routing) | ✓ | ✓ | ✓ | - | ✓ |
| `var` (variables) | ✓ | ✓ | ✓ | | ✓ |

### Encodings

Expand Down Expand Up @@ -233,12 +233,18 @@ but report no quality object; the interpreters and the generated JS agree on it,

**Embedded-optimized** - no dynamic allocation required.

- Full decode support
- Binary schema loading (no YAML)
- Programmatic schema building
- 32M msg/s throughput
- Missing: complex computed fields, definitions
- 8.5M decodes/s on a 15-field `flagged` frame - see Performance Benchmarks below,
and regenerate with `make bench-c` rather than trusting this figure
- Decodes every construct the corpus needs except `repeat`, and has no `transform`
pipeline at all, so no `polynomial`, `sqrt`, `pow`, `log`, `floor`, `clamp`
- Also missing: computed fields, `definitions` / `use`, `ports` (fPort routing)
- Has `tlv` (CR-2026-033) and `flagged` (CR-2026-034); `unknown: raw` is not
representable because there is nowhere to put the captured bytes
- No encode support (decode-only)
- Measured against the corpus by `tools/c-corpus-harness.py`: 488 of 1239 vectors are
in schemas the struct API can build, and all 488 decode exactly as the corpus expects

### JavaScript (`tools/generate_ts013_codec.py` output)

Expand Down Expand Up @@ -317,20 +323,32 @@ Tested with DL-5TM schema (8 fields, flagged construct, polynomial transform).

### C Interpreter (AMD Ryzen 9 7950X3D)

**Not comparable to the DL-5TM rows above** — the C interpreter has no `flagged` or
`polynomial` support, so this uses a simpler 5-field frame (u8 protocol, u16 device
id, s16 temperature with `div`, u8 humidity with `div`, u16 battery). The Python
figure was measured on the same machine with the same schema and payload, so the two
rows here are comparable to each other and to nothing else in this document.
Regenerate with `make bench-c` (`tools/benchmark-c-interpreter.py`). Frame:
`schemas/devices/decentlab/dl-lid.yaml`, vector `vendor_reference_2` — a 29-byte
payload decoding to **15 fields**: three plain, then a two-group `flagged` covering
twelve more, two of them scaled with `div`.

**Still not comparable to the DL-5TM rows above**, but for one reason now rather than
two. The C interpreter gained `flagged` in CR-2026-034, so the frame no longer avoids
it; what it has no support for at all is `transform` and `polynomial`, which DL-5TM
needs for two of its fields. Both rows below were measured on the same machine from
the same schema file and the same payload, so they are comparable to each other and to
nothing else in this document.

| Implementation | Throughput | Latency |
|----------------|------------|---------|
| C interpreter (`include/schema_interpreter.h`) | 20.5M ops/s | 0.05 µs |
| Python interpreter | 141K ops/s | 7.1 µs |
| **Ratio** | **145x** | |

Stripped executable including the whole interpreter and the schema: **18.6 KB**
(`gcc -O2 -Os`, header-only so everything inlines). That size and throughput are why
| C interpreter (`include/schema_interpreter.h`) | 8.5M ops/s | 0.12 µs |
| Python interpreter (`tools/schema_interpreter.py`) | 40K ops/s | 25 µs |
| **Ratio** | **~210x** | |

Both figures time `decode` alone, with the schema built once outside the loop, so
neither includes YAML parsing or schema construction. Across three runs C held
8.4–8.6M ops/s and Python 39–41K, so the ratio is good to about ±5%; the table is
rounded to match. The earlier numbers here (20.5M ops/s, 145x) were measured on a
5-field frame with no `flagged`, and are not comparable to these.

Stripped executable including the whole interpreter and the schema: **18.2 KB**
(`cc -O2 -Os`, header-only so everything inlines). That size and throughput are why
the C interpreter is a candidate for a full-featured embedded-Linux gateway decoder
and not only for the MCU binary-schema path — see AGENTS.md for what it still lacks.

Expand Down
Loading
Loading