diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index a2b1da2b..a054c341 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -196,6 +196,14 @@ jobs: # real 908. No browser, no built tree. - name: Verify the page-count drift guard (check_page_baseline.mjs) run: node scripts/check_page_baseline.mjs + # The book-coverage warnings say nothing when every page has an entry + # in docs/_book.yml, which is also all a check that had stopped working + # would say. Before they existed, whole sections dropped out of the PDF + # without a word. These probes give each of the five findings a fault + # to report, on a manifest and pages built in memory. No browser, no + # built tree. + - name: Verify the book-coverage warnings (check_book_coverage.mjs) + run: node scripts/check_book_coverage.mjs # Graphviz sizes each node box from a width table; the browser paints # the label with a real font. Nothing in the build compares the two, so # a mismatch ships as text hanging outside its box on a green build -- diff --git a/.github/workflows/tbdocs-gh-pages.yml b/.github/workflows/tbdocs-gh-pages.yml index f4a63fbf..0fb06dcc 100644 --- a/.github/workflows/tbdocs-gh-pages.yml +++ b/.github/workflows/tbdocs-gh-pages.yml @@ -149,6 +149,14 @@ jobs: # real 908. No browser, no built tree. - name: Verify the page-count drift guard (check_page_baseline.mjs) run: node scripts/check_page_baseline.mjs + # The book-coverage warnings say nothing when every page has an entry + # in docs/_book.yml, which is also all a check that had stopped working + # would say. Before they existed, whole sections dropped out of the PDF + # without a word. These probes give each of the five findings a fault + # to report, on a manifest and pages built in memory. No browser, no + # built tree. + - name: Verify the book-coverage warnings (check_book_coverage.mjs) + run: node scripts/check_book_coverage.mjs # Graphviz sizes each node box from a width table; the browser paints the # label with a real font, and nothing in the build compares the two -- so a # mismatch ships as text hanging outside its box on a green build. See the diff --git a/WIP.Build.md b/WIP.Build.md index 0839ca72..5b9927f3 100644 --- a/WIP.Build.md +++ b/WIP.Build.md @@ -151,7 +151,7 @@ Two implementations of one check is exactly the shape that rots quietly: **a che node scripts/check_links_diff.mjs --a script --b fused ``` -It diffs the two implementations' findings category by category across the real invocations -- `_site/` with sitemap + search + canonical, `_site-offline/` with the forbidden-prefix rule, `book.html`, and a `--baseurl` tree checked with the matching base path. It is deliberately *not* in `check.bat`: the script side costs ~3 s, which is the whole saving. +It diffs the two implementations' findings category by category across the real invocations -- `_site/` with sitemap + search + canonical, `_site-offline/` with the forbidden-prefix rule, `book.html` with the same rule (there it collects the links that leave the book for the website, reported as `OUT OF BOOK`), and a `--baseurl` tree checked with the matching base path. It is deliberately *not* in `check.bat`: the script side costs ~3 s, which is the whole saving. Two further modes matter: @@ -523,6 +523,44 @@ fence marker, so a probe with no fence *after* the admonition passes against the stasher it was written to catch. The damage is always to the prose **between** two fences. +### The book-coverage warnings + +**A page no `_book.yml` entry selected was left out of the PDF without a word**, and +by September 2026 that had taken 52 pages out of the book. Some were deliberate --- +the 404 page, Videos, Challenges --- and some were not: Data Types, Enumerations and +twinBASIC Additions are as plainly reference material as anything the book carries, +and nothing recorded why they were missing. The IDE section's pages with real prose +went the same way as its placeholders. The only trace was the book pass of the link +check, which listed the 32 links from the book to pages it did not carry as `BROKEN`, +on a pass marked informational --- so the list read as noise. + +Two halves fixed it, and the second is what makes the first worth having. **`left_out:` +in `_book.yml` names every page that is out on purpose, with a `reason:`**, and +`bookCoverage()` in `builder/book.mjs` warns about a page that is in neither. Every +page has an entry one way or the other, so a warning is a decision nobody has made. +Without the list the warning fired for 37 pages on every build, which is a warning +nobody reads after the first week. + +It reports five things, all empty on a consistent manifest: a page in no entry, a page +in the book and in `left_out:`, a book entry that selects no page, a `left_out:` entry +that matches none (a page renamed or deleted), and a landing or foreword URL no page +publishes at. **They are warnings, not failures**: the book is complete for the +manifest it was given, and a new page should not stop a build. They print under the +`pdf:` summary, and only when the book is built, so `--serve` does not repeat them on +every save. A link from the book to a page left out opens the website instead, and the +link check lists it as `OUT OF BOOK` --- which left-out pages the book still links to. + +**"In the book" has to mirror `emitPart`, not the selectors.** A chaptered part's +`landing_page` and a `foreword_page` are emitted by URL rather than selected, so a +check that walked only `_chapters` would report the Features landing and the Packages +foreword on every build. The emission sites are listed once, in `bookCoverage()`, and +two probes pin them. + +`scripts/check_book_coverage.mjs` is the gate on it, in `test.bat` and both CI +workflows: twelve probes over pages and a manifest built in memory, so it reads nothing +under `docs/`. Dropping the chaptered-landing site fails ten of the twelve; ignoring +`left_out:` fails nine. + ### Build-time counts as named values `{{tbdocs:pages}}` in a page renders as the number of pages the build diff --git a/WIP.md b/WIP.md index 901b0051..4001416b 100644 --- a/WIP.md +++ b/WIP.md @@ -97,7 +97,7 @@ The rest of this file is the maintenance guide for updating existing pages or ad - `docs/Reference/Procedures and Functions.md` — alphabetical index of procedures/functions. - `docs/LLVM/` — the LLVM section: compiling with the LLVM back end. A top-level section between Features and Reference Section in the nav, at `nav_order: 6` (Features moved to 5 to make room). `index.md` is the landing page and `Getting-Started.md` the only page so far, with its screenshots under `Images/`. Everything it describes arrived in **BETA 984**; the local BETA 983 compiler restricts `+llvm` to standard-module procedures and to Professional/Ultimate, and has no LLVM project settings at all. Both of the page's samples are marked `check_build` and compile clean on 983 --- but the harness asks only the front end, which accepts `[CompilerOptions]` with every CPU flag in it; nothing runs LLVM code generation, so a clean run says nothing about the 984 behaviour the page describes. - **A new top-level section needs four things besides its folder**, and no gate asks for the first three: a `nav_order` between its neighbours; a part in `docs/_book.yml`, since a page no part claims is left out of the PDF without a warning --- the IDE, Challenges and Videos sections are all absent from the book that way; a line in *Where content lives* in `docs/Documentation/Authoring.md`; and the page-count rise that `build.bat` writes to `builder/page-baseline.json`, committed with the pages. A section index lists its topics by hand and sets `has_toc: false`, or the template appends a second, automatic list of its children. + **A new top-level section needs four things besides its folder**, and nothing checks the first and the third: a `nav_order` between its neighbours; an entry in `docs/_book.yml` for every page, in a part or in `left_out:` with a reason, which the build warns about under its `pdf:` summary when one is missing; a line in *Where content lives* in `docs/Documentation/Authoring.md`; and the page-count rise that `build.bat` writes to `builder/page-baseline.json`, committed with the pages. The Challenges and Videos sections are in `left_out:`, and so are the IDE pages that are still screenshots and labels; the IDE part names its pages one by one, so a new IDE page warns until it goes into the part or into `left_out:`. A link from the book to a left-out page opens the website, and the pass over `book.html` lists it as `OUT OF BOOK`. A section index lists its topics by hand and sets `has_toc: false`, or the template appends a second, automatic list of its children. - Footer rendering — [builder/template.mjs](builder/template.mjs)'s `renderFooterCustom()` renders the copyright line and, when `vba_attribution: true` is set in a page's frontmatter, an additional CC-BY-4.0 attribution line beneath it. - Contributor authoring guide — [docs/Documentation/Authoring.md](docs/Documentation/Authoring.md) is the public "start here" page that distils this file's authoring conventions (page template, heading levels, formatting, plain-English prose, attribution policy, cross-section linking) for a new contributor. This file remains the exhaustive maintainer source of truth; keep the two in sync when a convention changes. @@ -452,7 +452,7 @@ Why the report separates the wedged task from the merely blocked ones, and why - `build.bat` — runs `node builder\tbdocs.mjs --src docs --check-audit-index` (which implies `--check`) and produces three trees in one pass: the online copy at `_site/`, a `file://`-browsable copy at `_site-offline/`, and the sparse pagedjs source at `_site-pdf/`. The offline pass adds ~700 ms and the PDF pass adds ~150 ms on top of the ~2 s online build. Toggle `also_build_offline` / `also_build_pdf` in `_config.yml` (or pass `--no-offline` / `--no-pdf`) to skip a sibling output. `--check` adds ~1.7 s and runs the link + integrity check over the HTML while it is still in worker memory; `build.bat --no-check` gets a plain build. - `serve.bat` — runs `tbdocs --serve`: initial build, then a long-lived process with watcher, debounced rebuilds, and SSE-driven browser auto-reload. Writes to `docs/_serve/` (disjoint from `build.bat`'s `_site*/`) and skips the offline + PDF passes — so a one-off `build.bat` for the PDF or offline mirror doesn't disturb the live preview. Ctrl+C to stop. - `check.bat` — the gates that read the built site: a freshness check that refuses a stale tree (`scripts/check_tree_fresh.mjs`), the DOT diagram fit check (`scripts/check_dot_fit.mjs`), the a11y sample-coverage check (`scripts/pick_a11y_sample.mjs --check`), then the accessibility check (`scripts/check_a11y.mjs`). The link + integrity check moved into `build.bat`. ~37 s. -- `test.bat` — the tests the *toolchain* has to pass: the publish-allowlist self-test (`scripts/check_publish_policy.mjs`), the gate-list check (`scripts/check_gate_lists.mjs`), the regex-safety gate (`scripts/check_regex_safety.mjs`), the code-region gate (`scripts/check_code_regions.mjs`), the page-count drift-guard probes (`scripts/check_page_baseline.mjs`), and the axe source-patch verification (`scripts/check_axe_patch_equiv.mjs`). ~8 s. See [What belongs in test.bat rather than check.bat](WIP.Build.md#what-belongs-in-testbat-rather-than-checkbat). +- `test.bat` — the tests the *toolchain* has to pass: the publish-allowlist self-test (`scripts/check_publish_policy.mjs`), the gate-list check (`scripts/check_gate_lists.mjs`), the regex-safety gate (`scripts/check_regex_safety.mjs`), the code-region gate (`scripts/check_code_regions.mjs`), the page-count drift-guard probes (`scripts/check_page_baseline.mjs`), the book-coverage probes (`scripts/check_book_coverage.mjs`), and the axe source-patch verification (`scripts/check_axe_patch_equiv.mjs`). ~8 s. See [What belongs in test.bat rather than check.bat](WIP.Build.md#what-belongs-in-testbat-rather-than-checkbat). - `book.bat` — renders the PDF from `docs\_site-pdf\book.html` via `node book\render-book.mjs` into `docs\_pdf\twinBASIC Book.pdf`. Run `build.bat` first to populate `_site-pdf/`; `book.bat` refuses a tree older than its sources rather than rendering the previous book (see [The book refuses a stale source tree](WIP.Build.md#the-book-refuses-a-stale-source-tree)). - `examples.bat` — compiles the documentation's own twinBASIC code samples, every `tb` fence marked `check_build`, and reports the ones the compiler refuses against the line in the page they came from. Needs a twinBASIC install and Windows, so it is outside every gate and outside CI; ~110 s over the 1,119 samples marked today. Two modes need no compiler at all: `--census` classifies every fence and says how many classifiable ones are still unmarked, and `--report ` groups a saved `--propose --json` survey by diagnostic, section and unresolved name. `--propose` itself does compile. See [Compiling the reference's own code samples](#compiling-the-references-own-code-samples) and [WIP.ExamplesBuild.md](WIP.ExamplesBuild.md). @@ -470,7 +470,7 @@ build.bat && check.bat On the dev box that is ~4 s of build against ~37 s of check, of which the axe scan is ~20 s. [builder/PLAN-checks.md](builder/PLAN-checks.md) records how the link checker got folded into the build's task graph, what it cost and what it saved; the axe follow-ons are designed there but not implemented. -**If the change touched `builder/`, `scripts/`, `book/`, `eval/` or `wisdom/`, run `test.bat` as well** --- another ~8 s. Four of its six gates cannot be affected by a content edit at all. **Two can.** `check_gate_lists.mjs` is the easy one to predict: it reads `README.md` and every page under `docs/Documentation/`, so an edit to any developer page that states a gate count can fail it. **`check_code_regions.mjs` is the one worth understanding**, and which half of it a content edit reaches is worth keeping straight. Its corpus sweep has `ROOT = /docs` and tokenises all 906 markdown files, so a page that provokes a rewrite into *altering* a code region fails it --- that half is content-dependent. Its fixed probes are not: they run against their own sources whatever the tree holds, and they cover the **mirror** fault, where a rewrite silently stops firing. The sweep structurally cannot see that one, because text the rewrite skipped is stashed and restored unchanged and every region still matches. So run `test.bat` after adding an unusual code construct --- a fence whose contents include a fence marker, a 4-space indented block, an admonition wrapping a fence --- and read the built page as well, because for the mirror fault the gate is asserting that the stasher still works rather than checking your page: +**If the change touched `builder/`, `scripts/`, `book/`, `eval/` or `wisdom/`, run `test.bat` as well** --- another ~8 s. Five of its seven gates cannot be affected by a content edit at all. **Two can.** `check_gate_lists.mjs` is the easy one to predict: it reads `README.md` and every page under `docs/Documentation/`, so an edit to any developer page that states a gate count can fail it. **`check_code_regions.mjs` is the one worth understanding**, and which half of it a content edit reaches is worth keeping straight. Its corpus sweep has `ROOT = /docs` and tokenises all 906 markdown files, so a page that provokes a rewrite into *altering* a code region fails it --- that half is content-dependent. Its fixed probes are not: they run against their own sources whatever the tree holds, and they cover the **mirror** fault, where a rewrite silently stops firing. The sweep structurally cannot see that one, because text the rewrite skipped is stashed and restored unchanged and every region still matches. So run `test.bat` after adding an unusual code construct --- a fence whose contents include a fence marker, a 4-space indented block, an admonition wrapping a fence --- and read the built page as well, because for the mirror fault the gate is asserting that the stasher still works rather than checking your page: ```sh build.bat && check.bat && test.bat @@ -495,7 +495,7 @@ wrapper: | `check.bat` | `pick_a11y_sample --check`, `check_a11y` | see [WIP.A11y.md](WIP.A11y.md) | | `test.bat` | `check_code_regions` | no source or HTML rewrite altered a code region | | `test.bat` | `check_regex_safety` | no regex in the tree can backtrack exponentially | -| `test.bat` | `check_publish_policy`, `check_gate_lists`, `check_page_baseline`, `check_axe_patch_equiv` | the gates on the gates | +| `test.bat` | `check_publish_policy`, `check_gate_lists`, `check_page_baseline`, `check_book_coverage`, `check_axe_patch_equiv` | the gates on the gates | **A gate belongs in `test.bat` rather than `check.bat` if it would still mean something with no documentation in the tree.** That is the whole rule; it is diff --git a/builder/REVIEW-USECASES-4a67e09.md b/builder/REVIEW-USECASES-4a67e09.md new file mode 100644 index 00000000..5737b9af --- /dev/null +++ b/builder/REVIEW-USECASES-4a67e09.md @@ -0,0 +1,426 @@ +# Use-case review, round 11 --- the re-runs recover, a console program that prints nothing, and an add-in reference the page calls automatic, at `4a67e09` + +Branch `staging` · reviewed 2026-09-24 · 9 cases + +The eleventh round of [the harness in `eval/`](../eval/README.md), and the fourth with isolated +evaluators. It ran the five re-runs round 10 named --- UC-62 and UC-66 against *Keeping a project +in Git from the IDE*, UC-69 against the DLL section, UC-67 against the callback section, UC-55 +against the single export command --- and UC-65 against its `Max` sample, made a whole file. It +opened three surfaces no case had read, each executed: a command-line tool built and run at a +command prompt (UC-70), two calculations on threads (UC-71), and an IDE add-in built, loaded and +clicked in a private copy of the install (UC-72). The corpus was built at `4a67e09`, round 10's +last commit. The [Round 11 section of eval/usecases.md](../eval/usecases.md) records the goals +verbatim. + +## Verdict + +**Round 10's fixes worked, on every axis but one.** Across the six re-runs, completeness rose ++1.00 and actionability +1.33, and UC-69, which scored 2/3/1 as a DLL the pages could not call, +scored 4/4/4 and ran from a 32-bit and a 64-bit caller as its answer predicted. Discoverability +moved ±0.00, exactly as it did across round 4's fix pass in round 5: the pages the fixes wrote +are right, and the evaluators' own words still reach them no more often. Measured on round 10's +own queries, the fixes did move it --- the sections that answer now are in the top ten for 21 of +36 of them, against 4 --- so what did not move is the next evaluator's choice of words. + +**The new surfaces failed where the pages were thin, and one page was wrong.** The tbIDE package +page says its reference "is added to addin projects automatically"; UC-72's evaluator believed +it, and its add-in failed with 18 errors, `TB5079 Unrecognized datatype symbol 'AddIn'` first. +With the reference added, it built, loaded and inserted the date as predicted --- and its next +build, once the IDE had loaded it, failed with *error code 32*, which no page mentions. The +console program was worse off: the Console Applications section is one paragraph, and both UC-70 +evaluators wrote programs that fail the goal in different ways. One printed nothing and always +exited 0, because `Debug.Print` writes nothing in a built `.exe` and `End` sets no exit code; +the other built only after the template's own `Sub Main` was removed, and wrote nothing when its +output was redirected. + +**And the harness lost a channel without saying so.** Two evaluators chained `site-search` after +a `cd`, were refused, and concluded the search box was not available. Their digests flag it --- +*no site search at all* --- and both were run again once the protocol told them to run the +command on its own. + +| | completeness | discoverability | actionability | +|---|---:|---:|---:| +| round 1 (16 cases) | 2.75 | 2.75 | 3.00 | +| round 2 (16 cases) | 2.56 | 2.69 | 2.69 | +| round 3 (8 cases) | 2.75 | 2.38 | 2.88 | +| round 4 (8 cases) | 3.00 | 2.50 | 3.00 | +| round 5 (8 cases) | 3.00 | 2.25 | 3.38 | +| round 6 (8 cases) | 3.13 | 2.75 | 3.00 | +| round 7 (8 cases) | 3.25 | 2.75 | 3.63 | +| round 8 (13 cases) | 3.62 | 3.23 | 3.31 | +| round 9 (11 cases) | 3.55 | 3.45 | 3.55 | +| round 10 (9 cases) | 3.00 | 3.33 | 2.78 | +| **round 11 (9 cases)** | **3.22** | **3.33** | **3.00** | + +Per case. The scores are the evaluators' own except where verification changed them, which is +in bold and explained under *Corrections* below: + +| case | compl. | disc. | act. | hazard | +|---|---:|---:|---:|---| +| UC-55 *(site, re-run)* a project in Git, and a rebuild script | 4 | **3** | 4 | pass; its export command has `--overwrite` now | +| UC-62 *(site, re-run)* a project in Git, kept from the IDE | 4 | 3 | 4 | pass; the route it asked for is there | +| UC-65 *(site, re-run)* a generic function and a generic class --- **executed** | 3 | 4 | 3 | none known; ran as predicted | +| UC-66 *(site, re-run)* rebuild a project kept by the IDE, from a fresh clone | 4 | **3** | **3** | fail, by the page's order: its answer imports before it removes the compiler packages | +| UC-67 *(site, re-run)* pass a function as an argument --- **executed** | 4 | 3 | 4 | none known; ran as the page shows | +| UC-69 *(site, re-run)* a twinBASIC DLL called from Excel VBA --- **built and called** | 4 | 4 | 4 | pass on all three; the pattern ran from a win32 and a win64 caller | +| UC-70 *(site)* a command-line tool with an exit code --- **executed at a prompt** | 2 | 3 | **1** | three that no page knew: `Debug.Print`, `End` and redirected output | +| UC-71 *(site)* two calculations on two threads --- **executed** | 2 | 3 | 2 | none known; ran as predicted | +| UC-72 *(site)* an IDE add-in with a toolbar button --- **built, loaded and clicked** | **2** | 4 | **2** | walked into the page's claim that the reference is automatic | + +### The re-runs + +| case | round 10 | round 11 | +|---|---|---| +| UC-55 project in Git | 4 / 3 / 3 | 4 / 3 / 4 | +| UC-62 Git from the IDE | 2 / 4 / 2 | 4 / 3 / 4 | +| UC-65 generics, executed | 3 / 4 / 2 | 3 / 4 / 3 | +| UC-66 fresh clone | 3 / 4 / 3 | 4 / 3 / 3 | +| UC-67 callbacks, executed | 3 / 2 / 3 | 4 / 3 / 4 | +| UC-69 DLL from VBA, built and called | 2 / 3 / 1 | 4 / 4 / 4 | +| **mean** | **2.83 / 3.33 / 2.33** | **3.83 / 3.33 / 3.67** | + +UC-62 and UC-66 each lost a point of discoverability: the phrasings this round's evaluators +chose rank the new section 3rd, 7th, 8th and 9th, or miss it. UC-67 and UC-69 each gained one, +from sections titled for their readers' words. All nine cases ran on the model and Claude Code +build of rounds 8--10, `claude-sonnet-5` through 2.1.280. + +## A discoverability fix, measured on the same queries + +Round 10's own queries for the six re-run cases, word for word, against round 10's snapshot of +the index and this round's. A hit is the section that answers the goal now, in the top ten: + +| case | round 10's queries | hits, round 10 index | hits, round 11 index | +|---|---|---:|---:| +| UC-55 | 4, from `git source control project file` to `command line build twinproj from folder` | 1 | 1 | +| UC-62 | `git integration IDE`, `auto commit on save`, `export project on save git`, `version control settings` | 0 | 2 | +| UC-65 | `generics`, `generic function max of two values`, `generic stack class example` | 3 | 3 | +| UC-66 | `put my twinBASIC project in git`, `open project without twinproj file`, `clone repository rebuild twinproj`, `export project every save git` | 0 | 3 | +| UC-67 | 7, among them `callback function as argument`, `pass function as parameter`, `AddressOf`, `delegate` | 0 | 3 | +| UC-69 | 14, among them `call twinBASIC DLL from VBA`, `Excel VBA declare function DLL`, `ActiveX DLL VBA` | 0 | 9 | + +**4 of 36 to 21 of 36.** Against round 10's broader targets --- any section of the page that +answers, which is what round 10 scored --- 21 of 36 became 23. The two readers' phrasings round 10 +titled the callback section for, `callback function as argument` and `pass function as +parameter`, rank it 1st, and `ActiveX DLL VBA`, 4th for the DLL page in round 10, ranks the DLL +section 1st. Still missing: `rebuild project file from source files`, `auto commit on save` and +`version control settings`. + +This round's own 56 queries rank as the evaluators reported, with two exceptions, both +overstatements: UC-69's `create ActiveX DLL twinBASIC` ranks the Standard DLLs section 4th, not +1st, and UC-66 swapped the ranks of *Export Project* (1st) and *Keeping a project in Git* +(2nd). Neither changes a score. + +## The session as evidence + +**UC-70 and UC-71 lost Channel 1 to the harness.** Each began by chaining the search after a +change of directory --- `cd ".../corpus" && site-search "command line arguments"` --- which the +evaluator's one allow rule refuses. Six other evaluators did the same and retried with the +bare command; these two concluded, in their own words, that "the `site-search` command requires +the Bash tool, which was denied". UC-70 then made eight full-text searches and scored its +discoverability 1; UC-71 made six, "only to check completeness after the answer was found". +Both were run again with the protocol's new sentence (finding 10), and both searched first: +UC-70 20 times, UC-71 8. The second runs are the ones scored; the first runs' answers were +executed as well. + +**Four other flags were smaller.** UC-65 grepped twice for a generic `Stack` class to confirm +there is none, UC-71's second run grepped for `Sub Main`, and UC-72 grepped for *Build Output +Path* while checking a variable; each report says Channel 3 was not needed. UC-66 made no +full-text search this time, where round 10's run found its answer by five. + +**Navigation was measured from the links, with `eval/nav_hops.mjs`**: the Git sections, +Delegates, Generics, Project Types and Multithreading are three hops from the welcome page, and +the tbIDE package and the Add Ins page one. UC-62 and UC-66 reported four hops and UC-72 two; +by links it is three and one. + +## The executed cases + +Each ran through `scripts/tbrun.mjs` on BETA 983, from one process that owned the registry tidy, +with the evaluator's code verbatim --- a bare module body wrapped in a `Module` block, as rounds +9 and 10 did --- and a `[RunAfterBuild]` Sub beginning `Debug.Cls` in place of the reader's click. + +**UC-65 ran as predicted**: ` 42 `, ` 3.14 `, `banana`, less the sign spaces rounds 9 and 10 also +noted. Its `Stack(Of T)`, which the evaluator composed from the page's `List(Of T)`, compiled +first time. **UC-67 ran as the page shows**, since its answer is the page's sample: the five +names alphabetically, then by length. + +**UC-69 was built and called from both bitnesses.** The DLL is the Standard DLL template's +settings with the page's two functions, built win32 and win64; each caller is a twinBASIC +project holding the evaluator's VBA verbatim, the `Lib` path aside, since a plain `Declare` +converts strings to and from ANSI as VBA's does. Both printed ` 5 ` and `Hello, Ann`, and both +returned `Hello, Zoë Łódź 東京` intact, character for character. This is round 10's second +*What to do next*, done: the pointer pattern works in a 64-bit caller. Excel itself was not +driven. + +**UC-71 ran as predicted, in both runs**: `Primes below 200000: 17984` and `Numbers 1 to 5000000 +divisible by 7 or 11: 1103895`. Two things it relied on without a page to say so both hold: +module-level variables are shared between threads, and a thread procedure declared as a `Sub` +with no parameters runs. One step past the answer, a thread whose calculation overflows with +nothing to handle the error never finishes: in the IDE it was still running ten seconds later, +so the answer's `WaitForSingleObject ..., INFINITE` would wait for ever (finding 5). + +**UC-70 was built into an `.exe` and run at a command prompt**, in a console window, redirected +to a file and piped, with the exit code read after each. The second run's answer prints with +`Debug.Print` and stops with `End`: **it printed nothing, in any case, and every exit code was +0.** The first run's answer, as handed over, did not build --- it adds a module with a `Sub Main` +to the Console App template, whose `MainModule` has one already: *'Main' is ambiguous*. Without +the template's module it printed `three.txt: 3 lines` for a file of three lines ending CRLF, +`linecount: file not found: missing.txt` with exit code 1, and the usage line with exit code 1 +--- and wrote nothing at all with its output redirected, because it calls `WriteConsoleA`. A +file with Unix line endings counted as one line, as the `Line Input #` page says it would. + +**UC-72 was built, loaded and clicked by the add-in test runner**, in a lane of its own with a +private copy of the install. As handed over, without the package reference its answer calls +automatic, it did not compile (finding 1). With the reference, the IDE loaded *Insert Date +AddIn*, its button showed *Insert Date* on the toolbar, and a click with the cursor in `Main.twin` +inserted `' 2026-09-24` there, as predicted --- the rest of the line moving down, unindented, +as inserting a line break at the cursor does. + +**Two builds failed that built on a retry**: UC-71's first, and one probe's, each with +`[TYPELIB] failed to finalize typelibrary. Disk error?` --- once among four concurrent builds and +once among two. The cause was not isolated (finding 13). + +## Tier 1 --- the site does something harmful, or says something the product does not do + +**1. The tbIDE page said the package reference is added automatically, and it is not.** *"It is +added to addin projects automatically; there is no need to add it manually through Project → +References."* No template makes an add-in project: a reader starts from the Standard DLL +template, as UC-72's evaluator did, and that template references OLE Automation and the App +global class object only. UC-72's add-in, built from it, failed with 18 errors --- `TB5079 +Unrecognized datatype symbol` for `AddIn`, `Host`, `Button` and `CodeEditor`. The Built-In +Packages page, and the pages of four other packages, say to add a built-in package through +Project → References → Available Packages. The add-in samples 10--16 reference it already. +A probe agent measured the dialog: the row is *twinBASIC - IDE Extensibility Package*, marked +*[BUILT-IN]*, library symbol *tbIDE*, and ticking it and pressing **Apply Changes** took the +Standard DLL template's three `TB5079` errors to none. *Fixed*: the page says to add it, in the +dialog's words, and the Add Ins page names Sample 10 as the project to start from. + +The same probe read the Assert package's row: *twinBASIC - Unit Testing Package*, library symbol +*Assert*. The testing tutorial said to "tick **Assert**. Click **OK**" --- a name the Name column +does not show, and a button the dialog does not have. *Fixed* too. + +**2. The Windows API tutorial said a `Long` handle fails in 64-bit mode, and for two of the three +kinds of handle it does not.** *"A Declare that uses `Long` for a handle type compiles and runs in +32-bit mode but fails or crashes in 64-bit mode because a 64-bit handle does not fit in 4 bytes."* +Measured with `tbrun --arch win64`, which round 10 asked for: a window handle and an event's +handle both fitted in a `Long` and worked; `GetModuleHandleW(0)` returned `&H7FF6436A0000` as a +`LongPtr` and `&H436A0000` as a `Long`, and `GetModuleFileNameW` given the `Long` failed with +error 126; a pointer returned `As Long` was cut the same way, and `lstrlenW` read nothing at it. +And what the sentence missed: in a win64 build, a `LongPtr` passed to a `Long` parameter does not +compile, *TB5001 cannot coerce type 'LongLong' to 'Long'*, and `CLng(StrPtr(s))`, which the +message suggests, raises error 6. *Fixed*: the sentence is three measured cases, and the advice +--- declare every handle and pointer `LongPtr` --- stands. + +## Tier 2 --- hazards the pages did not know + +**3. A console program: `Debug.Print` prints nothing, `End` sets no exit code, and the +template's `Console` class cannot be redirected.** Measured on built `.exe` files at a command +prompt: + +- `Debug.Print` writes to the IDE's Debug Console only; a built program prints nothing with it. +- `End` ends the program with exit code 0. `ExitProcess` sets the code, and a batch file's + `errorlevel` reads it. +- The template's `Console.WriteLine` calls `WriteConsoleW`, which writes nothing to a file or a + pipe. Redirected, it wrote nothing and raised error 5, its own *failed to write to the + console*. +- `Command$` keeps the quotes of a quoted argument: `"my file.txt"` arrives as `"my file.txt"`. +- The template's `MainModule` has a `Sub Main`, so a second one in the reader's module stops the + build with *'Main' is ambiguous*. + +*Fixed*: *Writing a command-line tool: output, exit code and arguments*, a `##` on Project Types +with a measured `linecount`. Its `WriteOut` and `WriteErr` use `WriteConsoleW` for a console +window and `WriteFile` for a file or a pipe, so the tool's output reaches a console, a +redirection and a pipe alike --- measured, with `Zoë Łódź` intact in a console window and +converted to the ANSI code page in a file. The template's class is described as the starting +point it is, and *Is Console Application* on Project Settings, an empty heading until now, has +the IDE's own description of it. + +**4. An add-in built into the IDE's own `addins` folder cannot be rebuilt once the IDE has loaded +it.** UC-72's answer, like the samples, builds to `${IdePath}\addins\${Architecture}\...`. +Measured in a private copy of the install: the first build wrote the DLL; the IDE loaded it when +it next opened; and the next build failed with `[LINKER] FAILED to create output file ... +(error code 32)`, naming the IDE's own compiler as the process that held it. WIP.HelpAddin.md +had recorded the lock as P8; no page had. *Fixed*: *Rebuilding an addin the IDE has loaded*, a +`##` on the tbIDE page --- build to the ordinary `Build` folder, close the IDE, copy the DLL over +the old one --- which is the loop the add-in test runner itself uses. Two copies of an add-in +under two names load as two add-ins, with two identical buttons, which the page says too. + +**5. An error nothing handles stops a thread for good.** UC-71's answer handled errors in its +thread procedures in its first run and not in its second, and the Multithreading page's example +has none. A thread whose `Integer` overflowed was still running ten seconds later, its wait timed +out, and a wait with `INFINITE`, as both answers used, never returns. *Fixed*: *Running code on +two threads and waiting for both to finish*, a `##` on the Multithreading page with an example +measured in 32-bit and 64-bit builds --- module-level results, a wait on each handle in turn, +`CloseHandle`, and a handler in each thread procedure, which, with an overflow forced, set its +result to `-1` and let the other thread's line print unchanged. The page now gives a thread +procedure the signature `CreateThread` calls: a `Function` taking one `LongPtr` and returning a +`Long`. + +**6. The fresh-clone steps put their precondition after themselves.** Round 10's fix told a +reader to remove the compiler packages from a clone "before step 1" --- in a paragraph after +step 3. UC-66's answer put the removal after the import, where it no longer prevents the dead +copy round 10 measured. *Fixed*: it is step 1. + +## Tier 3 --- the answer exists and the reader cannot reach it, or it does not exist + +**7. `${IdePath}` is not among the Build Output Path variables** on Project Settings, though every +add-in sample uses it; UC-72's evaluator flagged it as possibly unsupported. Measured: it is the +installation folder, written as `...\bin\..`. *Fixed*, with a pointer to finding 4. + +**8. Project Settings put eight ids and three classes on the wrong headings.** Found while +re-measuring a rank: the search result for *Register DLLs to HKLM* links to +`#typelib-auto-increment`. Each `{: #... }` line on the page stood after a blank line, and +kramdown applies such a line to the next block, so `#show-run-procedure` was on *Runtime Windows +Codepage*, `#enable-aslr` on *PE File Image Base Address (Win32)*, and *Register DLLs to HKLM* +had lost its own id. Nothing links to the ids yet; the IDE help add-in will. *Fixed*: each line +is under its heading. Three reference pages --- `Attributes`, `Date`, `Time` --- attach an id to +the paragraph under a heading the same way, which lands on the right place and was left. + +**9. Left open.** The per-user add-in folder the Add Ins page mentions still has no path: the +IDE hands the compiler `%APPDATA%\twinBASIC\addins`, and whether it loads from there is +WIP.HelpAddin.md's P6, which needs a DLL placed where the user's own IDE would load it --- not +run. Phrasings that still miss: `rebuild twinproj from exported files`, `version control +twinbasic project`, `export project every time it is saved`, `sort array with comparison +function`, `return larger of two values`, `WaitForMultipleObjects`, and round 10's finding 11. + +### The fix, measured on the readers' words + +The evaluators' phrasings, against this round's snapshot of the index and the rebuilt site. The +target is the section written for the case, which did not exist before: + +| case | query | before | after | +|---|---|---:|---:| +| UC-70 | `exit code errorlevel` | miss | 2 | +| UC-70 | `Console.WriteLine ExitCode` | miss | 3 | +| UC-70 | `set exit code End process` | miss | 6 | +| UC-70 | `command line arguments` | miss | 8 | +| UC-70 | `print to stdout console app` | miss | miss | +| UC-71 | `run code on two threads and wait for both to finish` | miss | 1 | +| UC-71 | `thread wait join` | miss | 2 | +| UC-71 | `run two calculations in parallel on separate threads` | miss | 7 | +| UC-72 | `rebuild addin` | miss | 1 | +| UC-72 | `addin build failed error code 32` | miss | 8 | + +`print to stdout console app` ranks *Is Console Application* 5th and *Console Applications* 6th, +and both link to the new section; `stdout` alone ranks it 3rd. UC-72's `add-in for twinbasic +ide` still ranks the Add Ins page 1st, which now names Sample 10 and says what a Standard DLL +lacks. + +## The harness + +**10. A search refused once was taken for a search that does not exist.** See *The session as +evidence*. *Fixed*: the protocol's search section says to run `site-search` as a command of its +own, and that chained after a `cd` or piped it is refused. Both re-runs searched first. The +refusal itself is right --- the allow rule is the whole of the evaluator's shell --- so the fix is +the sentence, not a wider rule. + +**11. Executed console programs need a real console.** `WriteConsole` writes nothing to a pipe, +which is the very behaviour UC-70 turned on, so the programs were run in a hidden console of +their own and the screen buffer read through `AttachConsole` --- a PowerShell script, for the +same reason `scripts/lib/tb-launch.ps1` is one. Two traps on the way: the harness's environment +sets `NoDefaultCurrentDirectoryInExePath`, so `cmd.exe` would not find a program in its own +folder without `.\`; and in a pipe to `find`, the PATH that Git Bash hands down found MSYS's +`find` before Windows', which listed a drive. The protocol records the method. + +**12. The executed cases' runner owned the registry tidy**, as round 10's finding 17 asked, and +every run left the IDE's lists as found. The protocol says so now. + +**13. Two of 29 `tbrun` builds failed with `[TYPELIB] failed to finalize typelibrary. Disk +error?`** and built on retry, unchanged. Not isolated; queued nowhere, since there is nothing narrowed to +report. Round 10's unexplained `check_examples` wedge (its finding 16) may or may not be the same +thing. + +## Corrections --- evaluator claims amended + +**UC-55's** split discoverability, search 3 and navigation 4, is recorded as 3: two of its four +phrasings still miss the page. + +**UC-66's** discoverability, split search 2 and navigation 3, is recorded as 3: its first query +ranks the section 3rd, and the page is three hops from the welcome page. Its actionability is +amended from 4 to 3: followed in its own order, it imports the clone before removing the +compiler packages (finding 6). + +**UC-69's** report puts `create ActiveX DLL twinBASIC` at rank 1; it is 4th. Its discoverability of +4 stands on its three other queries, each at rank 1. + +**UC-70's** actionability is amended from 2 to 1: as handed over, the tool prints nothing in any +case and always exits 0. Its first run, scored 1/1/2 without a single search, is not in the +tables. + +**UC-72's** completeness is amended from 3 to 2 and its actionability from 3 to 2: the page told it +that the one setting it lacked was automatic, and as handed over the add-in does not compile. +With the reference added, everything it predicted happened. + +**UC-65, UC-71 and UC-72** each report that Channel 3 was not needed; their sessions hold two, one +and one full-text searches. Their scores stand, from the ranks and the links. + +## What round 11 says about the method + +**Discoverability is flat across a fix pass for the second time, and for the same reason.** Round +5 found it: the pages a fix writes are right, and the next evaluator's words still miss them. +Round 10's fixes titled two sections for their readers' exact phrasings, and those phrasings now +rank them 1st; the re-runs used other words. The fair measure is the one that holds the words +still, and on round 10's own queries it moved from 4 hits of 36 to 21. + +**A page's claim that something is automatic is the claim to test first.** UC-72's evaluator read +the tbIDE page correctly, found the one thing it needed to do, and was told not to do it. The +executed case is what turned a sentence that read as reassurance into 18 compile errors. + +**The template is not the answer, and a page that points at it inherits its limits.** The +Console App template's `Console` class writes to a console window, which is what a template needs +to show; a command-line tool needs its output redirected. The fix documents a mechanism of the +project's own rather than a defect in the template, which is there for convenience. + +**Evaluators infer the absence of a tool from one refusal.** Nothing in the protocol said the +search box could be refused; two of nine evaluators concluded it was missing, and neither the +refusal nor the conclusion appeared anywhere but the session. The digest's flag is what caught it. + +## Method + +Corpus built at `4a67e09` with `eval/build_corpus.mjs`: 988 readable files and 291 stubbed +unreadable, 262 binary omitted; `WIP.md`, the twenty `WIP.*.md` files and the ten prior use-case +reviews withheld. The search index was snapshotted with it --- 3,815 entries, 80.2% reference, +4.1% developer docs --- and every case queried the snapshot. The smoke run's six checks passed on +2.1.280, for $0.06; the nine cases then ran in parallel through `eval/run_case.mjs`, goals +verbatim from the tables, and UC-70 and UC-71 again after the protocol fix. The nine scored runs +cost $2.61, from $0.15 (UC-69) to $0.67 (UC-70); all eleven, $3.52. + +Executed runs used `scripts/tbrun.mjs` on BETA 983, four lanes on ports 9911--9914, from one +process holding `startTidy`: UC-65, UC-67, UC-71 and UC-71b, UC-69's DLL and two callers for each +bitness, three UC-70 builds, the probes behind findings 2, 3 and 5, and the fix pass's two +examples. Command-line programs ran in a hidden console through a PowerShell script reading the +screen buffer. UC-72 ran through `scripts/addin_test.mjs` with a temporary lane on port 9931, and +the rebuild and two-copies probes on 9941--9942, each in a private copy of the install that was +deleted afterwards. A probe agent answered finding 1's question about the References dialog on +9951--9959. `%APPDATA%\twinBASIC\addins` held no DLL before, during or after. + +## What to do next + +**1. Round 12.** Re-run UC-70, UC-71 and UC-72 against their new sections, executed again, and +UC-66 against the reordered steps. Re-run this round's queries against round 12's index. + +**2. P6**, with the user's go-ahead, so the Add Ins page can give the per-user folder's path; +and the add-in rebuild loop without closing the IDE (P9), which would give readers a shorter one. + +**3. Still open:** the master list of run-time error numbers; the IDE section's empty headings, +75 now, 56 of them on Project Settings --- finding 1's probe read the dialog's own description +of every setting, which is the material to fill them from; `check_run`; the phrasings in +finding 9; and the transient `[TYPELIB]` failure. + +## Outcome + +**Every finding is fixed, except those left open in finding 9 and the transient build failure +of finding 13.** The documentation carries findings 1--8, the protocol and `eval/README.md` +findings 10--12. + +`build.bat`, `check.bat` and `test.bat` are green: 914 pages, 0 broken links and 0 integrity +findings in both real trees, 0 accessibility violations, every toolchain probe passing. The +book tree's informational broken links are 32, up from 31: the new one is the link from +*Console Applications* on Project Types, a page the book includes, to *Is Console Application* +in the IDE section, which it leaves out. The samples on the seven pages the round changed or +re-ran compile (`check_examples`, 37 samples). The two new examples are byte for byte what was +executed: the command-line tool at a command prompt, in a console window, redirected, piped and +with a quoted file name, and the threads in 32-bit and 64-bit builds and with an overflow +forced. + +The work is three commits on `staging`: the protocol fix, the documentation fixes, and this +review with round 11's goals. Nothing is pushed. diff --git a/builder/book.mjs b/builder/book.mjs index 9071ad90..46479edc 100644 --- a/builder/book.mjs +++ b/builder/book.mjs @@ -11,7 +11,11 @@ // _plugins/book-resolve-chapters.rb (resolver) // _plugins/book-sort.rb (sortByNavOrder) // -// Phase 8 surface (§B-§G): assembleBook + bookChapterTransform + +// Coverage (§G): bookCoverage + formatBookCoverage. Checks that every page +// has a manifest entry -- in the book, or in `left_out:` with a reason -- +// and that every entry still matches a page. Warnings only. +// +// Phase 8 surface (§B-§F): assembleBook + bookChapterTransform + // chapterAnchorFromUrl + rewriteBookHrefs. Builds the full book.html // string for the sparse PDF tree. See builder/PLAN-8.md. Ports: // docs/book.html (Liquid walker) @@ -230,12 +234,28 @@ function collectImagePaths(body, seen) { for (const m of body.matchAll(IMG_SRC_RE_BOOK)) { if (m[1] === undefined) continue; const url = m[2]; - const cleanPath = url.split(/[?#]/, 1)[0]; + const cleanPath = decodeUrlPath(url.split(/[?#]/, 1)[0]); if (!cleanPath || seen.has(cleanPath)) continue; seen.add(cleanPath); } } +// A src is a URL, and the renderer percent-encodes it: the file +// `IDE/Images/project settings description text.png` is referenced as +// `project%20settings%20description%20text.png`. Every consumer of the +// collected paths wants the name on disk -- pdf.mjs looks it up among the +// source files and copies it out under that name, and the browser that +// renders book.html decodes the URL before it opens the file. Left +// encoded, the lookup missed and the missing-image check stopped the +// build the first time a page with such an image entered the book. +function decodeUrlPath(p) { + try { + return decodeURIComponent(p); + } catch { + return p; // a stray `%` that begins no escape is part of the name + } +} + // --------------------------------------------------------------------------- // §B Chapter anchor + URL helpers // --------------------------------------------------------------------------- @@ -572,8 +592,8 @@ const MONTH_NAMES = [ // emits the title page + every
, runs the cross-ref rewrite + // landing-strip pass, runs html-compress. Pure compute; no I/O. The // returned `imagePaths` is an array of every page-relative `` path referenced from the assembled body, deduplicated in -// emit order (Set insertion order). +// src=>` path referenced from the assembled body, decoded to the file's +// own name and deduplicated in emit order (Set insertion order). export function assembleBook(site, pages) { const bookData = site.bookData; if (!bookData) { @@ -851,8 +871,9 @@ function chapteredFlags(part, chEntry) { const EXTERNAL_PREFIXES = ["http://", "https://", "mailto:", "#"]; // PLAN-8 §6.6: walk each
block, resolve relative -// hrefs, rewrite in-book targets to `#ch-...` anchors, strip the -// redundant landing-page heading. +// hrefs, rewrite in-book targets to `#ch-...` anchors, point every other +// site link at the page on the website, strip the redundant landing-page +// heading. // // tbdocs derives redirect-from stubs from each page's // `frontmatter.redirect_from` and passes an extended array to the map @@ -864,6 +885,9 @@ export function rewriteBookHrefs(html, site, pages) { const bookData = site.bookData; if (!bookData) return html; const baseurl = normalizeBaseurl(site.config?.baseurl); + // Same shape offline.mjs gives its own siteUrl, so a CI build given + // --url points the book at the deploy it belongs to. + const siteUrl = String(site.config?.url ?? "").replace(/\/+$/, ""); const pagesWithStubs = augmentWithRedirectStubs(pages); const urlToAnchor = buildUrlToAnchor(bookData, pagesWithStubs); if (urlToAnchor.size === 0) return html; @@ -883,14 +907,14 @@ export function rewriteBookHrefs(html, site, pages) { } const parentUrl = anchorToParent.get(anchorId); if (parentUrl) { - body = rewriteBodyHrefs(body, parentUrl, urlToAnchor, baseurl); + body = rewriteBodyHrefs(body, parentUrl, urlToAnchor, baseurl, siteUrl); } return open + body + close; }, ); } -function rewriteBodyHrefs(body, parentUrl, urlToAnchor, baseurl) { +function rewriteBodyHrefs(body, parentUrl, urlToAnchor, baseurl, siteUrl) { return replaceOutsideCode(body, /href="([^"]*)"/g, (whole, href) => { if (EXTERNAL_PREFIXES.some(p => href.startsWith(p))) return whole; const abs = resolveHref(href, parentUrl); @@ -903,8 +927,15 @@ function rewriteBodyHrefs(body, parentUrl, urlToAnchor, baseurl) { ? `href="#${target}-${fragPart}"` : `href="#${target}"`; } + // Not in the book. A site path is dead in a PDF -- the viewer + // resolves it against the file on the reader's disk -- so the link + // opens the page on the website instead. The book pass of --check + // lists each one as OUT OF BOOK (check.mjs, TREES.pdf): that list is + // what says which pages the book leaves out and still links to. const missPath = fragPart ? `${lookupPath}#${fragPart}` : lookupPath; - return `href="${missPath}"`; + return siteUrl + ? `href="${siteUrl}${baseurl}${missPath}"` + : `href="${missPath}"`; }); } @@ -1101,3 +1132,121 @@ function buildAnchorToParent(bookData, pages) { } return map; } + +// --------------------------------------------------------------------------- +// §G Coverage: every page has a manifest entry, in the book or out of it +// --------------------------------------------------------------------------- + +// A page no entry selects is simply absent from the book, and for a long +// time nothing said so: the IDE, Challenges and Videos sections were all +// missing that way, and so were pages as plainly book material as Data +// Types and Enumerations. `left_out:` in _book.yml names the pages that +// are out on purpose, each with a reason, so every page has an entry one +// way or the other and a warning here means a decision nobody has made. +// +// Runs after resolveBookChapters. Returns five lists, all empty on a +// consistent manifest: +// unlisted pages in no book entry and no left_out entry +// both pages a book entry selects and left_out also names +// emptyEntries book entries that select no page +// emptyLeftOut left_out entries that match no page -- the page was +// renamed or deleted, and the entry would outlive it +// missingUrls landing_page / foreword_page URLs no page publishes at +export function bookCoverage(bookData, pages) { + const out = { unlisted: [], both: [], emptyEntries: [], emptyLeftOut: [], missingUrls: [] }; + if (!bookData) return out; + + // The emission sites of emitFrontMatter and emitPart, and no others: a + // flat part's landing is the head of its _chapters, a chaptered part's + // is emitted on its own. + const inBook = new Set(); + for (const fm of bookData.front_matter ?? []) { + for (const p of fm._chapters ?? []) inBook.add(p); + } + for (const part of bookData.parts ?? []) { + if (part._foreword) inBook.add(part._foreword); + if (part.chapters && part._landing) inBook.add(part._landing); + for (const p of part._chapters ?? []) inBook.add(p); + for (const ch of part.chapters ?? []) { + for (const p of ch._chapters ?? []) inBook.add(p); + } + } + + const leftOut = new Set(); + for (const entry of bookData.left_out ?? []) { + const matched = collectMatches(entry, pages); + if (matched.length === 0) out.emptyLeftOut.push(describeEntry("left_out", entry)); + for (const p of matched) leftOut.add(p); + } + + for (const p of pages) { + // The book itself: layout book-combined, which assembleBook fills. + if (p.frontmatter?.layout === "book-combined") continue; + const inside = inBook.has(p); + const outside = leftOut.has(p); + if (!inside && !outside) out.unlisted.push(p); + else if (inside && outside) out.both.push(p); + } + // `pages` is in basename order (discover.mjs); by path, a section's + // pages read together. + const bySrc = (a, b) => (a.srcRel < b.srcRel ? -1 : a.srcRel > b.srcRel ? 1 : 0); + out.unlisted.sort(bySrc); + out.both.sort(bySrc); + + const urls = new Set(pages.map(p => p.permalink)); + const checkUrl = (where, key, url) => { + if (url && !urls.has(url)) out.missingUrls.push(`${where} ${key}: ${url}`); + }; + for (const fm of bookData.front_matter ?? []) { + if (!fm._chapters?.length) out.emptyEntries.push(describeEntry("front_matter", fm)); + } + for (const part of bookData.parts ?? []) { + const where = describeEntry("part", part); + checkUrl(where, "landing_page", part.landing_page); + checkUrl(where, "foreword_page", part.foreword_page); + if (!part.chapters && !part._chapters?.length) out.emptyEntries.push(where); + for (const ch of part.chapters ?? []) { + const chWhere = describeEntry("chapter", ch); + checkUrl(chWhere, "landing_page", ch.landing_page); + if (!ch._chapters?.length) out.emptyEntries.push(chWhere); + } + } + return out; +} + +function describeEntry(kind, entry) { + const name = entry.title ?? entry.reason; + return name ? `${kind} "${name}"` : `${kind} ${JSON.stringify(entry)}`; +} + +// The warning text for bookCoverage's result, one line per finding, each +// section headed by what to do about it. [] when there is nothing to say. +export function formatBookCoverage(c) { + const lines = []; + const count = (n, one, many) => `${n} ${n === 1 ? one : many}`; + const section = (items, head, fmt) => { + if (!items.length) return; + lines.push(head); + for (const x of items) lines.push(` ${fmt(x)}`); + }; + const page = p => `${p.srcRel} (${p.permalink})`; + section(c.unlisted, + `${count(c.unlisted.length, "page has", "pages have")} no entry in _book.yml -- ` + + `add each to a part, or to left_out with a reason:`, + page); + section(c.both, + `${count(c.both.length, "page is", "pages are")} in the book and in left_out as well -- ` + + `remove the left_out entry:`, + page); + section(c.emptyEntries, + `${count(c.emptyEntries.length, "book entry selects", "book entries select")} no page:`, + x => x); + section(c.emptyLeftOut, + `${count(c.emptyLeftOut.length, "left_out entry matches", "left_out entries match")} no page -- ` + + `remove or correct:`, + x => x); + section(c.missingUrls, + `${count(c.missingUrls.length, "landing or foreword URL names", "landing or foreword URLs name")} no page:`, + x => x); + return lines; +} diff --git a/builder/check.mjs b/builder/check.mjs index fa2b46fd..14a5b5d4 100644 --- a/builder/check.mjs +++ b/builder/check.mjs @@ -45,8 +45,9 @@ export { normalizeBasePath }; // The three passes, verbatim from check.bat. Fusion must not quietly // unify them: the online tree has a sitemap and a search index and the -// offline tree has neither, the offline tree is the only one that -// forbids live-site links, and the book pass is informational. +// offline tree has neither, a live-site link is a fault in the offline +// tree and an expected, listed one in the book, and the book pass is +// informational. export const FALLBACK_EXTS = ["html"]; export const INDEX_FILES = ["index.html", "."]; @@ -84,7 +85,13 @@ export const TREES = { suffix: "-pdf", label: "_site-pdf", checkOpts: null, - forbid: null, + // Every link book.mjs sent to the website because the page it names + // is not in the book. They are collected the way the offline tree + // collects its forbidden links, and reported under a name of their + // own: here they are expected, and the list says which pages the + // book leaves out and still links to. + forbid: ["https://docs.twinbasic.com"], + forbidReport: { tag: "OUT OF BOOK", reason: "not in the book; opens the website", noun: "out of book" }, crossFile: { sitemap: false, search: false, canonical: false }, // book.html is one flattened document whose links are almost // entirely internal fragments; there is no directory structure to @@ -339,7 +346,8 @@ export function formatReport(r) { // The script prints bare walk paths; prefix the tree so a fused run // covering three trees says which one each finding came from. - const linkReport = formatLinkReport(r.broken, r.forbiddenBySource); + const forbidReport = r.tree.forbidReport; + const linkReport = formatLinkReport(r.broken, r.forbiddenBySource, forbidReport ?? {}); if (linkReport) out.push(prefixPaths(linkReport, r.label)); const integrity = formatIntegrityReport(r.integrityByFile); @@ -364,7 +372,17 @@ export function formatReport(r) { // and exactly the wrong shape. const integrityFailed = integrityCount > 0 || r.errors.length > 0; - const forbidNote = r.forbiddenBySource ? `, ${forbiddenCount} forbidden` : ""; + // The book's live-site links are counted the way the report lists + // them, once per target, as broken links are. The offline tree keeps + // its count of occurrences, which scripts/check_links.mjs prints too. + let forbidNote = ""; + if (r.forbiddenBySource && forbidReport) { + const targets = new Set(); + for (const hits of r.forbiddenBySource.values()) for (const h of hits) targets.add(h.url); + forbidNote = `, ${targets.size} ${forbidReport.noun}`; + } else if (r.forbiddenBySource) { + forbidNote = `, ${forbiddenCount} forbidden`; + } const failNote = r.noFail && (linksFailed || integrityFailed) ? " (informational)" : ""; out.push( ` ${r.label.padEnd(14)} ${String(r.occurrences).padStart(7)} occurrences -- ` + diff --git a/builder/link-check.mjs b/builder/link-check.mjs index 34110247..4a091aad 100644 --- a/builder/link-check.mjs +++ b/builder/link-check.mjs @@ -895,7 +895,12 @@ export function checkCanonical(canonicalByRel, basePath) { // FORBIDDEN labels distinguishing them. Labels are padded to the wider // of the two so href columns line up. Returns "" when there is nothing // to say. -export function formatLinkReport(broken, forbiddenBySource) { +// +// `tag` and `reason` rename the forbidden entries for a tree where a +// live-site link is expected rather than a fault: the book, where +// book.mjs sends every link to a page it does not contain to the +// website. The defaults are what both front ends printed before. +export function formatLinkReport(broken, forbiddenBySource, { tag = "FORBIDDEN", reason = null } = {}) { if (!broken.length && !(forbiddenBySource && forbiddenBySource.size)) return ""; const bySource = new Map(); @@ -910,11 +915,12 @@ export function formatLinkReport(broken, forbiddenBySource) { let set = bySource.get(src); if (!set) { set = new Set(); bySource.set(src, set); } for (const fh of fhits) { - set.add(`F\0${fh.url}\0forbidden prefix '${fh.prefix}'`); + set.add(`F\0${fh.url}\0${reason ?? `forbidden prefix '${fh.prefix}'`}`); } } } + const width = Math.max(tag.length, "BROKEN".length); const lines = []; for (const src of [...bySource.keys()].sort()) { lines.push(""); @@ -924,9 +930,9 @@ export function formatLinkReport(broken, forbiddenBySource) { const j2 = item.indexOf("\0", j1 + 1); const kind = item.slice(0, j1); const href = item.slice(j1 + 1, j2); - const reason = item.slice(j2 + 1); - const label = kind === "F" ? "FORBIDDEN" : "BROKEN "; - lines.push(` ${label} ${href} -- ${reason}`); + const why = item.slice(j2 + 1); + const label = (kind === "F" ? tag : "BROKEN").padEnd(width); + lines.push(` ${label} ${href} -- ${why}`); } } lines.push(""); diff --git a/builder/pdf.mjs b/builder/pdf.mjs index e4332ff9..15c7240b 100644 --- a/builder/pdf.mjs +++ b/builder/pdf.mjs @@ -20,7 +20,7 @@ import { promises as fs } from "node:fs"; import path from "node:path"; -import { assembleBook } from "./book.mjs"; +import { assembleBook, bookCoverage, formatBookCoverage } from "./book.mjs"; import { WRITE_LIMIT, mkdirRec, @@ -75,6 +75,11 @@ export async function writePdf(pages, staticFiles, site, destRoot, { tolerateMis reportMissingImages(missingPaths, tolerateMissingImages, counters); + // A page with no _book.yml entry, or an entry with no page: lines for + // tbdocs to print under the pdf summary. Warnings, never a failure -- + // the book this build wrote is complete for the manifest it was given. + counters.coverage = formatBookCoverage(bookCoverage(site.bookData, pages)); + // --check: hand the assembled book and the tree's exact contents to // the link check rather than making it read 6.5 MB back off disk. // `missingPaths` are the images that were NOT copied, so they must diff --git a/builder/tbdocs.mjs b/builder/tbdocs.mjs index bfddb9fd..1c052eaf 100644 --- a/builder/tbdocs.mjs +++ b/builder/tbdocs.mjs @@ -1448,6 +1448,14 @@ export async function runBuild(opts) { console.log(` ${pc.bold("pdf:")} -> ${pc.cyan(`${destRoot}-pdf`)}`); console.log(` book.html (${mb} MB), ${pdfResult.css} CSS, ` + `${pdfResult.images} images${missingClause}`); + // Pages _book.yml says nothing about, and entries that no longer + // match a page -- see book.mjs §G. Printed only when the book is + // built, so a --serve session is not told about it on every save. + const [head, ...rest] = pdfResult.coverage ?? []; + if (head) { + console.log(` ${pc.bold(pc.yellow("book:"))} ${head}`); + for (const line of rest) console.log(` ${line}`); + } } // The Gantt injection rewrites BuildInfo.html in both trees, so it has // to happen before the check report is printed -- otherwise the check diff --git a/docs/Documentation/Authoring.md b/docs/Documentation/Authoring.md index 3c754e23..08dc1fcd 100644 --- a/docs/Documentation/Authoring.md +++ b/docs/Documentation/Authoring.md @@ -925,6 +925,8 @@ The indexes to join depend on what the page documents: Every one of these entries is a link plus a one-line description in the style of its neighbours, so the reliable way to write one is to copy the entry above the position you are inserting at and replace its contents. +**The PDF book is the one list that does catch the omission.** Every page needs an entry in `docs/_book.yml`: a part or chapter that selects it, or a `left_out:` entry that names it with a reason. A page with neither gets a `book:` warning in the build's summary. Most parts select by URL prefix, so a new page in a section the book already carries --- another operator, another VBA function --- usually has its entry already, and the warning appears only for a page somewhere new. [Book Configuration](Book-Configuration#pages-left-out-of-the-book) has the details. + ## Removing a page Deleting the file is the easy half, and removal is the more dangerous direction: @@ -943,6 +945,7 @@ stale entry is reported rather than shipped. Go through the same places - **A class, control, or enumeration inside a package** comes out of that package's `index.md`, and a VB control out of [Controls](../../tB/Controls). - **An enumeration** comes out of [Enumerations](../../Reference/Enumerations) in both places. The total on the [Reference Section](../../Reference) landing page is a [count name](#counts) and follows on its own. - **A whole package** comes out of [Default Packages](../../tB/Packages/Default/) or [Built-In Packages](../../tB/Packages/Built-In/), out of the *Built-in packages* section of the [welcome page](../../), and out of its `###` section in [Permanent Links](Permanent-Links). Package counts already written as [count names](#counts) follow on their own; any still written as digits do not. +- **Any page `docs/_book.yml` names on its own** --- as a `landing_page:`, or in a `left_out:` entry --- comes out of the manifest too. The build warns about an entry that no longer matches a page. Three things then have no counterpart in adding a page. diff --git a/docs/Documentation/Book-Configuration.md b/docs/Documentation/Book-Configuration.md index decf9077..15090b76 100644 --- a/docs/Documentation/Book-Configuration.md +++ b/docs/Documentation/Book-Configuration.md @@ -20,7 +20,7 @@ permalink: /Documentation/Development/Book-Configuration `data.mjs` loads `_book.yml` during Phase 2 and makes it available as `site.data.book`. The orchestrator then exposes `site.data.book` as `site.bookData` and passes it to `resolveBookChapters`. That call traverses the entire structure and resolves every selector to a concrete `Page[]` stored as `entry._chapters`, so Phase 8's `assembleBook` has no further page lookups to do. -Run `build.bat` then `book.bat` to see the effect of changes. The `check.bat` integrity check also runs a PDF build pass. +Run `build.bat` then `book.bat` to see the effect of changes. `build.bat` warns about a page the manifest does not mention; see [Pages left out of the book](#pages-left-out-of-the-book). ## Top-level structure @@ -32,6 +32,10 @@ front_matter: parts: - # one or more numbered Parts - ... + +left_out: + - # pages deliberately not in the book, each with a reason + - ... ``` **`front_matter`** entries are emitted between the title page and the first numbered Part. They produce no divider page and no part number. @@ -40,6 +44,8 @@ parts: Both `front_matter` entries and parts (and their chapters) share the [selector schema](#selector-schema) and [common entry options](#common-entry-options) described below. +**`left_out`** entries name the pages that are not in the book on purpose. They use the selector schema and a `reason:`, and emit nothing; see [Pages left out of the book](#pages-left-out-of-the-book). + ## Selector schema Every entry may combine any of these keys to select the pages it contributes to the book. All matches are `contains` by default --- the page's URL or nav-path must contain the prefix string. Set `no_descent: true` on the entry to switch all its matches to exact equality. @@ -54,6 +60,37 @@ Every entry may combine any of these keys to select the pages it contributes to All selector keys are combinable within one entry. An entry with both `page` and `nav_page` collects the union of both selections. Selectors on a chapter entry are independent of the selectors on the containing part --- a chapter collects its own pages; the part does not automatically inherit them. +## Pages left out of the book + +A page that no part, chapter or `front_matter` entry selects is not in the book. To leave a page out on purpose, name it in `left_out:` with the same selector keys and a `reason:`: + +```yaml +left_out: + - reason: Time-limited community contests + page: /Challenges +``` + +Every page has to be in one or the other. When the book is built, the build's summary prints a `book:` warning for: + +- a page that no entry selects and `left_out:` does not name; +- a page that is in the book and in `left_out:` as well; +- a book entry that selects no page, or a `left_out:` entry that matches none --- usually a page that was renamed or deleted; +- a `landing_page:` or `foreword_page:` URL that no page publishes at. + +A new page with no entry looks like this: + + book: 1 page has no entry in _book.yml -- add each to a part, or to left_out with a reason: + IDE/Probe.md (/tB/IDE/Project/Probe) + +These are warnings: the exit code does not change, and the book is complete for the manifest it was given. A build with nothing to report prints no `book:` line. `serve.bat` does not build the book, so it never prints one. + +A link from inside the book to a page left out opens that page on the website, because a site path goes nowhere in a PDF. The link check's pass over `book.html` lists each such link as `OUT OF BOOK`: + + _site-pdf/book.html: + OUT OF BOOK https://docs.twinbasic.com/tB/IDE/Project/Explorer -- not in the book; opens the website + +That list shows which left-out pages the book still links to. It is the first place to look for a page that belongs in the book after all. + ## Common entry options Front_matter entries, parts, and chapters all support these options. Where behaviour differs between parts and chapters, the part form is noted first with the chapter form in parentheses. diff --git a/docs/Documentation/Builder.md b/docs/Documentation/Builder.md index 985e33f8..45ae3b7d 100644 --- a/docs/Documentation/Builder.md +++ b/docs/Documentation/Builder.md @@ -565,6 +565,7 @@ The build aborts or flips the exit code under a handful of conditions: - **Redirect collision.** A `redirect_from:` entry becomes a stub page at that URL, so two ways of claiming one URL are refused in `deriveRedirectStubs()`: an entry pointing at a URL some page already publishes at, and two pages declaring the same entry. Both name the source file on each side --- a stub silently overwriting a real page, or one of two stubs silently winning, would be invisible in the output. - **Destination collision.** `assertNoDestinationCollisions()` runs before the write phase and throws if any static file's destination path equals a page's. The static-file copy and the page write run in parallel, so without the check which one survived would depend on I/O ordering. - **Missing PDF input.** Phase 8 aborts on three things the book cannot be assembled without: no page (or more than one) carrying `layout: book-combined`, a font listed in `REQUIRED_FONTS` absent from the source tree (naming `scripts/build_fonts.py`), and any image `book.html` references that is not under the source tree. The last is the one that fires in practice, and `--tolerate-missing-images` downgrades only that one to a warning. +- **A page the book manifest does not mention.** Not a failure: the book is complete for the manifest it was given. `bookCoverage()` compares every page with `_book.yml`'s entries and its `left_out:` list, and the summary prints a `book:` warning for a page in neither, a page in both, an entry that matches no page, and a landing or foreword URL that names no page. It runs only when the book is built, and [`check_book_coverage.mjs`](Tools#check-book-coverage) holds each finding to a probe. See [Book Configuration](Book-Configuration#pages-left-out-of-the-book). - **Worker crash.** A worker handler that throws posts `{ taskFailed, message, stack }` to main; the scheduler calls `_abort()`, the build rejects, and the orchestrator reports the error with the task name in the message. - **A worker that never returns at all.** The one failure with no error to report: a handler stuck in an unbounded loop, an exponentially backtracking regex or a promise that never settles posts nothing, so its successors' dependency counts never fall, `_remaining` never reaches zero, and the scheduler's promise never settles. Nothing in the SAB protocol can see it --- the scheduler is waiting on a message that is not coming. `Scheduler` therefore runs a `setInterval` watchdog: if no task completes for `--stall-timeout` seconds (default 120, `0` disables), it aborts with `{ stalled: true }` and prints the outstanding tasks split three ways --- claimed by a worker that never returned (the cause), runnable but unclaimed (including an `F_PIN_TO_PRED` task whose lane is the wedged one), and blocked on a predecessor (the consequence). A `render:` or `flush:` chunk additionally prints its source pages, through an optional `describe()` on the task def that nothing else reads. `Worker.terminate()` does end a thread spinning inside a regex, so the abort really ends the process. Under `--serve` the pool outlives a rebuild, so `serve.mjs` replaces the whole pool when it sees the `stalled` flag rather than identifying the wedged lane --- the SAB records the lane a task *completed* on, not the one that claimed it. See [when a build stops instead of failing](Building#when-a-build-stops) for the reader-facing form. - **Link and integrity check** (`--check`). Deliberately the one failure that does *not* abort: a broken link still produces a valid site you want on disk to inspect, unlike a nav ambiguity, where the output itself would be wrong. The check tasks collect findings and `runBuild()` sets the exit code afterwards --- 1 for link failures, 2 for integrity failures, 3 for both, OR'd into whatever the build's own failures already claimed. diff --git a/docs/Documentation/Building.md b/docs/Documentation/Building.md index 8444c799..5f6180ec 100644 --- a/docs/Documentation/Building.md +++ b/docs/Documentation/Building.md @@ -73,6 +73,7 @@ Each `.bat` opens with `@pushd "%~dp0"`, which is what lets it be invoked from a && node scripts/check_regex_safety.mjs \ && node scripts/check_code_regions.mjs \ && node scripts/check_page_baseline.mjs \ + && node scripts/check_book_coverage.mjs \ && node scripts/check_axe_patch_equiv.mjs `book.bat` has one step that is invisible from the command it ends with. `render-book.mjs` writes the PDF with a plain file write and never creates the directory above it, so `docs/_pdf/` has to exist first --- otherwise the render fails with `ENOENT` at the very last moment, after the whole page-breaking pass has already run. The deploy workflow does the same `mkdir` before its render, for the same reason: @@ -116,14 +117,20 @@ tell an ordinary run from a broken one. This is the shape of a clean one. Done in 4685ms: 908 pages, 247 static files _site 871099 occurrences -- 0 broken, 0 integrity _site-offline 869285 occurrences -- 0 broken, 0 forbidden, 0 integrity - _site-pdf 12703 occurrences -- 13 broken, 0 integrity (informational) + _site-pdf 13703 occurrences -- 0 broken, 18 out of book, 0 integrity (informational) **The third line is not a failure, and it is the one that looks like one.** The book is a -subset of the site, so every page it does not carry is a broken link from inside it; the -pass is marked *informational* and does not touch the exit code. The two lines above it -are the ones that must read `0 broken`. A few seconds is the normal duration --- the build -is around 2--3 seconds of work plus the link check --- so a run still going after a minute -is a [stall](#when-a-build-stops), not a slow machine. +subset of the site, so some of its links name pages it does not carry. Each of those opens +the page on the website instead, and the report lists it above the summary as +`OUT OF BOOK`: the list says which pages the book leaves out and still links to. The pass +is marked *informational* and does not touch the exit code. All three lines must read +`0 broken`. A few seconds is the normal duration --- the build is around 2--3 seconds of +work plus the link check --- so a run still going after a minute is a +[stall](#when-a-build-stops), not a slow machine. + +A healthy run also prints no `book:` line under its `pdf:` summary. That line is a warning +about a page `docs/_book.yml` does not mention, in the book or in its `left_out:` list --- +see [Book Configuration](Book-Configuration#pages-left-out-of-the-book). `check.bat` runs its four gates in order and ends on the scan's tally: @@ -229,7 +236,7 @@ The link check is part of the build. `build.bat` passes `--check-audit-index`, a build.bat -It covers all three trees --- `_site/` (the online tree), `_site-offline/` (the `file://`-browsable mirror, which also carries `--forbid 'https://docs.twinbasic.com'` so a surviving live-site link is flagged: the offline mirror should never navigate back to the live docs site), and `_site-pdf/book.html` (informational). Every tree is also checked for HTML well-formedness, duplicate `id`s, anchor resolution, accessibility hints and remote ``; the online tree adds sitemap, search-index and canonical-URL integrity. The same check runs in CI on every pull request and on every push to `staging`. +It covers all three trees --- `_site/` (the online tree), `_site-offline/` (the `file://`-browsable mirror, which also carries `--forbid 'https://docs.twinbasic.com'` so a surviving live-site link is flagged: the offline mirror should never navigate back to the live docs site), and `_site-pdf/book.html` (informational, and listing as `OUT OF BOOK` every link that leaves the book for the website). Every tree is also checked for HTML well-formedness, duplicate `id`s, anchor resolution, accessibility hints and remote ``; the online tree adds sitemap, search-index and canonical-URL integrity. The same check runs in CI on every pull request and on every push to `staging`. A failing check does not abort the build --- a broken link still produces a site worth looking at --- so it sets the exit code instead: 1 for link failures, 2 for integrity failures, 3 for both. @@ -587,7 +594,7 @@ release: | asset | what it is | |---|---| | `twinbasic-docs-offline.zip` | `_site-offline/` zipped from the inside, so `index.html` sits at the archive root. Extract anywhere and open it --- no server, and search, navigation and dark mode all work. | -| `twinBASIC Book.pdf` | The PDF book, A4, a little under 2,000 pages, bookmarked to `h1`--`h4`. | +| `twinBASIC Book.pdf` | The PDF book, A4, about 2,250 pages, bookmarked to `h1`--`h4`. | > [!IMPORTANT] > The release is marked *latest*, and the site's own two download buttons are diff --git a/docs/Documentation/PDF-Generation.md b/docs/Documentation/PDF-Generation.md index a197910b..5bff3203 100644 --- a/docs/Documentation/PDF-Generation.md +++ b/docs/Documentation/PDF-Generation.md @@ -17,7 +17,7 @@ Internals of the two-stage PDF pipeline: `tbdocs` Phase 8 assembles a sparse `_s ![A flow chart running top to bottom through two boxed stages. The first, tbdocs writePdf, holds three steps side by side: assembleBook combining the chapter HTML, copyPdfCss copying the two stylesheets, and copyPdfImages copying the referenced images. Together they produce _site-pdf/book.html with its stylesheets and images. That file feeds the second stage, render-book.mjs, whose three phases run in sequence: Phase 1 lays the document out with puppeteer and paged.js into one element per output page, Phase 2 extracts the metadata and outline tree and calls page.pdf for a raw buffer, and Phase 3 reloads that buffer through the fast pdf-lib shims, sets the metadata and outline, and saves. The result is the finished PDF under _pdf.](/assets/images/dot/pdf-render-pipeline.svg) -The book currently runs to a little under 2,000 pages. Page counts quoted in the performance notes --- [paged.js Fixes](Fixes/PagedJS) and [pdf-lib Fixes](Fixes/PDFLib) --- are the size of the book when that measurement was taken, not its size now. +The book currently runs to about 2,250 pages. Page counts quoted in the performance notes --- [paged.js Fixes](Fixes/PagedJS) and [pdf-lib Fixes](Fixes/PDFLib) --- are the size of the book when that measurement was taken, not its size now. The two stages are decoupled: `tbdocs` builds `_site-pdf/` as part of its normal run; `render-book.mjs` runs only when `book.bat` calls it explicitly. This keeps `puppeteer` and `pdf-lib` --- both large --- out of the site generator's dependency tree. @@ -129,7 +129,7 @@ The gate compares against everything under `docs/` and `builder/`, including fil Images abort two different commands for two different reasons. -**`pdf: missing image ` aborts Phase 8**, inside `build.bat`. The usual cause is a raw `` tag with a page-relative `src`: the book flattens every page into one document, so `Images/x.png` resolves against the book root rather than the page's folder. The markdown form is rewritten to a section-qualified path; the raw tag is not. See [Images](Authoring#images) in the authoring guide. +**`pdf: missing image ` aborts Phase 8**, inside `build.bat`. The usual cause is a raw `` tag with a page-relative `src`: the book flattens every page into one document, so `Images/x.png` resolves against the book root rather than the page's folder. The markdown form is rewritten to a section-qualified path; the raw tag is not. See [Images](Authoring#images) in the authoring guide. A space or other character in the file name is not a cause: the `src` is percent-encoded, and Phase 8 decodes it back to the file's name before it looks the file up. **`paged.js (forked): image not loaded at render time` aborts Phase 1**, inside `book.bat`: diff --git a/docs/Documentation/Pipeline-Stages.md b/docs/Documentation/Pipeline-Stages.md index 94054a24..0bac48db 100644 --- a/docs/Documentation/Pipeline-Stages.md +++ b/docs/Documentation/Pipeline-Stages.md @@ -436,7 +436,7 @@ Calls `writeOffline(state.pages, state.staticFiles, state.site, destRoot, { auxS writePdf.expected = ["flushJoin", "renderJoin", "dot", "resolveBookChapters"] ``` -Calls `writePdf(state.pages, state.staticFiles, state.site, destRoot, { tolerateMissingImages, highlightCss })` from `pdf.mjs`. Internally calls `assembleBook(site, pages)` from `book.mjs` for the `book.html` HTML string, writes `tb-highlight.css` from the highlight string passed in, copies `print.css` via the `staticFiles` inventory, copies every image referenced in `book.html`. Missing images throw by default; `--tolerate-missing-images` downgrades to a warning. +Calls `writePdf(state.pages, state.staticFiles, state.site, destRoot, { tolerateMissingImages, highlightCss })` from `pdf.mjs`. Internally calls `assembleBook(site, pages)` from `book.mjs` for the `book.html` HTML string, writes `tb-highlight.css` from the highlight string passed in, copies `print.css` via the `staticFiles` inventory, copies every image referenced in `book.html`. Missing images throw by default; `--tolerate-missing-images` downgrades to a warning. It also runs `bookCoverage` and returns its `formatBookCoverage` lines as `coverage`, which the build prints under a `book:` heading in its summary --- warnings about pages `_book.yml` does not mention, never a failure. `renderJoin` is listed although `execute()` ignores it: an `expected` list says what must have *merged*, not what the body reads, and the book is assembled from `page.renderedContent`. @@ -603,7 +603,9 @@ Runs two build-aborting integrity checks before building the tree: `validatePerm | `chapterAnchorFromUrl` | `(url, fallbackTitle?) → string` | Page URL → `ch-…` anchor slug. | | `bookChapterTransform` | `(body, baseurl, headingShiftN, chapterAnchor) → string` | Five per-chapter body transforms: baseurl-strip, `
` unwrap, whitespace-`` wrap for pagedjs, heading shift, chapter-anchor prefixing. | | `assembleBook` | `(site, pages) → string` | Phase 8 entry. Returns the assembled `book.html` string. | -| `rewriteBookHrefs` | `(html, site, pages) → string` | Rewrites intra-book absolute `href="/X"` references to `href="#ch-X"` fragment anchors. | +| `rewriteBookHrefs` | `(html, site, pages) → string` | Rewrites a link to a page in the book to that page's `href="#ch-X"` fragment anchor, and a link to any other page of the site to its absolute URL under `site.config.url`, because a site path is dead in a PDF. | +| `bookCoverage` | `(bookData, pages) → { unlisted, both, emptyEntries, emptyLeftOut, missingUrls }` | Runs after `resolveBookChapters`. Pages in no book entry and no `left_out:` entry, pages in both, entries that match no page, and landing or foreword URLs that name no page. Every list is empty on a consistent manifest. | +| `formatBookCoverage` | `(coverage) → string[]` | The `book:` warning lines for a `bookCoverage` result; `[]` when there is nothing to report. | ### `build-info.mjs` diff --git a/docs/Documentation/Tools.md b/docs/Documentation/Tools.md index 1c090659..fb75abd4 100644 --- a/docs/Documentation/Tools.md +++ b/docs/Documentation/Tools.md @@ -65,14 +65,15 @@ One of the four does not mean the same thing locally as it does in CI, on any pl test.bat -The tests the toolchain has to pass. Six steps, each stopping the run if it fails: +The tests the toolchain has to pass. Seven steps, each stopping the run if it fails: 1. [`scripts/check_publish_policy.mjs`](#check-publish-policy) --- verifies the publish allowlist still refuses the types it is meant to. Needs neither a browser nor a built tree, so it goes first. 2. [`scripts/check_gate_lists.mjs`](#check-gate-lists) --- verifies the two gate lists on this page still match the wrappers that run them. 3. [`scripts/check_regex_safety.mjs`](#check-regex-safety) --- refuses a regex that can backtrack exponentially, written as a literal or built from constants. 4. [`scripts/check_code_regions.mjs`](#check-code-regions) --- verifies no pre-render rewrite alters the contents of a code fence or code span. 5. [`scripts/check_page_baseline.mjs`](#check-page-baseline) --- verifies the page-count drift guard still refuses a fall. -6. [`scripts/check_axe_patch_equiv.mjs`](#check-axe-patch-equiv) --- verifies the vendored axe source patch still produces identical colour values. +6. [`scripts/check_book_coverage.mjs`](#check-book-coverage) --- verifies the build still warns about a page `docs/_book.yml` does not mention. +7. [`scripts/check_axe_patch_equiv.mjs`](#check-axe-patch-equiv) --- verifies the vendored axe source patch still produces identical colour values. POSIX: @@ -81,9 +82,10 @@ POSIX: && node scripts/check_regex_safety.mjs \ && node scripts/check_code_regions.mjs \ && node scripts/check_page_baseline.mjs \ + && node scripts/check_book_coverage.mjs \ && node scripts/check_axe_patch_equiv.mjs -**Four of the six cannot be affected by an edit confined to `docs/`**, which is why they are separate from `check.bat`. Run this one when the change touches `builder/`, `scripts/`, `book/`, `eval/` or `wisdom/`. Both CI workflows run all six unconditionally, as they always did, so skipping it locally cannot let a tooling regression reach `staging`. +**Five of the seven cannot be affected by an edit confined to `docs/`**, which is why they are separate from `check.bat`. Run this one when the change touches `builder/`, `scripts/`, `book/`, `eval/` or `wisdom/`. Both CI workflows run all seven unconditionally, as they always did, so skipping it locally cannot let a tooling regression reach `staging`. The two exceptions are [`check_code_regions.mjs`](#check-code-regions) and [`check_gate_lists.mjs`](#check-gate-lists), which reads this page. The first is worth knowing in detail. Its corpus sweep tokenises every markdown file under `docs/`, so a page that provokes a rewrite into altering a code region fails it. Its fixed probes are a different matter: they run against their own sources whatever the tree holds, and they cover the *mirror* fault, where a rewrite silently stops firing. The sweep cannot see that one --- text the rewrite skipped is stashed and restored unchanged, so every region still matches. Add a page with an unusual code construct and run `test.bat`, but read the built page too. @@ -321,13 +323,13 @@ Both default to `script`, and a bare invocation is refused rather than printing | `online` | `_site/` --- integrity + sitemap + search + canonical | yes | | `online-abs` | `online` again with an absolute `--root-dir`, asserted to reach identical findings | no | | `offline` | `_site-offline/` --- integrity + `--forbid` | yes | -| `book` | `_site-pdf/book.html` --- fragments only, `--no-fail` | yes | +| `book` | `_site-pdf/book.html` --- fragments and `--forbid`, `--no-fail` | yes | | `basepath` | a tree built with `--baseurl`, checked with the matching `--base-path` | yes | | `fixture` | a synthetic tree written at run time, carrying one fault of every kind | no | | `fixture-built` | `test/fixtures/check-src` built by `tbdocs` --- the online tree | yes | -| `fixture-built-offline` | the same build's offline tree, the only one with a forbidden prefix | yes | +| `fixture-built-offline` | the same build's offline tree, which has the forbidden prefix the online tree lacks | yes | -That last column is the part worth reading before trusting a green run. Under `--b fused` the two cases with no fused equivalent are skipped --- named in a `skipped:` line, not silently --- because the build's pass checks what the build produced and has nothing to say about a `--root-dir` shape variation or a hand-written tree it never wrote. The real-tree cases are empty in nearly every category on a healthy site, so for as long as `fixture` was the only fault-carrying case, **every `--b fused` run dropped the one case that gave the comparison anything to compare.** The built pair closes that: the same idea in a tree `tbdocs` produced, split across two cases because no single tree carries all nine categories --- the online tree has the sitemap, search and canonical checks, and the offline tree is the only one with a forbidden prefix. +That last column is the part worth reading before trusting a green run. Under `--b fused` the two cases with no fused equivalent are skipped --- named in a `skipped:` line, not silently --- because the build's pass checks what the build produced and has nothing to say about a `--root-dir` shape variation or a hand-written tree it never wrote. The real-tree cases are empty in nearly every category on a healthy site, so for as long as `fixture` was the only fault-carrying case, **every `--b fused` run dropped the one case that gave the comparison anything to compare.** The built pair closes that: the same idea in a tree `tbdocs` produced, split across two cases because no single tree carries all nine categories --- the online tree has the sitemap, search and canonical checks, and the offline tree has the forbidden prefix the online tree lacks. Both CI workflows run the harness, and neither runs it over the real site. `checks.yml` (pull requests) runs both halves: @@ -448,6 +450,19 @@ Two probes look redundant and are the two that caught real bugs while the guard Exits 1 on any failed probe. +### check_book_coverage.mjs +{: #check-book-coverage } + + node scripts/check_book_coverage.mjs + +Verifies the build still warns about a page [`docs/_book.yml`](Book-Configuration#pages-left-out-of-the-book) does not mention. Twelve probes over a manifest and pages built in memory, so nothing here reads `docs/`. No browser, no built tree, well under a second. + +The warnings say nothing when every page has an entry --- in a part, or in `left_out:` with a reason --- which is also all a check that had stopped working would say. Before they existed, whole sections dropped out of the PDF with nothing to report it: the IDE, Challenges and Videos, and Data Types, Enumerations and twinBASIC Additions with them. + +Eight probes give each of the five findings a fault to report: a page with no entry, a page in the book and in `left_out:`, an entry of each kind that selects no page, and a landing or foreword URL that names none. The other four hold the opposite: a consistent manifest reports nothing, and the three pages the book carries without a selector naming them --- a chaptered part's landing, a foreword, and the book page itself --- are never reported. Dropping any one emission site from `bookCoverage()` fails most of the twelve at once, and the probe named after that site says which. + +Exits 1 on any failed probe, 2 if it cannot run. + ### check_axe_patch_equiv.mjs {: #check-axe-patch-equiv } @@ -940,6 +955,6 @@ The build pipeline also reads a handful of declarative files. They are not execu | File | Effect | |---|---| | `docs/_config.yml` | Site config. `tbdocs` reads `url`, `baseurl`, `title`, `logo`, `also_build_offline`, `also_build_pdf`, `offline_exclude`, `exclude` (which filters the source walk but is **not** the publish safety net --- see [`check_publish_policy.mjs`](#check-publish-policy)), the footer / aux-link knobs, the GitHub edit-link knobs, and the download-link knobs (`gh_offline_link`, `gh_offline_link_url`, `gh_pdf_link_url`). Jekyll-only keys (`markdown`, `kramdown`, `theme`, `highlighter`, the `defaults` block, the `compress_html` block) are ignored. | -| `docs/_book.yml` | The PDF book's chapter manifest. Entries are resolved to pages via the selector schema (`page` / `pages` / `nav_page` / `nav_pages` / `no_descent`) and control PDF outline behaviour via `landing_page:`, `landing_is_target:`, `no_outline_entry:`, `no_heading_shift:`, and `outline_closed:`. Full schema is documented in the file header. Phase 2 resolves chapter arrays; Phase 8 assembles `book.html`. | +| `docs/_book.yml` | The PDF book's chapter manifest. Entries are resolved to pages via the selector schema (`page` / `pages` / `nav_page` / `nav_pages` / `no_descent`) and control PDF outline behaviour via `landing_page:`, `landing_is_target:`, `no_outline_entry:`, `no_heading_shift:`, and `outline_closed:`. Its `left_out:` list names the pages deliberately not in the book, each with a `reason:`; the build warns about a page that is in neither. Full schema is documented in the file header. Phase 2 resolves chapter arrays; Phase 8 assembles `book.html`. | | `builder/themes/Light.theme`, `Dark.theme`, `Classic.theme` | twinBASIC IDE theme files, vendored from the BETA installer. `builder/highlight-theme.mjs` parses them into a Symbol-keyed palette that determines both the renderer's scope-to-class mapping and the generated `tb-highlight.css`. Refresh from the installer when the IDE adds new palette entries. | | `builder/twinbasic.tmLanguage.json` | TextMate grammar for the twinBASIC language. Shiki uses it to tokenise every ` ```tb ` code block. | diff --git a/docs/Features/Advanced/Multithreading.md b/docs/Features/Advanced/Multithreading.md index 8b5fa926..6679ba64 100644 --- a/docs/Features/Advanced/Multithreading.md +++ b/docs/Features/Advanced/Multithreading.md @@ -18,7 +18,7 @@ Private Declare PtrSafe Function GetCurrentThreadId Lib "kernel32" () As Long Private Declare PtrSafe Function CreateThread Lib "kernel32" ( _ ByRef lpThreadAttributes As Any, _ - ByVal dwStackSize As Long, _ + ByVal dwStackSize As LongPtr, _ ByVal lpStartAddress As LongPtr, _ ByRef lpParameter As Any, _ ByVal dwCreationFlags As Long, _ @@ -42,9 +42,92 @@ Private Sub Command1_Click() Handles Command1.Click Text1.Text = "Wait end code " & CStr(hr) End Sub -Public Sub TestThread() +Public Function TestThread(ByVal parameter As LongPtr) As Long MsgBox "Hello thread" -End Sub +End Function ``` Under a single-threaded code, if you called `TestThread` before updating `Text1.Text`, the text wouldn't update until you clicked ok on the message box. But here, the message box in launched in a separate thread, so execution continues and updates the text, after which we manually choose to wait for the message box thread to exit. + +`CreateThread` passes the thread procedure one pointer-sized value, its `lpParameter` argument, and takes the procedure's return value as the thread's exit code. So a thread procedure is a **Function** with one **LongPtr** parameter that returns a **Long**, as `TestThread` is. + +## Running code on two threads and waiting for both to finish + +**RunBoth** below runs two calculations at the same time, one on each of two new threads, and prints both results once both threads have finished. Three things make it work: + +- **Each thread leaves its result in a module-level variable.** All threads see the same module-level variables, so **RunBoth** reads a result once the thread that wrote it has finished. +- **`WaitForSingleObject` on each thread's handle in turn** waits until both threads have finished: the second thread goes on running while **RunBoth** waits for the first. `CloseHandle` then releases the two handles. +- **Each thread procedure handles its own errors.** An error that nothing handles stops the thread where it happened, and the thread never finishes, so a wait with `INFINITE` never returns. Run from the IDE, a thread stopped by an overflow was still running ten seconds later. + +```tb check_build +Module Threads + Private Declare PtrSafe Function CreateThread Lib "kernel32" ( _ + ByRef lpThreadAttributes As Any, _ + ByVal dwStackSize As LongPtr, _ + ByVal lpStartAddress As LongPtr, _ + ByRef lpParameter As Any, _ + ByVal dwCreationFlags As Long, _ + ByRef lpThreadId As Long) As LongPtr + Private Declare PtrSafe Function WaitForSingleObject Lib "kernel32" (ByVal hHandle As LongPtr, ByVal dwMilliseconds As Long) As Long + Private Declare PtrSafe Function CloseHandle Lib "kernel32" (ByVal hObject As LongPtr) As Long + + Private Const INFINITE As Long = -1 + + ' Every thread sees the same module-level variables, so each thread leaves + ' its result here and RunBoth reads both once the threads have finished. + Private primeCount As Long + Private divisibleCount As Long + + Public Sub RunBoth() + Dim threadId As Long, first As LongPtr, second As LongPtr + first = CreateThread(ByVal 0, 0, AddressOf CountPrimes, ByVal 0, 0, threadId) + second = CreateThread(ByVal 0, 0, AddressOf CountDivisible, ByVal 0, 0, threadId) + WaitForSingleObject first, INFINITE ' returns when CountPrimes has finished + WaitForSingleObject second, INFINITE ' returns when CountDivisible has finished + CloseHandle first + CloseHandle second + Debug.Print "Primes below 200,000: " & primeCount + Debug.Print "Divisible by 7 or 11, 1 to 5,000,000: " & divisibleCount + End Sub + + ' A thread procedure takes one pointer-sized value, the lpParameter given + ' to CreateThread, and returns the thread's exit code. + Public Function CountPrimes(ByVal parameter As LongPtr) As Long + On Error GoTo Failed + Dim n As Long, d As Long, isPrime As Boolean + For n = 2 To 199999 + isPrime = True + For d = 2 To Sqr(n) + If n Mod d = 0 Then + isPrime = False + Exit For + End If + Next d + If isPrime Then primeCount = primeCount + 1 + Next n + Exit Function + Failed: + primeCount = -1 ' an error nothing handles would stop the thread for good + End Function + + Public Function CountDivisible(ByVal parameter As LongPtr) As Long + On Error GoTo Failed + Dim n As Long + For n = 1 To 5000000 + If n Mod 7 = 0 Or n Mod 11 = 0 Then divisibleCount = divisibleCount + 1 + Next n + Exit Function + Failed: + divisibleCount = -1 + End Function +End Module +``` + +Run from the IDE, in a 32-bit or a 64-bit build, **RunBoth** prints to the [Debug Console](../../tB/IDE/Project/DebugConsole): + +```text +Primes below 200,000: 17984 +Divisible by 7 or 11, 1 to 5,000,000: 1103895 +``` + +With an overflow forced at the start of **CountPrimes**, its handler set the first result to `-1`, and the second line was unchanged. A built `.exe` prints nothing with `Debug.Print`; see [Writing a command-line tool](../Project-Configuration/Project-Types#writing-a-command-line-tool-output-exit-code-and-arguments) for output from a console program. diff --git a/docs/Features/Packages/Import-export tool.md b/docs/Features/Packages/Import-export tool.md index 09a55a28..7441ed2d 100644 --- a/docs/Features/Packages/Import-export tool.md +++ b/docs/Features/Packages/Import-export tool.md @@ -316,11 +316,23 @@ for the save before committing. **Open a fresh clone** in the IDE. It holds `src` and the `.gitignore`, but no `.twinproj`: -1. Choose **File → New Project**, then **Import from folder...** (see [New +1. **Before importing, empty `src\Packages` of the compiler packages.** Delete any `VB`, + `VBA`, `VBRUN` or `AppGlobalClassProject` folder in it. Imported with them, the project + gets its own copy of each, and keeps it: its `.twinproj` measured 4,222,833 bytes, against + 4,207 bytes for the same folder without them. A fresh clone has them only if they were + committed before the `.gitignore` listed them; then remove them from the repository, and + commit that: + + ```batch + git rm -r --ignore-unmatch src/Packages/VB src/Packages/VBA src/Packages/VBRUN src/Packages/AppGlobalClassProject + git commit -m "Remove the compiler packages" + ``` + +2. Choose **File → New Project**, then **Import from folder...** (see [New Project](../../tB/IDE/Project/New#import-from-folder)). -2. In the *Browse For Folder* dialog, choose the clone's `src` folder. The project opens +3. In the *Browse For Folder* dialog, choose the clone's `src` folder. The project opens unsaved, with no file behind it yet. -3. Save it with CTRL + S. The project has no file, so **Save Project** +4. Save it with CTRL + S. The project has no file, so **Save Project** opens the *Save As* dialog. Save the `.twinproj` in the clone's top folder, beside `src`, and not inside `src`: *Export Path* is relative to the folder that holds the `.twinproj`, so a project saved inside `src` exports into `src\src`, and the `src` that Git tracks is @@ -328,22 +340,12 @@ for the save before committing. The same save exports the project into `src`, and from then on every save updates it. -**If the repository holds the compiler packages**, because they were committed before the -`.gitignore` listed them, remove them from the clone before step 1, and commit that: - -```batch -git rm -r --ignore-unmatch src/Packages/VB src/Packages/VBA src/Packages/VBRUN src/Packages/AppGlobalClassProject -``` - -Imported with them, the project gets its own copy of each: its `.twinproj` measured 4,222,833 -bytes, against 4,207 bytes for the same folder without them. - **After a pull or a merge, open the project from `src` again.** Every save empties `src` and writes the project as the IDE has it open, so once Git has changed `src`, the next save would undo those changes. Save and commit before pulling. Afterwards, close the project without -saving it, delete the compiler packages' folders from `src\Packages` --- the last export wrote -them there, though Git ignores them --- and follow the three fresh-clone steps above, saving -over the old `.twinproj`. +saving it, and follow the four fresh-clone steps above, saving over the old `.twinproj`. Step +1 matters here too: the last export wrote the compiler packages into `src\Packages`, though +Git ignores them. A clone that has no compiler packages, of a project that embeds no package, holds no folder under `src\Packages`, so the tB executable's `import` can pack it as well: see step 4 of diff --git a/docs/Features/Project-Configuration/Project-Types.md b/docs/Features/Project-Configuration/Project-Types.md index 63f2ac5c..7c176e41 100644 --- a/docs/Features/Project-Configuration/Project-Types.md +++ b/docs/Features/Project-Configuration/Project-Types.md @@ -84,7 +84,98 @@ For 32-bit Office, change the two paths to the win32 build. The rest of the code ## Console Applications -This project type allows making a true console project rather than a GUI project. Helpfully, it will also add a default `Console` class for reading/writing console IO and provided debug console. +This project type allows making a true console project rather than a GUI project: the program runs in the Command Prompt window it was started from, or in a console window of its own. Start one with **File → New Project → Standard EXE (Console App)**. The template turns on [*Is Console Application*](../../tB/IDE/Project/Settings#is-console-application), starts the program at `Sub Main` in its `MainModule`, and adds a `Console` class with `Cls`, `WriteLine` and `ReadLine` members for the console window. The class is a starting point, not a requirement; [Writing a command-line tool](#writing-a-command-line-tool-output-exit-code-and-arguments) replaces it with output that a batch file can capture. + +## Writing a command-line tool: output, exit code and arguments + +A tool that a batch file runs must put its output where the batch file can redirect it, and report failure through its exit code, which a batch file tests with `if errorlevel`. Four things behave differently from what the template and VBA suggest: + +- **`Debug.Print` writes only to the IDE's [Debug Console](../../tB/IDE/Project/DebugConsole).** The built `.exe` prints nothing with it. +- **The template's `Console.WriteLine` writes only to a console window.** It calls `WriteConsoleW`, which writes nothing to a file or a pipe. With the program's output redirected, as in `mytool > out.txt`, it writes nothing and raises error 5. +- **`End` stops the program with exit code 0.** To set the exit code, call the Windows `ExitProcess` function, after closing any files the program has open. +- **`Command$` returns the arguments as they were typed.** For `mytool "my file.txt"` it returns `"my file.txt"`, quotes included. + +The module below counts the lines in a file. It replaces the template's `MainModule`, whose own `Sub Main` has to go: with a second `Sub Main` in another module, the build fails with *'Main' is ambiguous*. `WriteOut` and `WriteErr` write a line to standard output and standard error, stdout and stderr. They use `WriteConsoleW` when the output goes to a console window, which takes the text unconverted, and `WriteFile` when it goes to a file or a pipe. + +```tb check_build +Module MainModule + Private Declare PtrSafe Function GetStdHandle Lib "kernel32" (ByVal nStdHandle As Long) As LongPtr + Private Declare PtrSafe Function GetConsoleMode Lib "kernel32" (ByVal hConsoleHandle As LongPtr, ByRef lpMode As Long) As Long + Private Declare PtrSafe Function WriteConsoleW Lib "kernel32" (ByVal hConsoleOutput As LongPtr, ByVal lpBuffer As LongPtr, ByVal nNumberOfCharsToWrite As Long, ByRef lpNumberOfCharsWritten As Long, ByVal lpReserved As LongPtr) As Long + Private Declare PtrSafe Function WriteFile Lib "kernel32" (ByVal hFile As LongPtr, ByRef lpBuffer As Any, ByVal nNumberOfBytesToWrite As Long, ByRef lpNumberOfBytesWritten As Long, ByVal lpOverlapped As LongPtr) As Long + Private Declare PtrSafe Sub ExitProcess Lib "kernel32" (ByVal uExitCode As Long) + + Private Const STD_OUTPUT_HANDLE As Long = -11 + Private Const STD_ERROR_HANDLE As Long = -12 + + Public Sub Main() + Dim fileName As String + fileName = Trim$(Command$()) + If Len(fileName) > 1 And Left$(fileName, 1) = """" And Right$(fileName, 1) = """" Then + fileName = Mid$(fileName, 2, Len(fileName) - 2) ' a quoted name keeps its quotes + End If + If fileName = "" Then + WriteErr "Usage: linecount " + ExitProcess 1 + End If + If Dir$(fileName) = "" Then + WriteErr "linecount: file not found: " & fileName + ExitProcess 1 + End If + + Dim f As Integer, lineText As String, count As Long + f = FreeFile + Open fileName For Input As #f + Do While Not EOF(f) + Line Input #f, lineText + count = count + 1 + Loop + Close #f + WriteOut fileName & ": " & count & " lines" + End Sub + + ' Writes a line to standard output or standard error: with WriteConsoleW to + ' a console window, and with WriteFile to a file or a pipe. + Public Sub WriteOut(ByVal text As String) + WriteTo STD_OUTPUT_HANDLE, text & vbCrLf + End Sub + + Public Sub WriteErr(ByVal text As String) + WriteTo STD_ERROR_HANDLE, text & vbCrLf + End Sub + + Private Sub WriteTo(ByVal stream As Long, ByVal text As String) + Dim handle As LongPtr, mode As Long, written As Long + handle = GetStdHandle(stream) + If GetConsoleMode(handle, mode) <> 0 Then + WriteConsoleW handle, StrPtr(text), Len(text), written, 0 + Else + Dim bytes() As Byte + bytes = StrConv(text, vbFromUnicode) + WriteFile handle, bytes(0), UBound(bytes) + 1, written, 0 + End If + End Sub +End Module +``` + +With the default [Build Output Path](../../tB/IDE/Project/Settings#build-output-path), a project named `linecount` builds `Build\linecount_win32.exe`. Rename the file, or change the setting, to run it as `linecount`. At a command prompt, with `three.txt` holding three lines: + +```text +C:\Tools>linecount three.txt +three.txt: 3 lines + +C:\Tools>linecount missing.txt +linecount: file not found: missing.txt + +C:\Tools>echo %errorlevel% +1 +``` + +The error message goes to standard error, so `linecount missing.txt 2> errors.txt` puts it in the file. `linecount three.txt > count.txt` writes `three.txt: 3 lines` into `count.txt`, and `linecount three.txt | find "lines"` passes it through the pipe. + +Output written to a file or a pipe is in the system's ANSI code page, as `StrConv` converts it. A character the code page does not have is replaced: on a Western European system, `Ł` becomes `L`. In a console window, `WriteConsoleW` writes the text unconverted, so `Zoë Łódź` shows as it is. + +`Line Input #` ends a line at a carriage return, so a file with Unix line endings, where each line ends with a line feed alone, counts as one line. See [Line Input #](../../tB/Core/Line-Input). ## Windows Services diff --git a/docs/IDE/AddIns/index.md b/docs/IDE/AddIns/index.md index 3aa0dd33..915513db 100644 --- a/docs/IDE/AddIns/index.md +++ b/docs/IDE/AddIns/index.md @@ -8,7 +8,7 @@ permalink: /tB/IDE/AddIns/ An addin is a Standard DLL that exports `tbCreateCompilerAddin` and returns an object implementing the [**AddIn**](../../Packages/tbIDE/AddIn) interface. Through the [**Host**](../../Packages/tbIDE/Host) object the IDE passes at startup, an addin can reach the toolbar, tool windows, debug console, current project, keyboard shortcuts, and themes. The [**tbIDE package**](../../Packages/tbIDE/) documents the full API. -The New Project dialog includes addin templates (samples 10 through 16), covering patterns from simple toolbar buttons to HTML DOM-backed tool windows. Community addins are listed on the [**Community**](Community) page. +The New Project dialog's **Samples** tab holds seven addin samples, numbers 10 to 16 (see [New Project](../Project/New#samples)), covering patterns from simple toolbar buttons to HTML DOM-backed tool windows. Each is a complete addin project, with the tbIDE package referenced and the build path set, and Sample 10, *twinBASIC IDE Addin*, is the plainest one to start from. A project started from the **Standard DLL** template has neither setting: [Building and loading an addin](../../Packages/tbIDE/#building-and-loading-an-addin) says what to add. Community addins are listed on the [**Community**](Community) page. twinBASIC supports two addin install locations. The IDE install directory is available to all user accounts on the machine but may require reinstallation after an IDE update. A per-user application data folder persists across IDE upgrades and requires no administrator rights. diff --git a/docs/IDE/Debug Console.md b/docs/IDE/Debug Console.md index 2abca7d2..466783f3 100644 --- a/docs/IDE/Debug Console.md +++ b/docs/IDE/Debug Console.md @@ -13,6 +13,12 @@ The Debug Console captures output from [**Debug.Print**](../../Modules/Debug#pri ## ![](Images/DebugConsole_AutoScroll.png) Auto Scroll +Keeps the newest output in view. While Auto Scroll is on, the console scrolls to each new line as it arrives: to the bottom, or to the top when **Invert Output Direction** is ticked. It is on each time the IDE starts, and the button is highlighted while it is on. + +Scrolling away from the newest line turns Auto Scroll off, so earlier output stays in view while the program goes on writing. Scrolling back until the newest line shows turns it on again. + +Clicking the button turns Auto Scroll off until it is clicked again. Scrolling back to the newest line then leaves it off. + ## ![](Images/DebugConsole_Clear.png) Clear Debug Console Empties the console. [**Debug.Cls**](../../Modules/Debug#cls) does the same thing from code. diff --git a/docs/IDE/Menu/Debug.md b/docs/IDE/Menu/Debug.md index c9dd2d0e..4ce00f4d 100644 --- a/docs/IDE/Menu/Debug.md +++ b/docs/IDE/Menu/Debug.md @@ -12,14 +12,18 @@ permalink: /tB/IDE/Project/Menu/Debug - Step Into F8 / F11 - Step Over SHIFT + F8 / F10 + --- - Add Watch... SHIFT + F9 - Clear Watches + --- - Toggle Breakpoint F9 - Clear All Breakpoints CTRL + SHIFT + F9 + --- - Set Next Statement (Jump To Line) CTRL + F9 + --- - Debugger Options diff --git a/docs/IDE/Menu/Edit.md b/docs/IDE/Menu/Edit.md index fb215492..97fac40f 100644 --- a/docs/IDE/Menu/Edit.md +++ b/docs/IDE/Menu/Edit.md @@ -12,25 +12,30 @@ permalink: /tB/IDE/Project/Menu/Edit - Undo CTRL + Z - Redo CTRL + Y + --- - Cut CTRL + X / SHIFT + DELETE - Copy CTRL + C / CTRL + INSERT - Paste CTRL + V - Delete DELETE - Select All CTRL + A + --- - Find... CTRL + F - Replace... CTRL + H - Find In Project... CTRL + SHIFT + F + --- - Indent CTRL + ] - Outdent CTRL + [ - Format Selection - Format Document + --- - Quick Find... ALT + F - Quick Replace... ALT + H - Select All Matches ALT + A + --- - Fold CTRL + { - Fold Procedures CTRL + ALT + ARROWLEFT @@ -38,8 +43,10 @@ permalink: /tB/IDE/Project/Menu/Edit - Unfold CTRL + } - Unfold Procedures CTRL + ALT + ARROWRIGHT - Unfold All + --- - Go To Line/Column... + --- - Transform To Uppercase - Transform To Lowercase diff --git a/docs/IDE/Menu/File.md b/docs/IDE/Menu/File.md index bc4e2a40..06ce190f 100644 --- a/docs/IDE/Menu/File.md +++ b/docs/IDE/Menu/File.md @@ -14,15 +14,19 @@ permalink: /tB/IDE/Project/Menu/File - Open Project... CTRL + O - Open Recent... - Close Project + --- - Save Project CTRL + S - Save Project As... + --- - Export Project... CTRL + E - Save Current Document + --- - Build - Clean + --- - Exit ALT + F4 diff --git a/docs/IDE/Menu/Format.md b/docs/IDE/Menu/Format.md index fd5236f3..304d5303 100644 --- a/docs/IDE/Menu/Format.md +++ b/docs/IDE/Menu/Format.md @@ -16,15 +16,19 @@ Every command on this menu acts on the controls selected in the form designer, s - Align - Make Same Size + --- - Horizontal Spacing - Vertical Spacing + --- - Center In Container (Horizontally) - Center In Container (Vertically) + --- - Bring To Front - Send To Back + --- - Lock Controls @@ -33,10 +37,12 @@ Every command on this menu acts on the controls selected in the form designer, s - Left ALT + ARROWLEFT - Center (Horizontal) - Right ALT + ARROWRIGHT + --- - Top ALT + ARROWUP - Center (Vertical) - Bottom ALT + ARROWDOWN + --- - To Grid diff --git a/docs/IDE/Menu/Help.md b/docs/IDE/Menu/Help.md index 54e870c2..e96f8ec1 100644 --- a/docs/IDE/Menu/Help.md +++ b/docs/IDE/Menu/Help.md @@ -13,14 +13,17 @@ permalink: /tB/IDE/Project/Menu/Help - About twinBASIC... - Licence Agreement... - Automatic IDE Error Reporting... + --- - Help & Support (Discord Server)... - Help & Support (GitHub repository)... - Twitter (News Feed)... + --- - Purchase A Licence... - Enter Licence Key... - Buy us a Coffee! (Ko-Fi)... + --- - Compiler services TRACE mode: Disabled diff --git a/docs/IDE/Menu/Project.md b/docs/IDE/Menu/Project.md index b8b8b79a..18c4549c 100644 --- a/docs/IDE/Menu/Project.md +++ b/docs/IDE/Menu/Project.md @@ -11,9 +11,11 @@ permalink: /tB/IDE/Project/Menu/Project ![The Project menu open with every command greyed out: Add, which carries a submenu arrow, References with CTRL+T, Project Settings, Open Project Folder and Open Build Output Folder.](Images/Menu_Project.png) - Add + --- - References... CTRL + T - Project Settings... + --- - Open Project Folder... - Open Build Output Folder... diff --git a/docs/IDE/Menu/View.md b/docs/IDE/Menu/View.md index d83a71c6..ecf76827 100644 --- a/docs/IDE/Menu/View.md +++ b/docs/IDE/Menu/View.md @@ -12,13 +12,16 @@ permalink: /tB/IDE/Project/Menu/View - Code Editor - Object Designer SHIFT + F7 + --- - Definition SHIFT + F2 / F12 - Last Position CTRL + SHIFT + F2 + --- - Object Browser - Zoom In - Zoom Out + --- - EDITOR - PROJECT EXPLORER diff --git a/docs/IDE/Menu/Window.md b/docs/IDE/Menu/Window.md index ec88b49b..100c293b 100644 --- a/docs/IDE/Menu/Window.md +++ b/docs/IDE/Menu/Window.md @@ -13,6 +13,7 @@ permalink: /tB/IDE/Project/Menu/Window - Panel Layouts - Panel Features - Keyboard Shortcuts + --- - Theme - Language @@ -23,8 +24,10 @@ permalink: /tB/IDE/Project/Menu/Window - Default Built-in Layout CTRL + # - Full Screen Editor Layout + --- - ✓ Custom Layout (Unsaved) + --- - Save Current Panel Layout As... - Manage Panel Layouts... @@ -207,6 +210,7 @@ permalink: /tB/IDE/Project/Menu/Window - ✓ Allow resizing of docked panels - ✓ Allow rearrangement of docked panels - ✓ Allow tear-out of docked panels + --- - ✓ Allow resizing of floating panels - ✓ Allow movement of floating panels @@ -216,6 +220,7 @@ permalink: /tB/IDE/Project/Menu/Window ![The Window menu with Keyboard Shortcuts highlighted and its submenu open to the right: a ticked Default Built-in Keyboard Shortcuts above Manage Keyboard Shortcuts.](Images/Menu_Window_KeyboardShortcuts.png) - ✓ Default Built-in Keyboard Shortcuts + --- - Manage Keyboard Shortcuts @@ -959,6 +964,7 @@ permalink: /tB/IDE/Project/Menu/Window - Classic (Light) - ✓ Dark - Light + --- - Reload from disk diff --git a/docs/IDE/New Project.md b/docs/IDE/New Project.md index fdcd688b..46d8aa39 100644 --- a/docs/IDE/New Project.md +++ b/docs/IDE/New Project.md @@ -1,5 +1,5 @@ --- -title: Project +title: New Project parent: IDE # nav_order: 2 permalink: /tB/IDE/Project/New @@ -34,7 +34,7 @@ The project opens with no `.twinproj` file behind it: it is unsaved, and marked If the folder holds the compiler packages under `Packages`, as an export by the IDE does, delete their folders first: imported with them, the project gets a copy of them that the compiler does not use. See [Packing the export back into a project](Menu/File#packing-the-export-back-into-a-project). [Keeping a project in Git from the IDE](../../../Features/Packages/Import-Export-Tool#keeping-a-project-in-git-from-the-ide) uses this command to open a fresh clone. -# Samples +## Samples ![The same dialog on its Samples tab, a scrolling column of sample projects headed by Sample 0. Reports (Experimental), which is selected, then Sample 1. HelloWorld, Sample 1a. WebView2 Examples, Sample 2. GetIPAddresses and Sample 3. MyCodeLibrary, with the list running on past the bottom of the panel.](Images/New_Project_Samples.png) @@ -66,7 +66,7 @@ Each row is labelled **Sample** followed by its number. The numbering is the dia - **22.** Windows Service Complex Example (inc Event Logging and IPC) - **23.** OOP Inheritance Example (Animals) -# Recent +## Recent If you haven't opened any projects, or removed all then this tab will be blank. diff --git a/docs/IDE/Project Settings.md b/docs/IDE/Project Settings.md index 2c8603fd..b0bd1a83 100644 --- a/docs/IDE/Project Settings.md +++ b/docs/IDE/Project Settings.md @@ -7,26 +7,42 @@ permalink: /tB/IDE/Project/Settings # Project Settings -Listed below are the project settings, in the same order as they appear in the Project Settings dialog. The explanations of those settings will be added below in the future. In the meantime, please refer to the setting descriptions built into the Project Settings dialog: +The Project Settings dialog, **Project → Project Settings...**, holds the settings of the open project. They are listed below in the order the dialog shows them. The dialog shows a description under each setting, and each entry below restates it: -![A fragment of the Project Settings dialog, indicating a description of a setting](Images/project settings description text.png) +![The Option Explicit On setting in the Project Settings dialog, set to Yes, with an arrow pointing at the description under it](Images/project settings description text.png) + +The drop-down list of a setting ends with a **COMPILER DEFAULT** entry, which names the value used when the project does not set one. Choosing it removes the setting from the project's `Settings` file, and so does emptying a box. [**File → Export Project**](Menu/File#export-project) writes that file beside the project's `Sources` folder, and the entries below give the settings' keys in it. {: .toc } ## Project Name +The name that code uses for the project, as a namespace. When the build produces a type library, this is also the library's name. In the `Settings` file it is `project.name`. + ## Project Description +The description of the type library, when the build produces one. It can include the variables `${Architecture}`, `${VersionMajor}`, `${VersionMinor}`, `${VersionBuild}` and `${VersionRevision}`. In the `Settings` file it is `project.description`. + ## Application Title +The title that [**MsgBox**](../../Modules/Interaction/MsgBox) and [**InputBox**](../../Modules/Interaction/InputBox) show when the call gives none, and the value that [**App.Title**](../../Packages/AppGlobalClassObject/_App/Title) returns. When [Product Name](#product-name) is not set, the build also writes it into the file's version resource as the product name. In the `Settings` file it is `project.appTitle`. + ## Application HelpFile +The help file of the program. [**App.HelpFile**](../../Packages/AppGlobalClassObject/_App/HelpFile) returns it at run time, and forms use it for **F1** and *What's This* help. A new project does not set it. In the `Settings` file it is `project.appHelpFile`. + ## Startup Object +The form that the program shows when it starts, or **Sub Main**. When it is not set, the default is **Sub Main** for an EXE and **(none)** for a DLL. For a Standard EXE the list does not offer **(none)**. In the `Settings` file it is `project.startupObject`. + ## Icon Form +The form whose icon becomes the program's icon. The list holds the project's forms, and the default is **(none)**. The build copies the form's icon into the file's icon resources, as entry `#1` of the `RT_GROUP_ICON` group, which is the icon Windows usually shows for the program. In the `Settings` file it is `project.iconForm`. + ## Library References +The type libraries and packages that the compiler uses for this project. In the `Settings` file it is `project.references`. + ![The Project Settings dialog on its Enabled Libraries tab, listing four ticked references in priority order with Library Symbol and Version columns: the twinBASIC VBA and VBRUN compatibility packages, OLE Automation as stdole, and the IDE Extensibility package as tbIDE.](Images/ProjectSettings_LibraryReferences.png) ![The same dialog on its Available COM References tab. A search box sits above an alphabetical list of unticked type libraries registered on the machine --- AccessibilityCplAdmin, Active DS, ActiveMovie, AgentWmiLib and so on --- against Library Symbol, Version and Publisher columns.](Images/ProjectSettings_AvailableCOMReferences.png) @@ -37,110 +53,222 @@ See [Packages](../../../Features/Packages/) ## Compiler Warnings +How the compiler reports each of its warnings in this project. The dialog lists every warning by its code and message, and each one can be set to **WARNING**, **HINT**, **INFO**, **IGNORE** or **ERROR**. In a new project, TB0015 and TB0030 are at **HINT**; TB0018 to TB0021, TB0024, TB0025 and TB0029 are at **IGNORE**; and every other warning is at **WARNING**. + +The [**IgnoreWarnings**](../../Core/Attributes#ignorewarnings), [**EnforceWarnings**](../../Core/Attributes#enforcewarnings) and [**EnforceErrors**](../../Core/Attributes#enforceerrors) attributes override these settings in a class, a module or a procedure. [Compiler Warnings](../../../Features/Compiler-IDE/Compiler-Warnings) describes some of the warnings. In the `Settings` file the setting is `project.warnings`, which holds a list of codes for each level: `errors`, `hints`, `ignored`, `info` and `warnings`. + ## Project ID +A GUID that identifies the project. The **↻** button beside it replaces it with a new GUID. In the `Settings` file it is `project.id`. + ## Use Project ID for type library ID +When set to **Yes**, the type library that the build produces takes the [Project ID](#project-id) as its ID. When set to **No**, the build generates a unique ID for it. It is **No** by default. In the `Settings` file it is `project.useProjectIdForTypeLibraryId`. + ## Build Output Path -The full path of the file the compiler creates. Its description in the dialog lists these variables: `${SourcePath}`, the folder that holds the `.twinproj` file, and `${ProjectName}`, `${ProjectID}`, `${FileExtension}`, `${Architecture}`, `${VersionMajor}`, `${VersionMinor}`, `${VersionBuild}` and `${VersionRevision}`. `${Architecture}` is `win32` or `win64`, whichever the toolbar's [build configuration](Toolbar#build-configuration) box is set to. +The full path of the file the compiler creates. Its description in the dialog lists these variables: `${SourcePath}`, the folder that holds the `.twinproj` file, and `${ProjectName}`, `${ProjectID}`, `${FileExtension}`, `${Architecture}`, `${VersionMajor}`, `${VersionMinor}`, `${VersionBuild}` and `${VersionRevision}`. `${Architecture}` is `win32` or `win64`, whichever the toolbar's [build configuration](Toolbar#build-configuration) box is set to. The IDE add-in samples use one more that the description leaves out, `${IdePath}`, the folder the IDE is installed in: their `${IdePath}\addins\${Architecture}\${ProjectName}.${FileExtension}` builds straight into the IDE's own `addins` folder, which works until the IDE has loaded the add-in --- see [Rebuilding an addin the IDE has loaded](../../Packages/tbIDE/#rebuilding-an-addin-the-ide-has-loaded). Every project template sets it to `${SourcePath}\Build\${ProjectName}_${Architecture}.${FileExtension}`: a `Build` folder beside the `.twinproj` file, and a different file name for each architecture. A Standard DLL project named `MathGreetLib` builds `Build\MathGreetLib_win32.dll` or `Build\MathGreetLib_win64.dll`, and a `Declare` that calls it has to name that file --- see [Calling a Standard DLL from VBA or Excel](../../../Features/Project-Configuration/Project-Types#calling-a-standard-dll-from-vba-or-excel). In the `Settings` file it is `project.buildPath`. +## ActiveX Fusion Host EXE Output Path + +The full path of the host EXE that the compiler creates for [Fusion](../../../Features/Fusion): the separate program that ActiveX controls run in. It can use the same variables as [Build Output Path](#build-output-path), except that `${Architecture}` is `win32host` or `win64host`. When it is not set, the compiler uses *Build Output Path* when it needs the file. A built program expects the host EXE in its own folder. For a host EXE kept anywhere else, the program has to set `App.FusionHostEXEPath` as it starts --- see [Runtime Behaviour and Deployment](../../../Features/Fusion#runtime-behaviour-and-deployment). In the `Settings` file it is `project.fusionBuildPath`. + ## Build Type The type of file the compiler creates: **Standard EXE**, **ActiveX DLL**, **ActiveX Control**, **Standard DLL** or **Package TWINPACK**. [Project Types](../../../Features/Project-Configuration/Project-Types) describes the Standard DLL, and code can test the setting with the [`TWINBASIC_BUILD_TYPE`](../../../Reference/Compiler-Constants#twinbasic_build_type) compiler constant. In the `Settings` file it is `project.buildType`. ## Licence Type +For a package published to the package database: its licence, shown to the people who use the package. A new project does not set it. [Creating a TWINPACK Package](../../../Features/Packages/Creating-TWINPACK) describes the settings of a package. In the `Settings` file it is `project.licence`. + ## Package Visibility +For a package published to the package database: **PUBLIC** makes it available to everyone, and **PRIVATE** only to its publisher. It is **PRIVATE** by default. In the `Settings` file it is `project.packageVisibility`. + ## VERSION Resource -### Major/Minor/Build +The version number of the build, and the text that the build writes into the file's version resource (`VERSIONINFO`). + +### Major/Minor/Build/Revision + +The four parts of the version number: *Major*, *Minor*, *Build* and *Revision*. The build writes them into the file's version resource, and [**App.Major**](../../Packages/AppGlobalClassObject/_App/Major), [**App.Minor**](../../Packages/AppGlobalClassObject/_App/Minor), [**App.Build**](../../Packages/AppGlobalClassObject/_App/Build) and [**App.Revision**](../../Packages/AppGlobalClassObject/_App/Revision) return them. When they are not set, the version is 1.0.0.0. In the `Settings` file they are `project.versionMajor`, `project.versionMinor`, `project.versionBuild` and `project.versionRevision`. ### Product Name +The `ProductName` text that the build writes into the file's version resource. When it is not set, the build writes the [Application Title](#application-title) there instead. At run time, [**App.ProductName**](../../Packages/AppGlobalClassObject/_App/ProductName) returns the product name. In the `Settings` file it is `project.versionProductName`. + ### Company Name +The `CompanyName` text that the build writes into the file's version resource. [**App.CompanyName**](../../Packages/AppGlobalClassObject/_App/CompanyName) returns it at run time. A new project does not set it. In the `Settings` file it is `project.versionCompanyName`. + ### File Description +The `FileDescription` text that the build writes into the file's version resource. [**App.FileDescription**](../../Packages/AppGlobalClassObject/_App/FileDescription) returns it at run time. A new project does not set it. In the `Settings` file it is `project.versionFileDescription`. + ### Legal Copyright +The `LegalCopyright` text that the build writes into the file's version resource. [**App.LegalCopyright**](../../Packages/AppGlobalClassObject/_App/LegalCopyright) returns it at run time. A new project does not set it. In the `Settings` file it is `project.versionLegalCopyright`. + ### Legal Trademarks +The `LegalTrademarks` text that the build writes into the file's version resource. [**App.LegalTrademarks**](../../Packages/AppGlobalClassObject/_App/LegalTrademarks) returns it at run time. A new project does not set it. In the `Settings` file it is `project.versionLegalTrademarks`. + ### Comments +The `Comments` text that the build writes into the file's version resource. [**App.Comments**](../../Packages/AppGlobalClassObject/_App/Comments) returns it at run time. A new project does not set it. In the `Settings` file it is `project.versionComments`. + ### Auto-Increment +The part of the version number that goes up by 1 after each successful build: **None**, **Revision**, **Build**, **Minor** or **Major**. It is **None** by default. VB6 always increases *Revision*. In the `Settings` file it is `project.versionAutoIncrement`. + ## Type Library Version -### Major/Minor +The version of the type library that the build generates for the project. +### Major/Minor {: #typelib-major-minor} -### Auto-Increment +The major and minor version numbers of the type library. When one of them is -1, the build uses the matching part of the [version number](#majorminorbuildrevision) instead. When they are not set, the type library's version is 1.0. In the `Settings` file they are `project.typeLibVersionMajor` and `project.typeLibVersionMinor`. +### Auto-Increment {: #typelib-auto-increment} +The part of the type library's version that goes up by 1 after each successful build: **None**, **Major** or **Minor**. It is **None** by default. In the `Settings` file it is `project.typeLibVersionAutoIncrement`. + ## Register DLLs to HKLM +For an **ActiveX DLL** or **ActiveX Control** build. When set to **Yes**, the DLL's `DllRegisterServer` and `DllUnregisterServer` register it under `HKEY_LOCAL_MACHINE` instead of `HKEY_CURRENT_USER`, as VB6 DLLs do. It is **No** by default. In the `Settings` file it is `project.dllRegisterLocalMachine`. + +> [!IMPORTANT] +> Registering under `HKEY_LOCAL_MACHINE` needs administrator rights, so with this setting at **Yes**, the IDE usually has to be started with **Run as administrator**. + +## Register DLL after build + +For an **ActiveX DLL** or **ActiveX Control** build. When set to **Yes**, the IDE registers the DLL as soon as the build finishes. It is **Yes** by default. **No** leaves the registration to the developer. In the `Settings` file it is `project.dllRegisterAfterBuild`. + ## COM Initialization +How a built EXE initializes COM and OLE, which sets its threading model: **OleInitialize STA (Single Threaded Apartment)**, **MTA (CoInitializeEx - Multi Threaded Apartment)** or **STA (CoInitialize - Single Threaded Apartment)**. The default is **OleInitialize STA (Single Threaded Apartment)**, which matches VB6. It has no effect on a DLL build. In the `Settings` file it is `project.comInitialization`. + ## Is Console Application +When set to **Yes**, the built executable is marked as a console application rather than a GUI application. It is **No** by default; the **Standard EXE (Console App)** template sets it to **Yes**. In the `Settings` file it is `project.isConsoleApplication`. [Writing a command-line tool](../../../Features/Project-Configuration/Project-Types#writing-a-command-line-tool-output-exit-code-and-arguments) shows how such a program writes its output and sets its exit code. + ## Native Subsystem +When set to **Yes**, the build marks the file as a native-subsystem image (`IMAGE_SUBSYSTEM_NATIVE`), which suits a kernel-mode device driver. It is **No** by default. In the `Settings` file it is `project.isNativeSubsystem`. + ## Override Entry Point +The name of a procedure to use as the entry point of the built file. That procedure then has to initialize everything itself, such as COM and OLE. It is mainly for native kernel-mode builds --- see [Native Subsystem](#native-subsystem). A new project does not set it. In the `Settings` file it is `project.overrideEntryPoint`. + ## Runtime Binding of DLL Declares +When set to **Yes**, the project's [**Declare**](../../Core/Declare) statements are resolved at run time. When set to **No**, they are added to the import address table (IAT) of the built file. It is **Yes** by default. A **Declare** that comes from a type library always goes into the import address table, whatever this setting says. In the `Settings` file it is `project.dllRuntimeBinding`. + ## Conditional Compilation Args +Conditional compilation constants for the whole project, as `name=value` pairs separated by colons: `foo=42:bar=-42`. Each value must be between -32768 and 32767. These are the project-wide constants that [**#If**](../../Core/Topic-Preprocessor) can test; **#Const** defines a constant for its own module only. A new project does not set it. In the `Settings` file it is `project.conditionalCompilationArguments`. + ## Option Explicit On +Whether [**Option Explicit**](../../Core/Option) is on in a file that has no **Option Explicit** statement of its own. It is **Yes** by default. In the `Settings` file it is `project.optionExplicit`. + ## Auto Prettify Source Code -## CodeLens - Show Run Procedure +When set to **Yes**, the editor corrects the capitalization of words as they are typed, and corrects spacing to the usual VBE layout. It is **Yes** by default. In the `Settings` file it is `project.autoPrettify`. +## CodeLens - Show Run Procedure {: #show-run-procedure } +When set to **Yes**, the editor shows **▶ run** and the procedure's name in the CodeLens above each procedure in a standard module that takes no arguments. [CodeLens](../../../Features/Compiler-IDE/CodeLens) describes running a procedure that way. It is **Yes** by default. In the `Settings` file it is `project.codeLensRunProcedure`. + ## Runtime Windows Codepage +The Windows code page that [**Chr**](../../Modules/Strings/Chr), **Chr$**, [**String**](../../Modules/Strings/String), **String$**, [**StrConv**](../../Modules/Strings/StrConv) and [**Asc**](../../Modules/Strings/Asc) use at run time. The choices are the system code page at compile time, the system code page at run time, fourteen code pages named by number, from **CODEPAGE 1252 - WESTERN EUROPEAN (Latin-1)** to **CODEPAGE 949 - KOREAN (KS C 5601)**, and **OTHER**. The default is **SYSTEM CODEPAGE [AT COMPILE TIME]**. In the `Settings` file it is `project.ansiCodePageRuntime`. + ## Use Unicode Standard Library +When set to **Yes**, the compiler uses the Unicode versions of Windows API calls wherever it can: [**MsgBox**](../../Modules/Interaction/MsgBox) calls `MessageBoxW` rather than `MessageBoxA`, for example. It is **Yes** by default. In the `Settings` file it is `runtime.useUnicodeStandardLibrary`. + ## Unicode Control Notifications -## Include Procedure Name Symbols in Built Executables +When set to **Yes**, twinBASIC container windows --- forms, UserControls, PictureBoxes and Frames --- answer the `WM_NOTIFYFORMAT` message from third-party controls and Common Controls by saying that they support Unicode. It is **Yes** by default. A project that does not subclass its windows to handle `WM_NOTIFY` messages itself does not usually need to change it. In the `Settings` file it is `runtime.useUnicodeCommonControlNotifications`. +## Include Procedure Name Symbols in Built Executables {: #include-procedure-name-symbols } +When set to **Yes**, the compiler includes the names of procedures in built EXEs and DLLs, and the call stack information read at run time uses them. When set to **No**, [**ErrorStackFrame**](../../Packages/VBRUN/ErrorStackFrame/) gives `{unknown}` for those names in a built executable. It is **No** by default. The names make the file slightly larger and do not slow it down. In the `Settings` file it is `compiler.includeStackSymbols`. + ## Trace Flags +What the trace logger records, together with [Trace Output](#trace-output). [Debug Trace Logger](../../../Features/Compiler-IDE/Debugging#debug-trace-logger) describes the logger. Each flag has a box, and no box is ticked by default: + +- **Trace Procedure Entry and Exit points** +- **Trace Procedure Arguments** -- needs *Trace Procedure Entry and Exit points* as well +- **Trace IDispatch::QueryInterface calls** +- **Trace IDispatch::GetIDsOfNames calls** +- **Trace IDispatch::Invoke calls** +- **Trace Window (HWND) Messages** +- **Trace Debug.TracePrint output** -- what [**Debug.TracePrint**](../../Modules/Debug#traceprint) writes +- **Buffered Trace Log File Writing** -- not for tracing a hard crash +- **Trace IClassFactory::CreateInstance and IClassFactory2::CreateInstanceLic calls for exposed COM classes** +- **Trace DllGetClassObject calls** -- COM and ActiveX creation of the project's exposed classes +- **Trace RaiseEvent calls and arguments** + +In the `Settings` file it is `compiler.traceFlags`. + ## Trace Output +Where the trace log goes: the full path of the log file. A new project does not set it. The path can use these placeholders: + +- `${SESSIONID}` -- a GUID that is unique to each session and each thread +- `${DATE}` -- the date, as `YYYYMMDD` +- `${TIME}` -- the time, as `HHNNSS` +- `${DEBUG}` -- sends the log to the IDE's [DEBUG CONSOLE](DebugConsole) only + +In the `Settings` file it is `compiler.traceOutput`. + +> [!IMPORTANT] +> A path to a file must include `${SESSIONID}`, so that each thread can create a log of its own. + ## Disable Overflow Checks +When set to **Yes**, the compiler leaves out every run-time check for integer overflow in arithmetic, which produces more efficient code. It is **No** by default. It is the same as [**IntegerOverflowChecks(False)**](../../Core/Attributes#integeroverflowchecks) on every procedure in the project, and the compiler then ignores any **IntegerOverflowChecks** attribute in the project. It affects the project only, not the packages it references. In the `Settings` file it is `compiler.disableOverflowChecks`. + ## Disable Array Bounds Checks +When set to **Yes**, the compiler leaves out every run-time check of array bounds when array elements are read and written, which produces more efficient code. It is **No** by default. It is the same as [**ArrayBoundsChecks(False)**](../../Core/Attributes#arrayboundschecks) on every procedure in the project, and the compiler then ignores any **ArrayBoundsChecks** attribute in the project. It affects the project only, not the packages it references. In the `Settings` file it is `compiler.disableArrayBoundsChecks`. + ## Disable FPU Error Checks +When set to **Yes**, the compiler leaves out every run-time check for floating-point (FPU) errors in arithmetic, which produces more efficient code. It is **No** by default. It is the same as [**FloatingPointErrorChecks(False)**](../../Core/Attributes#floatingpointerrorchecks) on every procedure in the project, and the compiler then ignores any **FloatingPointErrorChecks** attribute in the project. It affects the project only, not the packages it references. In the `Settings` file it is `compiler.disableFPUErrorChecks`. + ## Sanitize Booleans +When set to **Yes**, the compiler makes sure that a **Boolean** value from an external source is exactly **True** (-1) or **False** (0), so that a value such as 1 cannot end up in a **Boolean**. It is **No** by default. It costs a little performance. In the `Settings` file it is `compiler.sanitizeBooleans`. + ## Constant Function Folding -## Large Address Aware (LAA) +Experimental. When set to **Yes**, the compiler replaces some function calls with their results as it compiles, where the result is fully known at compile time. It affects only calls to the VBA standard library, and calls to standard-module functions marked with the [**ConstantFoldable**](../../Core/Attributes#constantfoldable) attribute. It is **No** by default. In the `Settings` file it is `optimizer.constantFunctionFolding`. +## Large Address Aware (LAA) {: .la-aware } -## Terminal Server Avare +When set to **Yes**, a 32-bit EXE built from the project has the `LARGE ADDRESS AWARE` flag, which lets it use up to 4 GB of memory. When set to **No**, a 32-bit EXE is limited to 2 GB. It is **No** by default. The project's own code and every third-party DLL it uses must be compatible with the flag, so set it with care. A 64-bit build always has the flag. In the `Settings` file it is `project.largeAddressAware`. +## Terminal Server Aware {: .ts-aware } -## Data Execution Prevention Aware (DEP) +When set to **Yes**, the build marks the file as Terminal Server aware. The mark changes how some Windows API calls, such as `GetWindowsDirectory`, behave when the program runs on a Terminal Server. It is **No** by default. In the `Settings` file it is `project.terminalServerAware`. +## Data Execution Prevention Aware (DEP) {: .dep-aware } +When set to **Yes**, the build marks the file as DEP-aware. On hardware that supports it, this protects the program against attacks that inject code while it runs. It is **No** by default. In the `Settings` file it is `project.depAware`. + ## Export > [!WARNING] @@ -148,7 +276,7 @@ The type of file the compiler creates: **Standard EXE**, **ActiveX DLL**, **Acti ### Export Path -The folder **File → Export Project** writes to. When it is set, the command exports there straight away instead of asking for a folder, and empties the folder first. +The folder **File → Export Project** writes to. When it is set, the command exports there straight away instead of asking for a folder, and empties the folder first. In the `Settings` file it is `project.exportPath`. The path can use these variables: `${SourcePath}`, the folder that holds the `.twinproj` file, and `${ProjectName}`, `${ProjectFileName}`, `${ProjectID}`, `${FileExtension}`, `${VersionMajor}`, `${VersionMinor}`, `${VersionBuild}` and `${VersionRevision}`. The dialog refuses `${SourcePath}` on its own, because the export would delete the project file, but it accepts the same folder written out in full. @@ -156,20 +284,25 @@ Every project template sets `"project.exportPathIsV2": true` in the `Settings` f ### Export After Save -When set to **Yes**, every save of the project also runs **Export Project** into *Export Path*. So every save empties that folder again, even a save with nothing changed. +When set to **Yes**, every save of the project also runs **Export Project** into *Export Path*. So every save empties that folder again, even a save with nothing changed. It is **No** by default. In the `Settings` file it is `project.exportAfterSave`. ### Export Verbose -When set to **Yes**, **Export Project** writes a line to the [Debug Console](DebugConsole) for each file and folder it deletes, `[EXPORT] DELETED: …`, and for each file it writes, `[EXPORT] DONE: …`. When set to **No**, the console shows only the line that starts the export, the `[EXPORT] COMPLETED` line that ends it, and any failure. +When set to **Yes**, **Export Project** writes a line to the [Debug Console](DebugConsole) for each file and folder it deletes, `[EXPORT] DELETED: …`, and for each file it writes, `[EXPORT] DONE: …`. When set to **No**, the console shows only the line that starts the export, the `[EXPORT] COMPLETED` line that ends it, and any failure. It is **No** by default. In the `Settings` file it is `project.exportVerbose`. ## Force DPI Awareness At Startup - {: #dpi-awareness } +How the program sets its DPI awareness as it starts: **NONE**, **SYSTEM_DPI_AWARE** or **PER_MONITOR_DPI_AWARE**. The default is **PER_MONITOR_DPI_AWARE**. The last two make the program call the `SetProcessDpiAwareness` API, where Windows has it. **NONE** turns DPI awareness off; it is also the choice for a program that sets its DPI awareness itself, in a manifest. In the `Settings` file it is `project.forceDpiAwarenessAtStartup`. + ## Runtime Command Line Args +The text that [**Command$**](../../Modules/Interaction/Command) returns while the project runs in a debug session in the IDE. A new project does not set it. In the `Settings` file it is `debugger.runtimeCommandLineArguments`. + ## Immediate Memory Invalidation +When set to **Yes**, the memory of a **String**, **Variant** or array is overwritten with garbage as soon as the value is released. The operating system does not usually clear freed memory until it is used again, so code that reads through a stale pointer to a released value usually still finds the old contents, and the bug shows only now and then. With this setting on, the stale pointer finds garbage, which makes the bug easier to detect. It is **No** by default, and it slows debugging slightly. In the `Settings` file it is `debugger.immediateMemoryInvalidation`. + ## Break On All Errors When set to **Yes**, a run-time error stops the program at the failing line even while an `On Error Resume Next` or `On Error GoTo` statement is in effect, with the same error panel as an error that nothing handles. It is **No** by default, and then the handler gets the error. **Debug → Debugger Options → Break On All Errors** turns the same setting on and off. In the `Settings` file it is `debugger.breakOnAllErrors`. @@ -178,28 +311,49 @@ When set to **Yes**, a run-time error stops the program at the failing line even ## Build Stack Reserve Size +The size of the stack reserved for the program, in bytes, which the build writes into the PE header of the file. The default is 1 MB, 1048576 bytes. If procedures crash at run time with stack overflow errors, try a larger value. In the `Settings` file it is `project.buildStackReserveSize`. + ## Target OS Version +The version of Windows that the built file is marked for. The build writes it into the `MajorOperatingSystemVersion`, `MinorOperatingSystemVersion`, `MajorSubsystemVersion` and `MinorSubsystemVersion` fields of the file's PE optional header. The choices run from **[v5.0] Windows 2000** to **[v10.0] Windows 10 / Windows 11 / Windows Server 2016**, and the default is **[v5.1] Windows XP**. In the `Settings` file it is `project.targetOsVersion`. + ## Codegen Model -## Strip PE File Relocation Symbols +The kind of code that the compiler generates for a built file, especially when it does not use [LLVM](../../../LLVM/Getting-Started#llvm-in-twinbasic): **SMALL** makes the file as small as it can, whatever the speed; **FAST** generates larger code, aiming to run slightly faster; **BALANCED** is between the two. The default is **BALANCED**. Only the main project's setting counts: a package uses the setting of the project that references it. In the `Settings` file it is `project.codegenModel`. +## Strip PE File Relocation Symbols {: #strip-pe-symbols } -## Enable Address Space Layout Randomization (ASLR) +Whether the build leaves the relocation data out of the file, which makes it smaller: **AUTO**, **YES** or **NO**. The default is **AUTO**, which strips the relocations from an EXE and keeps them in a DLL or OCX. **YES** is not usually right for a DLL: without its relocations, a DLL can fail to load when another DLL already uses its base address. In the `Settings` file it is `project.relocationSymbolsStripped`. +## Enable Address Space Layout Randomization (ASLR) {: #enable-aslr } -## PE File Image Base Address (Win32) +When set to **Yes**, Windows loads the EXE or DLL at a random base address. It is **Yes** by default. It works only in a file that keeps its relocation data, so with [Strip PE File Relocation Symbols](#strip-pe-symbols) at its default it applies to a DLL and not to an EXE. In the `Settings` file it is `project.addressSpaceLayoutRandomization`. +## PE File Image Base Address (Win32) {: #win32-base-address } +Overrides the image base address in the PE header of a Win32 EXE, DLL or OCX. The default is `&H400000`, for a DLL as well as for an EXE. In the `Settings` file it is `project.imageBaseAddress32`, as a decimal number. + ## PE File Image Base Address (Win64) +Overrides the image base address in the PE header of a Win64 EXE, DLL or OCX. The default is `&H140000000`, for a DLL as well as for an EXE. In the `Settings` file it is `project.imageBaseAddress64`, as a decimal number. + ## Debuggable +When set to **Yes**, breakpoints can be set in the project's procedures, and stepping into them works as usual. It is **Yes** by default. **No** is mainly useful in a package, to keep the projects that use the package from stepping into its code. The [**Debuggable**](../../Core/Attributes#debuggable) attribute changes it for a module, a class or a procedure. In the `Settings` file it is `project.debuggable`. + ## Feature Flags +Project features that can be turned off, to make the built file smaller. The dialog shows them as two settings, **Feature Flags** and **Feature Flags - continued**, with a box for each feature, and every box is ticked by default. + +**Feature Flags** has these: PictureBox Control, Label Control, TextBox Control, Frame Control, Multiframe Control (which needs Frame Control as well), CommandButton Control, Checkbox Control, OptionButton Control, ComboBox Control, ListBox Control, HScrollBar Control, VScrollBar Control, Timer Control, DriveListBox Control, DirListBox Control, FileListBox Control, Line Control, Shape Control, Image Control, CheckMark Control, QRCode Control, Data Control (which needs Data Bindings as well), Data Bindings (all controls), OLE Control, MDI Forms support, PropertyPages support, Reports support, Menus support, OLE Drag-Drop support, Printers support, Help CHM support for controls, and ActiveX and UserControls. + +**Feature Flags - continued** has these: Manual drawing on container controls, DTPicker Control, ImageList Control, ListView Control, MonthView Control, ProgressBar Control, Slider Control, TreeView Control, UpDown Control, Compress Runtime Class Dispatch Info, Compress Runtime Error Tables, Compress Misc Data, Buttons support Graphical style, and Runtime PNG support (via Global.LoadPicture). + +In the `Settings` file they are `project.featureFlagsUI` and `project.featureFlagsUI2`. + ## Compiler Options (BUILD) Turns on [LLVM compilation](../../../LLVM/Getting-Started#llvm-in-twinbasic) and its optimizations for the executable a build produces. @@ -207,4 +361,3 @@ Turns on [LLVM compilation](../../../LLVM/Getting-Started#llvm-in-twinbasic) and ## Compiler Options (DEBUG) Turns on [LLVM compilation](../../../LLVM/Getting-Started#llvm-in-twinbasic) and its optimizations when the project runs in the IDE. This is not recommended: the IDE cannot debug code compiled with LLVM. - diff --git a/docs/IDE/Toolbar.md b/docs/IDE/Toolbar.md index e48b4d92..f3443ef9 100644 --- a/docs/IDE/Toolbar.md +++ b/docs/IDE/Toolbar.md @@ -13,7 +13,7 @@ permalink: /tB/IDE/Project/Toolbar ![The same toolbar while the project runs. Start has gone grey, Break is blue and Stop is a red square; the red form and code button and the 100% zoom box are unchanged.](Images/Toolbar_4.png) - Save All Changes (CTRL + S) -- Find In Project... (CTRL + SHIFT + F) (CTRL + ⇧ + F) +- Find In Project... (CTRL + SHIFT + F) - Switch Between Form And Code - Undo - Redo diff --git a/docs/LLVM/Getting-Started.md b/docs/LLVM/Getting-Started.md index c8023d57..087cadca 100644 --- a/docs/LLVM/Getting-Started.md +++ b/docs/LLVM/Getting-Started.md @@ -32,7 +32,7 @@ The same options appear in two sections: > [!NOTE] > The IDE cannot debug code compiled with LLVM, so turning LLVM on under **Compiler Options (DEBUG)** is not recommended. -**Enable LLVM Compilation** turns LLVM on. With only this box ticked, LLVM runs during compilation and creates the intermediate representation of your code, but does not apply the optimizations for performance and size. You will usually want to tick one or both of the next two options as well: **LLVM: Generate optimized code** and **LLVM: Optimize for smaller filesize**. *Generate optimized code* is for optimizing for speed. This is not mutually exclusive with *Optimize for smaller filesize*, and when both are enabled, LLVM determines if an optimization doesn't provide a significant enough benefit to be worth the size increase.| +**Enable LLVM Compilation** turns LLVM on. With only this box ticked, LLVM runs during compilation and creates the intermediate representation of your code, but does not apply the optimizations for performance and size. You will usually want to tick one or both of the next two options as well: **LLVM: Generate optimized code** and **LLVM: Optimize for smaller filesize**. *Generate optimized code* is for optimizing for speed. This is not mutually exclusive with *Optimize for smaller filesize*, and when both are enabled, LLVM determines if an optimization doesn't provide a significant enough benefit to be worth the size increase. The remaining options, from **LLVM: Target CPUs with AES** to **LLVM: Target CPUs with XSAVES**, are for CPU features that not every CPU has. They range from features that almost every CPU from the last 25 years has, to features that only recent CPUs have. [CPU feature availability](#cpu-feature-availability) below gives an overview. diff --git a/docs/Reference/Built-In/tbIDE/index.md b/docs/Reference/Built-In/tbIDE/index.md index 215b2ec9..f9b4c3f2 100644 --- a/docs/Reference/Built-In/tbIDE/index.md +++ b/docs/Reference/Built-In/tbIDE/index.md @@ -11,7 +11,7 @@ has_toc: false The **tbIDE** package is the **addin SDK** for the twinBASIC IDE. An addin is a Standard DLL that the IDE loads at start-up; the DLL exports one factory function, returns one object implementing the [**AddIn**](AddIn) contract, and from there everything happens through the [**Host**](Host) object the IDE passes in. The package itself is **type-only** --- every public symbol is an interface or a CoClass; the actual implementations live in the twinBASIC IDE binary, and the addin DLL binds against the type declarations and lets the IDE marshal calls into its implementations at run time. -The package is a built-in *compiler* package shipped with twinBASIC. It is added to addin projects automatically; there is no need to add it manually through Project → References. +The package is a built-in *compiler* package shipped with twinBASIC. The addin samples 10 to 16 reference it already, but a project started from the **Standard DLL** template does not, and without the reference every name in the package --- `AddIn`, `Host` and the rest --- is *TB5079 Unrecognized datatype symbol*. Add it through Project → References (**Ctrl-T**) → Available Packages: tick the row **twinBASIC - IDE Extensibility Package**, marked **[BUILT-IN]**, whose library symbol is **tbIDE**, and press **Apply Changes**. * TOC {:toc} @@ -21,7 +21,7 @@ The package is a built-in *compiler* package shipped with twinBASIC. It is added An addin project has three distinguishing settings: - **Build type:** Standard DLL. -- **Build path:** `${IdePath}\addins\${Architecture}\${ProjectName}.${FileExtension}`. The output drops directly into the IDE's `addins\Win32\` or `addins\Win64\` folder, where the IDE scans for addins on start-up. It also scans the same two folders under `%APPDATA%\twinBASIC\addins\`, which an IDE update leaves in place; see [Add Ins](../../IDE/AddIns/). +- **Build path:** `${IdePath}\addins\${Architecture}\${ProjectName}.${FileExtension}`. The output drops directly into the IDE's `addins\Win32\` or `addins\Win64\` folder, where the IDE scans for addins on start-up. It also scans the same two folders under `%APPDATA%\twinBASIC\addins\`, which an IDE update leaves in place; see [Add Ins](../../IDE/AddIns/). Once the IDE has loaded the addin, this path can no longer be built to; see [Rebuilding an addin the IDE has loaded](#rebuilding-an-addin-the-ide-has-loaded). - **Compiler-package reference** to **tbIDE** (added to the project's references with `isCompilerPackage: true`, `publisher: TWINBASIC-COMPILER`, `symbolId: tbIDE`). This is the binding between the DLL's compile-time types and the IDE's run-time implementations. The DLL must export one function --- the entry point the IDE calls when it discovers and loads the addin: @@ -66,6 +66,22 @@ End Class The `WithEvents Host As Host` pattern is how the addin subscribes to IDE lifecycle events ([**OnProjectLoaded**](Host#onprojectloaded), [**OnChangedActiveEditor**](Host#onchangedactiveeditor), [**OnChangedTheme**](Host#onchangedtheme)). Almost every meaningful addin sets up its toolbar buttons and tool windows inside the [**OnProjectLoaded**](Host#onprojectloaded) handler --- that is the first moment the IDE is fully ready to accept extensibility commands. +## Rebuilding an addin the IDE has loaded + +The build path above writes the DLL into the IDE's own `addins` folder, and the IDE loads it the next time it starts. From then on the IDE's compiler keeps the file open, and building the addin again in that IDE fails. The build log reads: + +```text +[LINKER] FAILED to create output file '...\addins\win32\MyAddIn.dll' (error code 32) +``` + +and then names the process that holds the file, the IDE's own compiler, `twinBASIC_win32_noDEP.exe`. Error code 32 is Windows' *file in use*. To change an addin and build it again, build it somewhere else and copy it in while the IDE is closed: + +1. Set the addin project's [Build Output Path](../../IDE/Project/Settings#build-output-path) to `${SourcePath}\Build\${ProjectName}_${Architecture}.${FileExtension}`, the path the other project templates use. +2. Build. The DLL goes into a `Build` folder beside the `.twinproj`, as `MyAddIn_win32.dll`. +3. Close the IDE, copy the new DLL over the old one in the installation's `addins\win32` folder, keeping the old one's name, and start the IDE again. It loads the new build. + +Keep one copy of the addin in the folder: the IDE loads every DLL in it, so a second copy under another name loads as a second addin. + ## The class catalogue The package's twenty-four `.twin` files declare one interface-and-CoClass pair each (plus one concrete `Class`), grouped here by role for orientation. Every CoClass except [**AddinTimer**](AddinTimer) is **supplied to the addin by the IDE** --- never instantiated with `New`. diff --git a/docs/Reference/Compiler Constants.md b/docs/Reference/Compiler Constants.md index 33dd024a..7801e123 100644 --- a/docs/Reference/Compiler Constants.md +++ b/docs/Reference/Compiler Constants.md @@ -5,55 +5,63 @@ nav_order: 5 permalink: /Reference/Compiler-Constants --- -This is a guide to the built in compiler constants in twinBASIC. It includes the constants listed for VBA in its documentation even if they're not defined, as an undefined compiler constant can always be used, but will be 0. +# Compiler Constants +{: .no_toc } -## `Win16` +The constants twinBASIC predefines for conditional compilation, and how to test them with `#If`. + +The list includes the constants that VBA documents, even those twinBASIC does not define: an undefined compiler constant can always be used, and its value is 0. + +## Predefined constants + +### `Win16` **Purpose:** Indicates a 16-bit Windows compatible platform.\ -**Value:** Always 0 (False); 16 bit Windows is not supported. +**Value:** Always 0 (False); 16-bit Windows is not supported. -## `Win32` +### `Win32` -**Purpose:** Indicates a 32bit compatible Windows platform\ -**Value:** Always 1 (True) on supported Windows platforms, for both 32bit and 64bit. +**Purpose:** Indicates a 32-bit compatible Windows platform.\ +**Value:** Always 1 (True) on supported Windows platforms, for both 32-bit and 64-bit. -## `Win64` +### `Win64` -**Purpose:** Indicates a 64bit Windows AMD64 platform.\ -**Value:** 0 (False) when the compiler is in 32bit mode, 1 (True) when in 64bit mode. +**Purpose:** Indicates a 64-bit Windows AMD64 platform.\ +**Value:** 0 (False) when the compiler is in 32-bit mode, 1 (True) when in 64-bit mode. -## `VBA6` +### `VBA6` **Purpose:** Indicates compatibility with VBA6 syntax.\ **Value:** Always 1 (True). -## `VBA7` +### `VBA7` **Purpose:** Indicates compatibility with VBA7 syntax.\ **Value:** Always 1 (True). -## `MAC` +### `MAC` + **Purpose:** Indicates running on a MacOS platform.\ **Value:** Always 0 (False). Mac is not currently supported, although this will change in the future. -## `TWINBASIC` +### `TWINBASIC` **Purpose:** Indicates compatibility with twinBASIC syntax.\ **Value:** Always 1 (True). -## `TWINBASIC_BUILD` +### `TWINBASIC_BUILD` **Purpose:** Provides a `Long` value giving the current twinBASIC Build Number.\ **Value:** Currently this is the same as the "BETA" number, e.g. for Beta 610 it will have a value of 610. -## `TWINBASIC_BUILD_TYPE` +### `TWINBASIC_BUILD_TYPE` + **Purpose:** Allows conditional compilation based on whether the project is an exe, dll, or ocx.\ **Value:** A `String` that can be one of "Standard EXE", "Standard DLL", "ActiveX DLL", or "ActiveX Control", determined by the "Build Type" option in Project Settings. +## Usage -# Usage - -Usage of these follows the standard syntax of using a hashtag before the standard `If/Else/ElseIf` conditionals. For example, to differentiate between 32bit and 64bit VBA vs 64bit twinBASIC, +A compiler constant is tested with `#If`, `#ElseIf` and `#Else`: the `If`, `ElseIf` and `Else` keywords with a `#` in front. For example, to tell 32-bit and 64-bit VBA apart from 64-bit twinBASIC: ```tb check_build #If VBA7 Then @@ -81,7 +89,7 @@ Usage of these follows the standard syntax of using a hashtag before the standar #End If ``` -Or more simply, to determine whether to use `PtrSafe` then `DeclareWide` or other tB features: +Or more simply, to decide whether to use `PtrSafe`, and then `DeclareWide` or other twinBASIC features: ```tb check_build #If VBA7 Then @@ -96,17 +104,16 @@ Or more simply, to determine whether to use `PtrSafe` then `DeclareWide` or othe ``` > [!IMPORTANT] -> Reminder: Compiler Constants are not `Boolean` values, so you shouuldn't use syntax like `#If Not Win64 Then` as the result may not be desired, for instance that example evaluates to `True` for both 32bit and 64bit modes when you likely used it expecting `False` under 64bit to use 32bit-only code.\ -If you wish to treat these as `Boolean`, you can use the `CBool()` function, e.g. `#If Not CBool(Win64) Then`. +> Compiler constants are not `Boolean` values, so a test such as `#If Not Win64 Then` does not do what it appears to. It is `True` in both 32-bit and 64-bit mode, where the intent is usually `False` under 64-bit, to select 32-bit-only code. To treat a constant as a `Boolean`, convert it with `CBool()`, as in `#If Not CBool(Win64) Then`. -# Appearance +## Appearance -The tB editor has the helpful feature of showing you in real time which compiler constants are active. Code in `#If` blocks is inactive and will appear grayed out if it will not execute under current settings. Note that unlike VBx, inactive code is not evaluated for errors. +The twinBASIC editor shows in real time which compiler constants are active. Code in an `#If` block that will not run under the current settings is inactive, and appears greyed out. Unlike VBx, twinBASIC does not check inactive code for errors. -For example, in 32bit mode:\ +For example, in 32-bit mode:\ ![The editor in win32 mode, with the declares in the Win64 branch greyed out and those in the Else branch active](Images/oHpCiV1.png) -Then switching to 64bit mode:\ +Then after switching to 64-bit mode:\ ![The same code in win64 mode, with the Win64 branch now active and the Else branch greyed out](Images/TYizrRW.png) diff --git a/docs/Tutorials/Testing-with-Assert.md b/docs/Tutorials/Testing-with-Assert.md index 626a0172..46ad3984 100644 --- a/docs/Tutorials/Testing-with-Assert.md +++ b/docs/Tutorials/Testing-with-Assert.md @@ -36,7 +36,7 @@ An assertion that holds does nothing visible. One that fails stops the run on it ## Adding the package -Open **Project → References** (Ctrl+T) → **Available Packages** and tick **Assert**. Click **OK**. The three modules (`Exact`, `Strict`, `Permissive`) are now available, and every call names both the package and the module: `Assert.Exact.AreEqual`, never `Exact.AreEqual` or `AreEqual` alone, which do not compile. [Calling convention](../tB/Packages/Assert/#calling-convention) explains why. +Open **Project → References** (Ctrl+T) → **Available Packages** and tick **twinBASIC - Unit Testing Package**, the row whose library symbol is **Assert**. Press **Apply Changes**. The three modules (`Exact`, `Strict`, `Permissive`) are now available, and every call names both the package and the module: `Assert.Exact.AreEqual`, never `Exact.AreEqual` or `AreEqual` alone, which do not compile. [Calling convention](../tB/Packages/Assert/#calling-convention) explains why. ## The function under test diff --git a/docs/Tutorials/Windows-API.md b/docs/Tutorials/Windows-API.md index f6c338fe..83fcc62b 100644 --- a/docs/Tutorials/Windows-API.md +++ b/docs/Tutorials/Windows-API.md @@ -157,7 +157,13 @@ For `GetCursorPos` the distinction does not arise because all its types are conc | `BOOL` | **Long** | Always 32-bit | | `INT`, `int` | **Long** | Always 32-bit | -A Declare that uses `Long` for a handle type compiles and runs in 32-bit mode but fails or crashes in 64-bit mode because a 64-bit handle does not fit in 4 bytes. Always use `LongPtr` for handle and pointer parameters. +In a 32-bit build **LongPtr** and `Long` are the same size, so a Declare that uses `Long` for a pointer compiles and runs. In a 64-bit build the same code goes wrong in three ways: + +- A **LongPtr** passed where the Declare says `Long` does not compile: *TB5001 cannot coerce type 'LongLong' to 'Long'*. Converting it with `CLng`, as the message suggests, compiles, and raises error 6, *Overflow*, when the address is above 2 GB, as `StrPtr` of a string was in the test. +- A pointer or module handle returned `As Long` loses its upper half, with no error. `GetModuleHandleW(0)` returned `&H7FF6436A0000` declared `As LongPtr` and `&H436A0000` declared `As Long`, and `GetModuleFileNameW` given the `Long` failed with error 126, *module not found*. +- A window handle and a kernel handle, such as an event's, fitted in a `Long` in the same test and worked, because their values fitted in 32 bits. The Windows API declares both pointer-sized. + +So declare every handle and pointer **LongPtr**, as the table says. ### Example: GetForegroundWindow diff --git a/docs/_book.yml b/docs/_book.yml index ee62de20..d71f9245 100644 --- a/docs/_book.yml +++ b/docs/_book.yml @@ -112,6 +112,14 @@ # and common entry options above. No chapter-specific # options exist beyond those shared with parts. # +# ── left_out ────────────────────────────────────────────────────────── +# The pages deliberately not in the book. Each entry uses the selector +# schema above plus a `reason:`. Every page must be selected by a book +# entry or by a left_out entry: the build warns about a page that is in +# neither, about a page that is in both, and about an entry here that +# matches no page. So a new page cannot drop out of the book unnoticed, +# and a renamed one cannot leave a stale entry behind. +# # ── Sort order ──────────────────────────────────────────────────────── # Per-entry content lists are ordered by sort_by_nav_order: folder- # style index pages first (URL ends in `/`), then nav_order pages by @@ -199,6 +207,7 @@ parts: - title: Tutorials subtitle: The foundations, then worked code examples for Arrays, CEF, WebView2, and CustomControls + landing_page: /Tutorials/ outline_closed: true # DONE chapters: @@ -244,6 +253,54 @@ parts: no_heading_shift: true outline_closed: true + - title: The twinBASIC IDE + subtitle: Starting, configuring, building, and debugging a project + # Only the IDE pages with prose of their own. The rest of the section + # is in left_out at the end of this file; move a page from there to + # here once it explains something. Links to the pages left out open + # the website. + landing_page: /tB/IDE + outline_closed: true + chapters: + - title: New Project + page: /tB/IDE/Project/New + no_outline_entry: true + no_heading_shift: true + outline_closed: true + - title: Project Settings + page: /tB/IDE/Project/Settings + no_outline_entry: true + no_heading_shift: true + outline_closed: true + - title: File Menu + page: /tB/IDE/Project/Menu/File + no_outline_entry: true + no_heading_shift: true + outline_closed: true + - title: Toolbar + page: /tB/IDE/Project/Toolbar + no_outline_entry: true + no_heading_shift: true + outline_closed: true + - title: Debug Menu + page: /tB/IDE/Project/Menu/Debug + no_outline_entry: true + no_heading_shift: true + outline_closed: true + - title: Debugging Panes + pages: + - /tB/IDE/Project/DebugConsole + - /tB/IDE/Project/CallStack + - /tB/IDE/Project/Variables + - /tB/IDE/Project/Memory + outline_closed: true + - title: Add Ins + page: /tB/IDE/AddIns/ + no_descent: true + no_outline_entry: true + no_heading_shift: true + outline_closed: true + - title: The Core Language subtitle: Statements, operators, and built-in keywords outline_closed: true @@ -268,8 +325,15 @@ parts: - title: Operators nav_page: Reference Section/Operators outline_closed: true + - title: Data Types + page: /Reference/Data-Types + no_outline_entry: true + no_heading_shift: true + outline_closed: true - title: Compiler Constants page: /Reference/Compiler-Constants + no_outline_entry: true + no_heading_shift: true outline_closed: true - title: Attributes page: /tB/Core/Attributes @@ -279,12 +343,18 @@ parts: - title: Reference Section - subtitle: Controls and the project glossary + subtitle: Controls, enumerations, twinBASIC's additions, and the glossary outline_closed: true chapters: - title: Controls landing_page: /tB/Controls outline_closed: true + - title: Enumerations + landing_page: /Reference/Enumerations + outline_closed: true + - title: twinBASIC Additions + landing_page: /Reference/twinBASIC-Additions + outline_closed: true - title: Glossary landing_page: /tB/Gloss outline_closed: true @@ -414,4 +484,55 @@ parts: landing_page: /Documentation/Development/ page: /Documentation/Development/ landing_is_target: true - outline_closed: true \ No newline at end of file + outline_closed: true + +left_out: + - reason: The site's error page + page: /404.html + no_descent: true + + - reason: The Reference Section's index; the book's own parts and outline replace it + page: /Reference + no_descent: true + + - reason: Links to video recordings, with nothing to read in print + page: /Videos + + - reason: Time-limited community contests + page: /Challenges + + - reason: IDE pages that are still screenshots and the labels they show, or headings with nothing under them + no_descent: true + pages: + - /tB/IDE/Project/Diagnostics + - /tB/IDE/Project/FindReplace + - /tB/IDE/Project/Editor + - /tB/IDE/Project/Editor/Form + - /tB/IDE/Project/Explorer + - /tB/IDE/AddIns/Community + - /tB/IDE/Project/Menu + - /tB/IDE/Project/Menu/Edit + - /tB/IDE/Project/Menu/View + - /tB/IDE/Project/Menu/Project + - /tB/IDE/Project/Menu/Format + - /tB/IDE/Project/Menu/Run + - /tB/IDE/Project/Menu/AddIns + - /tB/IDE/Project/Menu/Help + - /tB/IDE/Project/Menu/Tools # one real line, under a visible TODO note + + - reason: IDE pages that are complete but too short, or too much a list, to earn space in print yet + no_descent: true + pages: + - /tB/IDE/Project/StatusBar + - /tB/IDE/Project/Toolbox + - /tB/IDE/Project/Editor/Report + - /tB/IDE/Project/Properties + - /tB/IDE/Project/Webpage + - /tB/IDE/Project/Splash + - /tB/IDE/Project/Watches + - /tB/IDE/Project/Outline + - /tB/IDE/Project/History + - /tB/IDE/Project/OpenEditors + - /tB/IDE/Project/PackagePublishing + - /tB/IDE/AddIns/GlobalSearch + - /tB/IDE/Project/Menu/Window # ~800 lines of shortcut JSON that would print unfolded diff --git a/eval/README.md b/eval/README.md index ef1f9117..0d86c47a 100644 --- a/eval/README.md +++ b/eval/README.md @@ -165,6 +165,12 @@ was wrong: UC-56's rank-1 query and its one-hop README link both check out. But discoverability score has to rest on what can be checked, the ranks and the links, and never on the report's word for how the answer was reached. +**A case with no site search at all lost Channel 1 to the harness, not to the pages: run it +again.** The digest flags it as `no site search at all`. In round 11, two evaluators chained +`site-search` after a `cd`, were refused, and concluded that the search box was not +available; six others made the same first call and retried the bare command. The protocol +now says to run it on its own, and both re-runs searched first. + ## Rounds so far | round | cases | outcome | @@ -179,6 +185,7 @@ never on the report's word for how the answer was reached. | 8 | 13 --- the first isolated evaluators: round 7's re-runs, round 1's four lowest, three new | [builder/REVIEW-USECASES-5b4cd37.md](../builder/REVIEW-USECASES-5b4cd37.md); 29 findings, no set hazard walked into, and the four most serious found by probing the product: an IDE export that empties a Git repository, constructors that fail in silence, error numbers that are not VBA's, and a debugger Stop that stops one procedure and so turns a failed unit test into a pass | | 9 | 11 --- round 8's seven re-runs, and four new site cases, four of the eleven executed | [builder/REVIEW-USECASES-d4b37ec.md](../builder/REVIEW-USECASES-d4b37ec.md); 12 findings, the re-runs' discoverability +1.00 and round 8's own queries from 2 hits of 14 to 11, and an IDE export the tB executable cannot pack back into a project | | 10 | 9 --- round 9's five re-runs, a fresh clone, and three new surfaces; five executed, and a DLL built and called | [builder/REVIEW-USECASES-16969e5.md](../builder/REVIEW-USECASES-16969e5.md); 17 findings: round 9's own warning closed the IDE route to Git, a twinBASIC DLL called from VBA fails three ways no page named, a delegate's signature is only a warning, and four unindexed API names froze the site's search box | +| 11 | 9 --- round 10's five re-runs and UC-65, and three new surfaces: a command-line tool, threads and an IDE add-in, all executed | [builder/REVIEW-USECASES-4a67e09.md](../builder/REVIEW-USECASES-4a67e09.md); 13 findings: the re-runs +1.00 completeness and +1.33 actionability with discoverability flat again, a console program that prints nothing and exits 0, an add-in reference the page called automatic, and two evaluators who took one refused search for no search box | Round 1's headline was a gradient: documentation quality fell monotonically with depth into the toolchain (contributor 3.8 discoverability, toolchain user 2.8, builder developer 1.8), diff --git a/eval/protocol.md b/eval/protocol.md index 70a89534..18de3f4b 100644 --- a/eval/protocol.md +++ b/eval/protocol.md @@ -29,6 +29,10 @@ logic: site-search "your query here" +Run it as a command of its own, exactly like that: it is on your PATH and works from any +directory. Chained after a `cd`, or piped into another command, it is refused, as every +other shell command is. + It prints ranked results as title + URL + snippet. A URL like `/Documentation/Development/Extending#adding-a-pipeline-task` corresponds to the corpus file `docs/Documentation/Extending.md`: to open a result, find the file whose frontmatter @@ -127,6 +131,18 @@ what it will print, then runs it: the evaluator's code verbatim in a template fr reader's click, and `scripts/tbrun.mjs`. Change one expected value for a second run, since the failure path is what the pages are least likely to have been checked against. A program that opens a `MsgBox` or waits for a user at a form cannot be run this way. +Several at once run from one process that owns the registry tidy --- `startTidy` and +`finishTidy` in `scripts/lib/tb-registry.mjs` --- so that the `tbrun` children leave the +registry alone; round 10's four did not, and left one probe project in the IDE's recent list +19 times. + +**A command-line program is run at a real command prompt, not through a pipe.** Build it +with `tbrun`, then run the `.exe` in a console of its own and read the console's screen +buffer. A program that writes with `WriteConsole` shows nothing through a pipe, and whether +its output survives redirection is one of the things such a case tests. Round 11's UC-70 +did this with a PowerShell script that attaches to a hidden `cmd.exe` console; it also +needed `.\` in front of the program's name, because the harness's environment sets +`NoDefaultCurrentDirectoryInExePath`. **Measure what an answer says the product does.** None of round 8's four most serious findings was in an evaluator's report: the IDE's Export Project emptying a Git repository, diff --git a/eval/usecases.md b/eval/usecases.md index 3bfffb1e..95920664 100644 --- a/eval/usecases.md +++ b/eval/usecases.md @@ -519,3 +519,38 @@ called from Excel VBA, built and called. | UC-67 | *(site)* I want to pass a function as an argument to another procedure, the way a callback works in other languages. Write me a routine that sorts an array of names using a comparison function it is given, and use it to sort the same few names twice --- alphabetically, and by length --- printing the names each time. I want the finished code exactly as I'd have it in my project, and what it prints. | executed. None known; no case has read the Delegates page | | UC-68 | *(site)* I need to write a text file in UTF-8 that holds names with accents and non-Latin letters --- Zoë, Łódź and 東京 --- and read it back later. Write me a routine that writes those three names to a UTF-8 file, one per line, then reads the file back line by line and prints each line and how many characters it has. I want the finished code exactly as I'd have it in my project, and what it prints. | executed. None known; no case has read File I/O | | UC-69 | *(site)* Some of my Excel VBA code is slow, and I'd like to move it into a DLL built with twinBASIC and call it from VBA. Get me a working example: a twinBASIC DLL with one function that adds two numbers and one that takes a name and returns a greeting such as "Hello, Ann", and the VBA declarations and a macro that calls both and prints the results. I want both sides exactly as I'd have them, the steps to build the DLL, and what the macro prints. | built and called, with a twinBASIC caller standing in for VBA. None known; no case has read Project Types | + +## Round 11 --- the re-runs round 10 named, and three surfaces no case has read + +**Run 2026-09-24 at `4a67e09`** --- [builder/REVIEW-USECASES-4a67e09.md](../builder/REVIEW-USECASES-4a67e09.md). +Round 10's last commit, with its fixes in. One session, every case in parallel through +`eval/run_case.mjs`, on the model and Claude Code build of rounds 8--10: `claude-sonnet-5` +through the desktop app's bundled 2.1.280, passed with `--claude`. + +**Re-run the five round 10 named**: UC-62 and UC-66 against *Keeping a project in Git from +the IDE*, UC-69 against *Calling a Standard DLL from VBA or Excel*, UC-67 against the callback +section, and UC-55 against the single export command; and UC-65 against the `Max` sample made +a whole file. Goals verbatim, each on the site protocol it was first run under. **Also re-run +round 10's own queries** for those cases against this round's index. + +**Open three surfaces no case has read**, each executed: a command-line tool with an exit +code, built and run at a command prompt; two calculations on threads; and an IDE add-in, built, +loaded and clicked in a private copy of the install by the add-in test runner. + +**UC-70 and UC-71 ran twice.** Their first evaluators chained `site-search` after a `cd`, were +refused, and concluded that the search box was not available, so neither ran Channel 1. The +protocol now says to run the command on its own, and the second runs are the ones scored. + +### Persona D: a twinBASIC developer on the published site + +| id | goal | hazard | +|----|------|--------| +| UC-55 | *(site, re-run)* The goal of round 7, verbatim. | **H** a second export without `--overwrite` leaves every changed file stale; step 2 now gives one command for every export | +| UC-62 | *(site, re-run)* The goal of round 9, verbatim. | **H** Export Project empties its folder; round 9's warning read as closing the IDE route, and *Keeping a project in Git from the IDE* was written for this | +| UC-65 | *(site, re-run)* The goal of round 9, verbatim. | executed. The `Max` sample is now a whole file | +| UC-66 | *(site, re-run)* The goal of round 10, verbatim. | **H** an export that holds the compiler packages rebuilds into a project with a dead copy of them; the section now says to delete them | +| UC-67 | *(site, re-run)* The goal of round 10, verbatim. | executed. *Passing a function as an argument or parameter (callbacks)* was written for this | +| UC-69 | *(site, re-run)* The goal of round 10, verbatim. | built and called, from a win32 and a win64 twinBASIC caller. **H** strings, bitness and the file's name; *Calling a Standard DLL from VBA or Excel* was written for this | +| UC-70 | *(site)* I want to write a small command-line tool in twinBASIC. Run as `linecount `, it prints how many lines the file has. Run with no argument, or with a file that does not exist, it prints what went wrong and exits with exit code 1, so that a batch file can test `errorlevel`. Get me the finished code exactly as I'd have it in my project, the steps to build it into an .exe, and exactly what it prints for a file of three lines and for a file that does not exist. | built, and run at a command prompt in a console window, redirected and piped. None known; *Console Applications* is one paragraph | +| UC-71 | *(site)* I have two slow calculations that don't depend on each other, and I want twinBASIC to run them at the same time on two separate threads, wait until both have finished, and then print both results. As the two calculations, use counting the prime numbers below 200,000 and counting how many of the numbers from 1 to 5,000,000 are divisible by 7 or by 11. I want the finished code exactly as I'd have it in my project, and what it prints. | executed. None known; no case has read Multithreading | +| UC-72 | *(site)* I want to write my own add-in for the twinBASIC IDE: a button on the IDE's toolbar that, when clicked, inserts a comment line holding today's date, such as `' 2026-09-24`, at the cursor in the code editor I'm working in. Get me the finished add-in project exactly as I'd have it, the steps to build it and get the IDE to load it, and what happens when I click the button. | built, loaded and clicked in a private copy of the install. None known; no case has read the tbIDE package | diff --git a/scripts/check_book_coverage.mjs b/scripts/check_book_coverage.mjs new file mode 100644 index 00000000..c20feb6a --- /dev/null +++ b/scripts/check_book_coverage.mjs @@ -0,0 +1,170 @@ +// Self-test for the book-coverage warnings. +// +// builder/book.mjs's bookCoverage() holds every page against _book.yml: a +// page must be selected by a book entry or named in `left_out:`, and every +// entry must still match a page. On a consistent manifest it reports +// nothing, so an ordinary build says exactly what a check that had stopped +// working would say. These probes make the other assertion -- that each of +// its five findings still fires -- and that the pages the book emits by a +// route other than a selector (a chaptered part's landing, a foreword, the +// book page itself) are never reported as missing. +// +// The pages and the manifest are built here in memory, not read from docs/, +// so the probes mean the same thing against an empty tree. That is what puts +// this in test.bat rather than check.bat. +// +// Before the warnings existed, a page no entry selected was left out of the +// PDF without a word: the IDE, Challenges and Videos sections, and Data +// Types, Enumerations and twinBASIC Additions, all went missing that way. +// +// node scripts/check_book_coverage.mjs + +import { resolveBookChapters, bookCoverage, formatBookCoverage } from "../builder/book.mjs"; + +// A crash is the harness failing, not a finding: exit 2, as Extending.md's gate +// conventions require. This file runs at top level, so there is no main().catch +// to do it; the handler also catches a rejected top-level await. +process.on("uncaughtException", (err) => { console.error(err); process.exit(2); }); + +const page = (srcRel, permalink, title, frontmatter = {}) => ({ + srcRel, permalink, navPath: title, frontmatter: { title, permalink, ...frontmatter }, +}); + +// Every way into the book, and both ways out of it. +const PAGES = [ + page("index.md", "/", "Welcome"), // front_matter + page("Guide/index.md", "/Guide/", "Guide"), // flat part: landing ... + page("Guide/One.md", "/Guide/One", "One"), // ... and its prefix sweep + page("Guide/Two.md", "/Guide/Two", "Two"), + page("Ref/index.md", "/Ref/", "Reference"), // chaptered part: landing + page("Ref/Intro.md", "/Ref/Intro", "Intro"), // ... foreword + page("Ref/Alpha.md", "/Ref/Alpha", "Alpha"), // ... a chapter + page("Ref/Beta/index.md", "/Ref/Beta/", "Beta"), // ... a chapter's landing + page("Ref/Beta/Gamma.md", "/Ref/Beta/Gamma", "Gamma"), + page("Extra/index.md", "/Extra", "Extra"), // left_out by prefix + page("Extra/Sub.md", "/Extra/Sub", "Sub"), + page("404.html", "/404.html", "Not found"), // left_out exactly + page("book.html", "/book.html", "", { layout: "book-combined" }), // the book itself +]; + +// A fresh manifest per probe: resolveBookChapters writes _chapters into it. +const manifest = () => ({ + front_matter: [{ title: "Introduction", page: "/", no_descent: true }], + parts: [ + { title: "Guide", landing_page: "/Guide/", page: "/Guide/" }, + { + title: "Reference", foreword_page: "/Ref/Intro", landing_page: "/Ref/", + chapters: [ + { title: "Alpha", page: "/Ref/Alpha" }, + { title: "Beta", landing_page: "/Ref/Beta/", page: "/Ref/Beta/" }, + ], + }, + ], + left_out: [ + { reason: "Not book material", page: "/Extra" }, + { reason: "The error page", page: "/404.html", no_descent: true }, + ], +}); + +function coverage(mutate = () => {}) { + const book = manifest(); + const pages = [...PAGES]; + mutate(book, pages); + resolveBookChapters(book, pages); + const c = bookCoverage(book, pages); + return { c, text: formatBookCoverage(c).join("\n") }; +} + +const KINDS = ["unlisted", "both", "emptyEntries", "emptyLeftOut", "missingUrls"]; +const counts = (c) => KINDS.map(k => `${k}=${c[k].length}`).join(" "); + +let failures = 0; +const results = []; + +function check(name, ok, detail) { + results.push({ name, ok, detail }); + if (!ok) failures++; +} + +// Exactly the findings `expect` names, as {kind: n}, and none of any other kind. +function only(c, expect) { + return KINDS.every(k => c[k].length === (expect[k] ?? 0)); +} + +// --- a consistent manifest is quiet, including the pages no selector names --- + +{ + const { c, text } = coverage(); + check("a consistent manifest reports nothing", only(c, {}) && text === "", counts(c)); + const unlisted = new Set(c.unlisted.map(p => p.permalink)); + check("a chaptered part's landing counts as in the book", !unlisted.has("/Ref/"), counts(c)); + check("a part's foreword counts as in the book", !unlisted.has("/Ref/Intro"), counts(c)); + check("the book page itself is never reported", !unlisted.has("/book.html"), counts(c)); +} + +// --- each finding fires, alone ------------------------------------------------ + +{ + const { c, text } = coverage((_, pages) => pages.push(page("New/Page.md", "/New/Page", "Page"))); + check("a page no entry mentions is reported, with the remedy", + only(c, { unlisted: 1 }) && text.includes("New/Page.md") && text.includes("/New/Page") && + text.includes("left_out"), text || counts(c)); +} +{ + const { c, text } = coverage((book) => + book.left_out.push({ reason: "Probe", page: "/Guide/One", no_descent: true })); + check("a page both in the book and in left_out is reported", + only(c, { both: 1 }) && text.includes("Guide/One.md"), text || counts(c)); +} +{ + const { c, text } = coverage((book) => + book.left_out.push({ reason: "Renamed since", page: "/Gone", no_descent: true })); + check("a left_out entry matching no page is reported by its reason", + only(c, { emptyLeftOut: 1 }) && text.includes("Renamed since"), text || counts(c)); +} +{ + const { c, text } = coverage((book) => + book.parts[1].chapters.push({ title: "Empty chapter", page: "/Nothing" })); + check("a chapter selecting no page is reported", + only(c, { emptyEntries: 1 }) && text.includes("Empty chapter"), text || counts(c)); +} +{ + const { c, text } = coverage((book) => + book.parts.push({ title: "Empty part", page: "/Nothing" })); + check("a flat part selecting no page is reported", + only(c, { emptyEntries: 1 }) && text.includes("Empty part"), text || counts(c)); +} +{ + const { c, text } = coverage((book) => + book.front_matter.push({ title: "Empty front matter", page: "/Nothing", no_descent: true })); + check("a front_matter entry selecting no page is reported", + only(c, { emptyEntries: 1 }) && text.includes("Empty front matter"), text || counts(c)); +} +{ + const { c, text } = coverage((book) => + book.parts[1].chapters.push({ title: "Typo", landing_page: "/Ref/Typo/", page: "/Ref/Alpha" })); + check("a landing_page naming no page is reported", + only(c, { missingUrls: 1 }) && text.includes("/Ref/Typo/"), text || counts(c)); +} +{ + // The foreword's page loses its only selector, so it is reported too: + // both findings are the truth, and the probe asserts both. + const { c, text } = coverage((book) => { book.parts[1].foreword_page = "/Ref/Missing"; }); + check("a foreword_page naming no page is reported, and the page it meant too", + only(c, { missingUrls: 1, unlisted: 1 }) && text.includes("/Ref/Missing") && + text.includes("Ref/Intro.md"), text || counts(c)); +} + +// --- report ------------------------------------------------------------------ + +for (const { name, ok, detail } of results) { + console.log(` ${ok ? "ok " : "FAIL"} ${name}`); + if (!ok && detail) console.log(` ${detail.replaceAll("\n", "\n ")}`); +} +console.log( + failures + ? `check_book_coverage: ${failures} of ${results.length} probes failed -- ` + + `bookCoverage() in builder/book.mjs no longer reports what the probe names` + : `check_book_coverage: ${results.length} probes, all pass` +); +process.exit(failures ? 1 : 0); diff --git a/scripts/check_links_diff.mjs b/scripts/check_links_diff.mjs index d5a33ff0..d04e0e11 100644 --- a/scripts/check_links_diff.mjs +++ b/scripts/check_links_diff.mjs @@ -37,7 +37,7 @@ // // online _site/ integrity + sitemap + search + canonical // offline _site-offline/ integrity + --forbid -// book _site-pdf/ book.html only, --no-fail, fragments +// book _site-pdf/ book.html only, --no-fail, fragments, --forbid // basepath a tree built with --baseurl, checked with the matching // --base-path. The only pass where isOutsideBasePath() and // stripBasePath() do anything. @@ -59,8 +59,8 @@ // only way the fused side can be held to it. Two cases over // one build: no single tree carries all nine categories, // because the online tree has the sitemap, search and -// canonical checks and the offline tree is the only one with -// a forbidden prefix. +// canonical checks and the offline tree has the forbidden +// prefix the online tree lacks. // // `online-abs` and `fixture` have no fused equivalent -- the fused pass // checks what the build produced, so it has nothing to say about a @@ -166,12 +166,15 @@ const CASES = { ], }, + // --forbid collects the links book.mjs sends to the website for pages + // the book leaves out; the build reports them as OUT OF BOOK. book: { - describe: "_site-pdf/book.html -- fragments only, --no-fail", + describe: "_site-pdf/book.html -- fragments + --forbid, --no-fail", fused: { tree: "pdf", baseurl: "" }, root: () => "docs/_site-pdf", argv: (root) => [ "--offline", "--no-fail", "--include-fragments", + "--forbid", "https://docs.twinbasic.com", "--root-dir", root, path.posix.join(root.replace(/\\/g, "/"), "book.html"), ], }, @@ -212,7 +215,8 @@ const CASES = { // // Two cases over one build, because no single tree carries all nine // categories: the online tree has the sitemap, search and canonical - // checks, and the offline tree is the only one with a forbidden prefix. + // checks, and the offline tree has the forbidden prefix the online tree + // lacks. "fixture-built": { describe: "a tree tbdocs built from test/fixtures/check-src -- online", fused: { tree: "online", baseurl: "", src: FIXTURE_SRC, dest: FIXTURE_TREE, offline: true }, @@ -222,7 +226,7 @@ const CASES = { }, "fixture-built-offline": { - describe: "the same build's offline tree -- the only one with --forbid", + describe: "the same build's offline tree -- the one with --forbid", fused: { tree: "offline", baseurl: "", src: FIXTURE_SRC, dest: FIXTURE_TREE, offline: true }, expect: FIXTURE_BUILT_OFFLINE, root: () => `${FIXTURE_TREE}-offline`, diff --git a/test.bat b/test.bat index 17a2687d..355864a6 100644 --- a/test.bat +++ b/test.bat @@ -76,6 +76,16 @@ node scripts/check_code_regions.mjs @rem tree, no browser, ~60 ms. node scripts/check_page_baseline.mjs @if errorlevel 1 goto :fail +@rem The book-coverage warnings say nothing when every page has an entry +@rem in docs\_book.yml -- in a part, or in left_out with a reason -- which +@rem is also all a check that had stopped working would say. Until they +@rem existed, whole sections dropped out of the PDF without a word: the +@rem IDE, Challenges and Videos, and Data Types and Enumerations with them. +@rem These probes give each of the five findings a fault to report, on a +@rem manifest and pages built in memory, so they mean the same against an +@rem empty docs\. No tree, no browser, well under a second. +node scripts/check_book_coverage.mjs +@if errorlevel 1 goto :fail @rem check_a11y.mjs injects a PATCHED axe bundle (plain-color-fields, @rem -26 % on a realistic page set). The patch asserts its substitution @rem targets, so an axe-core bump fails loudly; this catches the other