From 607cd08c91d0ca95e625073b1cd65a9d1b4251ec Mon Sep 17 00:00:00 2001 From: Nick DeMarco Date: Tue, 3 Feb 2026 16:14:52 -0500 Subject: [PATCH 1/2] Add swift example checking to , fix broken examples, add to CI --- .github/workflows/build.yml | 6 +- better-code/book.toml | 11 + better-code/src/chapter-2-contracts.md | 124 ++++++--- scripts/mdbook-swift-hidden.py | 131 ++++++++++ scripts/mdbook-swift-test.py | 332 +++++++++++++++++++++++++ 5 files changed, 563 insertions(+), 41 deletions(-) create mode 100755 scripts/mdbook-swift-hidden.py create mode 100755 scripts/mdbook-swift-test.py diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3d5059f..dcf67c5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -46,7 +46,11 @@ jobs: - name: Install mdBook and plugins (Windows) if: runner.os == 'Windows' run: .\scripts\install-tools.ps1 - + + # Install Swift for testing Swift code examples in the book + - name: Setup Swift + uses: swift-actions/setup-swift@v2 + - name: Build with mdBook run: mdbook build ./better-code diff --git a/better-code/book.toml b/better-code/book.toml index f626b05..6726e5c 100644 --- a/better-code/book.toml +++ b/better-code/book.toml @@ -8,6 +8,11 @@ description = "A principled and rigorous approach to software development" [build] build-dir = "book" +# Preprocessor to hide `# ` lines in Swift code blocks (like mdbook does for Rust) +# Only strips lines for HTML output; swift-test backend sees the full code +[preprocessor.swift-hidden] +command = "python3 scripts/mdbook-swift-hidden.py" + [output.html] git-repository-url = "https://github.com/stlab/better-code" edit-url-template = "https://github.com/stlab/better-code/edit/main/better-code/src/{path}" @@ -19,3 +24,9 @@ enable = true [output.html.print] enable = true + +# Swift code example testing backend +# Tests all Swift code blocks during `mdbook build` +# See scripts/mdbook-swift-test.py for implementation +[output.swift-test] +command = "python3 ../../../scripts/mdbook-swift-test.py" diff --git a/better-code/src/chapter-2-contracts.md b/better-code/src/chapter-2-contracts.md index 80d388b..59790be 100644 --- a/better-code/src/chapter-2-contracts.md +++ b/better-code/src/chapter-2-contracts.md @@ -167,13 +167,13 @@ when we write one statement after the next. For example, take these two independent lines of code: -```swift +```swift,ignore let m = (h - l) / 2 ``` and -```swift +```swift,ignore h = l + m ``` @@ -186,7 +186,7 @@ The following—more useful—triples will help illustrate the sequencing rule: - **{*l*≤*h*}**`let m = (h - l )/2`**{*m*≥ 0}**, i.e. - ```swift + ```swift,ignore // precondition: l <= h let m = (h - l) / 2 // postcondition: m >= 0 @@ -194,7 +194,7 @@ The following—more useful—triples will help illustrate the sequencing rule: - **{*m*≥0}**`h = l + m`**{*l*≤*h*}**, i.e. - ```swift + ```swift,ignore // precondition: m >= 0 h = l + m // postcondition: l <= h @@ -207,7 +207,7 @@ second postcondition. Thus there's a new valid triple: **{*l*≤*h*}**`let m = (h -l )/2; h = l + m`**{*l*≤*h*}**, i.e. -```swift +```swift,ignore // precondition: l <= h let m = (h - l) / 2 h = l + m @@ -236,6 +236,8 @@ there's an *invariant* that no element preceding the `i`th one is equal to `x`. ```swift +# let a = [1, 2, 3] +# let x = 1 var i = 0 while (i != a.count && a[i] != x) { i += 1 @@ -397,10 +399,15 @@ it has an invariant that `xs.count == ys.count`, but if we add this initializer, we'd have to weaken that invariant: ```swift +# struct PairArray { +# private var xs: [X] = [] +# private var ys: [Y] = [] +# /// An instance with the value of `zip(xs, ys)`. /// /// - Precondition: `xs.count <= ys.count`. init(xs: [X], ys: [Y]) { (self.xs, self.ys) = (xs, ys) } +# } ``` [Note: when sequences of unequal length are `zip`ped, the result @@ -420,12 +427,17 @@ meaning, the `append` method must account for these new internal states. Here's one way we could do it: ```swift +# struct PairArray { +# private var xs: [X] = [] +# private var ys: [Y] = [] +# /// Adds `e` to the end. public mutating func append(_ e: (X, Y)) { - ys.removeRange(xs.count...) // <===== + ys.removeSubrange(xs.count...) // <===== xs.append(e.0) ys.append(e.1) } +# } ``` Also, the `count` property needs to be changed to return `xs.count` @@ -444,23 +456,33 @@ approaches: requiring the `xs` and `ys` arguments to have the same length: ```swift +# struct PairArray { +# private var xs: [X] = [] +# private var ys: [Y] = [] +# /// An instance with the value of `zip(xs, ys)`. /// /// - Precondition: `xs.count == ys.count`. init(xs: [X], ys: [Y]) { (self.xs, self.ys) = (xs, ys) } +# } ``` - Or, we can remove the precondition entirely and normalize the internal representation upon construction: ```swift +# struct PairArray { +# private var xs: [X] = [] +# private var ys: [Y] = [] +# /// An instance with the value of `zip(xs, ys)`. init(xs: [X], ys: [Y]) { (self.xs, self.ys) = (xs, ys) let l = Swift.min(xs.count, ys.count) - self.xs.removeRange(l...) - self.ys.removeRange(l...) + self.xs.removeSubrange(l...) + self.ys.removeSubrange(l...) } +# } ``` Either approach is acceptable, and ordinarily one should favor the @@ -522,7 +544,7 @@ contracts are upheld. [^compile_time_checks] compile time, such as Dafny and Lean 4, but we do not think they are practical tools for programming at scale. -```swift +```swift,ignore struct MyArray { ... @@ -653,26 +675,26 @@ code. This is one of the most powerful code transformations you can make. ```swift +# struct SQLDatabase {} +# struct EmployeeID {} /// The employees of a company. /// /// Invariant: every employee has a manager in the database. struct EmployeeDatabase { /// The raw storage. - private var storage: SQLDatabase; + private var storage: SQLDatabase /// Adds a new employee named `name` with manager `m`, returning the /// new employee's ID. /// /// - Precondition: `m` identifies an employee. - public addEmployee(_ name: String, managedBy m: EmployeeID) -> EmployeeID + public func addEmployee(_ name: String, managedBy m: EmployeeID) -> EmployeeID { ... } /// Removes the employee identified by `e`. /// /// - Precondition: `e` identifies an employee who is not the /// manager of any other employee. - public remove(_ e: EmployeeID) - - ... + public func remove(_ e: EmployeeID) { ... } } ``` @@ -726,7 +748,7 @@ Design Guidelines](https://www.swift.org/documentation/api-design-guidelines/), only slightly modified. -```swift +```swift,ignore /// A resizable random-access `Collection` of `T`s. struct MyArray { @@ -756,7 +778,7 @@ thing *is* or *does*. The first one -```swift +```swift,ignore /// A resizable random-access `Collection` of `T`s. struct MyArray ``` @@ -767,7 +789,7 @@ at the declaration of dynamic array type that holds any number of Ts. Now let's look at the documentation for the first method, called "popLast." -```swift +```swift,ignore /// Removes and returns the last element. public mutating func popLast() -> T { ... } ``` @@ -803,7 +825,7 @@ and invariants of `popLast()`. What are the preconditions for removing an element? Obviously, there needs to be an element to remove. -```swift +```swift,ignore /// Removes and returns the last element. /// /// - Precondition: `self` is non-empty. @@ -819,7 +841,7 @@ and the function does not report an error, we'd say the method has a bug. The bug could be in the documentation of course, *which is a part of the method*. -```swift +```swift,ignore /// Removes and returns the last element. /// /// - Precondition: `self` is non-empty. @@ -831,7 +853,7 @@ part of the method*. The invariant of this function is the rest of the elements, which are unchanged: -```swift +```swift,ignore /// Removes and returns the last element. /// /// - Precondition: `self` is non-empty. @@ -864,7 +886,7 @@ we can assume everything else in the program is unchanged, so the invariant is also trivially implied. And that is also very commonly omitted. -```swift +```swift,ignore /// Removes and returns the last element. /// /// - Precondition: `self` is non-empty. @@ -885,7 +907,7 @@ this case it's not subtle that having a last element is required if you are going to remove the last element, so the original declaration should be sufficient: -```swift +```swift,ignore /// Removes and returns the last element. public mutating func popLast() -> T { ... } ``` @@ -907,6 +929,12 @@ Let's take a look at a traditional sorting algorithm on a fictitous collection type: ```swift +# struct DynamicArray {} +# extension Array { +# mutating func sort(areInIncreasingOrder: (Element, Element)->Bool) { +# self.sort(by: areInIncreasingOrder) +# } +# } extension DynamicArray { /// Sorts the elements so that `areInIncreasingOrder(self[i+1], /// self[i])` is false for each `i` in `0 ..< length - 2`. @@ -958,7 +986,7 @@ correct, but it's awkward and complex. To deal with the case where elements are equal, the postcondition has to compare adjacent elements in reverse order and negate the predicate. If we had just written: -```swift +```swift,ignore /// Sorts the elements so that `areInIncreasingOrder(self[i], /// self[i+1])` is true for each `i` in `0 ..< length - 2`. ``` @@ -970,7 +998,7 @@ The term “strict weak ordering,” which is not very well-known or understood, is another source of complexity. In fact we should probably put a link in the documentation to a definition. -```swift +```swift,ignore /// - Precondition: `areInIncreasingOrder` is [a strict weak /// ordering](https://simple.wikipedia.org/wiki/Strict_weak_ordering) /// over the elements of `self`. @@ -981,7 +1009,7 @@ documenting more laborious and can actually distract from the essential information—but because the statement of effects is tricky, this is a case where an example might really help. -```swift +```swift,ignore /// Sorts the elements so that `areInIncreasingOrder(self[i+1], /// self[i])` is false for each `i` in `0 ..< length - 2`. /// @@ -1005,7 +1033,7 @@ written to work with `<=` as an argument, producing the same result, the summary could have avoided swapping elements in the comparison and negating the result: -```swift +```swift,ignore /// Sorts the elements so that `areInOrder(self[i], /// self[i+1])` is true for each `i` in `0 ..< length - 2`. ``` @@ -1038,7 +1066,7 @@ that its arguments represent an _increase_. Instead, it tells us whether the order is correct. Because the summary is no longer tricky, we can drop the example, and we're left with this: -```swift +```swift,ignore /// Sorts the elements so that `areInOrder(self[i], /// self[i+1])` is true for each `i` in `0 ..< length - 2`. /// @@ -1052,7 +1080,7 @@ mutating func sort(areInOrder: (Element, Element)->Bool) { ... } But we can go further and use a much simpler and more natural summary: -```swift +```swift,ignore /// Sorts the elements so that all adjacent pairs satisfy /// `areInOrder`. ``` @@ -1084,7 +1112,7 @@ Secondly, the summary has become so simple that we can embed the precondition there without overly complicating it, making the final declaration: -```swift +```swift,ignore /// Sorts the elements so that all adjacent pairs satisfy the [total /// preorder](https://en.wikipedia.org/wiki/Weak_ordering#Total_preorders) /// `areInOrder`. @@ -1191,7 +1219,7 @@ extension Collection where Element: Equatable { /// - Precondition: `self` contains `x`. /// - Complexity: at most N comparisons, where N is the number /// of elements. - func offsetFromStart(_ x: Element) -> Int {...} + func offsetFromStart(_ x: Element) -> Int { ... } } ``` @@ -1205,14 +1233,14 @@ extension Collection where Element: Equatable { /// - Precondition: `self` contains `x`. /// - Complexity: at most N comparisons, where N is the number /// of elements. - func offsetFromStart(_ x: Element) -> Int {...} + func offsetFromStart(_ x: Element) -> Int { ... } } ``` Before the change, clients had a right to expect this assertion to pass: -```swift +```swift,ignore assert( c[c.index(c.startIndex, offsetBy: c.offsetFromStart(x))] == 'x`) ``` @@ -1231,13 +1259,16 @@ extension Collection where Element: Equatable { /// - Precondition: `self` contains `x` and `x` != `first`. /// - Complexity: at most N comparisons, where N is the number /// of elements. - func offsetFromStart(_ x: Element) -> Int {...} + func offsetFromStart(_ x: Element) -> Int { ... } } ``` After that change, the following line of code has developed a new bug! ```swift +# extension Collection where Element: Equatable { +# func offsetFromStart(_ x: Element) -> Int { fatalError() } +# } let i = [1, 2, 3].offsetFromStart(1) // 1 == [1, 2, 3].first ``` @@ -1255,7 +1286,7 @@ extension Collection where Element: Equatable { /// - Precondition: `self` contains `x`. /// - Complexity: at most N comparisons, where N is the number /// of elements. - func offsetFromStart(_ x: Element) -> Int {...} + func offsetFromStart(_ x: Element) -> Int { ... } } ``` @@ -1270,7 +1301,7 @@ From there, let's weaken the precondition by simply removing it: extension Collection where Element: Equatable { /// Returns the number elements between the start and the first /// occurrence of the value `x`, or `count` if `x` is not found. - func offsetFromStart(_ x: Element) -> Int {...} + func offsetFromStart(_ x: Element) -> Int { ... } } ``` @@ -1297,12 +1328,20 @@ Here, the conformance strengthens the performance guarantee given by the protocol requirement: ```swift +# struct SortedArray: Collection { +# var elements: [Element] = [] +# var startIndex: Int { elements.startIndex } +# var endIndex: Int { elements.endIndex } +# subscript(i: Int) -> Element { elements[i] } +# func index(after i: Int) -> Int { elements.index(after: i) } +# } +# protocol Searchable: Collection { /// Returns the first position where `x` occurs. /// /// - Complexity: at most `count` comparisons and index adjustment /// calls. - func index(of x: Element) -> Index + func index(of x: Element) -> Index? } extension SortedArray: Searchable { @@ -1310,7 +1349,7 @@ extension SortedArray: Searchable { /// /// - Complexity: at most log2(`count`) comparisons and index /// adjustment calls. - func index(of x: Element) -> Index + func index(of x: Element) -> Index? { ... } } ``` @@ -1320,13 +1359,12 @@ stronger guarantees than those specified by the callee. Take this function for instance: ```swift -extension Collection { +extension Collection where Element: Hashable { /// Returns a mapping from `bucket(x)` for all elements `x`, to the /// set of elements `y` such that `bucket(y) == bucket(x)`. /// /// - Precondition: `bucket(x)` is non-negative for all elements `x`. - func bucketed(per bucket: (Element)->Int) -> [Int: Set] - { ... } + func bucketed(per bucket: (Element)->Int) -> [Int: Set] { ... } } ``` @@ -1340,6 +1378,9 @@ that returns only 0 or 1; a strictly stronger postcondition: required of the function's *parameter*, `bucket`. ```swift +# extension Collection where Element: Hashable { +# func bucketed(per bucket: (Element)->Int) -> [Int: Set] { [:] } +# } (0..<10).bucketed { $0 % 2 } ``` @@ -1348,6 +1389,9 @@ postcondition is neither identical nor strictly stronger. That violates the requirements of `bucketed`: ```swift +# extension Collection where Element: Hashable { +# func bucketed(per bucket: (Element)->Int) -> [Int: Set] { [:] } +# } (0..<10).bucketed { $0 - 2 } ``` diff --git a/scripts/mdbook-swift-hidden.py b/scripts/mdbook-swift-hidden.py new file mode 100755 index 0000000..0f5c645 --- /dev/null +++ b/scripts/mdbook-swift-hidden.py @@ -0,0 +1,131 @@ +#!/usr/bin/env python3 +""" +mdbook-swift-hidden: A preprocessor that hides `# ` lines in Swift code blocks. + +This preprocessor strips lines starting with `# ` from Swift code blocks +when rendering for HTML output, mimicking mdbook's behavior for Rust. + +For the swift-test backend, lines are passed through unchanged so the +test backend can compile the full code including hidden setup. + +Usage: + Configured in book.toml as: + + [preprocessor.swift-hidden] + command = "python3 scripts/mdbook-swift-hidden.py" + +Hidden Line Syntax: + Lines starting with `# ` (hash + space) are hidden from readers. + A line that is just `#` becomes a blank line (for spacing). + + Example input: + ```swift + # import Foundation + # struct Point { var x: Int; var y: Int } + let p = Point(x: 1, y: 2) + print(p) + ``` + + Rendered output (for HTML): + ```swift + let p = Point(x: 1, y: 2) + print(p) + ``` +""" + +import json +import re +import sys + + +def strip_hidden_lines(content: str) -> str: + """ + Remove hidden lines from Swift code blocks. + + Lines starting with `# ` are removed entirely. + Lines that are exactly `#` are also removed. + """ + result_lines = [] + in_swift_block = False + + for line in content.split("\n"): + # Check for Swift code fence start + if re.match(r'^(\s*)```swift', line): + in_swift_block = True + result_lines.append(line) + continue + + # Check for code fence end + if in_swift_block and line.strip() == "```": + in_swift_block = False + result_lines.append(line) + continue + + # Process lines inside Swift blocks + if in_swift_block: + # Skip lines starting with `# ` or exactly `#` + stripped = line.lstrip() + if stripped.startswith("# ") or stripped == "#": + continue + result_lines.append(line) + else: + result_lines.append(line) + + return "\n".join(result_lines) + + +def process_book(book: dict) -> dict: + """Process all chapters in the book, stripping hidden lines.""" + + def process_item(item: dict) -> dict: + """Recursively process book items.""" + if "Chapter" in item: + chapter = item["Chapter"] + if "content" in chapter: + chapter["content"] = strip_hidden_lines(chapter["content"]) + + # Process nested items (sub-chapters) + if "sub_items" in chapter: + chapter["sub_items"] = [process_item(sub) for sub in chapter["sub_items"]] + + return item + + # Process all sections + if "sections" in book: + book["sections"] = [process_item(section) for section in book["sections"]] + + return book + + +def main(): + # Handle the "supports" check from mdbook + if len(sys.argv) > 2 and sys.argv[1] == "supports": + renderer = sys.argv[2] + # We support all renderers, but only modify content for html + sys.exit(0) + + # Read the preprocessor context and book from stdin + # mdbook sends [context, book] as a JSON array + try: + context, book = json.load(sys.stdin) + except json.JSONDecodeError as e: + print(f"Error: Failed to parse JSON from stdin: {e}", file=sys.stderr) + sys.exit(1) + except ValueError as e: + print(f"Error: Expected [context, book] array: {e}", file=sys.stderr) + sys.exit(1) + + # Check which renderer we're preprocessing for + renderer = context.get("renderer", "") + + # Only strip hidden lines for HTML output + # For swift-test, we want to keep the hidden lines for compilation + if renderer == "html": + book = process_book(book) + + # Output the (possibly modified) book as JSON + json.dump(book, sys.stdout) + + +if __name__ == "__main__": + main() diff --git a/scripts/mdbook-swift-test.py b/scripts/mdbook-swift-test.py new file mode 100755 index 0000000..8a4034e --- /dev/null +++ b/scripts/mdbook-swift-test.py @@ -0,0 +1,332 @@ +#!/usr/bin/env python3 +""" +mdbook-swift-test: An mdbook backend that tests Swift code examples. + +This backend extracts Swift code blocks from mdbook chapters and verifies +they compile using `swiftc -typecheck`. + +Usage: + Configured in book.toml as: + + [output.swift-test] + command = "python3 scripts/mdbook-swift-test.py" + +Code Block Attributes: + ```swift,ignore - Skip this block (incomplete/illustrative) + +Placeholder Bodies: + The pattern `{ ... }` is automatically replaced with `{ fatalError() }` + before compilation. This allows documentation to show elided implementations + while still compiling: + + func doSomething() -> Int { ... } + + Compiles as: + + func doSomething() -> Int { fatalError() } + +Hidden Lines (mdbook-style): + Lines starting with `# ` are included in compilation but can be used + to provide context (imports, type definitions, etc.) that readers + don't need to see in the rendered book. + + Example: + ```swift + # import Foundation + # struct Point { var x: Int; var y: Int } + let p = Point(x: 1, y: 2) + print(p) + ``` + + The `# ` prefix is stripped before compilation, so all lines are + compiled together. + +Note: Unlike Rust code blocks, mdbook does not automatically hide `# ` +lines for Swift. If you want them hidden in the rendered output, you'll +need a preprocessor or keep the context minimal. +""" + +import json +import os +import re +import subprocess +import sys +import tempfile +from dataclasses import dataclass +from pathlib import Path + + +@dataclass +class CodeBlock: + """A Swift code block extracted from a markdown chapter.""" + content: str + line_number: int + chapter_name: str + chapter_path: str + ignore: bool = False + + +def parse_attributes(info_string: str) -> dict: + """ + Parse code block attributes from the info string. + + Examples: + "swift" -> {"lang": "swift", "ignore": False} + "swift,ignore" -> {"lang": "swift", "ignore": True} + """ + attrs = {"lang": None, "ignore": False} + + if not info_string: + return attrs + + parts = info_string.split(",") + if parts: + attrs["lang"] = parts[0].strip().lower() + + for part in parts[1:]: + part = part.strip() + if part == "ignore": + attrs["ignore"] = True + + return attrs + + +def process_hidden_lines(content: str) -> str: + """ + Process hidden lines in the mdbook style. + + Lines starting with `# ` have the prefix stripped. + Lines that are exactly `#` become empty lines. + All other lines are kept as-is. + + This allows authors to include setup code (imports, type definitions) + that is compiled but could be hidden from readers. + """ + processed_lines = [] + for line in content.split("\n"): + if line.startswith("# "): + # Strip the `# ` prefix + processed_lines.append(line[2:]) + elif line == "#": + # A lone `#` becomes an empty line + processed_lines.append("") + else: + # Keep the line as-is + processed_lines.append(line) + return "\n".join(processed_lines) + + +def replace_placeholder_bodies(content: str) -> str: + """ + Replace placeholder function bodies with fatalError(). + + This allows authors to write `{ ... }` in documentation to indicate + an elided implementation, while still allowing the code to compile. + + Patterns replaced: + { ... } -> { fatalError() } + {...} -> { fatalError() } + """ + # Replace `{ ... }` (with optional whitespace) with `{ fatalError() }` + content = re.sub(r'\{\s*\.\.\.\s*\}', '{ fatalError() }', content) + return content + + +def extract_code_blocks(content: str, chapter_name: str, chapter_path: str) -> list[CodeBlock]: + """ + Extract all Swift code blocks from markdown content. + + Handles fenced code blocks with ``` markers. + """ + blocks = [] + lines = content.split("\n") + + i = 0 + while i < len(lines): + line = lines[i] + + # Check for code fence start + if line.strip().startswith("```"): + fence_match = re.match(r'^(\s*)```(\S*)', line) + if fence_match: + indent = fence_match.group(1) + info_string = fence_match.group(2) + attrs = parse_attributes(info_string) + + if attrs["lang"] == "swift": + # Found a Swift block, collect its content + start_line = i + 1 # 1-indexed for human readability + code_lines = [] + i += 1 + + # Find the closing fence + while i < len(lines): + if lines[i].strip() == "```": + break + code_lines.append(lines[i]) + i += 1 + + block = CodeBlock( + content="\n".join(code_lines), + line_number=start_line, + chapter_name=chapter_name, + chapter_path=chapter_path, + ignore=attrs["ignore"], + ) + blocks.append(block) + i += 1 + + return blocks + + +def compile_swift(source: str, block: CodeBlock) -> tuple[bool, str]: + """ + Compile Swift source using swiftc -typecheck. + + Returns (success, error_message). + """ + with tempfile.NamedTemporaryFile(mode="w", suffix=".swift", delete=False) as f: + f.write(source) + temp_path = f.name + + try: + result = subprocess.run( + ["swiftc", "-typecheck", temp_path], + capture_output=True, + text=True, + timeout=30, + ) + + if result.returncode == 0: + return True, "" + else: + # Combine stderr and stdout for error output + error = result.stderr or result.stdout or "Unknown compilation error" + return False, error + except FileNotFoundError: + return False, "swiftc not found in PATH" + except subprocess.TimeoutExpired: + return False, "Compilation timed out (30s)" + finally: + try: + os.unlink(temp_path) + except OSError: + pass + + +def process_book(book_data: dict) -> tuple[int, int, list[str]]: + """ + Process all chapters in the book and test Swift code blocks. + + Returns (passed_count, failed_count, error_messages). + """ + all_blocks: list[CodeBlock] = [] + errors: list[str] = [] + + def process_item(item: dict): + """Recursively process book items.""" + if "Chapter" in item: + chapter = item["Chapter"] + name = chapter.get("name", "Unknown") + path = chapter.get("path") or "unknown.md" + content = chapter.get("content", "") + + # Extract blocks from this chapter + blocks = extract_code_blocks(content, name, path) + all_blocks.extend(blocks) + + # Process nested items (sub-chapters) + for sub_item in chapter.get("sub_items", []): + process_item(sub_item) + + # Process all sections + sections = book_data.get("sections", []) + for section in sections: + process_item(section) + + # Now test all non-ignored blocks + passed = 0 + failed = 0 + + for block in all_blocks: + if block.ignore: + continue + + # Skip empty blocks + if not block.content.strip(): + continue + + # Process hidden lines and placeholder bodies, then compile + source = process_hidden_lines(block.content) + source = replace_placeholder_bodies(source) + + # Skip if the processed source is empty (all hidden lines) + if not source.strip(): + continue + + success, error = compile_swift(source, block) + + if success: + passed += 1 + else: + failed += 1 + location = f"{block.chapter_path}:{block.line_number}" + errors.append(f"FAIL: {location}\n{error}") + + return passed, failed, errors + + +def main(): + # Handle the "supports" check from mdbook + if len(sys.argv) > 1: + if sys.argv[1] == "supports": + # We support all renderers (we're a validation backend) + sys.exit(0) + + # Read the render context from stdin + try: + context = json.load(sys.stdin) + except json.JSONDecodeError as e: + print(f"Error: Failed to parse JSON from stdin: {e}", file=sys.stderr) + sys.exit(1) + + # Extract the book data + book = context.get("book", {}) + config = context.get("config", {}) + + # Get configuration options + swift_test_config = config.get("output", {}).get("swift-test", {}) + verbose = swift_test_config.get("verbose", False) + + print("=" * 60, file=sys.stderr) + print("Swift Code Example Testing", file=sys.stderr) + print("=" * 60, file=sys.stderr) + + # Process the book + passed, failed, errors = process_book(book) + + # Report results + total = passed + failed + if total == 0: + print("No Swift code blocks found to test.", file=sys.stderr) + sys.exit(0) + + print(f"\nResults: {passed} passed, {failed} failed, {total} total", file=sys.stderr) + + if errors: + print("\n" + "-" * 60, file=sys.stderr) + print("FAILURES:", file=sys.stderr) + print("-" * 60, file=sys.stderr) + for error in errors: + print(f"\n{error}", file=sys.stderr) + + print("=" * 60, file=sys.stderr) + + # Exit with appropriate code + if failed > 0: + sys.exit(1) + sys.exit(0) + + +if __name__ == "__main__": + main() From bcbaf31053f80c858a427d3e4f4d88abd5011123 Mon Sep 17 00:00:00 2001 From: Nick DeMarco Date: Wed, 4 Feb 2026 13:31:40 -0500 Subject: [PATCH 2/2] use swift@v3 --- .github/workflows/build.yml | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index dcf67c5..1e8c8dc 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -21,7 +21,7 @@ jobs: steps: - name: Checkout uses: actions/checkout@v6 - + # Cache Rust toolchain and cargo registry - name: Cache Rust uses: actions/cache@v4 @@ -36,28 +36,27 @@ jobs: key: ${{ runner.os }}-cargo-${{ hashFiles('versions.txt') }} restore-keys: | ${{ runner.os }}-cargo- - + - name: Install mdBook and plugins (Linux) if: runner.os != 'Windows' run: | chmod +x scripts/install-tools.sh ./scripts/install-tools.sh - + - name: Install mdBook and plugins (Windows) if: runner.os == 'Windows' run: .\scripts\install-tools.ps1 # Install Swift for testing Swift code examples in the book - name: Setup Swift - uses: swift-actions/setup-swift@v2 + uses: swift-actions/setup-swift@v3 - name: Build with mdBook run: mdbook build ./better-code - + - name: Upload build artifact uses: actions/upload-artifact@v4 with: name: mdbook-build-${{ matrix.os }} path: ./better-code/book retention-days: 1 -