feat(playground): schema-driven dynamic args form for the Run tab#3935
Conversation
ParamSchema/FieldSchema tree for the dynamic args form: inline-expanded class/enum bodies (cycle-guarded), nullable-union folding, alias resolution, dependency-package lookup, Unsupported fallback for non-data types and TIR sentinels. Class/enum names are the canonical dotted FQN the engine registers (user.shapes.Foo) so $baml markers round-trip.
…nsports WebSocket picks the new field up automatically (source struct is serialized directly); the WASM worker path gets tsify twins plus the mandatory From mapping. None (omitted) = no schema; [] = nullary.
- pkg-proto: $baml {enum, value} marker encodes InboundEnumValue so expr
functions receive real variants (no string→enum coercion exists)
- worker-protocol: ParamSchema/FieldSchema mirror, params on
FunctionInfo, add missing 'autoDerive' origin
- ArgsForm: recursive schema-directed widgets over a controlled value
record; pure logic split into args-form-model for unit testing
- new ui primitives: Switch (radix), Select (styled native)
Form-by-default with a raw-JSON toggle; both write through one setter (setArgsJson + typedArgsByFnRef) so prompt/cURL previews, per-function memory, and run-history snapshots stay in sync. Args that don't parse to a JSON object keep raw mode with a notice. Empty args are seeded once per function from schema defaults, injecting $baml class/enum markers. No schema (params undefined) degrades to the old raw input.
Primary Run (with platform shortcut hint) moves into the args strip on the Run tab; other tabs get a compact play icon in the top bar so re-running while watching the graph stays one click. Cmd/Ctrl+Enter runs from anywhere inside the panel without stealing the binding from the host code editor. Raw-JSON fallback fields now use the ui kit Textarea.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds parameter schema metadata from Rust into the playground transport, then renders those schemas in the args editor. Enum marker serialization is updated so playground values round-trip through the wire format. ChangesBackend param schema generation and wiring
Playground args form UI and enum marker encoding
Estimated code review effort: 4 (Complex) | ~75 minutes Sequence Diagram(s)sequenceDiagram
participant list_functions_with_metadata
participant function_param_schemas
participant build_project_update
participant PlaygroundNotification
list_functions_with_metadata->>function_param_schemas: extract ParamSchema + TypeSchema
function_param_schemas-->>build_project_update: FunctionListing
build_project_update->>PlaygroundNotification: populate FunctionInfo.params and ProjectUpdate.types
PlaygroundNotification-->>build_project_update: transport metadata to webview
sequenceDiagram
participant ExecutionPanel
participant ArgsForm
participant args-form-model
participant pkg-proto
ExecutionPanel->>args-form-model: typeLookupFrom(types)
ExecutionPanel->>ArgsForm: render params + current args
ArgsForm->>args-form-model: normalizeArgs / valueMatchesSchema
ExecutionPanel->>pkg-proto: serializeValue($baml.enum)
pkg-proto-->>ExecutionPanel: enumValue payload
Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
⏭️ Performance benchmarks were skippedPerf benchmarks (CodSpeed) are opt-in on pull requests — they no longer run on every push. They always run automatically after merge to To run them on this PR, do any of the following, then push a commit (or re-run CI):
|
Binary size checks passed✅ 7 passed
Generated by |
There was a problem hiding this comment.
Actionable comments posted: 6
🧹 Nitpick comments (3)
baml_language/crates/bridge_wasm/src/wasm_playground.rs (1)
11-143: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueDuplicated wire-format enum across two crates — manual sync risk.
FieldSchema/ParamSchema/FieldSchemaFieldhere are hand-duplicated twins ofbaml_project::FieldSchema/etc. (acknowledged in the comment at Lines 41-43). Any future variant/field added to thebaml_projectschema must be remembered here too, with matchingserdetags, or the WASM and WebSocket transports will silently diverge. The exhaustivematchin theFromimpl will at least fail to compile on a missing variant, which mitigates most of the risk.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@baml_language/crates/bridge_wasm/src/wasm_playground.rs` around lines 11 - 143, The wire-format types in wasm_playground.rs are duplicated copies of the project schema types, so they can drift if new fields or variants are added. Update the conversion layer centered on the FieldSchema, ParamSchema, and FieldSchemaField definitions to stay in lockstep with the source bex_project types, and add a serialization/round-trip test that compares the WASM and WebSocket shapes. Keep the exhaustive From<bex_project::FieldSchema> match in sync so any new variant forces a compile-time update.typescript2/pkg-proto/src/encode.ts (1)
66-86: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winMalformed enum marker silently falls through to generic map serialization.
If
$baml.enumis set butvalueis missing/non-string, the object skips this branch and (sincetypeis also absent) falls into the plain-object map path at Line 118-124, which — unlike thetypemarker branch — doesn't filter out the$bamlkey. The literal$bamlobject gets serialized as a map entry, producing a confusing runtime-side error instead of a clear client-side failure.🛡️ Proposed fix: fail fast on malformed enum markers
if ( bamlMarker && typeof bamlMarker === 'object' && - typeof (bamlMarker as Record<string, unknown>).enum === 'string' && - typeof (bamlMarker as Record<string, unknown>).value === 'string' + 'enum' in (bamlMarker as Record<string, unknown>) ) { + if ( + typeof (bamlMarker as Record<string, unknown>).enum !== 'string' || + typeof (bamlMarker as Record<string, unknown>).value !== 'string' + ) { + throw new Error( + `Malformed $baml enum marker: expected { enum: string, value: string }, got ${JSON.stringify(bamlMarker)}`, + ); + } const marker = bamlMarker as { enum: string; value: string }; return { value: { $case: 'enumValue', enumValue: { name: marker.enum, value: marker.value }, }, }; }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@typescript2/pkg-proto/src/encode.ts` around lines 66 - 86, In `encode.ts` within the `bamlMarker` handling in `encodeValue`, malformed `$baml.enum` markers are currently ignored and then serialized as a plain map entry. Update this branch to fail fast when `$baml.enum` is present but `value` is missing or not a string, instead of falling through to the generic object/map path. Keep the existing enum-marker success path intact, and ensure the validation is tied to the `bamlMarker`/`marker` logic so the `$baml` object is never emitted as a map entry on invalid enum markers.typescript2/app-vscode-webview/src/ExecutionPanel.strict-mode.test.tsx (1)
177-258: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winConsider adding regression coverage for switching away and back to a seeded function.
This test never switches to another function and back after the form auto-seeds defaults. That's the exact path where the
setArgsJson/updateArgsJsondesync flagged inExecutionPanel.tsx(seed effect) would surface as lost defaults.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@typescript2/app-vscode-webview/src/ExecutionPanel.strict-mode.test.tsx` around lines 177 - 258, Add regression coverage for the seed/reset path in ExecutionPanel: after selecting a function that auto-seeds defaults, switch to another function and then back to the original one to verify argsJson stays in sync. Update the strict-mode test around ExecutionPanel, the Greet selection flow, and the args form/raw toggle to assert defaults are preserved after the reselection path. Focus the scenario on the setArgsJson/updateArgsJson behavior triggered by the seed effect in ExecutionPanel.tsx.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@typescript2/pkg-playground/src/ArgsForm.tsx`:
- Around line 205-243: The NumberField input currently calls onChange(undefined)
when the draft is emptied, which causes required numeric args to disappear from
argsJson instead of showing a visible error state. Update NumberField to treat
an empty draft as invalid for non-optional/no-default params by keeping
aria-invalid set and/or surfacing an explicit required hint, and avoid emitting
undefined in that case; use the existing NumberField, useDraft, and onChange
flow to preserve the key until the user enters a valid number.
- Around line 269-284: The placeholder option in ArgsForm’s enum Select can be
chosen and currently feeds an empty string into enumValue, creating an invalid
enum marker. Update the Select onChange handling so choosing the “select…”
placeholder does not call onChange with enumValue(schema.name, ''); instead
treat it as no selection (for example by clearing the value or ignoring the
change) while keeping the placeholder rendering based on current being
undefined. Use the existing ArgsForm Select, onChange, and enumValue logic to
make the fix.
- Around line 151-153: The bigint handling in ArgsForm is still routed through
NumberField, which coerces input to Number and loses precision before encoding.
Update the case for bigint in ArgsForm to use a bigint-aware input/parsing path
instead of the integer NumberField branch, and ensure the value passed through
props remains a bigint so encode.ts can emit it without precision loss.
In `@typescript2/pkg-playground/src/components/ui/switch.tsx`:
- Around line 1-8: The Switch component in switch.tsx uses React.ComponentProps
without importing React, so the React namespace is undefined for type-checking.
Add the React import at the top of the file, matching the import pattern used by
the other UI components, and keep the Switch function signature using
React.ComponentProps<typeof SwitchPrimitive.Root>.
In `@typescript2/pkg-playground/src/ExecutionPanel.tsx`:
- Around line 2362-2370: The Cmd/Ctrl+Enter handler in ExecutionPanel bypasses
the same build-error gate used by the Run button because it calls
onRunFunction() directly. Update the shortcut path so it respects hasErrors the
same way the button does, either by adding the guard before invoking
onRunFunction or by centralizing the disabled check inside onRunFunction itself;
make sure the keyboard shortcut cannot run while errors exist, and keep the
behavior aligned with the existing Run button state.
- Around line 2054-2071: The seed initialization in ExecutionPanel’s args
seeding effect is bypassing the single source of truth by calling setArgsJson
directly, which leaves typedArgsByFnRef out of sync and causes seeded defaults
to disappear when the function is reselected. Update this effect to use
updateArgsJson instead of setArgsJson so the seeded JSON is written through the
same path as user edits, and keep the seededFnsRef/typedArgsByFnRef checks
intact to avoid reseeding already-typed functions.
---
Nitpick comments:
In `@baml_language/crates/bridge_wasm/src/wasm_playground.rs`:
- Around line 11-143: The wire-format types in wasm_playground.rs are duplicated
copies of the project schema types, so they can drift if new fields or variants
are added. Update the conversion layer centered on the FieldSchema, ParamSchema,
and FieldSchemaField definitions to stay in lockstep with the source bex_project
types, and add a serialization/round-trip test that compares the WASM and
WebSocket shapes. Keep the exhaustive From<bex_project::FieldSchema> match in
sync so any new variant forces a compile-time update.
In `@typescript2/app-vscode-webview/src/ExecutionPanel.strict-mode.test.tsx`:
- Around line 177-258: Add regression coverage for the seed/reset path in
ExecutionPanel: after selecting a function that auto-seeds defaults, switch to
another function and then back to the original one to verify argsJson stays in
sync. Update the strict-mode test around ExecutionPanel, the Greet selection
flow, and the args form/raw toggle to assert defaults are preserved after the
reselection path. Focus the scenario on the setArgsJson/updateArgsJson behavior
triggered by the seed effect in ExecutionPanel.tsx.
In `@typescript2/pkg-proto/src/encode.ts`:
- Around line 66-86: In `encode.ts` within the `bamlMarker` handling in
`encodeValue`, malformed `$baml.enum` markers are currently ignored and then
serialized as a plain map entry. Update this branch to fail fast when
`$baml.enum` is present but `value` is missing or not a string, instead of
falling through to the generic object/map path. Keep the existing enum-marker
success path intact, and ensure the validation is tied to the
`bamlMarker`/`marker` logic so the `$baml` object is never emitted as a map
entry on invalid enum markers.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: 875b74e6-bcd1-4abd-a808-27a133b75218
⛔ Files ignored due to path filters (1)
baml_language/Cargo.lockis excluded by!**/*.lock
📒 Files selected for processing (18)
baml_language/crates/baml_project/Cargo.tomlbaml_language/crates/baml_project/src/lib.rsbaml_language/crates/baml_project/src/param_schema.rsbaml_language/crates/baml_project/src/symbols.rsbaml_language/crates/bex_project/src/bex_lsp/mod.rsbaml_language/crates/bex_project/src/bex_lsp/multi_project/mod.rsbaml_language/crates/bex_project/src/lib.rsbaml_language/crates/bridge_wasm/src/wasm_playground.rstypescript2/app-vscode-webview/src/ExecutionPanel.strict-mode.test.tsxtypescript2/pkg-playground/src/ArgsForm.tsxtypescript2/pkg-playground/src/ExecutionPanel.tsxtypescript2/pkg-playground/src/args-form-model.test.tstypescript2/pkg-playground/src/args-form-model.tstypescript2/pkg-playground/src/components/ui/select.tsxtypescript2/pkg-playground/src/components/ui/switch.tsxtypescript2/pkg-playground/src/worker-protocol.tstypescript2/pkg-proto/src/encode.tstypescript2/pkg-proto/src/test/encode-decode.test.ts
- NumberField: empty/unparseable drafts are an error state, not a deletion — required numeric keys no longer silently drop from argsJson - enum Select: the placeholder option is inert instead of emitting an empty-string variant marker - Cmd/Ctrl+Enter respects the build-error gate like the Run buttons - seed defaults write through updateArgsJson so they survive switching functions and back (regression test added) - encode.ts: malformed $baml enum markers fail fast instead of serializing as a plain map - switch.tsx: import React explicitly like the other ui components - bridge_wasm: round-trip test pinning tsify twins to the bex_project serialization
Implements the TASK/REVIEW.md handoff. Named types now serialize once into a shared per-project table (ProjectUpdate.types, keyed by canonical dotted FQN) and are referenced by FieldSchema::Ref, so payload is proportional to distinct types instead of paths through the type graph (the inline-expansion P0.1 reproducer went from 88 MB to a few KB, pinned by byte-ceiling regression tests). Aliases are memoized into the table too — deliberate deviation from the handoff, which kept non-recursive aliases inline: inlining re-expands the target per reference site and is exponential on alias DAGs (reproduced 1.9 MB / 4 s on a 16-line file). Recursive types lose the raw-JSON cut-point: class sections resolve refs lazily on expand, guarded by a ref-visited set threaded through same-value hops so self-referential alias schemas (`type A = A | int` compiles clean) degrade to raw JSON instead of recursing the render. Also fixes, each with regression tests: - P0.2: drop the compiler-injected trailing `client: baml.llm.Client` param from LLM function schemas. - P0.3: union chips honor the explicit choice while the value inhabits it — `float` is now reachable in `int | float`. - P1.4a/b: skip extraction for $-companions and non-userDefined functions; give capture_cfg_snapshot a schema-free name listing. - P1.5: hydration normalizes bare enum strings and markerless class objects into wire markers, writing through the shared setter. - P1.6: seeding and normalization resolve the authoritative typed ?? argsByFunction ?? initialArgsJson chain (baseArgsFor) instead of the one-commit-stale parsedArgs, so host seeds can't be clobbered. - P1.7: Cmd/Ctrl+Enter no longer starts invisible runs from the collection/test-run views. - P1.8: RawJsonField keeps the last committed value on empty/invalid drafts instead of deleting map rows. - P1.9: unknown schema tags and dangling refs route to the raw-JSON path; defaults never return undefined. - P2.10-16 hygiene, including a wire-shape golden fixture asserted byte-identical by the Rust extractor test and validated against the worker-protocol.ts mirror. E2E-verified (13 checks) in promptfiddle over the rebuilt WASM worker: enum markers evaluate on expr functions, namespaced FQN markers, one-level seeding, hydration round-trip, recursive and alias params. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Conflict resolution: canary #3938 replaced the eager per-compile CFG snapshot (capture_cfg_snapshot / RetainedCfgSnapshots) with a lazy per-run RunCfgCache, deleting the function our P1.4b fix had switched to a schema-free listing. Took canary's side — the lazy rewrite supersedes P1.4b entirely (no function listing happens on build at all) — and removed the now-caller-less list_playground_function_names helper from baml_project. Verified post-merge: cargo check/test on baml_project, bex_project, bridge_wasm; pnpm typecheck + tests in pkg-proto (after regenerating the renamed baml_bridge protos), pkg-playground, app-vscode-webview. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@baml_language/crates/baml_project/src/param_schema.rs`:
- Around line 246-300: The named-type expansion in param_schema.rs resets
recursion depth by calling field_schema(field_ty, 0) and field_schema(resolved,
0), which allows long acyclic class/alias chains to recurse one frame per hop
and risk stack overflow. Update the TypeSchema::Class and Ty::TypeAlias
expansion paths in field_schema to thread through the current depth instead of
restarting at 0, or introduce a dedicated recursion limit for named-type
expansion, while preserving the existing memoization behavior in self.table and
lookup_type.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: 6f116edb-d737-49f3-aa6b-71f077a3c38b
⛔ Files ignored due to path filters (1)
baml_language/Cargo.lockis excluded by!**/*.lock
📒 Files selected for processing (17)
baml_language/crates/baml_project/src/lib.rsbaml_language/crates/baml_project/src/param_schema.rsbaml_language/crates/baml_project/src/symbols.rsbaml_language/crates/bex_project/src/bex_lsp/mod.rsbaml_language/crates/bex_project/src/bex_lsp/multi_project/mod.rsbaml_language/crates/bex_project/src/lib.rsbaml_language/crates/bridge_wasm/src/wasm_playground.rstypescript2/app-vscode-webview/src/ExecutionPanel.strict-mode.test.tsxtypescript2/pkg-playground/src/ArgsForm.tsxtypescript2/pkg-playground/src/ExecutionPanel.tsxtypescript2/pkg-playground/src/__fixtures__/param-schema-golden.jsontypescript2/pkg-playground/src/args-form-model.test.tstypescript2/pkg-playground/src/args-form-model.tstypescript2/pkg-playground/src/components/ui/select.tsxtypescript2/pkg-playground/src/param-schema-golden.test.tstypescript2/pkg-playground/src/worker-protocol.tstypescript2/pkg-playground/tsconfig.json
🚧 Files skipped from review as they are similar to previous changes (6)
- baml_language/crates/bex_project/src/lib.rs
- baml_language/crates/baml_project/src/lib.rs
- baml_language/crates/bex_project/src/bex_lsp/multi_project/mod.rs
- typescript2/pkg-playground/src/components/ui/select.tsx
- typescript2/pkg-playground/src/ArgsForm.tsx
- typescript2/pkg-playground/src/ExecutionPanel.tsx
wire_shape_matches_the_ts_golden_fixture byte-compares include_str! of the fixture against serde_json output; autocrlf on the Windows runner checked the fixture out with CRLF and failed the assert. Pin the fixture to LF in .gitattributes (same precedent as *.baml) and normalize CRLF in the test for stale checkouts that predate the attribute. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Class/alias bodies expand nested within the referencing frame, so resetting depth to 0 at each body let long acyclic named-type chains recurse one native frame per hop with MAX_DEPTH never firing — a generated chain of thousands of classes could overflow the stack (the WASM worker's is ~1 MB). Thread the current depth through class/alias body expansion instead; memoization is unchanged, and a body first reached past the bound bakes an Unsupported tail into its table entry (raw-JSON fallback in the UI). Regression test: a 101-class chain extracts bounded with an unsupported tail. Addresses the PR review comment on param_schema.rs depth resets. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Summary
Replaces the raw-JSON args input in the playground Run tab with a schema-driven form: selecting a function now renders one typed widget per parameter (primitives, enums, classes with nested fields, optionals, lists, maps, unions), while serializing into the existing
argsJsonpipeline so the run path, prompt/cURL previews, per-function memory, and run history are unchanged. A raw-JSON toggle and per-field raw fallbacks remain for anything the form can't render.How it works
Schema extraction (Rust). New
baml_project::param_schemabuilds a self-containedParamSchema/FieldSchematree per function from the Salsa-cachedpackage_interface: class fields and enum variants are expanded inline (cycle-guarded — recursive types cut to a raw-JSON node),T | nullfolds intoOptional, aliases resolve before dispatch, dependency-package (stdlib) types resolve through their own interface, and everything non-data (generics, interfaces, TIR sentinels while mid-edit) degrades toUnsupportedinstead of erroring.Transport. The schema rides as
params?: ParamSchema[]onFunctionInfoin theupdateProjectnotification, on both transports (WebSocket serializes the source struct directly; the WASM worker path gets tsify twins +Frommapping).undefinedmeans no schema (UI degrades to raw-only, old binaries keep working);[]means nullary — the distinction is load-bearing.Names are canonical dotted FQNs. Class/enum names in schemas (and the
$bamlmarkers the form emits) use exactly the form the engine registers:user.shapes.Foo,baml.time.PlainDate. This round-trips verbatim through the encoder and resolves in the engine registry without fallbacks, and makes nested class values decode as real instances — something raw-JSON users couldn't express ergonomically before.New enum wire marker.
serializeValuenow honors{ "$baml": { "enum": "user.Color", "value": "Red" } }→InboundEnumValue. Nothing on the args path coerces plain strings into enum variants, so expr functions previously received strings andparam == Color.Redwas silently false; the form now produces real VM variants.UI.
ArgsFormdispatches recursively on schema node type (mirroringValueRenderer's conventions), with pure logic split into a unit-testedargs-form-model. Form and raw input write through a single setter so previews and per-function memory never desync; empty args are seeded once per function from schema defaults (injecting required keys and$bamlmarkers automatically); parameters with declared defaults render as omitted with a "set" toggle. New thin UI primitives:Switch(radix) andSelect(styled native).Run ergonomics. The primary Run button (with a platform hint,
⌘↵/Ctrl+↵) is docked in the args strip on the Run tab; other tabs keep a compact play icon in the top bar so re-running while watching the graph stays one click. Cmd/Ctrl+Enter runs from anywhere inside the panel (scoped so it never steals the binding from the host code editor).Testing
baml_project: 12 new unit tests (enum, nested/namespaced/recursive class, recursive alias, unions, maps, unresolved and generic params, stdlib class, defaults, nullary).pkg-proto: enum-marker encoding tests, top-level and nested positions.pkg-playground: 14 model tests; typecheck clean.app-vscode-webview: integration tests for form rendering →argsJsonserialization →startRun, the nullary state, the no-schema degradation path, and the keyboard shortcut.c == Color.Green→true; nested class instance decodes ("Ada from Paris"); namespaceduser.shapes.Boxresolves (42); omitted defaulted param gets the engine default (15); form edits preserve unknown raw-JSON keys.Out of scope
Media upload widgets (raw-JSON per-field for now), generic class instances (encoder sends
typeArgs: []), test-args editing (tests run engine-side by name), and hydrating the form from a previous run's decoded input.Summary by CodeRabbit
$bamlenum-marker encoding/decoding, including nested and malformed marker handling.