diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 48d2b409009..d11a118bf17 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -320,7 +320,11 @@ jobs: self_host_ratchet: if: github.event.pull_request.draft != true runs-on: ${{ vars.CI_RUNNER || 'ubuntu-latest' }} - timeout-minutes: 30 + # Cold ubuntu-latest can spend ~20–23m on the first `--release` v3-compiler + # compile inside `determinism_test`, then `self_host_fixed_point` touches the + # same crate graph again — 30m wall leaves no headroom (job cancellation mid- + # compile). Align headroom with the `v3` job budget. + timeout-minutes: 60 needs: [v3] continue-on-error: true steps: diff --git a/ROADMAP.md b/ROADMAP.md index 7be8f9cbf4e..54ce53e70fb 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -429,8 +429,8 @@ Tracked debt not dispatched with the 2026-04-21 lane wave (Lane 1e P3.0, SG-2c-4 - **Lane E-I — port `SubValueRelation` + inductive fields + cost algebra (M)**: port Tier 1 (structural) + Tier 2 (lattice) + Tier 3 (`CostBound` + master theorem). Requires E-T, E-C. Pre-flight: verify v3 lowers `CostBound`'s self-referential-through-`List` shape. Acceptance: carrier types ported with round-trip tests; no lens row moves on E-I alone because consumer-visible facts still wait for E-P producer/consumer wiring. - **Lane E-P — per-call descent-evidence provenance (M)**: first P-c side-table producer slice has landed as `v3_compiler::dag::per_call_descent_evidence` (recursive self-call / arithmetic descent; unproven edges fail closed; `TransformNode` stays unwidened). v2 stores this class of fact on `ExprCall.descent_evidence` (`src/v2/00_core.dag:199`); v3 derives it off-substrate for now. Requires E-T, E-C, E-I. Remaining acceptance: cost/complexity lenses consume the side table, plus a v2-oracle-vs-v3 cementing test against `expr_call_descent_evidence`; carrier parity for `cost.dag` / `complexity.dag` on the non-method-dispatch slice closes there. - **Lane E-M — `MethodSemantics` port-or-subsume (S–M)**: **closed via M-b structural subsumption.** Transitive facts (`CollectionSizeEffect` / `CostShape` / `AlgebraFieldTemplate`) remain algebra-declaration facts; v3 method-like calls dispatch through `TransformTarget::{Callable, FieldProject, Operator}` and typed declaration/effect metadata. No `src/v3/std/method_semantics.dag`, no Rust mirror, and no `ExprMethodCall` attachment point. -- **CI ratchet architecture — PARTIAL (T-Receipts W1 second bundle)**: surfaced during the 2026-04-21 reflective analysis (`53b3110..ae8825a` range). Commits `37cd6128`, `4898983e`, `f84ed355` hardened the ratchet against CI-log instability, but `2d8396df` widened the exemption list. `scripts/slow-test-exemptions.txt` currently has **39** active non-comment exemptions; `scripts/check-test-timeout.sh` meta-ratchet default `TEST_TIMEOUT_MAX_EXEMPTIONS` matches that count. **Landed partial:** classification audit [docs/debt/ci-ratchet-exemption-audit-2026-04-24.md](docs/debt/ci-ratchet-exemption-audit-2026-04-24.md) (2026-04-24 baseline) plus follow-up routing audit [docs/debt/ci-ratchet-exemption-audit-2026-05-02.md](docs/debt/ci-ratchet-exemption-audit-2026-05-02.md) (bounded scope — not a completed Phase 1 per-test timing pass). **Still open:** fresh CI-shaped per-test timing audit, stale exemption deletion if any entries measure under 2s, and per-exempt duration budgets once fresh timings exist. Dispatch brief: `docs/briefs/ci-ratchet-architecture-audit.md`. -- **`IntegrationRsScan` / `integration_rs_active_line_contains` in `src/v3/compiler/tests/integration/common/mod.rs` is a latent source of byte-constant workarounds in downstream tests** (T-Receipts, surfaced by T-Demo #686). The scanner is "deliberately narrow" and does not model Rust character literals, so any test author touching a file in its scan path cannot write `b'\\'` / `b'"'` directly and must substitute numeric byte constants (e.g., `92` / `34`) instead. Historical evidence: #686's text-slicing helper introduced `DAG_ESCAPE_BYTE` / `DAG_QUOTE_BYTE` constants for exactly this reason; #705 deleted the helper when `TestRunner::run_suite` obsoleted it, and the constants went with it — so there is **no surviving in-tree instance of the workaround today**. The scanner constraint itself remains, which means the next test author landing in the scan path will recreate the pattern. Dissolution: either widen the scanner to model Rust char literals, or replace its scan with a structural reader that doesn't need to exclude char-literal syntax. Small scope; cosmetic for now, but the constraint is a workaround attractor. Owner: unassigned; P2 Boundary Discipline / T-Receipts bundle. +- **CI ratchet architecture — PARTIAL (T-Receipts W1 second bundle)**: surfaced during the 2026-04-21 reflective analysis (`53b3110..ae8825a` range). Commits `37cd6128`, `4898983e`, `f84ed355` hardened the ratchet against CI-log instability, but `2d8396df` widened the exemption list. `scripts/slow-test-exemptions.txt` currently has **40** active non-comment exemptions; `scripts/check-test-timeout.sh` meta-ratchet default `TEST_TIMEOUT_MAX_EXEMPTIONS` matches that count. **Landed partial:** classification audit [docs/debt/ci-ratchet-exemption-audit-2026-04-24.md](docs/debt/ci-ratchet-exemption-audit-2026-04-24.md) (2026-04-24 baseline) plus follow-up routing audit [docs/debt/ci-ratchet-exemption-audit-2026-05-02.md](docs/debt/ci-ratchet-exemption-audit-2026-05-02.md) (bounded scope — not a completed Phase 1 per-test timing pass). **Still open:** fresh CI-shaped per-test timing audit, stale exemption deletion if any entries measure under 2s, and per-exempt duration budgets once fresh timings exist. Dispatch brief: `docs/briefs/ci-ratchet-architecture-audit.md`. +- **`IntegrationRsScan` / `integration_rs_active_line_contains` in `src/v3/compiler/tests/integration/common/mod.rs` — PARTIAL (T-Receipts; char-literal slice closed PR #1791).** Historical context (#686 / #705): the scanner was narrow enough that authors on its scan path avoided `b'\\'` / `b'"'` in favor of numeric byte constants. **Landed:** explicit `ByteCharLiteral` / `CharLiteral` states with escape-aware skipping plus lifetime vs `'…'` disambiguation and focused unit tests — byte-char and character literals no longer false-green Band-C `#[path` / `mod` needles. **Residual:** raw strings (`r#"…"#`) and byte strings (`b"…"`, `br#"…"#`) are still intentionally loud via the existing opener probe (panic in `Code`). Dissolution for that remainder: widen `IntegrationRsScan` for those forms **or** replace this scan with a structural reader. Owner: unassigned; P2 Boundary Discipline / T-Receipts bundle. - **Stale-receipt sweep in `docs/briefs/` — PARTIAL (T-Receipts W1).** The cited `DeclarationLookup` cleanup brief now carries a historical-receipt banner stating the lane is closed by SG-4b-1-fix and that `DeclarationLookup` / `find_declaration` are gone from `src/v3/lenses/variant_payload.dag`; its stale INVARIANTS refs were also updated to stable anchors. Remaining broad `docs/briefs/` sweep stays open for later post-wave stale claims. **Include:** `docs/design-pure-bootstrap.md`'s hand-maintained count ("78 hand-maintained .rs files") was stale vs. the SG-0 census — **CLOSED in this R1 PR**: all five occurrences (lines 7, 49, 82, 105, 242 of the design doc) now point at the live census, reframed as deltas against the illustrative 78-file baseline, or scoped to the non-test + TESTING residual framing. No frozen absolute count remains. - **Compiler–`std/` consolidation program — specific migrations**: end-state defined in [docs/thesis/compiler-std-consolidation.md](docs/thesis/compiler-std-consolidation.md). Each of the migrations below is a standalone lane; together they collapse the compiler-specific type surface toward the positive definition (pipeline + regen + lens-specific return-type carriers + accessor). **Ratchet:** count of `type` declarations in `src/v3/compiler/*.dag` AND `src/v3/lenses/*.dag` that are NOT in the positive-def set AND NOT exempted → 0. Positive-def: pipeline/regen types + lens-API return carriers (`Origin`, `UnusedParameter`, `VariantPayloadShapeLookup` 3-variant, `TemplateArgumentBinding` semantic carrier, etc.). Exempted: `parse_tables.dag` (7 types, pending SG-2c-proper per-row classification). In-ratchet: **workaround-shaped infer-helper coproducts with named dissolution triggers (`TemplateArgumentsMatch`, `TemplateArgumentCursor`, `NormalizedInstantiationArgs`)** — Lookup-pattern carriers on lens surfaces import `v3.std.lookup::Lookup` (`complexity`, `cost`, `infer_helpers`). Primary count: 3. See thesis doc table for per-file disposition. - **`tokenize.dag` → `std/tokenize.dag`** — landed: `Token`, `TokenKind`, `KeywordTokenKind`, `PunctTokenKind`, `LocalPunctSpec`, `StringEscapeSpec` now live in `src/v3/std/tokenize.dag`; compiler-local duplicates deleted. Ratchet drop: 25 → 19. diff --git a/scripts/check-test-timeout.sh b/scripts/check-test-timeout.sh index 928c4976c82..48667af3ad3 100755 --- a/scripts/check-test-timeout.sh +++ b/scripts/check-test-timeout.sh @@ -46,7 +46,7 @@ # (default scripts/slow-test-exemptions.txt). # TEST_TIMEOUT_MAX_EXEMPTIONS # Ratchet floor for active exemption entries -# (default 39, captured 2026-04-25). Lower this +# (default 40, captured 2026-05-05). Lower this # value in the same PR that removes exemptions. set -euo pipefail @@ -55,7 +55,7 @@ log_file_arg=${1:-} budget_ms=${2:-${TEST_TIMEOUT_MS:-2000}} pkg=${TEST_TIMEOUT_PACKAGE:-v3-compiler} exempt_file=${TEST_TIMEOUT_EXEMPT:-scripts/slow-test-exemptions.txt} -max_exemptions=${TEST_TIMEOUT_MAX_EXEMPTIONS:-39} +max_exemptions=${TEST_TIMEOUT_MAX_EXEMPTIONS:-40} script_dir=$(cd "$(dirname "$0")" && pwd) repo_root=$(cd "$script_dir/.." && pwd) diff --git a/scripts/slow-test-exemptions.txt b/scripts/slow-test-exemptions.txt index 8bf6d06e206..4f4c0bc78cd 100644 --- a/scripts/slow-test-exemptions.txt +++ b/scripts/slow-test-exemptions.txt @@ -72,6 +72,8 @@ bootstrap::tests::kernel_bool_path_a_attaches_diagnostic_when_boolean_algebra_un p0_std_render_repeat_string_test::std_render_repeat_string_and_indent_text_match_interpreter # ROADMAP P0 (`dsl/std/render.dag` `repeat_string` / `indent_text`): observed 29.7s on PR #595's merge commit. Structural blocker: v2 `compile_to_resolved` + interpreter oracle cold-compile the full resolver for every test; no shared bootstrap cache exists between v2-oracle tests and the v3 integration binary. Named paydown trigger: when a shared v2-oracle bootstrap cache lands (TM-0 shared-bootstrap lane or an earlier TESTING.md §shared-setup pass introducing a `OnceLock`), re-measure and remove this exemption. +t_demo_fixture_test::t_demo_canonical_suites_are_runner_visible # ROADMAP T-Demo (`tests/integration.rs`): cold `compile_to_dag` on `t_demo_fixtures.dag` plus two `TestRunner::run_suite` passes; observed ~2419ms on ubuntu-latest (PR #1791 CI). Paydown: warm shared `Dag` via `OnceLock` across `t_demo_fixture_test` or slim suite probes — TESTING.md § shared-setup / runner-cost lane. + sg1_tokenize_authority_test::tokenize_generated_module_matches_checked_in_snapshot # SG-1 authority snapshot still regenerates/parses full tokenize projection; tokenize authority lane owns future speedup. sg2_parse_authority_test::parse_surface_dag_compiles_cleanly_for_regen_parse # SG-2 parser-staging: `compile_parse_surface_std_authority_dag` + cold bootstrap work can exceed 2s wall under default libtest parallelism; ROADMAP SG-2 / TESTING.md paydown (same lane as `parse_generated_module_matches_checked_in_snapshot`). sg2c1_parse_tables_authority_test::parse_tables_generated_module_matches_checked_in_snapshot # SG-2c-1 grammar-tables prototype snapshot: in-process authority compile (parse_tables.dag + tokenize.dag) + rustfmt stdout; same paydown class as SG-1 tokenize / SG-2 parse snapshot gates. diff --git a/src/v3/compiler/tests/integration/common/mod.rs b/src/v3/compiler/tests/integration/common/mod.rs index 94162a6f05b..8db0b72d51d 100644 --- a/src/v3/compiler/tests/integration/common/mod.rs +++ b/src/v3/compiler/tests/integration/common/mod.rs @@ -108,12 +108,39 @@ fn code_opens_raw_or_byte_string_literal(bytes: &[u8], i: usize) -> bool { } } +/// Byte index after the last byte of a Rust escape sequence starting at `backslash` +/// (`bytes[backslash] == b'\\'`). Matches the suffix forms used in character and byte +/// literals (`\n`, `\xNN`, `\u{…}`, `\uXXXX`, …). +fn integration_rs_escape_sequence_end(bytes: &[u8], backslash: usize) -> usize { + debug_assert_eq!(bytes.get(backslash), Some(&b'\\')); + let i = backslash + 1; + if i >= bytes.len() { + return bytes.len(); + } + match bytes[i] { + b'x' => (backslash + 4).min(bytes.len()), + b'u' if bytes.get(i + 1) == Some(&b'{') => { + let mut j = i + 2; + while j < bytes.len() && bytes[j] != b'}' { + j += 1; + } + (j + 1).min(bytes.len()) + } + b'u' => (backslash + 6).min(bytes.len()), + _ => (backslash + 2).min(bytes.len()), + } +} + #[derive(Clone, Copy, PartialEq, Eq)] enum IntegrationRsScan { Code, LineComment, BlockComment(u32), String, + /// Inside `b'…'` after consuming the `b'` prefix. + ByteCharLiteral, + /// Inside `'…'` after consuming the opening `'`. + CharLiteral, } fn integration_rs_panic_on_raw_string(bytes: &[u8], i: usize) { @@ -126,7 +153,9 @@ fn integration_rs_panic_on_raw_string(bytes: &[u8], i: usize) { } /// Byte offsets of `needle` in `integration_rs` that occur in **Code** (outside -/// `//`, `/* */`, and `"…"` strings). See [`integration_rs_active_line_contains`]. +/// `//`, `/* */`, `"…"` strings, `b'…'` byte literals, and `'…'` character literals). +/// Lifetimes and labels (`'a`, `'static`) are skipped without entering string/literal +/// states. See [`integration_rs_active_line_contains`]. fn integration_rs_code_substring_positions(integration_rs: &str, needle: &str) -> Vec { let mut hits = Vec::new(); if needle.is_empty() { @@ -153,6 +182,34 @@ fn integration_rs_code_substring_positions(integration_rs: &str, needle: &str) - continue; } } + if bytes.get(i) == Some(&b'b') && bytes.get(i + 1) == Some(&b'\'') { + state = IntegrationRsScan::ByteCharLiteral; + i += 2; + continue; + } + if bytes[i] == b'\'' { + if bytes.get(i + 1) == Some(&b'\\') { + state = IntegrationRsScan::CharLiteral; + i += 1; + continue; + } + if let Some(rest) = integration_rs.get(i + 1..) { + let mut chars = rest.chars(); + if let Some(c) = chars.next() { + let after_char = i + 1 + c.len_utf8(); + if bytes.get(after_char) == Some(&b'\'') { + i = after_char + 1; + continue; + } + } + } + i += 1; + while i < bytes.len() && (bytes[i].is_ascii_alphanumeric() || bytes[i] == b'_') + { + i += 1; + } + continue; + } if bytes[i] == b'"' { state = IntegrationRsScan::String; i += 1; @@ -199,6 +256,37 @@ fn integration_rs_code_substring_positions(integration_rs: &str, needle: &str) - i += 1; } }, + IntegrationRsScan::ByteCharLiteral => { + if bytes.get(i) == Some(&b'\\') { + i = integration_rs_escape_sequence_end(bytes, i); + } else { + i += 1; + } + if bytes.get(i) == Some(&b'\'') { + i += 1; + } + state = IntegrationRsScan::Code; + } + IntegrationRsScan::CharLiteral => { + if bytes.get(i) == Some(&b'\\') { + i = integration_rs_escape_sequence_end(bytes, i); + } else if let Some(rest) = integration_rs.get(i..) { + let mut ci = rest.chars(); + if let Some(c) = ci.next() { + i += c.len_utf8(); + } else { + state = IntegrationRsScan::Code; + continue; + } + } else { + state = IntegrationRsScan::Code; + continue; + } + if bytes.get(i) == Some(&b'\'') { + i += 1; + } + state = IntegrationRsScan::Code; + } } } hits @@ -312,17 +400,18 @@ pub fn integration_rs_cementing_path_attr_binds_mod_stem(integration_rs: &str, s } /// True when `needle` appears in `integration_rs` **outside** Rust line comments -/// (`//` … including `///` / `//!`), **nested** block comments (`/* … */`), and -/// normal `"…"` string literals. +/// (`//` … including `///` / `//!`), **nested** block comments (`/* … */`), normal +/// `"…"` string literals, `b'…'` byte character literals, and `'…'` character +/// literals (excluding lifetimes / labels such as `'a` or `'static`). /// /// Band-C wiring ratchets use this so `#[path = …]` / `mod …;` matches cannot /// false-green on commented-out or string-embedded copies. Needles are ASCII /// (`#[path`, `mod foo`); the scan is byte-oriented on UTF-8 boundaries. /// -/// **Not handled:** raw strings (`r#"…"#`), byte strings (`b"…"`, `br#"…"#`), or -/// char literals — none appear in today’s `tests/integration.rs` module list. If -/// they do, extend `IntegrationRsScan` **or** the scan will **panic** when the -/// raw/byte-string opener probe fires in `Code` (loud failure vs a silent false green). +/// **Not handled:** raw strings (`r#"…"#`), byte strings (`b"…"`, `br#"…"#`). If +/// those appear in scanned source, extend `IntegrationRsScan` **or** the scan will +/// **panic** when the raw/byte-string opener probe fires in `Code` (loud failure vs +/// a silent false green). pub fn integration_rs_active_line_contains(integration_rs: &str, needle: &str) -> bool { !integration_rs_code_substring_positions(integration_rs, needle).is_empty() } @@ -537,3 +626,68 @@ macro_rules! budgeted_test { } }; } + +#[cfg(test)] +mod integration_rs_scan_tests { + use super::integration_rs_active_line_contains; + + #[test] + fn integration_rs_scan_skips_byte_char_literals_so_path_ratchet_still_sees_mod_list() { + let src = concat!( + "fn demo_bytes() {\n", + "let _slash = b'\\\\';\n", + "let _quote = b'\"';\n", + "let _apos = b'\\'';\n", + "}\n", + "#[path = \"integration/cementing/real.rs\"]\n", + "mod real;\n", + ); + assert!(integration_rs_active_line_contains( + src, + r#"#[path = "integration/cementing/real.rs"]"#, + )); + } + + #[test] + fn integration_rs_scan_skips_char_literal_containing_double_quote() { + let src = concat!( + "let _c = '\"';\n", + "#[path = \"integration/cementing/real.rs\"]\n", + "mod real;\n", + ); + assert!(integration_rs_active_line_contains( + src, + r#"#[path = "integration/cementing/real.rs"]"#, + )); + } + + #[test] + fn integration_rs_scan_accepts_path_after_lifetime() { + let src = r#" +fn foo<'a>() { +#[path = "integration/cementing/real.rs"] +mod real; +} +"#; + assert!(integration_rs_active_line_contains( + src, + r#"#[path = "integration/cementing/real.rs"]"#, + )); + } + + #[test] + fn integration_rs_scan_does_not_match_needle_inside_byte_char_literal() { + assert!(!integration_rs_active_line_contains( + concat!("let _ = b'#';", "\n"), + "#", + )); + assert!(integration_rs_active_line_contains( + concat!( + "let _ = b'#';", + "\n#[path = \"integration/cementing/real.rs\"]\n", + "mod real;\n", + ), + "#[path", + )); + } +}