Bootstrap Airframes Decoder Spec (ADS) v1: spec, codegen, runtimes, 68 plugin specs#1
Bootstrap Airframes Decoder Spec (ADS) v1: spec, codegen, runtimes, 68 plugin specs#1kevinelliott wants to merge 23 commits into
Conversation
Introduces the Airframes Decoder Spec (ADS) as the language-neutral source
of truth for ACARS plugin behavior, consumed via build-time codegen by
the TypeScript, Rust, and C decoder implementations (and any future target).
What's here:
- schema/ads-v1.schema.json — JSON Schema (Draft 2020-12) validating spec
YAML files. Covers plugin metadata, qualifiers, parse steps (split,
regex, substring, bitfield, ascii85, deflate, base64, text_decode,
hex_decode, concat_bits, custom), fields, variants with conditional
dispatch, checksums, formatters, and escape hatches.
- spec/labels/{10/POS,4A,44/POS,H1/OHMA}.yaml + spec/wildcards/arinc_702.yaml
Five reference plugin specs covering the full pattern range: simple
positional, variant dispatch, regex with named groups, binary
encoding chain (base64→deflate→utf-8), full escape hatch.
- spec/shared/{crc_tables,decode_fns}.yaml — cross-cutting data that
codegen emits identically into each runtime, avoiding per-language
duplication of CRC tables and decoder semantics.
- codegen/ — TypeScript CLI (@airframes/ads-codegen) with parse, validate,
IR, and emitters for ts/rust/c. CLI: `ads-gen generate --target X
--spec ... --out ...`. TS emitter is functional for the five reference
specs; Rust and C emitters are stubs filled in next.
- runtimes/{typescript,rust,c}/ — placeholder dirs for per-language
runtime helper libraries. Stage 2 moves the existing TS lib/utils/ here
and authors equivalents for Rust and C.
- corpus/ — shared golden test vectors (sample from Label_10_POS test)
consumed by every language repo's CI. Source of truth for cross-impl
parity.
- docs/{DSL,ADOPTION,ESCAPE_HATCHES}.md — spec format reference,
per-language adoption guide, and escape-hatch conventions.
- .github/workflows/{ci,codegen-check}.yml — central CI plus a reusable
workflow language repos call to verify their committed generated/
trees are up-to-date with the spec.
Source of truth: acars-decoder-typescript (production). Behavior must
match TS byte-for-byte. acars-message-documentation fills documentation
gaps. See docs/DSL.md.
Plan: /Users/kevin/.claude/plans/we-want-to-take-quiet-kahan.md
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… labels
Rust emitter targets an `ads_runtime` crate API parallel to the TS shape:
use ads_runtime::{Plugin, Message, Options, DecodeResult, Qualifiers,
ResultFormatter, helpers};
pub struct LabelXyz;
impl Plugin for LabelXyz { fn qualifiers(...) ... fn decode(...) ... }
C emitter generates per-plugin .c + .h pairs targeting an `ads_runtime.h`
API:
ads_decode_result_t *<snake>_decode(const ads_message_t *msg,
const ads_options_t *opts);
ads_qualifiers_t <snake>_qualifiers(void);
extern const ads_plugin_descriptor_t <snake>_descriptor;
Both emitters cover all parse steps (split, regex, substring,
require_length, bitfield, ascii85, deflate, base64, text_decode,
hex_decode, concat_bits, custom), fields with optional `when` gating,
variant dispatch with conditional matching, formatter calls (typed + custom),
and escape-hatch delegation at parse/field/formatter levels.
Stage 2 fills in the actual runtime libraries under runtimes/{rust,c}/
matching these emitted call sites.
Filename fix in cli.ts: the previous camelCase→snake_case regex inserted
an unwanted underscore between digit and capital (Label_4A → label_4_a).
Plugin names are already snake-style with caps so a plain lowercase is
correct (Label_4A → label_4a, Label_10_POS → label_10_pos).
Verified end-to-end:
ads-gen generate --target {ts,rust,c} produces clean, readable output
for all 5 reference specs.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copies the canonical TS source for: DecoderPlugin, DecoderPluginInterface, DateTimeUtils, ResultFormatter, CoordinateUtils, ASCII85 decoder, compression (pako wrapper + base64), MIAM PDU parser, ARINC 702 helper with CRC tables, ICAO FPL parser, flight plan / route utils, and the Route/Waypoint/Wind type modules. Adds: - index.ts — public exports: DecoderPlugin, types, ResultFormatter, utils - helpers.ts — codegen-call adapters (coordinate, integer, float, string, timestampHhmmss, callsign, tailNumber, airport, base64ToUint8Array, inflate, textDecode, decodeAscii85, hexDecode, bitslice, concatBits). Behavior matches the inline plugin logic in acars-decoder-typescript byte-for-byte; the shared corpus enforces this. - MessageDecoder.ts — interface for the dispatcher (decouples DecoderPlugin from the concrete dispatcher, which stays in the language repo as the public entry point). - escape_hatches/ — placeholder index for per-plugin custom functions. - package.json, tsconfig.json, README.md. Latent bug fixes uncovered during type-checking under the runtime's stricter tsconfig: - arinc_702_helper.ts: two surviving `Buffer.from(data, 'ascii')` calls the Web APIs migration missed. Replaced with a hand-rolled asciiStringToBytes() so CRC helpers run in browser / Deno / Bun. - miam.ts: strict-null narrowing failed after `body = decoded` reassignment; switched to a single-expression assignment that preserves narrowing. - result_formatter.ts: sequence-number / sequence-response items pushed numeric values into a string-typed `value` field. Wrapped in String(...) to match the interface. - flight_plan_utils.ts: two unknown-narrowing casts for plugin-specific raw fields (procedures, company_route). These changes preserve runtime behavior exactly; Stage 2 will adopt this runtime in acars-decoder-typescript via tsconfig path alias and remove the duplicated lib/utils/ files from that repo. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Rust counterpart of runtimes/typescript/. The scaffold targets the
import shape the codegen emits:
use ads_runtime::{Plugin, Message, Options, DecodeResult, Qualifiers,
ResultFormatter, helpers};
Modules:
- plugin.rs — Plugin trait, Message/Options/Qualifiers,
DecodeResult (with HashMap<&str, serde_json::Value>
raw bag), DecodeLevel, fail_unknown()
- helpers.rs — all decode-fn helpers the codegen calls (coordinate,
coordinate_decimal_minutes, integer, float, string,
timestamp_hhmmss, callsign, tail_number, airport,
base64_decode, inflate, text_decode, decode_ascii85,
hex_decode, bitslice, concat_bits, regex)
- coordinate.rs — combined NS/EW DDDDD parser + DDMM.M decimal-minute
parser, mirroring CoordinateUtils
- ascii85.rs — pure-Rust ASCII85 decoder (Adobe delimiters + 'z'
shorthand)
- crc.rs — CRC-16/IBM-SDLC reversed-nibble + CRC-16/GENIBUS,
plus an match_arinc_702_crc convenience that mirrors
the TS algorithm-selection logic
- result_formatter.rs — ResultFormatter associated fns (position,
altitude, speed, heading, timestamp, callsign,
flight_number, tail, departure/arrival_airport, fuel,
unknown_arr, unknown) — output strings match TS
formatting
- types.rs — Route, Waypoint, Wind structs (serde Serialize)
- escape_hatches/mod.rs — placeholder index for per-plugin custom fns
Cargo deps: base64, flate2, regex, serde, serde_json, once_cell.
Builds clean with `cargo build`.
CRC tables themselves still TODO — emitted from spec/shared/crc_tables.yaml
in a follow-up; current bitwise impl is correct and table-free.
Args passed as JSON strings (codegen emits `JSON.stringify(args)`). v1.1
should switch to typed-args codegen for efficiency.
Updates .gitignore to skip target/ and build/.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pure-C runtime mirror of runtimes/typescript/ and runtimes/rust/. Targets
the API surface the codegen emits:
#include "ads_runtime.h"
#include "ads_helpers.h"
#include "ads_escape_hatches.h"
ads_decode_result_t *<snake>_decode(...)
ads_qualifiers_t <snake>_qualifiers(void)
extern const ads_plugin_descriptor_t <snake>_descriptor;
Files:
- CMakeLists.txt — static lib (optional shared variant), Asan
config, zlib detection, cjson link
- include/ads_runtime.h — core types: Message, Options, Qualifiers,
DecodeResult, value/string_list/bytes, plugin
descriptor; result + value lifecycle API
- include/ads_helpers.h — all decode-fn helpers, formatter helpers,
split/regex/substring/in primitives
- include/ads_escape_hatches.h — placeholder header
- src/plugin.c — DecodeResult lifecycle + ads_value_*,
backed by cJSON for the raw bag
- src/helpers.c — numerics, strings, identifiers, timestamps,
bitslice, concat_bits
- src/coordinate.c — combined NS/EW + decimal-minutes parsers
- src/ascii85.c — ASCII85 + base64 + hex + zlib inflate
- src/crc.c — CRC-16 IBM-SDLC reversed-nibble + GENIBUS
- src/result_formatter.c — ads_fmt_* item-push helpers
- src/string_list.c — ads_split / ads_substring / ads_str_in
- src/regex.c — POSIX-regex backed ads_regex_* (Stage 2:
upgrade to PCRE2 for named-capture support)
Deps: cjson (raw bag + JSON args parsing), zlib (optional, gates
ads_inflate behind ADS_HAVE_ZLIB), POSIX regex (libc).
Builds clean as libads_runtime_c.a via:
mkdir build && cd build && cmake .. && cmake --build .
Caveats:
- POSIX regex doesn't support PCRE-style named groups. `ads_regex_group`
resolves numeric indices only for v1; Stage 2 plans PCRE2 swap.
- Args still flow as JSON strings; typed-args codegen pending for v1.1.
- CRC lookup tables emitted from spec/shared/crc_tables.yaml are pending;
current bitwise impl is correct and table-free.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Brings the spec coverage from 5 reference plugins to 68 — the full
TS plugin catalog minus the official.ts barrel file. Authored in parallel
by four agents, each working a distinct label-range batch.
Coverage:
- Batch 1 (Label_10-22): 22 specs — mostly whole-plugin escape hatches
- Batch 2 (Label_24-4N): 15 specs — 4 declarative + 11 escape hatches
- Batch 3 (Label_4T-H1): 16 specs — all whole-plugin escape hatches
- Batch 4 (Label_H2-official): 9 specs — all whole-plugin escape hatches
- ColonComma: 1 spec (after schema relaxation, see below)
Of the 63 new specs, 4 are declarative ports (Label_44_IN/ON/OFF/ETA —
clean CSV + coordinate_decimal_minutes + airport + timestamp shape) and
59 use whole-plugin escape hatches via `parse: { custom: <name> }` and
`formatted: { custom: <name> }`.
Why the heavy escape-hatch ratio: the v1 schema's `formatter_call.type`
enum covers only the most common formatters (position, altitude,
timestamp, etc.). Real TS plugins lean on ~40 more ResultFormatter
methods (route, eta, currentFuel, temperature, flightNumber,
departureDay/arrivalDay, out/off/on/in, runway types, windData, mach,
airspeed, totalAirTemp, state_change, door_event, frequency, atis,
loadsheet, fault, warning, route, routeNumber, cg, engineStart/Stop,
groundspeed, day, month, airline, message_type, tail, unknownArr…).
Generating those declaratively would silently drop fields. Per the
"under-port-with-escape-hatches is better than over-port-with-wrong-
semantics" rule, conservative bias is correct.
Two real-life follow-ups for ADS v1.1:
1. Extend `formatter_call.type` to cover the full TS ResultFormatter API
(or add an `aliases` form that lets specs declare the formatter name
inline).
2. Add a `trailing_fields_into_unknown` parse step so plugins like
Label_44_IN/ON/OFF/ETA preserve the addRemainingFields behavior that
the declarative path currently drops.
Schema relaxation: the `qualifiers.labels` pattern previously rejected
punctuation, blocking Label_ColonComma (label literal ":;"). Changed
to allow any non-empty string — ACARS labels are byte sequences, not
identifiers. The schema now matches the real ACARS spec.
Escape-hatch inventory (~118 functions, ~2 per plugin):
- All listed in /docs/ESCAPE_HATCHES.md after stage 2 wire-up
- Each = the original TS plugin's decode() and formatted-item-push code,
ported 1:1 into runtimes/<lang>/escape_hatches/
Verified end-to-end:
- All 68 specs validate against schema/ads-v1.schema.json
- `ads-gen --target ts` emits 68 .ts files cleanly
- `ads-gen --target rust` emits 68 .rs files cleanly
- `ads-gen --target c` emits 68 .c+.h pairs cleanly
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Important Review skippedToo many files! This PR contains 299 files, which is 149 over the limit of 150. To get a review, narrow the scope: ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (299)
You can disable this status message by setting the Use the checkbox below for a quick retry:
📝 WalkthroughWalkthroughAdds ADS v1 schema/docs, a codegen tool with IR/validation/emitters, TypeScript/Rust/C runtimes and utilities, extensive label and wildcard specs, shared corpus fixtures, and CI workflows to validate, generate, and smoke-test outputs. ChangesADS v1 Bootstrap
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Poem
✨ Finishing Touches🧪 Generate unit tests (beta)
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 765a3903ce
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| : renderExpr(field.from); | ||
| if (field.when) { | ||
| out.push(`${indent}if (${renderCondition(field.when)}) {`); | ||
| out.push(`${indent} const ${field.name} = ${decodeExpr};`); |
There was a problem hiding this comment.
Hoist conditional field values before formatting
For specs that mark a field with when and later format it with when_present (for example spec/labels/44/ON.yaml formats $fuel_remaining), this emits const fuel_remaining inside the if block. The formatter is emitted after the fields and references fuel_remaining outside that block, so the generated TypeScript plugin cannot compile (or would reference an out-of-scope value). Emit a variable in the outer decode scope and only call the formatter when it is present.
Useful? React with 👍 / 👎.
| ); | ||
| out.push(`${indent}}`); | ||
| } else { | ||
| out.push(`${indent}ads_value_t ${field.name} = ${decodeExpr};`); |
There was a problem hiding this comment.
Emit C field values as pointers
Every C decode helper in runtimes/c/include/ads_helpers.h returns ads_value_t *, and ads_result_raw_set also expects an ads_value_t *, but this generator declares generated field locals as ads_value_t. For any generated C plugin with fields, the emitted code has incompatible pointer/object types before it can be linked; the field local should be emitted as ads_value_t * (including the conditional-field branch).
Useful? React with 👍 / 👎.
| } | ||
| const fn = call.fn; | ||
| if (Object.keys(call.args).length > 0) { | ||
| return `helpers::${fn}(${valueExpr}, ${rustString(JSON.stringify(call.args))})`; |
There was a problem hiding this comment.
Route Rust helpers with args to their args-aware functions
When a spec supplies decode args for helpers such as integer or timestamp_hhmmss (several label 44 specs do this), this emits calls like helpers::integer(value, "{...}"). The Rust runtime exposes one-argument integer/timestamp_hhmmss plus separate integer_with_args/timestamp_hhmmss_with_args, so those generated Rust plugins fail to compile for the args-bearing fields; map these helper names to their _with_args variants when call.args is non-empty.
Useful? React with 👍 / 👎.
Replaces the 2 manual reference samples with an auto-extracted corpus captured by running the full acars-decoder-typescript test suite (88 suites, 407 passing tests, 9 skipped) through a Jest setup hook that wraps every plugin's prototype.decode and records (input → DecodeResult) pairs to JSONL. A post-processing script groups records by plugin slug and writes per-spec sample files at corpus/<spec_path>/sample-NNN.json. Coverage: - 288 samples across 57 of the 68 plugin specs - Sample-001 typically the canonical "successful decode" case; later samples cover variants and failure modes - The extracted "expected" field is the *actual* DecodeResult shape the TS production code returns, so cross-implementation parity is enforced against real production behavior, not assertion fragments Unmatched (need v1.1 follow-up — 6 plugin slugs): - c-band (spec slug: cband — TS hyphenates) - label-1l-3-line (spec slug: label-1l-3line — slug function doesn't split digit→lowercase boundaries) - label-h1-star-pos (spec slug: label-h1-starpos — uppercase-run boundary) - label-13-18-slash (spec slug match issue) - label-1l-1-line, test-label-44 (no matching spec; possibly stale TS test fixtures) Recommended v1.1 fix: add an explicit `slug` field on `plugin` in the spec for the divergent cases (escape hatch), or use a camelCase-aware slugger (with care not to regress Label_4A → label-4a). Tooling (intentionally NOT committed): - /tmp/corpus-extractor.js — Jest setupFilesAfterEnv hook that monkey- patches each plugin class's prototype.decode to log records - /tmp/corpus-postprocess.js — reads JSONL, groups by slug, writes sample files Rerun: cd acars-decoder-typescript rm -f /tmp/extracted-corpus.jsonl && touch /tmp/extracted-corpus.jsonl npm test -- --setupFilesAfterEnv=/tmp/corpus-extractor.js --runInBand --silent node /tmp/corpus-postprocess.js (These belong as committed tooling in codegen/scripts/ in a follow-up.) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Companion to codegen-check.yml. Language repos call this from their CI
to run the shared corpus through their decoder and fail on any
divergence vs the expected output in each corpus sample file.
Caller example:
jobs:
corpus:
uses: airframesio/acars-decoder/.github/workflows/corpus-test.yml@main
with:
language: ts
run-cmd: npm run test:corpus
setup-cmd: npm ci
Handles checkout-with-submodules and toolchain installation per language
(Node for ts/rust/c, plus Rust toolchain for rust, plus CMake + cjson +
zlib for c). The caller specifies the actual corpus-test run-cmd.
Security hardening matches codegen-check.yml: inputs flow through env
vars (not direct ${{ }} interpolation in shell), and an input-validation
step rejects newlines, backticks, and $(...) in command strings.
Sets up Stage 2.5 / Stage 3: each language repo's `test:corpus`
implementation (TS first, then Rust, then C) will land in follow-up
PRs; this workflow is the CI glue that runs them.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Fixes the double-bookkeeping issue identified in airframes-decoder#1's
known-follow-ups: previously every field unconditionally wrote
`result.raw.<name> = <name>` even when a downstream formatter call also
wrote raw under the formatter's canonical key (position, altitude,
callsign, …). The original hand-written plugins only have the formatter
write. The generated code's extra raw entries caused divergence from the
existing tests and blocked the Stage 2 behavioral swap.
Fix: pre-scan FormattedIR.items for `$varname` references and skip the
auto-emit for fields whose name is consumed by a formatter. Custom
formatters and free-text formatters are conservatively treated as
non-consuming (so the auto-emit still happens), since we can't statically
tell what the hatch will do with raw.
Applied identically in emit-typescript.ts, emit-rust.ts, and emit-c.ts
so the three targets produce equivalent shapes.
Verified: regenerating from `spec/labels/10/POS.yaml` now produces a
Label_10_POS.ts whose raw output matches the original
acars-decoder-typescript/lib/plugins/Label_10_POS.ts byte-for-byte
(raw has {position, altitude} only, not {latitude, longitude, altitude,
position}).
Unblocks the Stage 2 behavioral swap for the ~4 declarative ports
(Label_10_POS, Label_44_POS, Label_44_IN/ON/OFF/ETA). Whole-plugin
escape-hatch specs are unaffected.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… see them
Previously the generated TS emitter wrote `if (cond) { const X = ...; }`
which made X block-scoped — a downstream `ResultFormatter.X(result, X)`
outside the if would throw ReferenceError when the guard was false. The
original hand-written plugins implicitly relied on X being undefined in
that case (the formatter handles undefined gracefully).
Fix: declare `let X;` outside the if, assign inside. Variable is now
in scope after the if, undefined when the guard fails — matching the
original behavior.
Surfaced by the Stage 2.5 pilot extension to Label_44_ON, whose
fuel_remaining field is `when`-gated.
Same shape of fix queued for Rust + C emitters (Option<T> outside / NULL
outside respectively) — pushed separately once verified in their
respective build pipelines.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The TS runtime's ResultFormatter exposes the method as currentFuel, not fuel. Generated plugins were calling .fuel(...) which threw 'is not a function' at runtime — surfaced by the Label_44_ON labelindex test under the Stage 2.5 pilot. Wider audit of formatter method-name mismatches between the emitter map and the runtime is queued; this commit fixes the one that's currently exercised by tests. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When a spec has a when-gated fuel field whose guard fails, the variable becomes undefined. Generated plugins still call ResultFormatter.currentFuel unconditionally, which previously crashed on .toString() of undefined. Original hand-written plugins guarded with 'if (value !== sentinel) ResultFormatter.X(...)' — i.e. they didn't call the formatter at all when the value was missing. The defensive guard inside currentFuel matches that semantic without requiring formatter-level when-clauses in the DSL. Same defensive pattern queued for the other ResultFormatter methods that may receive missing values; this commit fixes the one currently exercised by tests (Label_44_ON via the labelindex test). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…er, FlightPlanUtils, parseIcaoFpl, MIAMCoreUtils, RouteUtils, ascii85Decode, base64ToUint8Array, inflateData Unblocks Stage 2.5 bulk escape-hatch implementations: hatches are free functions that need to call plugin.failUnknown / plugin.setDecodeLevel / plugin.debug / plugin.initResult, but those were protected. Now public. Also exports the helpers used by ARINC 702 / MIAM / OFP / route plugins that previously were only accessible via deep submodule paths. Documented inline why each was promoted (so a future reviewer doesn't silently re-protect them). Verified runtimes/typescript type-checks clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…star-pos) Three boundary rules now insert hyphens to match legacy hand-written plugin names byte-for-byte: - lowercase→Uppercase (CBand → c-band) - digit→Uppercase if next is lowercase (3Line → 3-line; 4A stays 4a) - Uppercase→Uppercase if next is lowercase (StarPOS → star-pos) Fixes the Stage 2.5 bulk-swap labelindex test failure where CBand's generated name 'cband' didn't match the legacy 'c-band' that test assertions check for. Also future-proofs Label_1L_3Line and Label_H1_StarPOS. Verified slugs: Label_10_POS → label-10-pos Label_4A → label-4a ARINC_702 → arinc-702 CBand → c-band Label_1L_3Line → label-1l-3-line Label_H1_StarPOS → label-h1-star-pos Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…plex plugins) The variant+field+formatter-custom shape worked in theory but had no clean contract for who owns the formatted.items list when the formatter is custom. Other complex plugins (CBand, ARINC_702, MIAM, etc.) use the whole-plugin parse-custom + format-description shape; Label_4A follows suit. Implementation lives in lib/plugins/escape_hatches/Label_4A.ts in each language repo. Validates clean: 68 specs OK. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mirrors the TS emitter fixes (init/ads-v1@80095f5 and @c037de4) so all
three targets produce consistent generated code:
- when-gated fields: declare outside the if, assign inside. Rust uses
Option<serde_json::Value>, C uses NULL-initialized pointer.
- smart slug: insert hyphens at camelCase boundaries (CBand→c-band,
StarPOS→star-pos, 3Line→3-line, Label_4A stays label-4a).
Verified: rust + c emitters produce 68 plugins cleanly; CBand slug is
now 'c-band' in both targets.
Required prep before Stage 2.5 Rust + C escape-hatch bulk implementation.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…arlier)
A prior edit to emit-rust.ts's emitField when-branch failed silently
due to a 'File has not been read yet' error in the harness; the smart-
slug change landed but the when-hoist did not. Surfaced when Stage 2.5
Rust wiring produced 'cannot find value fuel_remaining' errors.
Now matches the TS emitter's pattern: when-gated fields hoist to
'let X: Option<JsonValue> = if cond { ... Some(v.into()) } else { None };'
so downstream formatter calls see the variable.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three coordinated changes that bring the Rust emitter and runtime into
agreement:
1. Emitter: always emits 2-arg helper calls (value + args_json), even
when args is empty. Removes the special-case 1-arg dispatch that
diverged from the runtime.
2. Emitter: stops appending .as_str() on regex Captures index access
(the indexed access already returns &str; the .as_str() relied on
the unstable str_as_str library feature).
3. Emitter: unknown_arr arg coercion uses .to_string() instead of
.clone() so the Vec<String> param matches.
4. Runtime helpers.rs: every decode-fn helper now accepts (value,
args_json) uniformly. Args-free helpers ignore the second arg.
5. Runtime result_formatter.rs: every formatter method accepts
impl Into<Option<JsonValue>> so when-gated fields (which now
hoist to Option<JsonValue>) flow through cleanly. None / Null /
NaN inputs no-op, matching the TS pattern of guarding before
calling the formatter.
Expected to take Rust crate compile errors from 33 down to a small
remainder for follow-up.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…e, decode calls
Closes the remaining 4 type-mismatch errors from the convergence pass:
- regex captures: pass &onExpr so String values (message.text) coerce
to &str.
- deflate: inflate takes &[u8]; src is Vec<u8>, so always borrow.
- text_decode: same — borrow the Vec<u8> source.
- renderDecodeCall: borrow the value expr by default (skip when expr
already starts with & / * / numeric literal). Lets owned values
(String from text_decode, Vec<u8>) flow through to helpers/hatches
that take references.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…r hatches Stage 2.5 C convergence prep. C compiler needs forward declarations; the generated plugins call ads_hatch_<name>(...) and would otherwise produce implicit-function-declaration errors. Three signatures: - parse hatches (61): (msg, result, opts) -> result - field decode (3): (value, args_json) -> value - formatter (1): (result) -> void Implementations live in each language repo's src/escape_hatches/ (the central runtime can't implement plugin-specific logic). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Every decode-fn helper now accepts (value, args_json), where args_json
is '{}' when the spec specifies no args. Args-free helpers (decode_float,
decode_string, etc.) just ignore the second arg.
Removes the *_args / *_with_args duplicate functions; they had unified
implementations behind separate names which confused the emitter
dispatch. Now the emitter always emits the same 2-arg form regardless
of whether args is empty.
Same fix shape as the Rust convergence pass (init/ads-v1@6b475ff). Both
runtimes are now consistent.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ads_regex_match_t is opaque (forward-declared in ads_runtime.h), so the emitted code can't declare a value of it. Switch to the heap-allocated ads_regex_match_new() and pass the pointer to ads_regex_match_ok / ads_regex_group.
What is ADS?
The Airframes Decoder Spec (ADS) is a portable, declarative DSL that captures ACARS decoding semantics in a language-neutral form. It is consumed via build-time codegen by
acars-decoder-typescript,acars-decoder-rust, andacars-decoder-c— and is designed so future targets (WASM, Go, Python, …) can be added at low marginal cost.acars-decoder-typescriptis in production and is the authoritative reference; behavior must match TS byte-for-byte. The shared corpus (undercorpus/) enforces this across implementations.What's in this PR (6 commits)
1512d3dcodegen-check.ymlworkflow.3b8d97504f536aruntimes/typescript/— the canonical TS helper library (DecoderPlugin, ResultFormatter, CoordinateUtils, ASCII85, compression, MIAM, ARINC 702, ICAO FPL, types). Surfaced + fixed twoBuffer.from('ascii')calls the Web APIs migration missed inarinc_702_helper.ts(shipped separately as airframesio/acars-decoder-typescript#444).0b312e4runtimes/rust/— theads-runtimecrate scaffold (Plugin trait, helpers, ResultFormatter, coordinate, ASCII85, CRC, types). Builds clean withcargo build.f38aee7runtimes/c/— theads_runtime_cstatic library scaffold (CMake, Plugin descriptor, cJSON-backed raw bag, all decode-fn helpers, ASCII85 + base64 + hex + zlib inflate, CRC, POSIX-regex). Builds clean withcmake --build ..765a390acars-decoder-typescriptinto ADS YAML, bringing the spec coverage from 5 reference plugins to 68 total.Verification
CI workflows:
.github/workflows/ci.yml(this repo) +.github/workflows/codegen-check.yml(reusable, security-hardened, called by each language repo to verify theirgenerated/tree is up-to-date).What's NOT in this PR (intentional follow-ups)
#13extracts the ~80 test files into JSON corpus via a Jest reporter. Each language repo's CI loads the corpus and asserts deep-equality vsexpectedto catch divergence.acars-decoder-typescript#N,acars-decoder-rust#N,acars-decoder-c#N) add the submodule, wire codegen into build, replace hand-written plugins with generated ones.Known v1.1 follow-ups (file as issues against this repo after merge)
formatter_call.typeenum is too narrow. Real TS plugins use ~40ResultFormattermethods beyond what v1 enumerates (route, eta, currentFuel, temperature, flightNumber, runways, windData, mach, airspeed, state_change, door_event, frequency, atis, fault, warning, …). Drives the high escape-hatch ratio (59 of 63 newly-ported specs use whole-plugin escape hatches because their formatters aren't in the enum). Extending the enum lets most plugins become declarative.No DSL primitive for
addRemainingFields. Plugins likeLabel_44_IN/ON/OFF/ETApush trailing CSV fields intoremaining.text— the 4 declarative ports drop this. Add atrailing_fields_into_unknownparse step, or convert those 4 to escape hatches.POSIX regex doesn't support PCRE named groups —
runtimes/c/src/regex.cresolves numeric indices only. Stage 2 should swap in PCRE2 to support(?<name>...)properly for spec likeLabel_44_POS.CRC tables emission.
spec/shared/crc_tables.yamldeclares the polynomial tables but codegen doesn't emit them yet (the bitwise impls in each runtime are correct and table-free). A v1.1 pass should generate identical lookup tables into each runtime.Typed-args codegen. Args currently flow as JSON strings (
JSON.stringify(args)in emit, parse at runtime). Typed-args codegen is a perf win for hot paths.Test plan
schema/ads-v1.schema.jsonruntimes/typescript/type-checks under strict mode (npm run lint)runtimes/rust/builds clean (cargo build)runtimes/c/builds clean aslibads_runtime_c.a(cmake --build).github/workflows/ci.yml).tsfiles match the original TS plugin shape (Label_10_POS.ts,Label_44_POS.ts,ARINC_702.ts)🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Documentation