The Machines · Core numerics

bool

The Binary Machine — machines/bool.shoddy

the bool machine's icon

Summary

Shoddy has no bitwise builtins, and this machine is why you do not need any. Bitwise operations work on the individual binary digits — the bits — of a number. And, Or and Not are Boolean operators only: there is no integer AND, no XOR (exclusive or), no shift, no rotate and no way to reach a double's bits. bool supplies all of it as ordinary words — number bases, bit patterns, masks and fields, two's complement (the standard binary form for negative numbers), the codes (Gray, BCD, parity, Hamming), the logic gates the operators do not give you, truth tables (every input combination and its output), and Quine–McCluskey minimisation. Every word and type is prefixed Bool. Pure throughout — no word here touches anything outside its own answer.

Two things a caller gets wrong, so both are said here first. Exactly three words are width-freeBoolBitAnd, BoolBitOr and BoolBitXor — because their answers do not depend on the width at all. A width parameter there would buy nothing but noise. Everything else whose answer depends on where the word ends takes one. The single word that needs none for a different reason is BoolShr: a logical right shift cannot produce a bit the input did not have. And variable 1 is the most significant bit of a row index, matching every digital-logic textbook: row 5 of a 3-variable table is { True, False, True }.

The Story Behind the Machine

Joseph Marie Jacquard (1752–1834) put on show in Lyon in 1804 a loom head that read its pattern from a chain of punched cards. Hole or no hole, one bit per hook, one card per pick. It was the first machine to take its instructions from data rather than from its own construction. Charles Babbage took the card from it for the Analytical Engine, Herman Hollerith for the 1890 United States census, and IBM from Hollerith. The eighty-column card lying on a computer-room floor in 1970 is a direct descendant of a device for weaving figured silk.

It is the right story twice over. A Jacquard card is a bit vector, exactly and without metaphor — which is why BoolCard renders a word as one and keeps the ancestry visible at the call site. And a Jacquard head is a loom. West Riding mills ran them for figured worsteds through the whole nineteenth century. For a language named after a textile trade, the punched card is the binary story that already lives in the mill.

Why It's Useful

One fact sits underneath everything here: with no bitwise operators, every bit operation is Mod and 2 ^ n written out at the call site. Extracting a length field from a binary frame becomes four lines of Floor and Mod that look plausible and are wrong one time in five. This machine writes that arithmetic once, gets the edges right, and puts a name on it.

The domain is checked, never masked. Every value is a non-negative whole number below 253, and every word errors naming itself on anything else — BoolBitAnd(-1, 1) refuses rather than answering. That is the design decision the rest of the machine hangs from. Silently masking a bad input is what makes bit code impossible to debug: the answer is plausible, the bug is upstream, and nothing points at it.

The width ceiling is honest. Widths run 1 to 53, because doubles — the floating-point numbers Shoddy computes with — hold whole numbers exactly to 253, and a 54-bit word cannot represent its own top bit. 2 ^ 53 + 1 is 2 ^ 53, so a rotation would lose a bit without complaining. Three words are written to reduce before they multiply or add, because the naive formula overflows 253 before its Mod could run: (a * 2 ^ n) Mod 2 ^ width forms a product that has already been rounded. A word exact only to width 26 while the banner claimed 53 would be precisely the plausible-wrong-answer failure this machine exists to prevent.

What it makes possible, none of which anything in the tree does today because nothing in the tree can:

Not eng, and not alg. The three do not overlap and none includes another. eng is continuous mathematics over Number — trigonometry, calculus, units, constants. alg is symbolic algebra over an expression type. This machine is discrete — how an integer is written and how truth values combine. Base conversion lives here because a hex formatter belongs beside the bits it renders and not beside the determinant.

User's Guide

Include the file and the words are yours. Nothing here touches a file, a socket or the screen.

Include "bool.shoddy"

