Skip to content

Nothing here ran zig build; it does not build on the version it declares - #97

Merged
gHashTag merged 7 commits into
mainfrom
fix/analyse-public-surface
Aug 11, 2026
Merged

Nothing here ran zig build; it does not build on the version it declares#97
gHashTag merged 7 commits into
mainfrom
fix/analyse-public-surface

Conversation

@gHashTag

Copy link
Copy Markdown
Owner

Nothing in this repository ran zig build. Adding a CI that does, plus refAllDeclsRecursive in src/root.zig — the module root every consumer imports, which no test target rooted — makes the state visible for the first time.

The package does not build on the version it declares

minimum_zig_version = "0.15.0", and both consumers (zig-hdctrinity) build with 0.15.2. The public surface spans three incompatible Zig versions:

targets evidence
0.14 Io.getStdOut, atomic.fence, fmt.fmtSliceHexLower, fs.File.OpenFlags.read — all removed in 0.15
0.15 what the manifest claims
0.16 tools/gen/* use std.Io, std.Io.Dir, std.process.Init, and say so in their own comments

Plus five genuine defects that are not version drift at all:

expected type '*HybridBigInt', found '*const HybridBigInt'
expected type 'i2', found 'i8'
incompatible types: 'u32' and 'i32'
incompatible types: 'u64' and 'f64'
value with comptime-only type 'comptime_float' depends on runtime control flow

The first and third are the same errors that surfaced from zig-hdc in gHashTag/zig-hdc#2 — so they originate here, not there.

What this PR does and does not do

It does: add a readable CI, force the module root through the compiler, and move the 0.16-only tri_gen out of the default install so the library is not made unbuildable to keep a tool nobody can run at 0.15. That tool is still built by zig build tools, and CI asserts the step still exists so it cannot be silently dropped.

It does not repair the drift. That is nine sites across three version targets, and each needs a decision about which version this package is actually for.

Left open rather than merged. Landing it turns main red with no repair attached, and a permanently red build is the condition that destroys a signal's information content. The finding belongs in review, not in a build nobody can read.

…be read

src/root.zig is what every consumer imports, and no test target rooted it, so
the one surface that matters was the one never compiled. Zig analyses top-level
declarations lazily: a green test run proved only that the declarations the
other tests happened to reference compile.

That gap is not hypothetical. The same omission in gHashTag/zig-hdc hid five
distinct API-drift errors against the version of THIS package that it pins,
while its CI stayed green throughout.

The new workflow is separate from test-bindings.yml on purpose. That one has
been failing for its own reasons, and a verdict arriving into an already-red
workflow carries no information -- the point of this file is to produce a
verdict that can be read.
zig build failed on 0.15.2, the version declared in minimum_zig_version and the
one both consumers use. The library was never the problem: the only failure was
tools/gen/tri_gen.zig, which is written against Zig 0.16 on purpose -- its own
comments say so, and it uses std.Io, std.Io.Dir and std.process.Init, none of
which exist in 0.15.

Installing a 0.16-only tool by default made the LIBRARY unbuildable for
everybody in order to keep a tool nobody can run at that version. It moves
behind an explicit `zig build tools` step, so the version claim becomes true
rather than aspirational.

This is not turning a build green by deleting what failed. What failed is still
built, by a step that names the toolchain it needs, and CI asserts that step
still exists so it cannot be silently dropped.

None of this was visible before, because nothing in this repository ran
zig build at all.
…lares

Eleven errors across five files, from the full-surface analysis added in the
previous commit. They fall into two kinds and it is worth keeping them apart.

Four were APIs that existed in 0.14 and were removed in 0.15, so this code had
been left behind by a version bump nobody could see because nothing ran
zig build:

  std.fmt.fmtSliceHexLower  -> {x} on the slice
  std.io.getStdOut          -> std.fs.File.stdout().writer(&buf).interface
  std.atomic.fence(.acquire)-> folded into fetchSub(1, .acq_rel), which is
                               where that acquire always belonged
  OpenFlags .read/.write    -> .mode = .read_write

Seven were plain defects, none of them about versions:

  sacredChecksum multiplied a u64 by the f64 PHI and then called @intFromFloat
  on the u64 result. It has never compiled, so no stored checksum can depend on
  it; it now multiplies by 2^64/phi, which is what phi IS in integer hashing
  rather than a number I picked.

  getEffectivePriority switched on a runtime value into comptime_float arms.
  Annotated f64.

  qbind declared *const and called bind, which calls ensureUnpacked, which fills
  a cache inside the value and so must mutate it. qbind takes mutable pointers
  now: making bind const would have been a lie about what it does, and qbind has
  no callers yet.

  applyPhase took @abs of an i32, which is a u32, and asked for it modulo an
  i32. The modulus is unsigned now.

  Three sites assigned an i8 from unpacked_cache into an i2 without a cast. The
  compiler named three; there are six, the other three differing only in whether
  they read a or b. Fixing the named three would have left half the defect
  standing.
Fixing eleven errors uncovered five more, which is the normal shape: a compiler
stops at the first failure in a unit and the ones behind it are invisible until
it does not.

applyPhase declared *const and called permute, which takes a mutable pointer for
the same reason bind does -- the value caches its own unpacked form. Mutable
now, and it has no callers, so nothing else moves.

Three shift sites packed a u2 into a byte with `encoded << bit_offset`. Zig
types a shift amount by the width of what is being shifted, so a u2 admits only
a u1 -- meaning a bit_offset of 0, 2, 4 or 6 could not be used there at all. The
value is going into a u8, so it is widened first and the amount typed to match.

init bound the interface with const and then called ping, which takes *Self. A
const binding cannot hand out the mutable pointer a method asks for.

I left one thing alone deliberately. The `if (bit_offset >= 6)` branch writes
the spill of a 2-bit code into the next byte, and a 2-bit code at offset 6
occupies bits 6 and 7 without spilling, so that branch looks dead. That is a
question about the packing, not about the types, and answering it by changing
behaviour while fixing a compile error is how a repair becomes a regression.
…ready done

Two more shift sites, this time reading rather than writing: response[byte_idx]
is a u8, so the amount has to be a u3. Same rule as the packing side.

And the sign extension in similarity. `if (dot_msb & 0x04)` handed a u8 to an
`if`, which takes a bool, so this line never compiled -- and what it reached for
had already happened: @bitcast from u11 to i11 reads the top bit as the sign,
and bit 2 of dot_msb IS that bit. The line also or-ed 0xF800 into an i11, a
value that does not fit in one, so it could not have run even with a well typed
condition. Removed rather than repaired, because there is nothing left for it to
do.
The mask already narrows the value to two bits; the cast states what the mask
guarantees, and decodeTrit takes a u2.
With the surface compiling, the tests ran, and 266 of 267 passed. The one that
did not panicked on integer overflow at its first iteration: i is a usize, so
@Rem(i, 3) is a usize, and 0 - 1 in unsigned arithmetic is not -1. The cycle it
wanted is -1, 0, +1, which needs a signed type to live in.

This test has never run. It could not have: the file it is in did not compile
until four commits ago.
@gHashTag
gHashTag merged commit 68a24af into main Aug 11, 2026
6 of 8 checks passed
@gHashTag
gHashTag deleted the fix/analyse-public-surface branch August 11, 2026 06:14
gHashTag added a commit to gHashTag/zig-hdc that referenced this pull request Aug 11, 2026
gHashTag/zig-golden-float#97 repaired sixteen defects across that package,
including the two this one tripped over: HybridBigInt without an allocator
field, and the u32/i32 mismatch. Both originated there.
gHashTag added a commit to gHashTag/zig-hdc that referenced this pull request Aug 11, 2026
The hash came from the runner, which has the toolchain that computes it. The
previous pin predated gHashTag/zig-golden-float#97, so sixteen repaired defects
were still arriving here through the dependency.
gHashTag added a commit to gHashTag/zig-hdc that referenced this pull request Aug 11, 2026
* One implementation of src/vsa/*, not two

Both this repository and gHashTag/zig-golden-float carried src/vsa/core.zig,
common.zig, concurrency.zig, 10k_vsa.zig, hrr.zig and fpga_bind.zig. They were
edited independently and they diverged, which is why repairing sixteen defects
in golden-float (#97) left every one of them standing here: the fixes went into
different files with the same names.

The decision was made by measurement rather than preference. Comparing the two
public surfaces symbol by symbol, they are identical -- one constant apart --
so neither copy is more capable and there is nothing to lose by choosing. The
direction then follows the dependency that already exists: this package depends
on golden-float, golden-float's copies build and pass 267 tests on 0.15.2, and
these do not.

Each duplicated file becomes a one-line re-export instead of being deleted. That
keeps all twenty-three relative imports across this repository working exactly
as they did, so nothing else in the tree moves, and it makes re-divergence
impossible rather than merely discouraged: there is one implementation behind
the path now.

gen_core.zig and gen_encoding.zig stay as they are. golden-float does not export
them, so they are genuinely this package's own, and so are the other eighteen
files under src/vsa/ that have no counterpart there.

* Name the re-exports one by one: usingnamespace is gone in 0.15

The first attempt re-exported each duplicated file with usingnamespace, which
was removed in Zig 0.15 -- the version this package targets -- so the build
answered 'expected function or variable declaration after pub'. The names are
listed individually now, generated from the upstream sources rather than typed.

That is a real cost and worth naming: a symbol added upstream does not appear
here until it is added here too. It is still cheaper than a second
implementation, and unlike a second implementation it fails loudly, because the
name is simply missing rather than quietly different.

MAX_PACKED_BYTES has no counterpart in golden-float's vsa/common.zig, so it
comes from packed_trit, where the value actually lives, rather than being
written out as a literal. Nothing here uses it, but it was in this module's
public surface and dropping it silently would break a consumer who does.

* Analyse the whole public surface, now that there is one implementation behind it

The build went green with the duplicated files re-exporting golden-float, but
green without this line means only that the declarations the tests happen to
walk compile. That is exactly the condition that hid five API-drift errors here
while the badge stayed green.

Now the surface goes through the compiler as a whole, and the green means what
a consumer would take it to mean.

* Re-pin golden_float, and drop a re-export that never pointed at anything

Two things the full-surface analysis found, neither of them caused by the
deduplication.

This branch was cut from main, which pins the golden-float from before its
sixteen defects were repaired -- so atomic.fence and the rest of the 0.14 API
came back through the dependency rather than from any file here. Re-pinned.

And src/vsa.zig re-exported concurrency.LockFreePool, which has never existed
in this repository's concurrency module nor in golden-float's. Both export the
same twenty-three names and that is not one of them. The line referred to
nothing and nothing complained, because lazy analysis never asked what it
pointed at.

* Temporary: have CI print the golden_float hash

The local zig used to compute it does not survive between sessions here and the
download keeps timing out, so the runner that already has the right toolchain
computes it instead. Removed in the next commit.

* Pin the repaired golden_float

The hash came from the runner, which has the toolchain that computes it. The
previous pin predated gHashTag/zig-golden-float#97, so sixteen repaired defects
were still arriving here through the dependency.

* text_encoding calls bundle2 and add with an allocator they no longer take

Nine call sites passed std.heap.page_allocator to core.bundle2 and to
HybridBigInt.add. Both take their operands and nothing else -- bundle2(a, b) and
add(self, other) -- so the argument is dropped rather than replaced: there is
nothing for it to become.

This file is one of the eighteen under src/vsa/ that have no counterpart in
golden-float, so it is genuinely this package's own, and it had been calling an
API that moved without it. It compiled for nobody and nothing said so, because
nothing referenced it.

* permuted must be var: add takes a mutable pointer

The value caches its own unpacked form, so filling that cache is a mutation and
add is declared *Self. A const binding cannot hand out the pointer it asks for.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant