diff --git a/.gitattributes b/.gitattributes index 5441e169..36c36995 100644 --- a/.gitattributes +++ b/.gitattributes @@ -3,3 +3,8 @@ # Windows checkout, say -- hands the hook to the kernel, which reads the # carriage return after #!/bin/sh as part of the interpreter's name. .githooks/* text eol=lf + +# The Workflow tool refuses a script that contains a carriage return, and the +# Wisdom extract script is passed to it by path, so a CRLF checkout of it +# cannot be run. +wisdom/extract/workflow.mjs text eol=lf diff --git a/.github/actions/run-gates/action.yml b/.github/actions/run-gates/action.yml index f09bc791..9ba154d3 100644 --- a/.github/actions/run-gates/action.yml +++ b/.github/actions/run-gates/action.yml @@ -151,6 +151,15 @@ runs: - name: Verify the twinBASIC source and attribute scanners (check_twin_parsers.mjs) shell: bash run: node scripts/check_twin_parsers.mjs + # The attribute sweep asks the compiler where every attribute is legal, and + # none of its failures announces itself: a wrong site skeleton reads as + # "every attribute is refused here", an ignored control as a recognised + # attribute, a refusal taken for an acceptance as a finding about the + # compiler. The IDE is what cannot run here, so the parts that decide what + # an answer means are probed on fixed inputs: no IDE, no tree, no install. + - name: Verify the attribute sweep's logic (check_attribute_sweep.mjs) + shell: bash + run: node scripts/check_attribute_sweep.mjs # Nothing else tests how a tool reads its command line, which is how a # value flag given no value came to be read as NaN or as the next flag. # lib/cli.mjs's probes, then each tool's recorded command-line errors, each diff --git a/BUGS-TO-REPORT.md b/BUGS-TO-REPORT.md index e1e99eb9..7493e337 100644 --- a/BUGS-TO-REPORT.md +++ b/BUGS-TO-REPORT.md @@ -1253,3 +1253,45 @@ IDE's add-in samples leaves the id out. **Observed** on 2026-09-25 with the panes probe's third button, operated by `test/addin/panes.test.mjs`, which reads `toolWindowsById` over CDP. + +## `[PopulateFrom]` with no arguments crashes the compiler + +**Build:** BETA 987 +**Severity:** the compiler process dies while the project is being parsed, which +`tbbuild` reports as a crash (its exit code 4), so a person who forgets the arguments is not +told what is missing. + +The whole reproduction is one Enum: + +``` +Public Module CrashProbe + [PopulateFrom] + Public Enum E + End Enum +End Module +``` + +The Enum's body does not matter: the same crash comes with a member in it, and with the +Enum inside a Class instead of a Module. The documented shape is five strings, +`("json", "/Resources/PROBE/Strings.json", "events", "name", "id")`, and the other wrong +shapes tried are handled: + +| argument list | result | +|---|---| +| none, `[PopulateFrom]` | **the compiler crashes** | +| `(True)`, `(False)`, `(1)` | TB5155 `This attribute is not supported in this context` | +| `("probe")`, on the reproduction above | TB5083 `unsupported data source` | +| the documented five strings, with a resource that exists | compiles | + +So a missing argument list is the one wrong shape that is not checked. + +**Observed** on 2026-09-30 in two ways. `scripts/sweep_attributes.mjs` builds every attribute +at every declaration site in batches of 400 and halves a batch the compiler crashes on; each +of the three Enum sites (an Enum with a member, an empty Enum, an Enum in a Class) was +narrowed to one probe beside the three canaries the tool adds to every batch, which build +clean without it. The four-line reproduction above was then built **exactly as written**, in +a project holding only it and a two-line `Sub Main`, with no resources: `tbbuild` exits 4, +`the compiler crashed 2x -- this project takes it down`, `last parsing: CrashProbe.twin`. +The same project with `[PopulateFrom("probe")]` builds and reports the one TB5083 row. The +rows for `(True)`, `(False)` and `(1)` come from the sweep's batches, not from that +project. diff --git a/WIP.ExamplesBuild.md b/WIP.ExamplesBuild.md index f512da23..a37280b1 100644 --- a/WIP.ExamplesBuild.md +++ b/WIP.ExamplesBuild.md @@ -408,6 +408,77 @@ Two things a batch runner must do that a single-fence runner need not: the same finding and the other eight still reporting. Until then the name went unread --- `buildStaged` kept `tbbuild`'s report and nothing looked at it --- so every crash paid for the whole bisect. +- **A batch that reports nothing is not a clean batch: a canary rides in every one.** + `waitForCompile` reads the IDE's window --- the status counters and the Problems panel of + the open project --- once the compiler status is OPERATIONAL and five one-second samples + match. It waits for no build. An IDE under load can sit OPERATIONAL with an empty panel + before it has published anything, and every sample of that batch then reads as compiling, + with nothing to tell it from a batch that has no errors. `sweep_attributes` met it first, + because its canaries exist to: one build in 136 of its last full run drew `ACCEPT` for all + three of them, including the invented name that can only ever draw an error, while + `build.bat`, `check.bat` and `test.bat` ran on the same machine (a coincidence, not a + measurement of the cause; none of the earlier full runs had one). `check_examples` had no + such protection, so that read would have passed every sample of its batch. Each batch now + carries `tbxCanary`, a module with `[EnforceWarnings(TB0005)]` and a `#Warning` directive, + which must draw the warning TB0005. Its rows are taken out, at any severity, before + anything else reads them. + + **It is a warning, and enforced, because that measured robust and leaves a clean batch + clean** (BETA 987, `canary-warn.mjs`, ten cases). TB0005 appeared beside an unterminated + `Sub`, an `If` with no `End If`, a stray `End Sub` with garbage after it, a class inheriting + itself with an unknown type, ten undefined names, under Option Explicit off, and beside a + module with `[IgnoreWarnings(TB0005)]`. A plain `#Warning` vanished in a project that + ignores TB0005 and became an ERROR in one that promotes it; the enforced form stayed a + warning in both. No template changes `project.warnings` today (the defaults leave every + warning on), so the attribute is insurance, not a fix. An error canary put an error in + every batch, so `tbbuild` never exited 0; with the warning a clean batch does. + + The history: the first version carried two error canaries, an unknown attribute (TB5182) + and an undefined symbol (TB5079). The second is only the warning TB0002 under **Option + Explicit off**, where the name is declared implicitly, and stopped a full run on + `Core/Deftype.md:56`, a sample that compiles, in the `[implicit]` template. The next version + kept TB5182 alone; the warning replaced it at the user's suggestion. A batch that crashes the + compiler returns no rows at all, canary included; `runBatch` hands a crash to + `isolateCrash` before it reads the canary, so that case never reaches it. + + **Measured with real failures** (a temporary page of 14 samples, deleted after): ten + failing samples across the console and `[implicit]` templates, among them an unterminated + `Sub`, garbage after a stray `End Sub` and an `If` with no `End If`, were each reported + with their own errors, with no canary event and no split, in about 13 s --- with the TB5182 + canary and again with the warning, the same result both times. + + **The canary is required only by a read with no errors in it** (the user's rule). It + proves that the IDE published something, never that it published everything --- a read + that includes it and misses a later file's diagnostics would still pass that file --- so a + read holding real errors has already shown what the canary would, and is taken as read: + no rebuild, no split, no stop. `heard()` decides what is real: an ERROR in one of the + batch's samples, or an unattributed row the template does not draw by itself. **The + template's own rows do not count**, or a template that always draws one would switch the + canary off for every batch of it; an earlier version let an early read holding only such + a row pass the sample (found by review). A canary missing *beside* real errors is printed + as a note and not acted on: it has never been seen, and would be the first sign of a read + that holds some files and not others. + + A silent read (no errors, no canary) is built once more; then split, as for a crash, until + each part reports the canary or errors of its own. A unit that is still silent stops the + run (exit 2, its name) without being blamed, since its clean may be false and a finding + would claim something about its code that a build with no diagnostics cannot. A + `projname` group is one program and cannot be split, and needs no splitting: if any member + fails, the read was not silent. This is what keeps a corpus that legitimately fails (a + `--propose` survey) from ever being stopped or slowed by the canary; the version before + it stopped the run on a group with only some members failing. + + `ownRowsOf`'s empty-template build follows the same rule: a silent read is read again and + refused when silent twice, and a read with rows of its own needs no canary. It is + memoised for the whole run, and a silent read there believed would leave `own` empty, so + every batch with a template-own row would bisect to single samples and blame each one. + + Tested against a fake lane: an early read answered by one build; a sample that hides it, + with no errors of its own, found and named; the same sample with errors of its own taken as + read in one build, with the note; a group with only some members failing taken as read in + one build; the hiding sample with only the template's own row named; two that hide it only + together separated by halving; a batch that never reports; the empty template read again, + refused when it never reports, and believed in one read when it has rows of its own. - **A crash that needs two samples used to vanish.** Halving separates any pair by the time it reaches single samples; both halves then build clean, and every sample in the batch counted as compiling --- the false clean that the crash check exists to prevent, one level diff --git a/WIP.Harness.md b/WIP.Harness.md index 29568a48..ebc9cef5 100644 --- a/WIP.Harness.md +++ b/WIP.Harness.md @@ -143,6 +143,75 @@ confusion publishes a wrong number with nothing to notice it by. The bar is that report's unresolved count is **0**, which it currently is; a non-zero one is a scanner bug, not a corpus oddity. +## Sweeping every attribute at every site + +A census says where the packages *use* an attribute, and `gen_attribute_probes.mjs` probes +only the targets `Attributes.md` already claims, so an entry that was too short stayed too +short: `[ComExport]` was documented as "constants in a Module" because a Sub and a Const were +the two targets tried, and an API `Declare` never was. +[scripts/sweep_attributes.mjs](scripts/sweep_attributes.mjs) asks every question --- every +name (the page's, the compiler's token table's, `--names`) at each of about 60 sites in +[scripts/lib/attribute-sites.mjs](scripts/lib/attribute-sites.mjs), in each argument shape --- +and lays the answers against the page. Its reader-facing description is +[Tools and Scripts](docs/Documentation/Tools.md#sweep-attributes); this is why it is built as +it is. + +**Against BETA 987: 34,526 probes in 134 builds, 8 to 16 minutes on four lanes** (three runs +took 667, 946 and 489 seconds; the middle one carried a stage 2 inflated by a noise site since +removed). That is the whole matrix, and nothing in it was slow enough to need the sampling the +design first allowed for. **A run of one name is a minute or a few, not always one:** an +attribute the compiler allows once per project (`[RunAfterBuild]`, `[RunBeforeStartupObject]`) +goes in one probe per batch, so it needs about a hundred builds by itself. + +**The token table is a string, and it holds the names you cannot guess.** The compiler binary +has one 2,496-character run of 261 pipe-separated identifiers, from `On|Off|Explicit` to +`UserDefinedTypeIsAnAlias`, with `DllExport|ComExport` in the middle of it; it is where +`ComExport` was first seen. It mixes +keywords, attributes and object members (`Debug`, `Circle`, `PSet`), so a name in it is only a +candidate. Swept, exactly one name the page does not document was accepted anywhere: +`[PropertyPage]`, at eighteen sites, all of them members of a Class or an Interface. The +other 193 were refused at every site. + +**Six things the sweep had to learn, each of which produced a wrong report first**, found by +the Opus review that was run over the tool and by its own first output: + +- **`parseCli` camel-cases its keys.** `values["dry-run"]` is `undefined`, so `--dry-run` was + ignored and the first "dry run" was a full four-lane sweep. Read `values.dryRun`; a new + tool that takes a hyphenated option should be run once with each of them before it is + trusted. +- **A control must be refused for a site to mean anything.** An unknown name is built at every + site, and a site whose control *compiles* is voided. An Enum body accepts any own-line + `[...]` --- `[ClassId("guid")]` and `[Hidden(True)]` included, neither of which can be a + member name --- so what it does with the line is unchecked, and nothing accepted there is + evidence. Inline, `[Name] X = 1`, the compiler refuses every attribute, `[Hidden]` included, + though the page documents it on an Enum member. An Enum member target is therefore reported + as one the sweep cannot test, which is true. (The mechanism is not established; only the + behaviour was measured.) A local variable is different: an own-line `[Name]` in a Sub is a + *call* statement, so that variant was dropped and only the inline one is kept. +- **A probe that draws what the control draws is a refusal, whatever the code.** The signature + compared has the attribute's name (whole word, any case) and the probe's own generated names + (`S000062`, `S000062_U`) taken out, or the control's message never equals the probe's. +- **What a canary must draw is fixed in the script.** Reading it from the preflight lets a + build that contains the very masking the canaries exist to catch calibrate the check to it. + The tool builds the canaries alone first and stops unless they draw what is recorded. +- **Batch by shuffle, and one probe per singleton.** `[RunAfterBuild]` is once per project + (TB5114), read as acceptance if a second reaches the same batch, and `[PopulateFrom]` fills + an enum with the same two members every time, and enum members are project-global. Member + names are unique per probe (`Probe` becomes `S000062_m`) for the same reason. +- **A form nobody built cannot be refused.** A cell whose other forms were refused and whose + one form able to pass never compiled is inconclusive, not refused, and the same holds for a + run cut short. + +**It found a compiler crash the documentation never would:** a bare `[PopulateFrom]` on an +Enum kills the compiler, at all three Enum sites; the tool isolated it by halving to one +probe beside the canaries. It is in [BUGS-TO-REPORT.md](BUGS-TO-REPORT.md). + +**Read the report's "accepted at most sites" section before believing an acceptance.** +`[Description]` is taken at 53 of 61 sites and `[Hidden]` and `[Restricted]` at 42: either +they apply nearly everywhere or the compiler tolerates what it does not check. The sweep +cannot say which, and neither can a clean build, which is also why *accepted but not +documented* is a list to read and not a list to copy into the page. + ## Compiling a twinBASIC project without the IDE in front of you Exported sources say what the compiler *accepts today*; they cannot answer a question no @@ -190,6 +259,34 @@ on it. Moving the code there was checked against 14 fixture cases run before and every exit code and every line of output the same, apart from the two fixes below --- and against a full `examples.bat` run. +**A build is also a function, [scripts/lib/tb-build.mjs](scripts/lib/tb-build.mjs)'s +`compileProject`**, which is `tbbuild` without its command line and returns +`{code, message, rows, counts, dialogs, crashFiles, ...}`. `check_examples` and +`sweep_attributes` used to start `tbbuild` as a subprocess and read its JSON and its stderr +back, each with its own copy of that parse, and `check_examples` took the crashed files out of a +regex over the stderr text. The cost of the subprocess was never speed (about 100 ms against an +IDE start of about 10 s); it was that parse, and that a harness failure and a compile error +both arrived as an exit code. Moving `tbbuild` onto the function was checked the same way as the +move above: seven cases (clean, errors, `--json`, warnings only, a compiler crash, `--arch +win64`, a relative path) run before and after with every stdout, stderr and exit code +identical, and a full `check_examples` run (1,135 samples, no findings; the old code took 174 s +for 1,134 and the new 145 s, on a machine that was not quiet, so no speedup is claimed). + +Running in one process changes four things, each of which is a rule now: + +- **The function never tidies the registry and never exits.** The caller owns both; a tool that + builds many projects calls `startTidy` once, and `tbbuild` does it for its one. +- **It ends its IDE with `shutdownIdeAsync`.** `shutdownIde`'s `taskkill` and its wait hold the + event loop still (up to five seconds), which with four lanes lets the others' CDP timers run + out with their answers unread in a socket. The sync version stays for the paths that end in + `process.exit()`. +- **An uncaught exception ends every lane's work, not one child's.** `exitOnCrash(cleanup)` runs + a cleanup first; `sweep_attributes` uses it to write the report of what it had learned + (`salvage`), and `tb-cdp` drops a frame that is not JSON instead of throwing from an event + callback. +- **The name.** tb-ide already exports a `buildProject(c)` that builds an exe through an open + connection, and the notes below mean that one; the new function is `compileProject`. + Two bugs came out of the move, and neither had been noticed: - **A relative project path never loaded.** The IDE is given the resolved path and echoes @@ -604,11 +701,12 @@ leave everything as it was found**, and it takes four forms: `JSON.stringify`, which is how the IDE writes it, so the other entries keep their exact text and order, and the write is refused if the value changed after it was read. -**One process owns the registry per run.** `check_examples` runs four lanes of `tbbuild` -children at once; each restoring its own snapshot would put back whatever the registry held -when that lane started, in whatever order the lanes finished. `startTidy` sets -`TB_REGISTRY_OWNER`, the children inherit it and leave the registry alone, and the owner -sweeps once after the last lane. An owner pid that is no longer running does not count, or a +**One process owns the registry per run.** `check_examples` and `sweep_attributes` run four +lanes of builds at once, in one process (`compileProject`, which never tidies); each +restoring its own snapshot would put back whatever the registry held when that lane started, +in whatever order the lanes finished. The tool calls `startTidy` once, which sets +`TB_REGISTRY_OWNER` --- a `tbbuild` started from that process would inherit it and leave the +registry alone --- and sweeps once after the last lane. An owner pid that is no longer running does not count, or a variable left set in a shell would switch tidying off for good. Under `--keep` nothing is tidied, because the kept IDE is still writing. `shutdownIde` waits for the IDE's process to be gone before anything is tidied, because `taskkill` only asks. @@ -789,9 +887,10 @@ WebView2 processes and two console hosts. WebView2 runs inside the job without c the children it spawns into a kill-on-close job of its own, so when the Node process ends, the launcher ends with it, closing the IDE's job. Normally that is exactly what is wanted: killing only `tbbuild` in the middle of a compile now takes its whole IDE down with it, -where before the IDE lived on, on a desktop nobody could see. `check_examples` should get -the same protection one level up, since its `tbbuild` children die with it by the same -mechanism; that step has not been measured separately. +where before the IDE lived on, on a desktop nobody could see. `check_examples` and +`sweep_attributes` build in their own process (`compileProject`), so their IDEs are +launched by it and go when it does, by the same mechanism; that has not been measured +separately. **A kept IDE is the exception, and it gets no job.** Both other arrangements were tried, and both fail: diff --git a/WIP.md b/WIP.md index 658e2339..00e474c7 100644 --- a/WIP.md +++ b/WIP.md @@ -130,6 +130,7 @@ because an install path contains a username. ```sh "$TB/bin/twinBASIC_win32.exe" export ".twinproj" "C:\out\dir\" --overwrite node scripts/census_attributes.mjs --out census.md # every attribute, by enclosing construct +node scripts/sweep_attributes.mjs --out sweep.md --dump-results sweep.json # every attribute at every site, 8 to 16 min node scripts/tbbuild.mjs C:/probe/Thing.twinproj # does it compile node scripts/tbrun.mjs # what does it print ``` @@ -141,6 +142,7 @@ node scripts/tbrun.mjs # what does it print - **Keep a probe that might crash the compiler in a project of its own.** twinBASIC runs the compiler in the same process as user code, so one bad probe can take the run down and cost the other thirty their answer. - **`tbrun` takes an exported tree, not a `.twinproj`**, because it has to pin `project.buildPath` in its own staged copy --- a project still on the default template opens a native Save dialog that is invisible on the private desktop, and the build simply never happens while every health check says the IDE is fine. The probe is a module with a `[RunAfterBuild]` Sub, and must start with `Debug.Cls`. Its exit codes: 0 the probe ran and its output was captured, 1 the project has compile errors, 2 the harness failed or the build did after a clean compile, 3 no output, 4 the compiler crashed, as `tbbuild` reports it. - **A census is evidence, not applicability.** The corpus not using an attribute somewhere does not mean the compiler refuses it there, and the reverse also holds. Only a probe settles that. +- **The sweep is the probe for a whole attribute.** Before writing or changing an attribute's `Applicable to:` line, run `sweep_attributes.mjs --names ` (a minute or a few; the once-per-project ones, `RunAfterBuild` and `RunBeforeStartupObject`, take several); it tries every declaration site, not only the ones already claimed. **Always pass `--out` and `--dump-results`**: a run piped through `tail` keeps one line of a report that took ten minutes. A clean build there means the compiler accepts the attribute, not that it does anything, and an Enum member cannot be tested at all (see [WIP.Harness.md](WIP.Harness.md#sweeping-every-attribute-at-every-site)). - **End an IDE by its pid, never by image name.** `taskkill /IM twinBASIC.exe` ends every other run's IDE, another session's included, and the user's own. `tbbuild --keep` prints the pid for this reason. Why each of those is true, what the WebView/CDP route costs, why the compiler's @@ -451,7 +453,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 CI-workflow roster check (`scripts/check_ci_workflows.mjs`), the lint gate (`scripts/check_lint.mjs`), the site-search unit tests (`node --test test/search.test.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`), the symbol-index probes (`scripts/check_symbol_index.mjs`), the twinBASIC-scanner probes (`scripts/check_twin_parsers.mjs`), the command-line probes and cases (`scripts/check_cli.mjs`), the pdf-lib shim comparison (`scripts/check_pdf_shims_equiv.mjs`), the impexp parity check (`scripts/check_impexp_parity.mjs`), and the axe source-patch verification (`scripts/check_axe_patch_equiv.mjs`). ~23 s, of which the regex-safety gate is ~9 s and the impexp check ~4 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 CI-workflow roster check (`scripts/check_ci_workflows.mjs`), the lint gate (`scripts/check_lint.mjs`), the site-search unit tests (`node --test test/search.test.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`), the symbol-index probes (`scripts/check_symbol_index.mjs`), the twinBASIC-scanner probes (`scripts/check_twin_parsers.mjs`), the attribute-sweep probes (`scripts/check_attribute_sweep.mjs`), the command-line probes and cases (`scripts/check_cli.mjs`), the pdf-lib shim comparison (`scripts/check_pdf_shims_equiv.mjs`), the impexp parity check (`scripts/check_impexp_parity.mjs`), and the axe source-patch verification (`scripts/check_axe_patch_equiv.mjs`). ~23 s, of which the regex-safety gate is ~9 s and the impexp check ~4 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; ~120 s over the 1,129 samples marked as of 2026-09-25. 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). @@ -469,7 +471,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/`, `lib/`, `book/`, `eval/`, `wisdom/`, `test/`, the site's scripts in `docs/assets/js/`, a wrapper or a workflow, run `test.bat` as well** --- another ~23 s. Twelve of its fifteen gates cannot be affected by an edit under `docs/` at all. **Three can.** `check_lint.mjs` lints the site's two scripts in `docs/assets/js/` along with the tooling. `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 reads `DOCS_DIR`, which is `/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/`, `lib/`, `book/`, `eval/`, `wisdom/`, `test/`, the site's scripts in `docs/assets/js/`, a wrapper or a workflow, run `test.bat` as well** --- another ~23 s. Thirteen of its sixteen gates cannot be affected by an edit under `docs/` at all. **Three can.** `check_lint.mjs` lints the site's two scripts in `docs/assets/js/` along with the tooling. `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 reads `DOCS_DIR`, which is `/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 @@ -504,6 +506,7 @@ wrapper: | `test.bat` | `check_regex_safety` | no regex in the tree can backtrack exponentially | | `test.bat` | `check_symbol_index` | the symbol index still places each kind of symbol, from fixtures | | `test.bat` | `check_twin_parsers` | every word of the shared modifier list reaches all three scanners of twinBASIC source; the census's declaration kinds and `parseTargets`' targets hold for the shapes each once misread | +| `test.bat` | `check_attribute_sweep` | the sweep's site skeletons render and are each in a family of `Applicable to:` targets or listed as in none; its reading of a probe's diagnostics puts the refusals, the control's fold and a skeleton's own errors in the right states; every real `Applicable to:` line reads to its pinned targets; probes batch once each and never two of a once-per-project attribute; a cell missing a form is not reported refused; and the isolating runner, against a scripted fake in place of the IDE, finds a crash, a hang, a disturbed canary or a stray row and stops at each cap. Twenty-three injected faults, one at a time, each fail a probe | | `test.bat` | `check_cli` | `lib/cli.mjs` parses as a strict `parseArgs` does, and also refuses an empty value unless the option allows one; every tool refuses an unknown flag, and every tool with a value option an empty value; each tool's recorded command-line errors still exit and print as recorded, run with an IDE and a browser that do not exist | | `test.bat` | `check_ci_workflows` | both CI workflows run every wrapper gate, with the same arguments and order, and build with `build.bat`'s flags | | `test.bat` | `check_lint` | Biome finds nothing in the tooling, warnings included, and checked at least one script | diff --git a/docs/Documentation/Building.md b/docs/Documentation/Building.md index 8d02504f..a125ae42 100644 --- a/docs/Documentation/Building.md +++ b/docs/Documentation/Building.md @@ -79,6 +79,7 @@ Each `.bat` opens with `@pushd "%~dp0"`, which is what lets it be invoked from a && node scripts/check_book_coverage.mjs \ && node scripts/check_symbol_index.mjs \ && node scripts/check_twin_parsers.mjs \ + && node scripts/check_attribute_sweep.mjs \ && node scripts/check_cli.mjs \ && node scripts/check_pdf_shims_equiv.mjs \ && node scripts/check_impexp_parity.mjs \ diff --git a/docs/Documentation/Tools.md b/docs/Documentation/Tools.md index 9e8d2c02..67bce053 100644 --- a/docs/Documentation/Tools.md +++ b/docs/Documentation/Tools.md @@ -71,7 +71,7 @@ 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. Fifteen steps, each stopping the run if it fails: +The tests the toolchain has to pass. Sixteen 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. @@ -84,10 +84,11 @@ The tests the toolchain has to pass. Fifteen steps, each stopping the run if it 9. [`scripts/check_book_coverage.mjs`](#check-book-coverage) --- verifies the build still warns about a page `docs/_book.yml` does not mention. 10. [`scripts/check_symbol_index.mjs`](#check-symbol-index) --- verifies the symbol index still places each kind of symbol, and its drift guard still refuses a lost URL. 11. [`scripts/check_twin_parsers.mjs`](#check-twin-parsers) --- verifies the scanners of twinBASIC source and of the attribute reference still read the shapes each once misread. -12. [`scripts/check_cli.mjs`](#check-cli) --- verifies `lib/cli.mjs`, the command-line parser, and each tool's recorded command-line errors. -13. [`scripts/check_pdf_shims_equiv.mjs`](#check-pdf-shims-equiv) --- verifies the book's pdf-lib shims write what stock pdf-lib writes, patch the members of pdf-lib it lists, and run. -14. [`scripts/check_impexp_parity.mjs`](#check-impexp-parity) --- verifies the two editions of the impexp tool pass the same built-in tests, and exit, print and write the same for one sequence of commands. Without Python it reports itself skipped and passes, except in CI. -15. [`scripts/check_axe_patch_equiv.mjs`](#check-axe-patch-equiv) --- verifies the vendored axe source patch still produces identical colour values. +12. [`scripts/check_attribute_sweep.mjs`](#check-attribute-sweep) --- verifies the logic of the attribute sweep: its site skeletons, how it reads a probe's diagnostics, how it batches probes, and how it compares the answers with `Attributes.md`. +13. [`scripts/check_cli.mjs`](#check-cli) --- verifies `lib/cli.mjs`, the command-line parser, and each tool's recorded command-line errors. +14. [`scripts/check_pdf_shims_equiv.mjs`](#check-pdf-shims-equiv) --- verifies the book's pdf-lib shims write what stock pdf-lib writes, patch the members of pdf-lib it lists, and run. +15. [`scripts/check_impexp_parity.mjs`](#check-impexp-parity) --- verifies the two editions of the impexp tool pass the same built-in tests, and exit, print and write the same for one sequence of commands. Without Python it reports itself skipped and passes, except in CI. +16. [`scripts/check_axe_patch_equiv.mjs`](#check-axe-patch-equiv) --- verifies the vendored axe source patch still produces identical colour values. POSIX: @@ -102,6 +103,7 @@ POSIX: && node scripts/check_book_coverage.mjs \ && node scripts/check_symbol_index.mjs \ && node scripts/check_twin_parsers.mjs \ + && node scripts/check_attribute_sweep.mjs \ && node scripts/check_cli.mjs \ && node scripts/check_pdf_shims_equiv.mjs \ && node scripts/check_impexp_parity.mjs \ @@ -109,7 +111,7 @@ POSIX: Exit codes: **0** every step passed; otherwise the code of the step that stopped the run, as that step's entry gives it. -**Twelve of the fifteen 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/`, `lib/`, `book/`, `eval/`, `wisdom/` or `test/`, the site's scripts in `docs/assets/js/`, a wrapper, or a workflow. Both CI workflows run all fifteen unconditionally, so skipping it locally cannot let a tooling regression reach `staging`. +**Thirteen of the sixteen 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/`, `lib/`, `book/`, `eval/`, `wisdom/` or `test/`, the site's scripts in `docs/assets/js/`, a wrapper, or a workflow. Both CI workflows run all sixteen unconditionally, so skipping it locally cannot let a tooling regression reach `staging`. The three exceptions are [`check_code_regions.mjs`](#check-code-regions), [`check_gate_lists.mjs`](#check-gate-lists), which reads this page, and [`check_lint.mjs`](#check-lint), which lints the site's scripts in `docs/assets/js/`. 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. @@ -588,6 +590,19 @@ The modifier words that may precede a declaration keyword are one list, in `scri Exit codes: **0** every probe passed, **1** a probe failed, **2** the gate could not run: a refused command line, or a crash. +### check_attribute_sweep.mjs +{: #check-attribute-sweep } + + node scripts/check_attribute_sweep.mjs + +Verifies the logic of [`sweep_attributes.mjs`](#sweep-attributes), the tool that asks the compiler where every attribute is legal. None of that tool's failures announces itself: a site skeleton that is wrong reads as "every attribute is refused here", a control the classifier ignores reads as a recognised attribute, and a refusal taken for an acceptance is published as a finding about the compiler. The IDE is what cannot run here, so the parts that decide what an answer *means* live in `scripts/lib/attribute-sweep.mjs` and `scripts/lib/attribute-sites.mjs`, and every probe is a fixed input: no IDE, no built tree and no twinBASIC install. Under a second. + +The probes cover, in turn: the site skeletons --- what each renders, that the attribute lands on the line `attributeLine` names, that every name a skeleton declares belongs to its probe alone, that a `$&` in an attribute is written literally, and that every site is in a family of `Applicable to:` targets or is listed in the gate as being in none, so a new site is a decision and not an accident; how a probe's diagnostics are read, including which refusal wins, what counts as accepted, the errors a skeleton draws by itself, and what the control's fold does and does not fold; which probe each of the compiler's rows belongs to; the argument shapes each name is tried in, and the batches, in which every probe appears once and none holds two of an attribute the compiler allows once per project; how probes become one cell per site, where a form nobody built and the `(False)` form must not decide the answer; and how an `Applicable to:` line is read and laid against the cells, including every `Applicable to:` line the page has, each pinned to the targets it reads to. The runner that isolates what goes wrong is probed with a scripted fake in place of the IDE, which is what lets a crash that names the probe, one that names an innocent one, one that needs two probes together, a hang, a disturbed canary, a stray error row and a build that could not run each be checked, along with the cap on every one of them; so are the preflight's verdict on a site and the comparison `--verify` makes. + +The probes were checked the way a gate should be, by breaking the code they guard: twenty-three injected faults, one at a time, each failing at least one probe. + +Exit codes: **0** every probe passed, **1** a probe failed, **2** the gate could not run: a refused command line, or a crash. + ### check_cli.mjs {: #check-cli } @@ -770,9 +785,9 @@ twinBASIC has no command-line build. The compiler executable's whole surface is **The IDE it starts ends with it.** The IDE runs inside a Windows job object, so when `tbbuild` ends --- finished, failed, or stopped with Ctrl+C --- every process the IDE started ends too. That includes a compiler the IDE was restarting after a crash, which a plain process-tree kill can miss and leave running. Two exceptions: under `--keep` the IDE runs outside the job and lives until you close it, and under `--show` it is started directly on your desktop, without the job. -**It leaves the IDE's own settings as it found them.** Every IDE it starts writes to the same registry keys as your own IDE: a saved state for the project (open tabs, watch expressions, Debug Console history), a place at the top of the recent-projects list, and, when the run switches the target, the target the IDE remembers for the project. Once the IDE has exited, `tbbuild` puts all three back. An entry the run created is deleted, and a project that already had one --- one of your own --- gets its old state, its old place in the list and its old target back. The `.twinproj` file association is restored too, if the IDE changed it. When [`check_examples.mjs`](#check-examples) runs `tbbuild`, `check_examples` does this once for all its lanes instead. +**It leaves the IDE's own settings as it found them.** Every IDE it starts writes to the same registry keys as your own IDE: a saved state for the project (open tabs, watch expressions, Debug Console history), a place at the top of the recent-projects list, and, when the run switches the target, the target the IDE remembers for the project. Once the IDE has exited, `tbbuild` puts all three back. An entry the run created is deleted, and a project that already had one --- one of your own --- gets its old state, its old place in the list and its old target back. The `.twinproj` file association is restored too, if the IDE changed it. When [`check_examples.mjs`](#check-examples) or [`sweep_attributes.mjs`](#sweep-attributes) builds many projects, it does this once for all its lanes instead. -Four files under `scripts/lib/` belong to it and are never run directly. `tb-ide.mjs` holds the mechanics `tbbuild.mjs` and `tbrun.mjs` share: starting the IDE, attaching to it, waiting for the compile, and reading the diagnostics and the DEBUG CONSOLE. `tb-registry.mjs` records and restores the registry entries described above, through .NET's registry API by way of PowerShell, because `reg.exe` mangles any path holding a character outside the console code page; [`check_tb_registry.mjs`](#check-tb-registry) is its self-test. `tb-cdp.mjs` is a minimal CDP client over Node's global `WebSocket`, raw rather than puppeteer because a pending `alert()` blocks the renderer and puppeteer's `connect()` handshake talks to the renderer --- so it hangs on precisely the state you need to recover from. Every call it makes has a time limit, so a blocked page ends a run with a message rather than holding it forever. `tb-launch.ps1` holds the Win32 calls Node cannot make without a native FFI addon: `CreateDesktop` and `CreateProcess` with `STARTUPINFO.lpDesktop` for the private desktop, and the job object described above. It is the only PowerShell file under `scripts/`, and it is not executed as a file: `tb-ide.mjs` reads the text and passes it through `-EncodedCommand`, so the execution policy never comes into it and nobody has to be told to bypass one. +Five files under `scripts/lib/` belong to it and are never run directly. `tb-build.mjs` is `tbbuild` without its command line: `compileProject` opens a project in the IDE and returns its diagnostics as an array, which is how `check_examples.mjs` and `sweep_attributes.mjs` build many projects without starting a process for each. It never exits the process and never tidies the registry, so its caller owns both. `tb-ide.mjs` holds the mechanics `tb-build.mjs` and `tbrun.mjs` share: starting the IDE, attaching to it, waiting for the compile, and reading the diagnostics and the DEBUG CONSOLE. `tb-registry.mjs` records and restores the registry entries described above, through .NET's registry API by way of PowerShell, because `reg.exe` mangles any path holding a character outside the console code page; [`check_tb_registry.mjs`](#check-tb-registry) is its self-test. `tb-cdp.mjs` is a minimal CDP client over Node's global `WebSocket`, raw rather than puppeteer because a pending `alert()` blocks the renderer and puppeteer's `connect()` handshake talks to the renderer --- so it hangs on precisely the state you need to recover from. Every call it makes has a time limit, so a blocked page ends a run with a message rather than holding it forever. `tb-launch.ps1` holds the Win32 calls Node cannot make without a native FFI addon: `CreateDesktop` and `CreateProcess` with `STARTUPINFO.lpDesktop` for the private desktop, and the job object described above. It is the only PowerShell file under `scripts/`, and it is not executed as a file: `tb-ide.mjs` reads the text and passes it through `-EncodedCommand`, so the execution policy never comes into it and nobody has to be told to bypass one. Exit codes: **0** the project compiled without errors; **1** the project has errors; **2** a refused command line (a path that is not a `.twinproj` included), no IDE, an IDE that did not start or expose a debug port, or a crash; **3** the compile never settled: the IDE did not report the project open, or its diagnostics did not match its status bar; **4** the project crashes the compiler. @@ -994,7 +1009,7 @@ usually run, and [Authoring Pages](Authoring#checking-that-a-sample-compiles) is sample opts in. Each marked sample is generated into its own `Module tbx_`, packed with a template -project, and handed to [`tbbuild.mjs`](#tbbuild). A diagnostic comes back against a +project, and built the way [`tbbuild.mjs`](#tbbuild) builds. A diagnostic comes back against a generated file and a generated line; the report converts both, so what you read is the page and the line in it: @@ -1059,6 +1074,10 @@ the batch crashes by itself. The samples it needs are then searched for as a set of them are reported. The cost is paid only on failure. The finding names the sample, or the set, and points at `BUGS-TO-REPORT.md`. +**A batch can report nothing when it should report something.** `tbbuild` does not wait for a build: it reads the IDE's own window, the status bar and the Problems panel for the project the IDE has open, once the compiler's status reads OPERATIONAL and has stopped changing. An IDE under load can be OPERATIONAL with an empty panel before it has published its diagnostics, and a batch read then reports every sample as compiling, which looks exactly like a batch with nothing wrong. So every batch carries a canary: a module holding a `#Warning` directive, whose warning (`TB0005`) is known. A read with no errors in it must report the canary, or it is not believed. The module carries `[EnforceWarnings(TB0005)]`, so a project setting that ignores the warning, or turns it into an error, does not change it. The warning is reported whatever else the batch holds: unterminated blocks, stray `End` statements, broken classes and many undefined names in other files do not hide it. It is a warning rather than an error so that a batch with nothing wrong still builds clean. A batch that crashes the compiler reports nothing at all and is isolated as a crash; its canary is never read. + +The canary proves only that the IDE published something, not that it published everything: a read that includes the canary but not a sample's later diagnostics would still pass that sample. So a read that holds real errors needs no canary --- the IDE was plainly not silent --- and is taken as read, whatever the canary did. Real errors here are errors in the batch's samples, and errors outside every sample that the template does not draw by itself; a template's own errors do not count, or a template that always draws one would switch the canary off for every batch built from it. A canary missing beside real errors has never been seen, and is printed as a note if it happens. A read with no errors and no canary is built once more, because a read that came too early says nothing about the batch. If it is silent again, the batch is split in half repeatedly, as for a crash, until each part reports the canary or errors of its own. A single unit --- one sample, or a group compiled as one program --- that is still silent stops the run with exit code 2 and its name, because its clean result cannot be trusted and it is not blamed for errors nobody saw. The template built with no samples, which is how the tool learns the errors a template draws by itself, follows the same rule: it needs its canary only if it has no errors, is read again when it is silent, and stops the run when it is silent twice. + **A sample can be compiled against a file.** A fence carrying `resource=` --- in any language, typically ` ```json ` --- is written into the generated project at that path instead of being compiled, and travels with its page the way a `hidden` fence @@ -1141,6 +1160,52 @@ The report ends with what the scanner could not resolve, and **that section is e Exit codes: **0** the report was produced, **2** a refused command line, no install, an install with no compiler or no package project, or a crash (a package that fails to export is left out of the census). +### sweep_attributes.mjs +{: #sweep-attributes } + + node scripts/sweep_attributes.mjs [--ide ] [--names ] [--sites ] + [--forms bare|smart|all] [--no-tokens] [--jobs ] [--port ] + [--batch-size ] [--verify ] [--out ] + [--dump-results ] [--work ] [--keep] [--preflight] + [--dry-run] [--list-sites] [--show | --hide] [--timeout ] + +Asks the compiler where every attribute is legal. It writes each attribute name at each declaration site --- a Module, a Class member, an API `Declare`, a Type field, a parameter, an `Implements ... Via` statement, and so on --- in each argument shape, builds the projects the way [`tbbuild.mjs`](#tbbuild) does, and lays the answers against the `Applicable to:` lines in `Reference/Attributes.md`. The report lists the documented targets the compiler refuses, the targets that hold only partly, and the sites it accepts that the page never mentions. + +It exists because the two older tools each leave a gap. [`census_attributes.mjs`](#census-attributes) says where the shipped packages *use* an attribute, and [`gen_attribute_probes.mjs`](#gen-attribute-probes) probes only the targets the page already *claims*, so an entry that is too short stays too short. `[ComExport]` was documented as "constants in a Module" because a Sub and a Const were the two targets tried; an API `Declare` never was. This tool asks every question, so a missing target shows up as a row of the report and not as something a person has to think of. + +The names come from three places: every entry in `Attributes.md`, every name in the compiler's own token table (a long pipe-separated string in the compiler binary, holding keywords, attributes and object members together), and `--names`. A token-table name is a *candidate*: it is called an attribute only if some site accepts it. `Debug` and `ExecuteHostCommand` are in the table and neither is one. + +**How an answer is made readable.** A clean build is not proof by itself, so the tool guards against the ways one goes wrong: + +- **Baselines.** Every site is built with no attribute first, and one that does not build clean is voided, so a wrong skeleton cannot read as "every attribute is refused here". +- **Controls.** TB5155 and TB5182 do not separate "wrong place" from "no such attribute". An invented name is built at every site, and a probe that draws exactly what the control draws is a refusal whatever its code. A site whose control compiles is voided. +- **Canaries.** Three probes (one clean, one refused for context, one unknown) ride in every batch. What they must draw is fixed in the script, not read from a build, and the tool builds them alone first and stops unless they draw it. A batch whose canaries differ, or that holds an error row belonging to no probe, is halved rather than believed. That is what would catch a compiler that stops reporting after so many errors, or a syntax error that suppresses the diagnostics of other files. +- **Isolation.** Halving also finds the probe behind a compiler crash or hang, and a crash that needs several probes together is reported as such. +- **Batching.** Batches are shuffled, and an attribute the compiler allows once per project (`[RunAfterBuild]`) goes in one probe to a batch, or its TB5114 would read as acceptance. +- **`--verify N`** rebuilds N random probes in fresh batches and compares. + +| Flag | Effect | +|---|---| +| `--names`, `--sites` | Restrict to these attribute names (any case) or site ids. `--list-sites` prints the ids. | +| `--forms` | `bare`, `smart` (the default) or `all`. Smart gives every documented attribute every argument shape, a token-table name its bare form, and more shapes only where a site recognised it. A shape known to be required is always tried. | +| `--no-tokens` | Leave out the token table. | +| `--jobs`, `--port` | Concurrent IDE lanes (default 4) and the first DevTools port; a lane uses one more each (default 9560). | +| `--batch-size` | Probes per project (default 400). | +| `--verify N` | Rebuild N random probes and compare. | +| `--out ` | Write the Markdown report there. Without it the report goes to standard output. | +| `--dump-results ` | Also write every result, raw, as JSON: each name at each site with the answer for each argument shape. | +| `--work`, `--keep` | Where projects are staged, which must be under the system temp folder, and whether to keep them. | +| `--preflight` | Build only the canaries, baselines and controls, and stop. About 20 seconds, and the way to check a change to a site. | +| `--dry-run` | Count the probes and build nothing. | + +**An Enum member cannot be tested.** An Enum body accepts any attribute written on its own line, including one that applies nowhere, and refuses every attribute written inline, so the site is voided and a target on an Enum member is reported as one the sweep could not test. + +**A clean build says the compiler accepts an attribute at a site.** It does not say the attribute does anything, and the IDE's background compile is what is read, so a check made only when linking is not seen. Where the report points at a target worth documenting, an A/B probe like X29 to X33 in `gen_attribute_probes.mjs` is what shows the effect. + +Like [`check_examples.mjs`](#check-examples) it needs a twinBASIC install and Windows with a private desktop, so it is outside every gate and outside CI. **Pass `--out` and `--dump-results`.** The report is the only product, and a run piped through `tail` keeps one line of it. + +Exit codes: **0** the report was produced and its self-checks held; **1** a self-check found a fault --- a probe disturbed the canaries even beside nothing else, or `--verify` found a probe that answered differently the second time; **2** a refused command line, no install, canaries that do not draw what the script records, a harness failure, a run cut short (its report is written all the same, and says so at the top), or a crash. + ### impexp.mjs and impexp.py {: #impexp } diff --git a/docs/Reference/Attributes.md b/docs/Reference/Attributes.md index 56f349e0..ce8c23e6 100644 --- a/docs/Reference/Attributes.md +++ b/docs/Reference/Attributes.md @@ -167,14 +167,31 @@ Indicates whether the class can be created through COM. It does not govern [**Ne Syntax: **[ComExport** [ **( True** \| **False )** ] **]** -Applicable to: constants in a [**Module**](Module) +Applicable to: [**Declare** (API declaration)](Declare) and constants, in a [**Module**](Module) -The COM counterpart of [DllExport](#dllexport), and it takes the same target: a **Public Const**, not a procedure and not a variable. +The attribute marks a given API for export from the ActiveX control DLL being built from the project the attribute is used in. - +The **Declare** can be a **Function** or a **Sub**, including the **PtrSafe** and **DeclareWide** forms. The compiler rejects the attribute on a procedure with a body, on a variable, and on a **Declare** or a constant inside a class. + +```tb check_build +[ComExport] +Public Declare Function GetTickCount Lib "kernel32" () As Long +``` + + + +See also [DllExport](#dllexport). ## COMExtensible (optional Bool) {: #comextensible } @@ -583,7 +600,7 @@ Applicable to: [**Class**](Class), [**CoClass**](CoClass), [**Interface**](Inter Hides the declaration from certain IntelliSense and other lists. It applies to a whole type --- a **Class**, **CoClass**, **Interface** or **Module** --- and equally to a single member of one, so a member can be kept out of those lists without hiding the type that declares it. Within a **Class** that covers procedures, variables, constants and events; within an **Interface**, the member prototypes; within a **Module**, procedures, variables, constants and [**Declare**](Declare) statements. > [!NOTE] -> A **CoClass** can only be hidden whole. Its body holds nothing but **Interface** lines, and the attribute is refused there with TB5155 --- unlike [**Default**](#default) and [**Source**](#source), which are interface-line attributes. It is likewise refused on an **Enum** or [**Type**](Type) declaration, on a **Type** member, and on a procedure parameter, though an individual **Enum** *member* does accept it. +> A **CoClass** can only be hidden whole. Its body holds nothing but **Interface** lines, and the attribute is refused there with TB5155 --- unlike [**Default**](#default) and [**Source**](#source), which are interface-line attributes. It is likewise refused on an **Enum** or [**Type**](Type) declaration, on a **Type** member, and on a procedure parameter. An individual **Enum** *member* is hidden the same way: the **Encoding** constants of [**Open**](Open) are marked `[Hidden, Restricted]`.