Def Main()
    ' A status register, 1011 0100. There are no hex literals in Shoddy,
    ' so a fixture is decimal with the binary in a comment.
    Let reg = 180
    Print(BoolBinW(reg, 8))                  ' 10110100
    Print(BoolCard(reg, 8))                  ' the punched-card row
    Print(Str(BoolGetField(reg, 5, 2)))      ' 13 - bits 5..2
    If BoolBitAt(reg, 7) Then
        Print("MODE BIT SET")

    ' Write the layout down once, decode in one call.
    Let fields = { BoolField("MODE", 7, 6), BoolField("COUNT", 5, 2),
                   BoolField("FLAGS", 1, 0) }
    Let vals = BoolDecode(reg, fields)
    Each(Range(1, Length(fields)),
         Fn(k) => Print(FName(Nth(fields, k)) & " = " & Str(Nth(vals, k))))

Errors, Result and Option are three different answers, and the machine keeps them apart deliberately. A negative, non-integer or out-of-width argument is a bug in the calling program: it aborts, naming the word. Reading external text is not a bug, so BoolFromBase and BoolFromBcd return the language's Result — a bad digit is a fact about the input, exactly as a malformed document is for JsonRead. And absence is neither: BoolLowBit(0) and BoolHighBit(0) answer None(), because "no bits set" is an ordinary answer in a scan. No −1 sentinel — a special value standing in for "nothing" — appears anywhere in the machine.

Bit patterns and truth values never share a name. A word operating on a bit pattern carries Bit in the name (BoolBitAnd, BoolBitXor). A word operating on Booleans is the bare gate name (BoolAnd, BoolXor). So BoolBitAt returns a Boolean and never 0 or 1. If BoolBitAt(reg, 5) Then is the sentence you want, and BoolBitAt(reg, 5) = 1 is a type error rather than a subtle one. BoolPopCount returns a Number, and exists precisely because folding + over a list of Booleans is not a thing.

A Boolean function is a quotation, not a parsed string. BoolTruthTable, BoolMinterms, BoolEquiv and BoolIsTautology all take [ List Of Boolean -- Boolean ]. There is no expression syntax here and no parser — no "A AND (B OR C)" anywhere. That is the same line eng draws against symbolic algebra. The output side is text.

Def Carry(vs As List Of Boolean) As Boolean
    BoolOr(BoolAnd(Nth(vs, 1), Nth(vs, 2)),
           BoolAnd(Nth(vs, 3), BoolXor(Nth(vs, 1), Nth(vs, 2))))

Let terms = BoolMinimise(BoolMinterms(Carry, 3), 3)
Print(BoolSopText(terms, { "A", "B", "Cin" }))    ' AB + ACin + BCin

Lists compare by identity, so nothing here hides one in a Result. { 2, 3 } = { 2, 3 } is False, which means a caller writing = Ok(...) against a list payload would silently never match. Every Result and Option payload in this machine is a Number, so BoolFromBase("ff", 16) = Ok(255) is a valid test. BoolTerm and BoolField are all-scalar and compare with =. BoolRow carries a list and does not, so assert on Output and on Inputs element-wise.

The scale ceiling is enforced, not merely stated. Truth tables and minimisation are comfortable to n = 8 (256 rows), slow at 10 and hopeless at 16, because a List is a linked list — a 65,536-element table is not a slow answer but a hang. BoolMaxVars() is 12, and every word that takes a variable count refuses above it, so the far end is a named error rather than a stall.

Word Reference

Constants

Zero-arg Defs, as a machine constant must be: a machine publishes a constant with a Def that returns it. Where a word and a constant compete for a name, the word wins and the constant is suffixed — hence BoolHexR, beside the BoolHex formatter.

WordDescription
BoolBin() / BoolOct() / BoolDec() / BoolHexR()The radix constants: 2, 8, 10, 16. Pass one to BoolInBase rather than writing the number.
BoolBits()32 — the conventional default width. Offered, never imposed.
BoolMaxWidth()53 — the arithmetic ceiling, and the largest width any word accepts.
BoolMaxVars()12 — the hard refusal for truth tables and minimisation. The comfort figure is 8.
BoolDigits()The uppercase digit alphabet for bases 2..36.

Bases and rendering

