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-free —
BoolBitAnd, 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 }.
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.
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:
r g b as three separate numbers and
plotter carries a Type Rgb record,
because there is no way to pack a triple into one number and get it back
out.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.
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.
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.
| Word | Description |
|---|---|
| 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. |
| Word | Description |
|---|---|
| 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. |
| Word | Description |
|---|---|
| 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. |
| Word | Description |
|---|---|
| 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 BoolField | A 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 BoolField → List 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.
| Word | Description |
|---|---|
| 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. |
The reverse trap of this machine: these all look like they should take Booleans, and all take Numbers.
| Word | Description |
|---|---|
| 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. |
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.
| Word | Description |
|---|---|
| BoolAnd / BoolOr / BoolNot | The three the language already has as operators. |
| BoolNand / BoolNor / BoolXor / BoolXnor | The 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. |
All of these refuse a variable count below 1 or above
BoolMaxVars().
| Word | Description |
|---|---|
| Type BoolRow | Inputs 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. |
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.
| Word | Description |
|---|---|
| Type BoolTerm | Bits (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(...)).
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.
| Message | When |
|---|---|
{WORD}: '{a}' IS NEGATIVE - THE DOMAIN IS WHOLE NUMBERS FROM 0 | Any word, on a negative value. |
{WORD}: '{a}' IS NOT A WHOLE NUMBER | Any 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 ^ 53 | Any word, on a value no longer exactly representable. |
{WORD}: WIDTH '{w}' IS OUTSIDE 1..53 | Any 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..52 | BoolBitAt, BoolSetBit, BoolMaskFrom, BoolGetField, BoolSetField, BoolFromBitList. |
{WORD}: BIT COUNT '{n}' IS OUTSIDE 0..53 | BoolMask, BoolHexW, BoolBinW. |
{WORD}: SHIFT '{n}' IS OUTSIDE 0..53 | The shifts and rotates. |
{WORD}: BASE '{b}' IS OUTSIDE 2..36 | BoolInBase, BoolFromBase. A bad base is a caller bug, not bad input. |
{WORD}: {n} VARIABLES IS OUTSIDE 1..12 - COMFORTABLE TO 8, SLOW AT 10 | Every truth-table and minimisation word. |
{WORD}: MINTERM '{m}' IS OUTSIDE 0..{max} FOR {n} VARIABLES | A minterm the variable count cannot hold. |
BOOLGROUPED: GROUP SIZE '{n}' IS NOT A WHOLE NUMBER FROM 1 | A 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 NUMBER | Its own check, since this is the one word taking a signed value. |
BOOLUNSIGNED: '{v}' IS NOT REPRESENTABLE IN {w} SIGNED BITS | Outside −(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 INSTEAD | And it names the total word to use instead. |
BOOLNEXTPOW2: '{a}' HAS NO POWER OF TWO ABOVE IT BELOW THE 53-BIT CEILING | Above 252. |
BOOLBCD: '{n}' IS AT OR ABOVE 10 ^ 13 - PACKED BCD HOLDS 13 NIBBLES IN 53 BITS | The BCD ceiling. |
BOOLROWINPUTS: ROW '{k}' IS OUTSIDE 0..{max} FOR {n} VARIABLES | A row index the table does not have. |
BOOLROWINDEX: {len} INPUTS FOR {n} VARIABLES | An input vector of the wrong length. |
BOOLTABLETEXT: {len} NAMES FOR {n} VARIABLES | A name list of the wrong length. |
BOOLTERMTEXT: {len} NAMES FOR A TERM OVER {v} VARIABLES | Likewise, and the same from BOOLTERMSUMTEXT. |
BOOLMINIMISEWITH: A DONT-CARE IS ALSO A MINTERM - THE TWO SETS MUST NOT OVERLAP | The 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. |
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.
| Machine | Why | |
|---|---|---|
| seq | CountIf 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. | |
| sinq | Quine–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. | |
| str | StrRep 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.