Avatar for the oxabl-project user
oxabl-project
oxabl
BlogDocsChangelog

Performance History

Latest Results

docs: keep lower crate manifests engine-neutral
feat/reverse-dependency-edges
6 days ago
feat(daemon): report reverse graph size
feat/daemon-session-core
6 days ago
fix: analyze include roots as fragments
fix/fragment-root-analysis
6 days ago
feat(daemon): discovery and a socket listener the daemon owns A client has to find a running daemon for a workspace root, or start one, and a crashed daemon must not leave a client waiting on a socket nobody holds. Discovery is a registration file per root under the XDG cache directory, recording pid, socket path, and contract version. A registration whose pid is not alive is treated as absent and replaced — liveness is checked rather than the socket tried, because a stale socket can accept a connection that is then never answered and no timeout is right for both a cold start and a busy pass. Writes go through a rename, so a concurrent reader sees the old registration or the new one and never a half-written file. The daemon owns its accept loop because it has to: the transport crate's listen helper binds and accepts exactly once, and its socket transport is crate-private and TCP-only. What is public is the part that matters — message read and write, and the connection's channel fields — so the framing and the request ids are reused and only the socket type differs. A thread per client, over a Unix socket, where filesystem permissions do the access control. A second daemon on one root is refused rather than allowed to steal the socket: a socket file whose owner is alive belongs to that daemon, and unlinking it would leave the running one unreachable while this one served a second index over the same workspace — the exact duplication the daemon exists to prevent. Handlers now receive a session host rather than a mutable session map, which puts the locking rule where it can be read: take the lock to write or to clone a snapshot, then release it before querying. A handler that queries under the lock serialises the whole daemon, and that is the difference between one client's slow answer costing that client and costing everyone. **Unfinished, and the tests say so.** The per-client framing threads and the accept loop do not tear down cleanly, so `tests/multi_client.rs` blocks rather than failing. Every test there is a U7 acceptance scenario and all nine are `#[ignore]`d with that reason, because a hang stalls CI indefinitely instead of reporting. Discovery is fully covered and green. The launchable `oxabl daemon` artifact is not wired yet either.
feat/daemon-session-core
6 days ago
test(parity): hold every client to the same dependency edges The parity table observed diagnostics and the format decision, so a reverse dependency query was invisible to it: two clients could disagree about what a file depends on and every leg would stay green. The table now carries an expected-edge channel, and the pipeline leg asserts it exactly. Each claim states what withholding the fixture's siblings does, because the withheld half is the load-bearing one — a row that only pinned the supplied answer would pass just as well if the edges arrived unconditionally. Two new rows cover the kinds no existing fixture reached: real nested includes, where direct and transitive must not be confused, and a `RUN` target beside the file whose `DEFINE NEW SHARED` the root consumes. All six kinds are now claimed by some row, and a test says so, so a kind cannot quietly become unobservable. Two claims deliberately pin a *gap* rather than the answer we would prefer. Withholding a class named only by `USING`, `NEW`, or a declared type leaves no unresolved row, and neither does a missed `RUN` target or `SHARED` producer: those links are recorded only when the index answers. The findings still reach the diagnostic channel, so nothing is under-reported to a user — but the edge set is silent, and writing that down is what will make the fix visible when edge-kind fidelity work lands. A cross-file row can now need a capability the browser lacks, so the browser's cross-file comparisons filter on comparability the way its other ones already do.
feat/reverse-dependency-edges
6 days ago
chore: release master
release-please--branches--master
6 days ago
feat: judge the cross-file population, and drain the top of the unmodelled-statement suppression (#153) * fix(lint): name ABL types in type-mismatch messages instead of internal ids LINT0004 interpolated a `ResolvedType` with `{:?}`, so a buffer mismatch reached the user as `Buffer(SymbolId(7))` and a narrowing warning as `Primitive(Decimal)`. Neither is a type a reader can look up in their own source, and a symbol id shifts whenever symbol counts change. Add `ResolvedType::display_abl`, a wrapper that borrows the symbol table and the schema — the type alone cannot name itself, since a class or buffer carries a symbol id and a table carries a schema id no symbol table maps. Primitives render their ABL keyword, a buffer renders the table it is over, an array renders its element type and extent, and the two lattice bottoms get named renderings so a future interpolation cannot fall back to a debug print. Both message sites in the rule go through one helper, and a new test file pins the property two ways: a battery of sources whose messages must contain no internal id, and a scan asserting no rule debug-formats a value outside its own test module. Closes #152 * feat(semantic)!: judge an inherited member's declared type An inherited member's type was parked in `SymbolTable::inherited_member_types` so that attaching a workspace index could be merged on evidence that it changed no diagnostic. That evidence has been collected; the valve has outlived its purpose. The type now goes onto `Symbol::data_type` at the point the resolve pass synthesizes the member, exactly as a local declaration's would, and the side map and both its accessors are deleted rather than bypassed. A cross-file resolution was already shape-identical to a local one in `references`/`symbols`; it is now shape-identical in the one place the rules read, so an assignment through an inherited member is type-checked like any other. The analyze envelope keeps `data_type_source`, which answers what the type alone cannot — whether the declaration that supplied it is in this file or another one the index reached — and derives it from `symbol_origin` so the two fields cannot disagree about a row. Three suites asserted the silence and now state the answer: the inheritance sweep becomes an enumeration of which scenarios gain a finding and which stay silent, the mismatch test becomes the positive assertion, and the pipeline's firewall becomes the same enumeration one client down. New tests pin the promotion's edges — a `VOID` member stays untyped, a property's type travels the same path as a return type, a three-level chain carries the grandparent's type, and a `:`-qualified call stays deliberately unjudged. BREAKING CHANGE: `SymbolTable::inherited_member_type` and `record_inherited_member_type` are removed; read `Symbol::data_type`. Refs #102 * feat(semantic)!: type a class-typed declaration from the workspace A declaration whose `AS CLASS` name only the workspace index could find kept `ResolvedType::Unknown`, with the resolved class parked in `CrossFileState::indexed_receiver_class` where member resolution could read it and the type lattice could not. `check.rs` did the same job one level up, typing any index-synthesized class symbol as lattice bottom. Both existed so attaching an index could add no finding; both are deleted. The `Foreign` branch of `upgrade_class_types` now writes `ResolvedType::Class` exactly as the `Local` branch does, and `receiver_indexed_class` reaches the same answer through the declaration's own type — one channel, not two. So a primitive assigned into a workspace-class-typed variable, and a `NEW` of a workspace class assigned into a primitive, are the mismatches they always were. A synthesized class symbol also records the supertypes the index read from its header. Without that, `ClassLattice` sees a class that inherits nothing and reports a subclass assigned into its parent-typed variable — a false positive minted by attaching an index, on the one shape inheritance widening exists to allow. Its supertype spans point at the use site in this file, the header being in another one. The assignability suite's two cross-file tests asserted the mechanism, not just the silence, so both now state the mechanism that replaced it: each is still silent, and the lattice is why. BREAKING CHANGE: an index-synthesized class symbol types as `ResolvedType::Class` rather than `ResolvedType::Unknown`. Refs #102 * test(lint): state the cross-file contract as answers, not as silence The cross-file suites existed to pin what changed on the day the rules turned onto the cross-file population. Collecting that payoff: the `RUN`/`SHARED` sweep keeps its zero but compares exact codes and spans instead of per-rule counts, so a finding moving between codes can no longer be absorbed; the reason suite gains producer tests that assert which situation mints which reason through a real index, so "searched and absent", "not statically knowable", and "we did not look" cannot collapse into each other. `nothing_produces_the_new_reasons_yet` is renamed `no_index_produces_no_cross_file_reason` and keeps its body. It was misnamed rather than obsolete: it runs with no index attached and asserts that ordinary ABL mints no cross-file reason, which is the invariant the `External` case rests on and outlives the index landing. Module docs say what each suite now asserts, so a reader arriving at an enumeration does not have to reconstruct why it used to be a zero. Refs #102 * feat(analyze): announce the judged cross-file type, and pin it across all four clients The `symbols` section goes to 4. No row key was added — `data_type` changed meaning for a cross-file row. Its absence used to be a reliable marker for "this row is a cross-file member", because the type was held off the symbol to keep it out of the type lattice; it is populated now, and a consumer branching on the absence would silently change behavior. `data_type_source` is the field to branch on instead, and the module docs say so. The parity table gains a row whose siblings make a finding **appear**. Every cross-file row so far could only get quieter when its siblings arrived — the removed `undefined-symbol` false positive — so `CrossFileEffect` gains a `Judged` arm and `expected_without_siblings` stops being a superset of `expected`. The new row declares both directions on one name: withheld, the inherited call is an `undefined-symbol`; supplied, that finding goes away and the parent's declared `INTEGER` assigned into a `LOGICAL` is a type mismatch. A resolver that stopped resolving trips the first arm; one that resolved without typing trips the second. The containment check keeps its teeth. An addition is allowed only where a `Judged` resolution declares it, and a declared addition that fails to arrive is also a failure — so a finding conjured out of a cross-file resolution nobody wrote down still fails the suite. All four legs, browser included, agree on the new row. Refs #102 * refactor(semantic)!: split the overloaded cross-file unresolved reason `NotFoundInWorkspace` named four situations and only one of them was "absent": a genuine path-search miss, an inherited member the class declares but does not expose here, a member lookup that came up empty against a class the index did answer for, and a file that was located and could not be parsed. That was harmless while every rule skip-listed the reason. It stops being harmless the moment `undefined-symbol` reports an absent name, because the rule would render "no such symbol" over three situations where the symbol demonstrably exists — including one that is a gap in oxabl's own parser rather than anything about the user's code. So the reason splits at the producer: `AbsentFromWorkspace` for the searches that came back empty, `PresentButUnusable` for the other three. A rule then fires on a situation rather than on an enum variant, and no per-rule exception list is needed. Telling a broken file from a missing one needs the index seam to say so, which it could not: `IndexAnswer::NotFound` folded "located but unusable" in by documented design. It gains `Unusable`, and both backends answer it from the `parsed` flag they already had — the batch cache and the language server's salsa queries alike. `ClassLookup` gains the same distinction so the analyze envelope stops reporting a file that is sitting on disk as absent, and the `dependencies` section goes to 2 for the changed reason strings. R17 rides along, derived where `index_loaded` is: an index with no configured search path answers NotFound to everything without having looked anywhere, so a miss stays `External`. That is the truthful answer, and it keeps the browser — which has no filesystem — from disagreeing with the CLI about a diagnostic. Behavior-preserving by construction: both new reasons are skip-listed everywhere, and the A/B across this commit alone is empty. BREAKING CHANGE: `UnresolvedReason::NotFoundInWorkspace` is replaced by `AbsentFromWorkspace` and `PresentButUnusable`; `IndexAnswer` gains `Unusable` and `WorkspaceIndex` gains a defaulted `searches_any_path`. Refs #102 * feat(lint): report a name absent from every configured search path `undefined-symbol` now fires on `AbsentFromWorkspace` as well as `NotInScope`. ABL cannot reference a symbol or a procedure whose code is not on the PROPATH, so a `USING`, a `NEW`, or a literal `RUN` target that no configured path supplies is genuinely undefined rather than merely unseen. The rule's positive `if let` on the one reason it reported becomes an exhaustive wildcard-free match, so the next reason has to be decided about here rather than absorbed silently, and the finding carries a help line naming the search-path configuration — a missing source root produces the same finding as missing code, and that line is its whole remediation. The corpus said the first cut over-reported by more than twenty times, in classes that are all correct ABL, so four narrowings landed with it. Each is a case where a path search is not the mechanism the AVM would have used, which makes a miss prove nothing: - `RUN name IN handle` resolves an entry point in another *running* program; no file could supply it, so it is `Unknowable`. - An extension-less `RUN name` means an internal procedure first, including any registered `SUPER PROCEDURE`'s. oxabl models neither, so the miss is inconclusive and stays `External`. - A bare class name searched under a shipped namespace — `USING Progress.Json.ObjectModel.*` and `NEW SomeShippedClass()` — is checked against the spellings actually *tried*, not the reference's own, so the AVM's class library is carved out wherever it is reached from. - A located file that does not visibly declare the class is `Unusable`, not absent: the declaration may be spliced in from an `{include}` the index does not expand, which is an ordinary ABL idiom. Two real defects surfaced on the way and are fixed here rather than worked around. A literal `RUN` target accepted a dotted second segment anywhere on the same line, so `RUN write-header. RUN build-list.` produced the target `write-header. RUN`; adjacency to the period now distinguishes a dotted name from a statement terminator. And the name search applied the *walk's* extension set to a target the author spelled out, so `RUN util/row-count.pp` reported a file that plainly exists as absent — a `RUN` target may now carry any extension but `.i`. An index's self-exclusion answers `Unusable` too, so a program that runs itself persistently is not told its own path is missing. `check --json` goes to 3: a diagnostic row carries its `help`. Refs #102 * feat(parser)!: head-parse DELETE OBJECT so its handle is a real read `DELETE OBJECT` was one of the ~30 recognized-but-unmodelled forms: matched by keyword, skipped to the statement end, and every identifier it passed over harvested lexically and marked `TOUCHED_BY_UNMODELLED_STATEMENT`. That mark is per-symbol and file-wide, so one `DELETE OBJECT` silenced the three count-gated rules for names elsewhere in the file — measured across a large real-world codebase, it is one of the two forms that dominate the suppression. It skipped for a real reason: the operand is an expression, so `Delete { buffer: Identifier }` could not hold `DELETE OBJECT ttbl:HANDLE.` or `DELETE OBJECT hArray[i].`. Hence a new `StatementKind::DeleteObject` carrying the target as an `Expression` plus the `NO-ERROR` flag. The resolve pass walks it like any other expression, so the handle is credited an ordinary read, and nothing in the statement is marked. Worth noting what this removes: `DELETE PROCEDURE`, `DELETE WIDGET`, and `DELETE SERVER` already fell through to a real `Delete` node. Only the `OBJECT` spelling skipped. `docs/design/ast-invariants.md` §8 describes the new node in the same commit, per the repo's standing obligation for a public `oxabl_ast` change. BREAKING CHANGE: `StatementKind` gains a `DeleteObject` variant; an exhaustive match over it must handle the new arm. Refs #102, #136 * feat(parser): stop COMPILE harvesting its own file path `COMPILE some/path.p SAVE.` was skipped like any other unmodelled form, which harvested every identifier-shaped token it passed over — the path segments and `SAVE` — and marked each one `TOUCHED_BY_UNMODELLED_STATEMENT`. None of them is a symbol reference: the operand is a file path. So the harvest credited nothing true and suppressed the count-gated rules for any real variable whose name collided with a path word or with `SAVE`, file-wide. Measured over a large real-world codebase, this is the second of the two forms that dominate the suppression, and the one whose share of it is entirely spurious. Head-parsing it would be worse than deleting the harvest: crediting reads inside a file path would invent references that do not exist. So the form keeps emitting `Skipped` — it *was* recognized, which is a different fact from a parse failure — with an empty name list, through a narrow `skipped_stmt_no_names` constructor beside the existing helpers rather than an `Option`-ised parameter at thirty call sites. The `#[must_use]` discipline on the skip helpers is intact. `ast-invariants.md` §8 now records both shapes #136 chooses between: symbol-shaped operands earn a head-parse, path-shaped ones earn an empty name list. Refs #102, #136 * docs: record the judged population, the drained suppression, and #136's measured tail The status section and the handoff both described a resolver deliberately walled off from the rules. That wall is gone: three valves deleted, `undefined-symbol` reporting names absent from the configured search paths, and two statement forms drained out of the unmodelled-statement suppression. `CLAUDE.md` now says what the four unresolved reasons license a consumer to claim, which envelope sections moved and why (`symbols` 4 for a *changed meaning* rather than a new key, `dependencies` 2 for split reason strings), and which single valve stays closed — `:`-qualified member access, the larger half of the population and its own piece of work. `HANDOFF.md` gains the session's own section, including the part most worth carrying forward: the first cut of the absent-name rule over-reported by more than twenty times, and every one of those findings was correct ABL. The four narrowings that fixed it are each a case where a path search is not the mechanism the AVM would have used, and the two real defects found on the way are named so nobody re-derives them. #136 keeps its scope but loses its guesswork — the issue carries a measured ranking of the remaining forms and the two-shape taxonomy that decides each one's fix. Refs #102, #136 * docs(parser,index): use synthetic names in the new examples The comment, test fixture, and handoff examples added alongside the `RUN` target and extension-policy fixes carried incidental names rather than obviously synthetic ones. Swapped for neutral placeholders, which is what every other fixture in these crates uses. No behavior change. * fix(lexer): accept UNIX backslash escapes outside string literals ABL on UNIX takes backslash as an alternative escape character, and it is legal outside a quoted literal — so `ab\cd` names the identifier `abcd`. The lexer treated a backslash there as `Invalid`, which turned an ordinary identifier into a parse error and cost the whole statement. Escape markers are removed from the compiler-visible name while the authored span is preserved, so a diagnostic still underlines what the author wrote. The existing tilde escape and the backslash line continuation are untouched. * fix(semantic): stop four cross-file shapes reporting or erasing wrongly Four corrections to the newly-judged cross-file population, each a case where a name was judged on evidence that did not apply to it. A wildcard import of a namespace the AVM ships no longer exempts every type name in the file. The carve-out exists because a *bare* name under such an import could be a shipped class, so a path-search miss proves nothing; a qualified spelling can never be supplied by a wildcard, so one `USING Progress.Lang.*` was silently exempting unrelated qualified typos. `USING ... FROM ASSEMBLY` and `FROM PROPATH` are carried on the AST instead of being parsed and discarded. An assembly-supplied type has no source on any path, so searching for one and reporting the miss made `undefined-symbol` fire at error severity on code whose author had already said where the type comes from. An imported supertype resolves by symbol identity rather than by the spelling in the header, so `CLASS child INHERITS base` under `USING pkg.base` reaches the same class the reference does. Matching by name meant the inheritance edge was missing, and a legal widening assignment was reported as a type mismatch. An overload set is judged by the return type its members agree on. Only a genuine disagreement goes to the lattice bottom. The previous rule erased the type whenever a name appeared more than once in the reachable surface, which is an `OVERRIDE` or an inherited interface contract far more often than an overload — so it unjudged most real OO-ABL, including the inherited-member typing this population was opened to reach. * docs: record the cross-file corrections and the repository agent guides CLAUDE.md picks up the corrected cross-file behavior, and the agent guides referenced from it (issue tracker, triage labels, domain) are added so those references resolve. * fix(scripts): measure LINT0004, and refuse to diff two incomparable sides The A/B instrument collected five rules. LINT0004 was not among them — the script was written for suppression work, before cross-file typing existed — so the rule that cross-file type resolution moves reported no delta rather than reporting zero. Adding it surfaced both a 44% increase from the type valves and four findings that were being judged on an arbitrary overload's return type, neither of which was visible before. The second half is about inputs. A collection's most consequential input is the `oxabl.toml` the run discovers, which need not be named on the command line and, for a corpus kept outside the repo, is typically untracked. Two collections days apart can therefore be driven by different configs, and nothing in the resulting JSONL shows it: the ratio still computes, still looks plausible, and attributes a config change to the code change. Each `collect` now writes a manifest of the inputs it ran under — rule set, config hash, an environment-override hash, corpus revision — and `diff` exits 5 rather than comparing two sides that disagree. A side with no manifest still diffs, with a warning, so an older collection is not stranded. The manifest carries only hashes, counts and revisions, never a path or a corpus fact, so it can be published beside a ratio to make that ratio reproducible. Same failure class as the defaulted coverage-key lookup this script already guards: silence that reads as a measurement. * docs: retract an A/B ratio whose config could not be recovered The recorded LINT0001 ratio came from a run driven by an out-of-repo `oxabl.toml` that is discovered by walking up from each analyzed file, so it never appeared on the command line, and was untracked. Two runs days apart used different include paths and nothing in the output said so. Re-measured against a pinned config, and the section now says which figure to cite and why the old one is gone. The instrument enforces this now rather than relying on the reader.
master
6 days ago

Latest Branches

CodSpeed Performance Gauge
-14%
feat: answer who depends on this, and what must rebuild#164
6 days ago
43df518
feat/reverse-dependency-edges
CodSpeed Performance Gauge
0%
6 days ago
b37dea2
feat/daemon-session-core
CodSpeed Performance Gauge
0%
6 days ago
8312cfa
fix/fragment-root-analysis
© 2026 CodSpeed Technology
Home Terms Privacy Docs