WordDescription
BoolInBase(n, base)n written in the given base (2..36), uppercase letters past 9. BoolInBase(0, b) is "0".
BoolFromBase(s, base)Result: Ok(n), or Err(why, at) with the 1-based offset of the offending character — 0 when there is no position. Case-insensitive, so "ff" and "FF" both give Ok(255). A sign is not a digit: "-1" is an Err at 1.
BoolFromBaseOr(s, base, dflt)The one-liner over it, in the shape of str's ToBoolOr and json's JGetOr.
BoolHex(n) / BoolBinS(n) / BoolOctS(n)The three shortcuts. BoolBinS and BoolOctS carry the S because the bare names are the radix constants.
BoolHexW(n, digits)Hex zero-padded to a digit count. Never truncates — a value wider than the field comes back whole.
BoolBinW(n, width)Binary zero-padded to a bit count, same rule.
BoolGrouped(s, size, sep)A digit string grouped from the right, following str's CommaGroup and the hex/binary convention: BoolGrouped("110101100", 4, " ") is "1 1010 1100", the short group leading.
BoolCard(a, width)The punched-card row, most significant bit leftmost. Not decoration: a 32-bit mask printed as 4294901760 tells a reader nothing and "FFFF0000" tells them a little, where sixteen filled holes followed by sixteen empty ones tells them at once. File I/O is Latin-1, so a card written to a file becomes question marks — the default glyphs are for a terminal.
BoolCardWith(a, width, on, off)The same row in glyphs you choose, which is how you get one a file can hold.

Bit operations

WordDescription
BoolBitAnd(a, b)Width-free. BoolBitAnd(12, 10) is 8 whether the word is 8 bits or 48.
BoolBitOr(a, b)Width-free.
BoolBitXor(a, b)Width-free. These three are the only ones.
BoolBitNot(a, width)Complement within the width — which is why it needs one.
BoolBitNand / BoolBitNor / BoolBitXnor (a, b, width)The complemented pairs, each within the width.
BoolShl(a, n, width)Left shift; bits above the width fall off. The doomed bits are dropped before the multiply, not after.
BoolShr(a, n)Logical right shift, and the one shift that needs no width: it cannot produce a bit the input did not have.
BoolShrA(a, n, width)Arithmetic right shift — the sign bit is replicated. A separate word because the difference is the single most common bit-shifting bug there is: BoolShr on a negative two's-complement value gives a large positive number and nothing tells you.
BoolRotl / BoolRotr (a, n, width)Rotates. The distance is taken mod the width, so a full turn is the identity.

Masks, fields and inspection

WordDescription
BoolMask(n)The low n bits set. BoolMask(0) is 0.
BoolMaskFrom(hi, lo)Bits hi..lo set, inclusive, 0-based from the low end.
BoolTest(a, mask)Boolean: are all the mask's bits set in a?
BoolTestAny(a, mask)Boolean: is any of them?
BoolSet / BoolClear / BoolToggle (a, mask)Set, clear or flip the mask's bits.
BoolGetField(a, hi, lo)A bit field, shifted down to zero. Named BoolGetField and not BoolField because that name is the type's constructor, and the two would be a duplicate definition at the same arity.
BoolSetField(a, hi, lo, v)Insert one. Errors when the value does not fit, rather than quietly writing its low bits and corrupting the neighbouring field.
Type BoolFieldA named field: FName, HiBit, LoBit (inclusive, 0-based). Not Name/Hi/Lo, because hi and lo are parameters on three words here and a local outranks a field accessor of the same name silently.
BoolDecode(a, fields)List Of BoolFieldList Of Number. Write the register layout down once and get every value out in one call — the word that makes this machine worth including for embedded work. The names do not come back: the result is positionally aligned with the field list. Overlapping fields are allowed, because real registers document overlapping views.
BoolBitAt(a, k)Boolean, never 0 or 1.
BoolSetBit(a, k, on)One bit, set from a Boolean.
BoolPopCount(a)Number: how many bits are set.
BoolLowBit(a) / BoolHighBit(a)Option: Some(index), or None() when nothing is set.
BoolLog2Floor(a)BoolHighBit's strict twin, for a caller who wants a Number. Aborts on zero, which has no logarithm.
BoolLeadZeros(a, width)Leading zeros within the width; width when a is 0. It survives beside BoolHighBit because it genuinely needs a width.
BoolReverse(a, width)The bits end to end.
BoolBitList(a)List Of Number: the indices that are set, ascending.
BoolFromBitList(ks)The inverse. Set semantics, so duplicates are idempotent; an index outside 0..52 aborts.
BoolIsPow2(a) / BoolNextPow2(a)BoolIsPow2(0) is False; BoolNextPow2(0) is 1, BoolNextPow2(1) is 1 and BoolNextPow2(5) is 8.

