Skip to content

Commit 3f697d5

Browse files
committed
feat(cli): promote Zig as canonical executable
1 parent f330f06 commit 3f697d5

31 files changed

Lines changed: 488 additions & 85 deletions

.dirtree-state

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ annotate=[
55
MEMORIES/WASM benchmarks must stream inputs and APE needs a clean environment = WASI and Cosmopolitan runtime requirements for benchmark adapters.
66
PLAN.md = Active project plan; records measured Zig codec performance wins, a rejected scalar ASCII experiment, and remaining API-design questions.
77
bm/benchmark-zig-opt = Benchmarks all available CLI variants, including Rust stdin, WASM, and APE adapters.
8+
build_all = Builds the current-platform C, Zig, WASM, and Linux APE distributions through the canonical build script.
89
docs/plans/2026-06-26-printable-binary-file-container-design.md = Design spec: printable-binary-file.json container + web decode workflow (issue #1)
910
elixir/lib/printable_binary.ex = Elixir ~PB compile-time sigil: decodes printable-binary glyphs to raw bytes at compile time (reads character_map.txt via @external_resource); decode/1 runtime helper (whitespace-tolerant)
1011
elixir/mix.exs = Mix project for the ~PB sigil demo (no deps)
@@ -15,8 +16,11 @@ annotate=[
1516
rust/src/main.rs = Minimal Rust CLI (encode default, -d decode; stdin->stdout) for cross-impl verification
1617
src/container_json.h = Shared pure transport-resistant flat-JSON helpers for the .pbf.json container (C FFI + standalone C)
1718
src/zig/ffi.zig = C ABI (FFI) export surface: all 12 pb_* C functions; root of libprintable_binary.a; keeps C symbols OUT of the importable printable_binary module so static (musl) consumers don't collide
19+
src/zig/main.zig = Zig canonical CLI adapter with input-throughput status reporting and compact-map codec integration.
1820
src/zig/printable_binary.zig = Pure Zig codec core; default encode/decode use a portable 16-byte SIMD literal-prefix gate, padded encode slots, and compact direct UTF-8 decode tables.
1921
test/module_consumer.zig = Test fixture: minimal downstream importer of the printable_binary Zig module (mirrors how difz/blip consume it) for the FFI-symbol-leak test
22+
test/test_build_all = Hermetic delegation test proving build_all invokes the complete build target.
23+
test/test_cli_layout = Nix-backed behavioral test for canonical Zig and retained LuaJIT executable names.
2024
test/test_container = Container (.pbf.json) CLI round-trip + self-verify test, parameterized by IMPLEMENTATION_TO_TEST (issue #1)
2125
test/test_container_cross = Cross-impl container differential: impl-A container decodes via impl-B (MFIC, issue #1)
2226
test/test_docs_benchmark_contract = Guards implementation docs, mapping labels, benchmark inventory, and Mechatron CI manifest/badge contract.

.mechatron-prime/targets

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,16 @@ checks.x86_64-linux.test-container-ffi
99
checks.x86_64-linux.test-container-lua
1010
checks.x86_64-linux.test-container-node
1111
checks.x86_64-linux.test-container-zig
12+
checks.x86_64-linux.test-build-all
13+
checks.x86_64-linux.test-cli-layout
1214
checks.x86_64-linux.test-docs-benchmark-contract
1315
checks.x86_64-linux.test-elixir
1416
checks.x86_64-linux.test-embedded-map-sync
1517
checks.x86_64-linux.test-ffi-cli
1618
checks.x86_64-linux.test-js
1719
checks.x86_64-linux.test-js-unit
1820
checks.x86_64-linux.test-leak
21+
checks.x86_64-linux.test-lua
1922
checks.x86_64-linux.test-no-ffi-symbols
2023
checks.x86_64-linux.test-rust
2124
checks.x86_64-linux.test-zig

FUTURE_WORK.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
# Future Work / Proposed Ideas
22

3-
- CLI wrapper resilience: add a smoke test so `bin/printable-binary` errors clearly when neither LuaJIT nor a WASM runner is available.
3+
- [x] Canonical CLI migration: `test/test_cli_layout` verifies that the Zig
4+
executable installs as `printable-binary`, LuaJIT remains explicitly available
5+
as `printable-binary-luajit`, and Linux `build_all` includes APE.
46
- JS/Node perf: cache the parsed `character_map` inside `js/printable_binary.js` to avoid repeated fs reads in tight loops.
57
- Map override parity: allow `PRINTABLE_BINARY_MAP` in the WASM test harness, mirroring Node/Deno behavior.
68
- CI ergonomics: run `test_all` per implementation (LuaJIT, C, APE, Node) in parallel to shorten logs and isolate failures.

Makefile

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ STRIP ?= strip
2626

2727
# Zig build outputs
2828
ZIG_LIB = zig-out/lib/libprintable_binary.a
29-
ZIG_CLI = zig-out/bin/printable-binary-zig
29+
ZIG_CLI = zig-out/bin/printable-binary
3030

3131
COSMOCC_VERSION ?= 4.0.2
3232
COSMOCC_URL ?= https://cosmo.zip/pub/cosmocc/cosmocc-$(COSMOCC_VERSION).zip
@@ -379,7 +379,7 @@ benchmark: $(TARGET)
379379
# Compare with LuaJIT version
380380
.PHONY: compare
381381
compare: $(TARGET)
382-
@if [ ! -f bin/printable-binary ]; then \
382+
@if [ ! -f bin/printable-binary-luajit ]; then \
383383
echo "Error: LuaJIT version not found"; \
384384
exit 1; \
385385
fi
@@ -392,13 +392,13 @@ compare: $(TARGET)
392392
@time $(BIN_DIR)/$(TARGET) compare_test.bin > compare_c_encoded.tmp 2>/dev/null
393393
@echo
394394
@echo "LuaJIT version encoding:"
395-
@time ./bin/printable-binary compare_test.bin > compare_lua_encoded.tmp 2>/dev/null
395+
@time ./bin/printable-binary-luajit compare_test.bin > compare_lua_encoded.tmp 2>/dev/null
396396
@echo
397397
@echo "C version decoding:"
398398
@time $(BIN_DIR)/$(TARGET) -d compare_c_encoded.tmp > compare_c_decoded.tmp 2>/dev/null
399399
@echo
400400
@echo "LuaJIT version decoding:"
401-
@time ./bin/printable-binary -d compare_lua_encoded.tmp > compare_lua_decoded.tmp 2>/dev/null
401+
@time ./bin/printable-binary-luajit -d compare_lua_encoded.tmp > compare_lua_decoded.tmp 2>/dev/null
402402
@echo
403403
@echo "Verifying output compatibility:"
404404
@if cmp compare_c_encoded.tmp compare_lua_encoded.tmp; then \
@@ -422,13 +422,13 @@ hyperfine: $(TARGET)
422422
echo "Running hyperfine benchmark..."; \
423423
hyperfine --warmup 3 \
424424
"$(BIN_DIR)/$(TARGET) hyperfine_test.bin" \
425-
"./bin/printable-binary hyperfine_test.bin" \
425+
"./bin/printable-binary-luajit hyperfine_test.bin" \
426426
--export-markdown benchmark_results.md; \
427427
echo "Encode benchmark results saved to benchmark_results.md"; \
428428
$(BIN_DIR)/$(TARGET) hyperfine_test.bin > hyperfine_encoded.tmp 2>/dev/null; \
429429
hyperfine --warmup 3 \
430430
"$(BIN_DIR)/$(TARGET) -d hyperfine_encoded.tmp" \
431-
"./bin/printable-binary -d hyperfine_encoded.tmp" \
431+
"./bin/printable-binary-luajit -d hyperfine_encoded.tmp" \
432432
--export-markdown decode_benchmark_results.md;
433433
echo "Decode benchmark results saved to decode_benchmark_results.md"; \
434434
rm -f hyperfine_test.bin hyperfine_encoded.tmp; \

PLAN.md

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,23 @@ maintained_by: agent
2020
build. Pushed as `1281b38`; Mechatron reported `PASSING` on
2121
2026-07-22 10:28 PM EDT.
2222

23+
## Canonical Zig CLI migration — active (2026-07-24 EDT)
24+
25+
- [x] Make the compiled Zig CLI the `printable-binary` installation and PATH
26+
name; retain the legacy LuaJIT CLI as `printable-binary-luajit`.
27+
Curiosity poke: package, development, benchmark, documentation, and
28+
direct-script entry points must all select the same implementation while
29+
both CLIs remain independently usable. Nix package/install, local `bin/`,
30+
benchmark discovery, docs, and `test-cli-layout` now agree; `./build_all`
31+
builds Linux C, Zig, WASM, and APE outputs. (2026-07-24 02:49 PM EDT)
32+
- [x] Add a common stderr processed-input throughput line to every CLI
33+
implementation. Curiosity poke: rate must use elapsed wall time and
34+
input bytes (the stable denominator across encode/decode), must not
35+
pollute stdout, and must remain finite for tiny inputs. Every CLI now
36+
emits `Input throughput: … MB read … (… MB/s)` with decimal MB and MB/s
37+
rounded to two places; tests cover encode/decode where supported.
38+
(2026-07-24 02:49 PM EDT)
39+
2340
## Deferred performance investigation
2441

2542
- [x] Baseline the deterministic 10 MB mixed-byte core benchmark against Rust,
@@ -202,7 +219,7 @@ ALL-EXECUTABLES STATUS (Peter's ask):
202219
- [ ] Zig CLI (src/zig/main.zig) — core has crc32; use std.json for the envelope.
203220
- [ ] C FFI CLI (src/printable_binary_ffi_main.c) — pb_crc32 + hand-rolled flat JSON.
204221
- [ ] C standalone (src/printable_binary.c) — own crc32 + hand-rolled flat JSON.
205-
- [ ] Lua (bin/printable-binary) — own crc32 + Lua JSON.
222+
- [ ] Lua (bin/printable-binary-luajit) — own crc32 + Lua JSON.
206223
PATTERN for each: add `-C` (encode→container, `-d -C` decode→restore+self-verify),
207224
crc32 VECTOR-PINNED to CRC32("123456789")=0xCBF43926 (so all impls agree), then add
208225
a test-container-<impl> flake check running test/test_container against it. Once ≥2

README.md

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -91,18 +91,21 @@ The optional PGO path (`make pgo-ffi`) adds a further ~1–3% via profile-guided
9191

9292
```bash
9393
# Use any implementation:
94-
# LuaJIT version: ./bin/printable-binary
94+
# Zig CLI (default): ./build zig && ./bin/printable-binary
95+
# LuaJIT CLI: ./bin/printable-binary-luajit
9596
# Node.js CLI: ./bin/printable-binary-node.js
9697
# C version: ./build native && ./bin/printable-binary-c
9798
# APE version: ./build ape && ./bin/printable-binary-ape.com
98-
# Zig version: ./build zig && ./bin/printable-binary-zig
9999
# WASM version: ./build wasm && wazero run bin/printable-binary.wasm < input.bin
100100
# Rust raw codec: cargo build --release --manifest-path rust/Cargo.toml
101101
# < input.bin rust/target/release/printable-binary-rs
102-
# (Examples below use the LuaJIT full CLI. Rust intentionally offers only
102+
# (Examples below use the canonical Zig full CLI. Rust intentionally offers only
103103
# stdin→stdout encoding and -d/--decode; the other listed CLI variants share
104104
# the full option surface.)
105105

106+
# Build every compiled distribution on Linux (C, Zig, WASM, and APE).
107+
./build_all
108+
106109
# Encode binary data
107110
echo -n "Hello, World!" | ./bin/printable-binary
108111
# Output: Hello,␣World﹗
@@ -154,6 +157,16 @@ echo -n "Hello, World!" | ./bin/printable-binary --passthrough 2>encoded.txt | w
154157
./bin/printable-binary-c large_file.bin > encoded_large.txt
155158
```
156159

160+
Every CLI writes a final stderr line for informal comparisons:
161+
162+
```text
163+
Input throughput: 4999.36 MB read in 49.939 s (100.11 MB/s)
164+
```
165+
166+
`MB` means decimal input megabytes (1,000,000 bytes). The rate uses bytes read,
167+
not the larger encoded result, and covers input read, codec work, and output
168+
write. Set `PRINTABLE_BINARY_MUTE_STATS=1` to suppress it.
169+
157170
### Web Interface
158171

159172
- Live demo: <https://pmarreck.github.io/printable_binary/>

README_IMPLEMENTATIONS.md

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ PrintableBinary is available in multiple high-performance implementations:
2424
- **Full CLI parity**: Same arguments and mapping behavior as the native C binary
2525
- **Measured honestly**: The benchmark includes wazero startup and runtime overhead
2626

27-
### 🦎 **Zig Implementation** (Modern, Memory-Safe)
27+
### 🦎 **Zig Implementation** (Canonical CLI)
2828
- **Memory-safe**: Zig's safety features catch bugs at compile time and runtime
2929
- **Cross-compilation**: Easy cross-compilation to many platforms from a single host
3030
- **Fast compilation**: Incremental builds and fast compile times
@@ -41,7 +41,7 @@ PrintableBinary is available in multiple high-performance implementations:
4141
- **Portable runtime**: Runs in browsers, Node.js, and compatible bundlers
4242
- **CLI parity**: Node exposes the full user-facing command surface
4343

44-
### **LuaJIT Implementation** (Original)
44+
### **LuaJIT Implementation** (Explicit Legacy CLI)
4545
- **Reference implementation**: Easy to modify and extend
4646
- **Well-tested**: Extensive test suite and battle-tested
4747
- **Development-friendly**: Rapid prototyping and debugging
@@ -115,11 +115,11 @@ WASM has the C CLI's feature set. The runtime does not automatically inherit hos
115115
```bash
116116
# Build with Nix
117117
nix build .#printableBinaryZig
118-
./result-zig/bin/printable-binary-zig file.bin
118+
./result-zig/bin/printable-binary file.bin
119119

120120
# Or build directly with Zig
121121
zig build -Doptimize=ReleaseFast
122-
./zig-out/bin/printable-binary-zig file.bin
122+
./zig-out/bin/printable-binary file.bin
123123
```
124124

125125
### Rust Implementation
@@ -141,8 +141,8 @@ Use Rust as an embedded codec or a Rust↔Zig transport boundary. Its CLI curren
141141

142142
```bash
143143
# Already optimized and ready to use
144-
./bin/printable-binary file.bin
145-
./bin/printable-binary -d encoded_file.txt
144+
./bin/printable-binary-luajit file.bin
145+
./bin/printable-binary-luajit -d encoded_file.txt
146146
```
147147

148148
## Installation Options
@@ -490,7 +490,7 @@ dd if=/dev/urandom of=test.bin bs=1M count=1
490490

491491
# Binary compatibility test
492492
./bin/printable-binary-c test.bin > c_output.txt
493-
./bin/printable-binary test.bin > lua_output.txt
493+
./bin/printable-binary-luajit test.bin > lua_output.txt
494494
cmp c_output.txt lua_output.txt && echo "✓ Outputs identical"
495495
```
496496

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -699,6 +699,34 @@ if arg then
699699
end
700700
end
701701

702+
-- Use a wall clock for CLI pipeline timing; fall back to Lua CPU time on a
703+
-- non-POSIX LuaJIT host where gettimeofday is unavailable.
704+
local now_seconds = os.clock
705+
if ffi.os ~= "Windows" then
706+
local cdef_ok = pcall(ffi.cdef, [[
707+
struct timeval { long tv_sec; long tv_usec; };
708+
int gettimeofday(struct timeval *tv, void *tz);
709+
]])
710+
if cdef_ok then
711+
local tv = ffi.new("struct timeval[1]")
712+
now_seconds = function()
713+
if ffi.C.gettimeofday(tv, nil) == 0 then
714+
return tonumber(tv[0].tv_sec) + tonumber(tv[0].tv_usec) / 1000000
715+
end
716+
return os.clock()
717+
end
718+
end
719+
end
720+
721+
local function report_input_throughput(input_bytes_read, started_at)
722+
local elapsed = math.max(now_seconds() - started_at, 0.0005)
723+
local megabytes = input_bytes_read / 1000000
724+
stats_write(string.format(
725+
"Input throughput: %.2f MB read in %.3f s (%.2f MB/s)\n",
726+
megabytes, elapsed, megabytes / elapsed
727+
))
728+
end
729+
702730
-- Main program
703731
if #arg == 0 then
704732
-- Check if stdin is a terminal
@@ -987,13 +1015,17 @@ if arg then
9871015
os.exit(0)
9881016
end
9891017

1018+
-- Measure the user-visible pipeline: input read, codec work, and output write.
1019+
local throughput_started_at = now_seconds()
1020+
9901021
-- Read input data
9911022
local input_data
9921023
if file_path then
9931024
input_data = read_file(file_path)
9941025
else
9951026
input_data = read_stdin()
9961027
end
1028+
local input_bytes_read = #input_data
9971029

9981030
-- Apply byte range if specified
9991031
if range_start ~= nil or range_end ~= nil then
@@ -1076,6 +1108,7 @@ if arg then
10761108
os.exit(1)
10771109
end
10781110
io.write(decoded)
1111+
report_input_throughput(input_bytes_read, throughput_started_at)
10791112
os.exit(0)
10801113
else
10811114
local data = PrintableBinary.encode(input_data, { spaces = spaces_mode })
@@ -1088,6 +1121,7 @@ if arg then
10881121
.. json_escape(fname) .. '",\n "byte_length": ' .. #input_data
10891122
.. ',\n "crc32": "' .. crc_orig .. '",\n "crc32_encoded": "' .. crc_enc
10901123
.. '",\n "data": "' .. data .. '"\n}\n')
1124+
report_input_throughput(input_bytes_read, throughput_started_at)
10911125
os.exit(0)
10921126
end
10931127
end
@@ -1258,6 +1292,8 @@ if arg then
12581292
-- Normal encode mode, send output to stdout
12591293
io.write(output)
12601294
end
1295+
1296+
report_input_throughput(input_bytes_read, throughput_started_at)
12611297
end
12621298

12631299
return PrintableBinary

0 commit comments

Comments
 (0)