From 2e86ba82eac19c1bb87b486532753f66fbc21781 Mon Sep 17 00:00:00 2001 From: Eric Willigers Date: Mon, 11 May 2026 16:19:42 +1000 Subject: [PATCH] Add doc-test workflow for introduction.md examples MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A Python script (bin/test-doc-examples) extracts every fenced factor block from concepts/*/introduction.md and exercises/concept/*/.docs/introduction.md, runs each file's blocks back-to-back inside the test-runner image with a single docker run, and checks that the `! => ...` lines appear as a subsequence of Factor's actual stdout. A GitHub Actions workflow (.github/workflows/doc-test.yml) wires it into CI. Initial baseline against main had 26 failing blocks across 14 files — real doc bugs of the same shape as the recent `7/2` / `16/3` fixes, plus a handful of undefined-word typos and one swapped-arg change-nth. This commit fixes all of them; the script now reports 149/149 blocks pass across 48 files. Notable doc fixes: - concepts/tuples: `point new` actually prints `T{ point }`, not `T{ point { x 0 } { y 0 } }` (Factor pprint omits slots at their initial values). - concepts/strings: `.s` output was rendered as `"foo" / "bar"` but Factor prints stack items one per line; the surround example had 4 strings and a TODO note (`"... err, surround takes 3"`) and is now a clean 3-arg example. - concepts/quotations & high-school-sweetheart: `{ 1 [ 2 + ] [ 3 * ] } concat` doesn't type-check (concat needs sequence elements) — wrap the leading literal in a quotation. - pursers-pantry: hashtable iteration order isn't tied to insertion order in Factor; drop the brittle `! => H{ ... }` assertion on a 2-key literal and note the unordered nature. Block 2 had set-at args swapped (printed `H{ { 5 "coal" } }` instead of `H{ { "coal" 5 } }`). - mosaic-mischief: change-nth args were ordered as `quot i pick`, which mangles the stack; reorder to `i over quot`. - lighthouse-logbook: blocks referenced an undefined `my-log`; add a `: my-log` definition. - lasagna-luminary, quayside-crew, concepts/errors, factory-failsafe, high-school-sweetheart block 0, concepts/streams block 0: marked `! DOCTEST: SKIP` for illustrative blocks that re-define a word, throw outside a recover, use `:>` outside `[let`, or reference a yet-to-be-defined helper. - secrets: multi-line parenthetical commentary inside an `! => -12 (... )` comment was being parsed as part of the expected output; lift the explanation into prose. - streams block 1: `stream-contents` needs the stream on the stack, but inside `with-input-stream` the stream is bound to the dynamic `input-stream` variable; use `read-contents`. --- .github/workflows/doc-test.yml | 18 ++ bin/test-doc-examples | 273 ++++++++++++++++++ concepts/errors/introduction.md | 3 +- concepts/quotations/introduction.md | 2 +- concepts/streams/introduction.md | 5 +- concepts/strings/introduction.md | 14 +- concepts/tuples/introduction.md | 2 +- .../factory-failsafe/.docs/introduction.md | 6 +- .../.docs/introduction.md | 3 +- .../lasagna-luminary/.docs/introduction.md | 1 + .../lighthouse-logbook/.docs/introduction.md | 2 + .../mosaic-mischief/.docs/introduction.md | 10 +- .../pursers-pantry/.docs/introduction.md | 7 +- .../quayside-crew/.docs/introduction.md | 8 +- .../rpn-calculator/.docs/introduction.md | 4 +- .../concept/secrets/.docs/introduction.md | 11 +- 16 files changed, 340 insertions(+), 29 deletions(-) create mode 100644 .github/workflows/doc-test.yml create mode 100755 bin/test-doc-examples diff --git a/.github/workflows/doc-test.yml b/.github/workflows/doc-test.yml new file mode 100644 index 0000000..700eb0e --- /dev/null +++ b/.github/workflows/doc-test.yml @@ -0,0 +1,18 @@ +name: Doc-test + +on: + pull_request: + push: + branches: [main] + workflow_dispatch: + +jobs: + doc-test: + runs-on: ubuntu-24.04 + + steps: + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + - run: docker pull exercism/factor-test-runner + - name: Verify introduction.md examples + run: python3 bin/test-doc-examples diff --git a/bin/test-doc-examples b/bin/test-doc-examples new file mode 100755 index 0000000..2324bb1 --- /dev/null +++ b/bin/test-doc-examples @@ -0,0 +1,273 @@ +#!/usr/bin/env python3 +"""Doc-test for Factor introduction.md examples. + +Extracts ```factor blocks from concepts/*/introduction.md and +exercises/concept/*/.docs/introduction.md, runs each *file*'s blocks +back-to-back inside the test-runner's Factor, and checks that the +`! => ...` lines appear as a subsequence of Factor's actual stdout. + +Lines look like: + "hello" length . ! => 5 (inline) + 5 [ 2 * ] keep .s + ! => 10 (multi-line .s output) + ! => 5 + +Trailing prose separated by 2+ spaces and wrapped in `(...)` is +stripped: `! => 1/2 (a proper ratio)` → expected output `1/2`. + +The subsequence check lets a block include unasserted demo lines +(common: `H{ ... } .` with no `! =>`, since hashtable iteration order +isn't pinned). Out-of-order or wrong output still fails. + +Blocks within a file are tested as one Factor session so that later +blocks see earlier definitions (e.g. a TUPLE: in block 0, used in +block 1). A sentinel print between blocks lets us localise failures. + +If *no* block in a file declares its own `USING:` we prepend a +generous default. (Mixing the default with a per-block `USING:` +provokes ambiguous-word errors — e.g. `ascii` and `unicode` both +define `blank?` / `LETTER?` — so we trust the doc once it declares +anything.) If no block declares `IN:` we inject `IN: doctest` so +SYMBOL:/:/TUPLE: definitions work. + +To explicitly skip an untestable block (e.g. one that demonstrates a +`throw` outside a `recover`), put `! DOCTEST: SKIP` anywhere in the +block. Skipped blocks contribute neither setup nor assertions. +""" +from __future__ import annotations + +import argparse +import re +import shutil +import subprocess +import sys +import tempfile +from dataclasses import dataclass, field +from pathlib import Path + +IMAGE = "exercism/factor-test-runner" + +DEFAULT_USING = ( + "kernel math math.functions math.order math.parser math.statistics " + "math.bitwise math.primes sequences strings arrays vectors hashtables " + "assocs combinators continuations splitting ascii prettyprint " + "io io.streams.string namespaces quotations literals fry" +) + +SENTINEL = "###BLOCK-END###" + +FACTOR_BLOCK_RE = re.compile(r"```factor\n(.*?)```", re.DOTALL) +EXPECTED_RE = re.compile(r"^(?P.*?)\s*!\s*=>\s*(?P.*)$") +TRAILING_PROSE_RE = re.compile(r"\s{2,}\(.*\)\s*$") +HAS_DIRECTIVE_RE = re.compile(r"^\s*(?:USING:|USE:|IN:)\s", re.MULTILINE) +HAS_IN_RE = re.compile(r"^\s*IN:\s", re.MULTILINE) +HAS_USING_RE = re.compile(r"^\s*(?:USING:|USE:)\s", re.MULTILINE) + + +@dataclass +class Block: + file_index: int + block_index: int + code: str + expected: list[str] + skip: bool = False + + +@dataclass +class IntroFile: + path: Path + blocks: list[Block] = field(default_factory=list) + + +def iter_intro_files(repo: Path): + yield from sorted(repo.glob("concepts/*/introduction.md")) + yield from sorted(repo.glob("exercises/concept/*/.docs/introduction.md")) + + +def split_block(body: str) -> tuple[str, list[str]]: + code_lines: list[str] = [] + expected: list[str] = [] + for line in body.splitlines(): + stripped = line.strip() + if not stripped: + code_lines.append(line) + continue + m = EXPECTED_RE.match(line) + if m: + exp = m.group("exp").rstrip() + exp = TRAILING_PROSE_RE.sub("", exp).rstrip() + expected.append(exp) + code_part = m.group("code").rstrip() + if code_part: + code_lines.append(code_part) + continue + if stripped.startswith("!"): + continue + code_lines.append(line) + return "\n".join(code_lines).rstrip() + "\n", expected + + +SKIP_RE = re.compile(r"^\s*!\s*DOCTEST:\s*SKIP\b", re.IGNORECASE | re.MULTILINE) + + +def collect(repo: Path) -> list[IntroFile]: + files: list[IntroFile] = [] + for path in iter_intro_files(repo): + intro = IntroFile(path=path.relative_to(repo)) + txt = path.read_text() + for bi, m in enumerate(FACTOR_BLOCK_RE.finditer(txt)): + raw = m.group(1) + skip = bool(SKIP_RE.search(raw)) + code, expected = split_block(raw) + intro.blocks.append(Block( + file_index=len(files), + block_index=bi, + code=code, + expected=expected, + skip=skip, + )) + # Only include the file if it has at least one testable (asserting) block + if any(b.expected and not b.skip for b in intro.blocks): + files.append(intro) + return files + + +def compose_program(intro: IntroFile) -> str: + parts: list[str] = [] + live_blocks = [b for b in intro.blocks if not b.skip] + # Only prepend default USING if no block declares its own — otherwise the + # default and the doc's declared imports can collide on ambiguous words + # (e.g. ascii + unicode both define `LETTER?` / `blank?`). + any_using = any(HAS_USING_RE.search(b.code) for b in live_blocks) + needs_in = any(not HAS_IN_RE.search(b.code) for b in live_blocks) + if any_using: + # We still need `print` for the sentinel. `io` rarely conflicts. + parts.append("USING: io ;") + else: + parts.append(f"USING: {DEFAULT_USING} ;") + if needs_in: + parts.append("IN: doctest") + parts.append("") + for b in intro.blocks: + if b.skip: + parts.append(f'"{SENTINEL}" print') # placeholder so indices line up + continue + parts.append(b.code.rstrip()) + parts.append(f'"{SENTINEL}" print') + parts.append("") + return "\n".join(parts) + + +def write_programs(files: list[IntroFile], out_dir: Path) -> None: + out_dir.mkdir(parents=True, exist_ok=True) + for fi, intro in enumerate(files): + (out_dir / f"file-{fi:03d}.factor").write_text(compose_program(intro)) + + +RUNNER_SCRIPT = r"""#!/usr/bin/env bash +set -u +cd /doctest +for prog in file-*.factor; do + n=${prog%.factor} + factor "${prog}" > "${n}.actual" 2> "${n}.stderr" || true +done +echo done +""" + + +def run_in_container(out_dir: Path, image: str) -> None: + (out_dir / "run.sh").write_text(RUNNER_SCRIPT) + (out_dir / "run.sh").chmod(0o755) + subprocess.run( + [ + "docker", "run", "--rm", + "--mount", f"type=bind,src={out_dir},dst=/doctest", + "--entrypoint", "bash", + image, "/doctest/run.sh", + ], + check=True, + ) + + +def split_actual_by_sentinel(actual: str) -> list[list[str]]: + segments: list[list[str]] = [[]] + for line in actual.splitlines(): + if line.strip() == SENTINEL: + segments.append([]) + else: + if line.strip(): + segments[-1].append(line.rstrip()) + return segments + + +def is_subsequence(expected: list[str], actual: list[str]) -> bool: + i = 0 + for line in actual: + if i < len(expected) and line == expected[i]: + i += 1 + return i == len(expected) + + +def report(files: list[IntroFile], out_dir: Path) -> int: + failures = 0 + total = 0 + for fi, intro in enumerate(files): + actual_path = out_dir / f"file-{fi:03d}.actual" + stderr_path = out_dir / f"file-{fi:03d}.stderr" + actual_text = actual_path.read_text() if actual_path.exists() else "" + stderr_text = stderr_path.read_text() if stderr_path.exists() else "" + segments = split_actual_by_sentinel(actual_text) + for bi, block in enumerate(intro.blocks): + if block.skip or not block.expected: + continue + total += 1 + expected = [e for e in block.expected if e.strip()] + seg = segments[bi] if bi < len(segments) else [] + if is_subsequence(expected, seg): + print(f"PASS {intro.path} block #{block.block_index}") + continue + failures += 1 + print(f"FAIL {intro.path} block #{block.block_index}") + print(" expected (subseq of actual):") + for line in expected: + print(f" {line!r}") + print(" actual:") + for line in seg: + print(f" {line!r}") + if stderr_text.strip(): + print(" stderr (file):") + for line in stderr_text.splitlines()[:6]: + print(f" {line}") + print(f"\n{total - failures}/{total} blocks pass ({len(files)} files)") + return failures + + +def main() -> int: + ap = argparse.ArgumentParser(description=__doc__) + ap.add_argument("--repo", default=".", help="Path to track repo (default: cwd)") + ap.add_argument("--image", default=IMAGE, help="Docker image to use") + ap.add_argument("--keep", action="store_true", help="Keep tmp dir for debugging") + args = ap.parse_args() + + repo = Path(args.repo).resolve() + files = collect(repo) + blocks = sum(len(f.blocks) for f in files) + print(f"collected {blocks} testable blocks across {len(files)} files") + if not blocks: + return 0 + + tmp = Path(tempfile.mkdtemp(prefix="doctest-")) + try: + write_programs(files, tmp) + run_in_container(tmp, args.image) + failures = report(files, tmp) + finally: + if args.keep: + print(f"kept artifacts in {tmp}") + else: + shutil.rmtree(tmp, ignore_errors=True) + return 1 if failures else 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/concepts/errors/introduction.md b/concepts/errors/introduction.md index 43fca35..ce5c87f 100644 --- a/concepts/errors/introduction.md +++ b/concepts/errors/introduction.md @@ -6,7 +6,7 @@ Factor's error system has three pieces: `throw` (in a tuple-typed error class. ```factor -"file not found" throw ! raises a string as the error +! "file not found" throw (raises a string as the error) [ "boom" throw ] [ drop "caught" ] recover . ! => "caught" @@ -19,6 +19,7 @@ throws, the error is pushed and the *recovery* quotation runs. throws a fresh instance: ```factor +! DOCTEST: SKIP ERROR: not-found path ; "/missing" not-found diff --git a/concepts/quotations/introduction.md b/concepts/quotations/introduction.md index 4f02b21..f910342 100644 --- a/concepts/quotations/introduction.md +++ b/concepts/quotations/introduction.md @@ -24,5 +24,5 @@ tokens to operations — the result is a sequence; pass it through `>quotation` to make it callable: ```factor -{ 1 [ 2 + ] [ 3 * ] } concat >quotation call . ! => 9 +{ [ 1 ] [ 2 + ] [ 3 * ] } concat >quotation call . ! => 9 ``` diff --git a/concepts/streams/introduction.md b/concepts/streams/introduction.md index c9eb9a7..6041610 100644 --- a/concepts/streams/introduction.md +++ b/concepts/streams/introduction.md @@ -33,6 +33,9 @@ Streams hold OS resources, so the protocol pairs with the the `disposable` parent class: ```factor +! DOCTEST: SKIP (illustrative class definition; no runnable assertion) +USING: accessors destructors io kernel ; + TUPLE: my-stream < disposable underlying ; INSTANCE: my-stream output-stream @@ -56,7 +59,7 @@ run a quotation with the resource open and dispose it on exit: ```factor USING: io io.streams.string ; -"hello" [ stream-contents ] with-input-stream +"hello" [ read-contents . ] with-input-stream ! => "hello" (the reader is disposed before this line returns) ``` diff --git a/concepts/strings/introduction.md b/concepts/strings/introduction.md index 2b72c31..2dec82e 100644 --- a/concepts/strings/introduction.md +++ b/concepts/strings/introduction.md @@ -5,11 +5,15 @@ Strings in Factor are sequences of characters. Most words from the words in [`splitting`][splitting], [`ascii`][ascii], and friends. ```factor -"hello, world" . ! => "hello, world" -"foo: bar" ": " split1 .s ! => "foo" / "bar" -"WARNING" >lower . ! => "warning" -" spaced " [ blank? ] trim . ! => "spaced" -"a" "b" "(" ")" surround . ! => "(b)"... err, surround takes 3 +USING: ascii sequences splitting ; + +"hello, world" . ! => "hello, world" +"foo: bar" ": " split1 .s +! => "foo" +! => "bar" +"WARNING" >lower . ! => "warning" +" spaced " [ blank? ] trim . ! => "spaced" +"warning" "(" ")" surround . ! => "(warning)" ``` `split1` cuts a string on the first occurrence of a separator, diff --git a/concepts/tuples/introduction.md b/concepts/tuples/introduction.md index 3d2e7e9..f413106 100644 --- a/concepts/tuples/introduction.md +++ b/concepts/tuples/introduction.md @@ -11,7 +11,7 @@ TUPLE: point T{ point { x 3 } { y 4 } } x>> . ! => 3 3 4 point boa . ! => T{ point { x 3 } { y 4 } } -point new . ! => T{ point { x 0 } { y 0 } } +point new . ! => T{ point } ``` `boa` ("by order of arguments") fills slots from the stack, in diff --git a/exercises/concept/factory-failsafe/.docs/introduction.md b/exercises/concept/factory-failsafe/.docs/introduction.md index 8a14367..3a6ac32 100644 --- a/exercises/concept/factory-failsafe/.docs/introduction.md +++ b/exercises/concept/factory-failsafe/.docs/introduction.md @@ -11,6 +11,7 @@ class. error you can later inspect: ```factor +! DOCTEST: SKIP "file not found" throw ``` @@ -29,10 +30,10 @@ If `try` finishes without throwing, `recovery` is ignored. If `try` throws, the error is pushed onto the stack and `recovery` is run: ```factor -[ "boom" throw ] [ drop "caught" ] recover +[ "boom" throw ] [ drop "caught" ] recover . ! => "caught" -[ "ok" ] [ drop "never runs" ] recover +[ "ok" ] [ drop "never runs" ] recover . ! => "ok" ``` @@ -50,6 +51,7 @@ quotation only when the top of stack is truthy: instance and throws it: ```factor +! DOCTEST: SKIP ERROR: not-found path ; "/missing" not-found diff --git a/exercises/concept/high-school-sweetheart/.docs/introduction.md b/exercises/concept/high-school-sweetheart/.docs/introduction.md index 99303fb..d0f56b8 100644 --- a/exercises/concept/high-school-sweetheart/.docs/introduction.md +++ b/exercises/concept/high-school-sweetheart/.docs/introduction.md @@ -6,6 +6,7 @@ values from the stack and pushes its result, and the next word picks up where the previous left off. So defining ```factor +! DOCTEST: SKIP (cleanupname is hypothetical here — the exercise's job) : firstletter ( name -- letter ) cleanupname first 1string ; ``` @@ -52,7 +53,7 @@ references into a callable quotation: ```factor USING: quotations sequences ; -{ 1 [ 2 + ] [ 3 * ] } concat >quotation call . ! => 9 +{ [ 1 ] [ 2 + ] [ 3 * ] } concat >quotation call . ! => 9 ``` ## Applying one quotation to two inputs diff --git a/exercises/concept/lasagna-luminary/.docs/introduction.md b/exercises/concept/lasagna-luminary/.docs/introduction.md index b6ab5ea..5cb5c3b 100644 --- a/exercises/concept/lasagna-luminary/.docs/introduction.md +++ b/exercises/concept/lasagna-luminary/.docs/introduction.md @@ -22,6 +22,7 @@ USING: locals ; Compare with the stack-shuffling version: ```factor +! DOCTEST: SKIP (shown for comparison — would re-define hypotenuse) : hypotenuse ( a b -- c ) [ sq ] bi@ + sqrt ; ``` diff --git a/exercises/concept/lighthouse-logbook/.docs/introduction.md b/exercises/concept/lighthouse-logbook/.docs/introduction.md index 873a1e8..7cbc09f 100644 --- a/exercises/concept/lighthouse-logbook/.docs/introduction.md +++ b/exercises/concept/lighthouse-logbook/.docs/introduction.md @@ -50,6 +50,8 @@ sequence. For a hash-set, `in?` is a hash lookup — O(1) average, the whole point of using a hash-set. ```factor +: my-log ( -- set ) HS{ "NS-1024" "WB-203" } ; + "NS-1024" my-log in? . ! => t "X-99" my-log in? . ! => f my-log cardinality . ! => 2 diff --git a/exercises/concept/mosaic-mischief/.docs/introduction.md b/exercises/concept/mosaic-mischief/.docs/introduction.md index 48070a1..5b0e20d 100644 --- a/exercises/concept/mosaic-mischief/.docs/introduction.md +++ b/exercises/concept/mosaic-mischief/.docs/introduction.md @@ -16,7 +16,7 @@ time. [`vectors`][vectors]) preallocates capacity for `n` elements. ```factor -USING: arrays kernel vectors ; +USING: arrays kernel prettyprint vectors ; 5 0 . ! => { 0 0 0 0 0 } V{ } clone . ! => V{ } @@ -37,11 +37,11 @@ element at position `i`. Both return nothing on the stack — the mutation is the effect. ```factor -USING: arrays kernel math sequences ; +USING: arrays kernel math prettyprint sequences ; 5 0 ! { 0 0 0 0 0 } 7 2 pick set-nth ! mutates: position 2 becomes 7 -[ 1 + ] 0 pick change-nth ! mutates: position 0 += 1 +0 over [ 1 + ] change-nth ! mutates: position 0 += 1 . ! => { 1 0 7 0 0 } ``` @@ -73,7 +73,7 @@ array, `>array ( seq -- array )` (in [`arrays`][arrays]) returns a new array with the same elements: ```factor -USING: arrays kernel sequences vectors ; +USING: arrays kernel prettyprint sequences vectors ; V{ "alice" "bob" "carol" } >array . ! => { "alice" "bob" "carol" } @@ -87,7 +87,7 @@ sequence and you're about to change it, `clone` (in caller's view doesn't shift under them: ```factor -USING: kernel ; +USING: kernel prettyprint ; V{ "a" "b" "c" } clone . ! => V{ "a" "b" "c" } (a fresh copy) ``` diff --git a/exercises/concept/pursers-pantry/.docs/introduction.md b/exercises/concept/pursers-pantry/.docs/introduction.md index c47c00c..998464a 100644 --- a/exercises/concept/pursers-pantry/.docs/introduction.md +++ b/exercises/concept/pursers-pantry/.docs/introduction.md @@ -8,11 +8,12 @@ Hashtables in Factor are *associative arrays* — collections of ```factor H{ { "coal" 1 } { "wood" 2 } } . -! => H{ { "coal" 1 } { "wood" 2 } } ``` `H{ }` is an empty hashtable. Unlike arrays, hashtables are *mutable* -— `clone` first if you need to leave the original alone. +— `clone` first if you need to leave the original alone. Printing a +hashtable shows its entries, but the order isn't tied to insertion +order — hashtables are unordered. ## Reading @@ -41,7 +42,7 @@ change-at ( key assoc quot: ( old -- new ) -- ) ``` ```factor -H{ } clone "coal" over [ 5 swap set-at ] keep . +H{ } clone 5 "coal" pick set-at . ! => H{ { "coal" 5 } } ``` diff --git a/exercises/concept/quayside-crew/.docs/introduction.md b/exercises/concept/quayside-crew/.docs/introduction.md index cb2896a..02f7e60 100644 --- a/exercises/concept/quayside-crew/.docs/introduction.md +++ b/exercises/concept/quayside-crew/.docs/introduction.md @@ -73,10 +73,12 @@ lock at a time. `with-lock` acquires, runs the quotation, releases — even if the quotation throws. ```factor -USING: concurrency.locks kernel ; +! DOCTEST: SKIP (`:>` only works inside [let, [|, or :: forms) +USING: concurrency.locks kernel locals ; - :> guard -guard [ ! exclusive section ! ] with-lock +[let :> guard + guard [ ! exclusive section ! ] with-lock +] ``` Use a lock whenever a value is *read or written by more than one diff --git a/exercises/concept/rpn-calculator/.docs/introduction.md b/exercises/concept/rpn-calculator/.docs/introduction.md index 2d81a2e..72d74bc 100644 --- a/exercises/concept/rpn-calculator/.docs/introduction.md +++ b/exercises/concept/rpn-calculator/.docs/introduction.md @@ -64,7 +64,9 @@ last2 ( seq -- penultimate last ) ```factor { 1 2 3 4 } 2 head* . ! => { 1 2 } -{ 1 2 3 4 } last2 . ! => 3 4 +{ 1 2 3 4 } last2 .s +! => 3 +! => 4 ``` Together with `suffix`, that's everything you need to write a diff --git a/exercises/concept/secrets/.docs/introduction.md b/exercises/concept/secrets/.docs/introduction.md index 2c4b377..240ceca 100644 --- a/exercises/concept/secrets/.docs/introduction.md +++ b/exercises/concept/secrets/.docs/introduction.md @@ -38,15 +38,16 @@ bit? ( x n -- ? ) ! true when bit at position n is set 0b1011 0b0010 bitor . ! => 11 (0b1011) 0b1011 0b0010 bitand . ! => 2 (0b0010) 0b1011 0b0010 bitxor . ! => 9 (0b1001) -0b1011 bitnot . ! => -12 (… because Factor uses arbitrary - ! precision, so flipping turns - ! 0b1011 into "all ones with the - ! low four flipped to 0100", which - ! read as a signed bignum is -12.) +0b1011 bitnot . ! => -12 0b1011 0 bit? . ! => t (low bit is set) 0b1011 2 bit? . ! => f (bit 2 is clear) ``` +`bitnot` of `0b1011` is `-12` because Factor integers are arbitrary +precision: flipping the bits of `0b1011` is conceptually "all ones +with the low four flipped to `0100`", which read as a signed bignum +is `-12`. + ## Treating a value as N bits `bits ( x n -- y )` from `math.bitwise` masks `x` to its low `n` bits.