There is no BoolTrailZeros. The count of trailing zeros is by definition the index of the lowest set bit, so it would be BoolLowBit under a second name with a worse contract at zero.

Two's complement and fixed-width arithmetic

WordDescription
BoolSigned(a, width)The bit pattern read as the signed value it represents: BoolSigned(255, 8) is −1.
BoolUnsigned(v, width)The other way. The one word whose input may be negative, its domain being −(2width−1) to 2width−1−1: BoolUnsigned(-128, 8) is 128 and BoolUnsigned(128, 8) errors.
BoolSignExtend(a, from, to)Widen, replicating the sign bit. Requires 1 <= from <= to <= 53.
BoolNeg(a, width)Two's complement negation. Zero negates to zero — the case an implementation built on BoolBitNot plus one gets wrong.
BoolAddW / BoolSubW / BoolMulW (a, b, width)Wrapping arithmetic, exact at width 53. The add compares before it adds and the multiply is shift-and-add reducing at every step, because a single product needs up to 2106 and the naive form is exact only to about width 26.
BoolCarries(a, b, width)Boolean: would the unsigned add carry out?
BoolOverflows(a, b, width)Boolean: would the signed add overflow? Two words because they answer different questions — at width 8, 127 + 1 overflows signed but does not carry, and 255 + 1 carries but does not overflow.

Codes

The reverse trap of this machine: these all look like they should take Booleans, and all take Numbers.

WordDescription
BoolGray(n) / BoolFromGray(g)Reflected Gray code, both ways. Consecutive values differ in exactly one bit.
BoolParity(a)Boolean: True when the number of set bits is odd.
BoolParityBit(a)Number: the even-parity bit to append. These two look like one word and are not: that is the predicate, this is the bit.
BoolHamming(a, b)How many bit positions differ.
BoolBcd(n)Decimal to packed BCD, one nibble (four bits) per digit. Packed BCD holds 13 nibbles in 53 bits, so 10 ^ 13 and above aborts.
BoolFromBcd(b)Result: an encoding read from outside, so a nibble above 9 is a fact about the input, with At the 1-based nibble position from the low end.
BoolFromBcdOr(b, dflt)The one-liner over it.

Logic gates

All on Boolean. BoolAnd, BoolOr and BoolNot duplicate built-in operators and ship anyway, because an operator cannot be passed as a bare name. BoolTruthTable and BoolEquiv need them as ordinary words they can hold in a quotation.

WordDescription
BoolAnd / BoolOr / BoolNotThe three the language already has as operators.
BoolNand / BoolNor / BoolXor / BoolXnorThe four it does not.
BoolImplies(p, q)False on exactly one row: p true, q false.
BoolIff(p, q)Material equivalence — the same table as BoolXnor, under the name a logician reaches for.

Truth tables

All of these refuse a variable count below 1 or above BoolMaxVars().

WordDescription
Type BoolRowInputs As List Of Boolean, Output As Boolean. Carries a list, so a BoolRow does not compare with =.
BoolRowInputs(k, n)Row k's input vector. Variable 1 is the most significant bit, so BoolRowInputs(5, 3) is { True, False, True }. This word fixes the order once and everything else depends on it.
BoolRowIndex(vs, n)The same conversion the other way.
BoolTruthTable(f, n)List Of BoolRow, in row order.
BoolMinterms(f, n) / BoolMaxterms(f, n)The rows where f is True, and where it is False.
BoolFromMinterms(ms, n)Returns a quotation — a closure over the minterm set. Closes the loop: minimise a function, rebuild it from its minterms, and BoolEquiv the two.
BoolIsTautology(f, n) / BoolIsContradiction(f, n)True on every row, and true on none.
BoolEquiv(f, g, n)Exhaustive comparison — and the only way to compare two quotations at all, since a quotation compares by reference identity.
BoolTableText(f, n, names)The table as text, one row per line.

