Skip to content

docs: correct Hoon stdlib signatures that mislead readers into broken code - #267

Open
sigilante wants to merge 3 commits into
urbit:masterfrom
sigilante:docs/pr3-hoon-signatures
Open

docs: correct Hoon stdlib signatures that mislead readers into broken code#267
sigilante wants to merge 3 commits into
urbit:masterfrom
sigilante:docs/pr3-hoon-signatures

Conversation

@sigilante

Copy link
Copy Markdown
Contributor

Second of three PRs from an audit against urbit/urbit@08026c84b2. This batch is arms whose documented signature or behaviour no longer matches source. Every claim was reproduced on a fake ship booted from urbit-408k-rc1.pill ([%zuse 408]) unless noted as source-only.

Companion PRs: mechanical corrections, and user-manual commands.

!@ zappat — branches are reversed

87adc94d16 (2025-07-21, "!@ reverse polarity") changed the compile of %zppt from ?: to ?.. .q is now the does-not-exist branch and .r the exists branch. Verified both directions:

> =foo 42
> !@(foo 'exists' 'does not exist')
'does not exist'
> !@(baz 'exists' 'does not exist')
'exists'

The Discussion and both example outputs asserted the opposite, so a reader following them writes inverted logic that still compiles. Examples rewritten so the branch labels match the new order. No rationale is given upstream (empty commit body), so none is invented here.

+trim — dry tape gate → wet list gate

Now |* [a=@ b=(list)] / ^+ [p=b q=b] (hoon.hoon:4317). The example outputs also changed, since the product is no longer cast to $tape:

> (trim 5 "lasok termun")
[p=[i='l' t="asok"] q=[i=' ' t="termun"]]

Added a generic-list example and a note that only the inferred type and its printed form changed, not the value.

+bif:by — takes the key alone, and old callers fail silently

Signature went from |* [b=* c=*] to |* b=* (hoon.hoon:1445), and it no longer inserts. Critically, the old two-argument call does not error — with sample b=* the arguments arrive as the single cell [b c], so the map is split at that cell instead of at the key:

> `[(map @tas @) (map @tas @)]`(~(bif by a) %b)
[{[p=%e q=5]} {[p=%d q=4] [p=%a q=1] [p=%c q=3]}]

> `[(map @tas @) (map @tas @)]`(~(bif by a) b+2)
[{[p=%e q=5] [p=%b q=2] [p=%d q=4] [p=%a q=1]} {[p=%c q=3]}]

Note [p=%b q=2] survives in the second — the key was never removed. The doc's own examples used that broken form, so the page was demonstrating the failure while claiming the correct output.

+inde / +iny — new delimiter arguments

+inde is now |* [[ope=rule end=rule] sef=rule] (hoon.hoon:4910, d5af20fec1). The documented one-argument call fails to compile with mull-grow / -find.sef. +iny is now simply ++ iny inde, where the doc carried a stale 19-line copy of the old body.

The replacement example is not invented — +inde's only call site in the tree is +quote-innards parsing """ block strings (hoon.hoon:11842), so it mirrors that shape, verified verbatim:

> `tape`(scan "\"\"\"\0afoo\0abar\0a\"\"\"" (inde [(jest '"""') (jest '"""')] (star ;~(pose prn (just `@`10)))))
"foo\0abar"

+mink / +mock / +mack~ scry sentinel

The scry argument is now $@(~ $-(^ (unit (unit)))) (hoon.hoon:6021, :6182). Passing ~ means "no scry handler": ordinary formulas evaluate, a Nock 12 blocks immediately.

> (mink [[1 2] [0 2]] ~)
[%0 product=1]
> (mink [0 [12 [1 0] [1 0]]] ~)
[%2 trace=~]

+mong was checked and is unchanged — still a bare gate — so it is deliberately left alone.

+shas — salt-length behaviour

Now =/ len (max 32 (met 3 sal)) / (shay len ...) (hoon.hoon:3314). Worth stating precisely: a 1-byte salt and a 40-byte salt both still agree with the old formula. The forms diverge only when the XOR result has fewer significant bytes than max(32, (met 3 sal)) — when leading bytes cancel to zero. Confirmed by constructing such a salt.