Minimisation

Quine–McCluskey, in the two stages it is taught in. First, combine adjacent terms until nothing more combines — the survivors are the prime implicants. Then cover the minterms (the input rows where the function is true) by taking the essential prime implicants first and greedily filling the rest. The cover step is greedy — the result is minimal in practice, not proven minimal, because exact cover is NP-hard: no known method finds the guaranteed best answer quickly.

WordDescription
Type BoolTermBits (the fixed values), Care (1 where Bits matters, 0 where the variable is dropped) and Vars (how many variables the term is over). Care rather than Mask because BoolMask is a word; Vars is carried so two terms of different widths cannot be combined by accident. All-scalar, so it compares with =.
BoolPrimeImplicants(minterms, n)The first stage on its own, for a caller who wants the implicant chart rather than one cover of it.
BoolMinimise(minterms, n)A minimised sum of products. The empty minterm set gives the empty term list; a full cover gives one term with every variable dropped.
BoolMinimiseWith(minterms, dontCares, n)The same with don't-cares: they may grow an implicant but need not be covered. A don't-care that is also a minterm aborts — a caller mistake, not a set to reconcile.
BoolTermCovers(t, m) / BoolTermMinterms(t)Does the term cover this minterm, and which ones does it cover.
BoolTermText(t, names)The textbook notation: a dropped variable omitted, a complemented one with a trailing apostrophe — A'BC. A term with every variable dropped is the constant and renders as "1".
BoolSopText(terms, names)"A'B + BC'". The empty term list is "0" — the empty sum.
BoolPosText(terms, names)"(A + B')(B' + C)". The empty term list is "1", and that pair with BoolSopText's "0" is what makes the algebra close.
BoolTermSumText(t, names)One clause of the above, on its own.

BoolPosText is not a free dual of a SOP term list. Rendering BoolMinimise's output as a product of sums would be meaningless. A minimised product-of-sums is the minimised sum-of-products of the complement, read through De Morgan: each product term becomes a sum clause and every literal flips. So the terms to hand BoolPosText are BoolMinimise(BoolMaxterms(f, n), n) — not BoolMinimise(BoolMinterms(...)).

The refusals

Every message this machine can raise, each naming the word that raised it. Shoddy has no catchable errors, so these stop the program — that is the point. The two Result-returning words are the deliberate exception and are listed last.

MessageWhen
{WORD}: '{a}' IS NEGATIVE - THE DOMAIN IS WHOLE NUMBERS FROM 0Any word, on a negative value.
{WORD}: '{a}' IS NOT A WHOLE NUMBERAny word, on a fraction. The test is a <> Floor(a).
{WORD}: '{a}' IS ABOVE THE 53-BIT CEILING - WHOLE NUMBERS ARE EXACT ONLY TO 2 ^ 53Any word, on a value no longer exactly representable.
{WORD}: WIDTH '{w}' IS OUTSIDE 1..53Any width-taking word.
{WORD}: '{a}' DOES NOT FIT {w} BITS - THE LIMIT IS {max}A value at or above 2 ^ width.
{WORD}: BIT INDEX '{k}' IS OUTSIDE 0..52BoolBitAt, BoolSetBit, BoolMaskFrom, BoolGetField, BoolSetField, BoolFromBitList.
{WORD}: BIT COUNT '{n}' IS OUTSIDE 0..53BoolMask, BoolHexW, BoolBinW.
{WORD}: SHIFT '{n}' IS OUTSIDE 0..53The shifts and rotates.
{WORD}: BASE '{b}' IS OUTSIDE 2..36BoolInBase, BoolFromBase. A bad base is a caller bug, not bad input.
{WORD}: {n} VARIABLES IS OUTSIDE 1..12 - COMFORTABLE TO 8, SLOW AT 10Every truth-table and minimisation word.
{WORD}: MINTERM '{m}' IS OUTSIDE 0..{max} FOR {n} VARIABLESA minterm the variable count cannot hold.
BOOLGROUPED: GROUP SIZE '{n}' IS NOT A WHOLE NUMBER FROM 1A group size of zero would not terminate.
BOOLMASKFROM: HIGH BIT '{h}' IS BELOW LOW BIT '{l}'An inverted field, in BoolMaskFrom and everything over it.
BOOLSETFIELD: '{v}' DOES NOT FIT BITS {hi}..{lo}A value too wide for the field it is going into.
BOOLUNSIGNED: '{v}' IS NOT A WHOLE NUMBERIts own check, since this is the one word taking a signed value.
BOOLUNSIGNED: '{v}' IS NOT REPRESENTABLE IN {w} SIGNED BITSOutside −(2w−1) to 2w−1−1.
BOOLSIGNEXTEND: CANNOT WIDEN FROM {f} BITS TO {t}The target width is narrower than the source.
BOOLLOG2FLOOR: ZERO HAS NO LOGARITHM - BOOLHIGHBIT ANSWERS NONE INSTEADAnd it names the total word to use instead.
BOOLNEXTPOW2: '{a}' HAS NO POWER OF TWO ABOVE IT BELOW THE 53-BIT CEILINGAbove 252.
BOOLBCD: '{n}' IS AT OR ABOVE 10 ^ 13 - PACKED BCD HOLDS 13 NIBBLES IN 53 BITSThe BCD ceiling.
BOOLROWINPUTS: ROW '{k}' IS OUTSIDE 0..{max} FOR {n} VARIABLESA row index the table does not have.
BOOLROWINDEX: {len} INPUTS FOR {n} VARIABLESAn input vector of the wrong length.
BOOLTABLETEXT: {len} NAMES FOR {n} VARIABLESA name list of the wrong length.
BOOLTERMTEXT: {len} NAMES FOR A TERM OVER {v} VARIABLESLikewise, and the same from BOOLTERMSUMTEXT.
BOOLMINIMISEWITH: A DONT-CARE IS ALSO A MINTERM - THE TWO SETS MUST NOT OVERLAPThe two sets must be disjoint.
Err("BOOLFROMBASE: '{s}' HAS NO DIGIT {c} IN BASE {b}", at)Not an abort. External text, so a Result, with the 1-based position.
Err("BOOLFROMBASE: THE EMPTY STRING IS NOT A NUMBER IN BASE {b}", 0)No position to report, so At is 0.
Err("BOOLFROMBASE: '{s}' IS ABOVE THE 53-BIT CEILING", at)A digit string too long to read exactly.
Err("BOOLFROMBCD: '{b}' HAS THE NIBBLE {d}, WHICH IS NOT A DECIMAL DIGIT", at)At is the 1-based nibble position from the low end.

Who Uses It

No machine and no mill includes it yet. Its words are already at the reckoner's prompt through its seed. A mill that puts it to work will appear here; a decoder for a real datasheet's status register, or a logic-gate simulator over the truth-table words, is the obvious one.

The Machines It Uses

MachineWhy
seqCountIf is BoolPopCount and scores the greedy cover, Contains and Append dedupe the implicant sets by value, Flatten collects each combining round, and All/Any carry the tautology and coverage tests.
sinqQuine–McCluskey is query work: SelectMany pairs the terms a round can combine, Distinct dedupes them by value, and MaxBy picks the implicant covering most of what is left — scoring each one once, where the fold it replaced re-scored the incumbent every step.
strStrRep zero-pads a rendering in any base — PadZero takes a Number and so cannot — and Join assembles the truth table, the sum of products and the product of sums.

Two includes, and deliberately only two: a machine about bits should not drag in statistics or matrices. Nothing from either appears in a Bool* signature or return type, so a program that includes bool alone gets a closed surface — BoolBitList answers a List Of Number and BoolTruthTable a List Of BoolRow, never seq's Pair. Option and Result need no include either: they are the language's.