Smaller

  • +peg (1b.md) — undocumented crash on a zero second argument; source has ?< =(0 b) alongside ?< =(0 a). Verified both (peg 4 0) and (peg 0 4) crash.
  • +apt:in (2h.md) — gained strict-inequality checks at every node. (+apt:by was checked and is already correct.)
  • +dif:by (2i.md) — dropped its =+ b=a |@ ++ $ wrapper for |* b=_a and now calls (bif p.n.b) with one argument. Knock-on from the +bif change. +dif:by itself is correct upstream — verified it still returns the right difference and ~(apt by ...) on the result is %.y; only the listing was stale.
  • +del:in / +del:by (2h.md, 2i.md) — source blocks gained => .(b ...). Source-verified only: the coercion does not produce a mint error for a wrong-typed key ((~(del in s) "abc") returns the set unchanged), so no behavioural claim is documented.

🤖 Generated with Claude Code

sigilante and others added 3 commits August 4, 2026 11:45
Four arms whose documented behaviour no longer matches sys/hoon.hoon at
urbit/docs.urbit.org@08026c84b2. Every claim below was reproduced on a fake ship
(v4.6 pill) rather than inferred from source.

!@ "zappat" (rune/zap.md): branches are REVERSED.
  Upstream 87adc94d16 (2025-07-21, "!@ reverse polarity") changed the
  compile of %zppt from ?: to ?., so .q is now the does-not-exist branch
  and .r the exists branch. Verified both directions in dojo:
    =foo 42
    !@(foo 'exists' 'does not exist')  ->  'does not exist'
    !@(baz 'exists' 'does not exist')  ->  'exists'
  The doc's Discussion and both example outputs asserted the opposite, so
  a reader following them writes inverted logic that still compiles. The
  examples are rewritten so the branch labels match the new order. No
  rationale is given upstream (empty commit body), so none is invented here.

+trim (stdlib/4b.md): dry tape gate -> wet list gate.
  Now `|*  [a=@ b=(list)]` / `^+  [p=b q=b]` (hoon.hoon:4317), not
  `|=  [a=@ b=tape]` / `^-  [p=tape q=tape]`. Both example OUTPUTS also
  changed, because the product is no longer cast to $tape:
    (trim 5 "lasok termun") -> [p=[i='l' t="asok"] q=[i=' ' t="termun"]]
    (trim 5 "zam")          -> [p=[i='z' t="am"] q=~]
  Added a generic-list example and a note that only the inferred type and
  its printed form changed, not the value.

+bif:by (stdlib/2i.md): now takes the key alone and never inserts.
  Signature went from `|*  [b=* c=*]` to `|*  b=*` (hoon.hoon:1445).
  Critically, the old two-argument call does NOT error: with sample b=*,
  the arguments arrive as the single cell [b c], so the map is split at
  that cell instead of at the key and the item at .b survives in the
  product. Verified:
    (~(bif by a) %b)   -> splits at %b, %b excluded  (correct)
    (~(bif by a) b+2)  -> splits at [%b 2], returns a pair still
                          containing [p=%b q=2]      (silently wrong)
  The doc's own examples used the b+2 form, so they were demonstrating the
  broken call. Rewritten to the key-only form with verified output, plus a
  Discussion note that old callers fail silently rather than loudly.

+peg (stdlib/1b.md): undocumented crash on a zero second argument.
  Source has `?<  =(0 b)` alongside `?<  =(0 a)` (hoon.hoon:213); the doc
  showed only the .a guard. Verified both (peg 4 0) and (peg 0 4) crash.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Second batch for PR-3. Sources checked against urbit/docs.urbit.org@08026c84b2. Items are
marked below as ship-verified (reproduced on a fake ship, v4.6 pill) or
source-verified (read from sys/hoon.hoon only).

+mink / +mock / +mack (4n.md) -- ship-verified.
  The scry argument is now `$@(~ $-(^ (unit (unit))))`, not a bare gate
  (hoon.hoon:6021, :6182). Passing `~` means "no scry handler": ordinary
  formulas still evaluate, but a Nock 12 immediately blocks. Verified:
    (mink [[1 2] [0 2]] ~)            -> [%0 product=1]
    (mink [0 [12 [1 0] [1 0]]] ~)     -> [%2 trace=~]
  +mack correspondingly calls `(mink [sub fol] ~)` rather than
  `|~(^ ~)`. Documented the sentinel in +mink's Accepts with both cases.
  NOTE: +mong (4n.md:534) was checked and is UNCHANGED -- it still takes a
  bare gate (hoon.hoon:6256). Left alone deliberately.

+shas (3d.md) -- ship-verified.
  Now `=/  len  (max 32 (met 3 sal))` / `(shay len ...)`, not `(shax ...)`
  (hoon.hoon:3314). The audit characterised this as "differs for salts over
  32 bytes"; that is wrong. Verified that a 1-byte salt and a 40-byte salt
  both agree with the old formula. The forms diverge only when the XOR
  result has fewer significant bytes than max(32, (met 3 sal)) -- i.e. when
  leading bytes cancel to zero. Constructed such a salt and confirmed
  divergence (27.383... vs 71.053...).

+apt:in (2h.md) -- source-verified.
  Gained strict-inequality checks at every node: `&((gor n.a u.l) !=(...))`
  where the doc showed a bare `(gor n.a u.l)` (hoon.hoon:1224).
  NOTE: +apt:by in 2i.md was checked and is ALREADY CORRECT; the audit
  reported it as drifted. No change made there.

+dif:by (2i.md) -- source-verified, knock-on from the +bif change.
  Dropped its `=+ b=a |@ ++ $` wrapper for a direct `|*  b=_a`, and now
  calls `(bif p.n.b)` with one argument instead of `(bif p.n.b q.n.b)`
  (hoon.hoon:1478). Not previously connected to the +bif finding.

+del:in / +del:by (2h.md, 2i.md) -- source-verified, source block only.
  Both gained `=>  .(b `_?>(?=(^ a) ...)`b)` (hoon.hoon:1254, :1461). The
  audit claimed this makes a wrong-typed key a mint error rather than a
  silent no-op. That was NOT reproducible: `(~(del in s) "abc")` and
  `(~(del by mm) [1 2])` both return the container unchanged with no error.
  The source block is corrected; the behavioural claim is deliberately NOT
  documented, since it could not be demonstrated.

DEFERRED: +inde / +iny (4f.md, 4i.md). Source has the new
`|*  [[ope=rule end=rule] sef=rule]` signature, but the shipping v4.6 pill
does not: the documented one-argument example still works on the ship and
the two-argument form fails. This change is in develop ([%zuse 408]) but not
in the released kernel ([%zuse 409]), so the current docs are correct for
anyone running released Vere. Blocked on the same 408-vs-409 decision as the
kelvin updates.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…n 408

Follow-up to the previous two PR-3 commits. These were deferred because they
could not be verified on the pill I had booted; that pill was one kelvin
behind. Re-verified on a fake ship booted from urbit-408k-rc1.pill, which
reports [%zuse 408] -- the current kernel. (Kelvin counts down, so 408 is
newer than the 409 of the v4.6 pill used earlier.)

+inde (4f.md): signature changed from `|*  sef=rule` to
  `|*  [[ope=rule end=rule] sef=rule]` (hoon.hoon:4910, upstream d5af20fec1,
  2025-12-17, shipped to develop in the 408 merge a92492ed4b). The body was
  rewritten as well. The documented one-argument call now fails to compile:
    (inde (star ...))  ->  mull-grow / -find.sef
  Signature, body, Accepts and the worked example are all replaced. The new
  example was verified verbatim on the ship:
    > `tape`(scan "\"\"\"\0afoo\0abar\0a\"\"\"" (inde [(jest '\"\"\"') (jest '\"\"\"')] (star ;~(pose prn (just `@`10)))))
    "foo\0abar"
  The delimiter form is not invented: it mirrors the kernel's own use in
  +quote-innards (hoon.hoon:11842) for parsing \"\"\" block strings, which is
  the only call site in the tree. Confirmed `'\"\"\"'` and `(@t 0x22.2222)`
  are the same cord before writing the literal form.

+iny (4i.md): source is now just `++  iny  inde` (hoon.hoon:5258); the doc
  carried a stale 19-line copy of the old +inde body. Replaced with the
  one-line source, a pointer to +inde, and a verified example.

+bif:by (2i.md): examples regenerated on 408. Under the newer kernel the cast
  product pretty-prints as maps rather than structurally, so the corrected
  call now yields exactly what the docs had wrongly attributed to the old
  two-argument form:
    > `[(map @tas @) (map @tas @)]`(~(bif by a) %b)
    [{[p=%e q=5]} {[p=%d q=4] [p=%a q=1] [p=%c q=3]}]
  The silent-failure note now carries its own captured output, which shows
  [p=%b q=2] still present in the product:
    > `[(map @tas @) (map @tas @)]`(~(bif by a) b+2)
    [{[p=%e q=5] [p=%b q=2] [p=%d q=4] [p=%a q=1]} {[p=%c q=3]}]

Re-verified unchanged on 408: !@ polarity, +trim product and printing, +peg
crash on a zero argument, +mink's ~ scry sentinel yielding [%2 trace=~].

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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