test: prune duplicate tests, re-sight two blind coverage guards
Follow-up to a fan-out audit of the whole test suite (742 tests, 21
read-only assessors + synthesis). Two clusters of the audit were
actioned here; the #66 hashing-removal cohort it surfaced is left for
the #66 scope, and the RED-first doc-rot sweep is deferred.
§2 — true duplicates removed (each kept test is a strict superset or
identical fixture+assertions of the deleted one):
- ailang-check param_in_reject_message_names_allowed_set (kept the
_renders_allowed_set_in_ailang_syntax superset)
- ailang-check cross_module_pat_ctor_typedriven_resolves (kept ct2_;
shared cross_module_ws helper retained, 2 other callers)
- ailang-core mq1_qualified_instancedef_class_accepted (kept ct1_)
- ail e2e bool_to_str_emits_true_branch (kept the rc-stats superset)
- ail loop_recur_str adt-leg guard (the standalone heap pin owns it;
dropped the dangling header reference too)
- ailang-check over_strict_mode_silent_when_param_is_borrow (its
assertion is subsumed by explicit_fn_with_no_uses_is_clean; its own
doc claimed a "borrowing body" the Lit body never provided)
§4 — meta-guards that had gone partially blind re-sighted:
- spec_drift::spec_mentions_every_def_kind now pushes Class/Instance
exemplars and asserts their `(class `/`(instance` anchors; the
"wait for 22b.4" exclusion was stale (anchors shipped in e809f45).
- schema_coverage now tracks Term::New (VariantTag + EXPECTED +
visitor insert); the new_*/raw_buf_* fixtures already exercise it,
so the "no fixture emits new" exclusion was stale.
- prose doc_wrap_widow_control_skips_when_combined_too_long was
tautological: its 32/32/42-char words gave combined=75<=80, so
widow control actually fired. Rebuilt the fixture (short head + two
40-char words → combined=81>80) so it hits the real skip branch,
and added the missing "last line stays 1 word" assertion. The
obvious 2-word fixture does NOT work — it skips via the
prev_words<2 branch, not the combined-over-budget branch.
ailang-check 124->121, ailang-core 55->54, e2e 102->101,
loop_recur_str 4->3; spec_drift 8 green, schema_coverage green,
prose lib 63 green.
This commit is contained in:
@@ -3039,16 +3039,6 @@ fn bool_to_str_drop_balances_rc_stats() {
|
|||||||
assert_eq!(live, 0, "no leaked heap-Str slabs allowed; live={live}");
|
assert_eq!(live, 0, "no leaked heap-Str slabs allowed; live={live}");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// stdout-smoke for the true branch — same fixture as
|
|
||||||
/// `bool_to_str_drop_balances_rc_stats` but checked here without
|
|
||||||
/// the RC-stats overhead. Pins the byte content of `ailang_bool_to_str`'s
|
|
||||||
/// "true" slab.
|
|
||||||
#[test]
|
|
||||||
fn bool_to_str_emits_true_branch() {
|
|
||||||
let out = build_and_run("bool_to_str_drop_rc.ail");
|
|
||||||
assert_eq!(out, "true\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// stdout-smoke for the false branch. Pins the byte
|
/// stdout-smoke for the false branch. Pins the byte
|
||||||
/// content of `ailang_bool_to_str`'s "false" slab.
|
/// content of `ailang_bool_to_str`'s "false" slab.
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -31,10 +31,6 @@
|
|||||||
//! `ailang_rc_stats: allocs=4 frees=1 live=3` — the three superseded Str
|
//! `ailang_rc_stats: allocs=4 frees=1 live=3` — the three superseded Str
|
||||||
//! slabs (seed "x" plus two intermediate concat results) leak while
|
//! slabs (seed "x" plus two intermediate concat results) leak while
|
||||||
//! stdout is already correct (`xyyy`).
|
//! stdout is already correct (`xyyy`).
|
||||||
//!
|
|
||||||
//! This file also re-pins the boxed-ADT leg (`alloc_rc_adt_*`) as a
|
|
||||||
//! GREEN guard: the f488d31 fix must stay green while the Str leg is
|
|
||||||
//! closed — fixing Str must not regress the ADT case.
|
|
||||||
|
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
@@ -131,28 +127,6 @@ fn alloc_rc_str_loop_binder_replaced_by_recur_does_not_leak() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// GREEN guard: the boxed-ADT leg fixed in f488d31 must stay leak-clean
|
|
||||||
/// while the Str leg is closed. Fixing Str must not regress this.
|
|
||||||
#[test]
|
|
||||||
fn alloc_rc_adt_loop_binder_replaced_by_recur_stays_leak_clean() {
|
|
||||||
let (allocs, frees, live, stdout) =
|
|
||||||
build_run_stats("loop_recur_heap_binder_no_leak_pin.ail");
|
|
||||||
assert_eq!(
|
|
||||||
stdout.trim_end(),
|
|
||||||
"2",
|
|
||||||
"ADT loop result mis-built (allocs={allocs} frees={frees} live={live})"
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
live, 0,
|
|
||||||
"the f488d31 boxed-ADT recur-dec regressed: live={live} \
|
|
||||||
(allocs={allocs} frees={frees})"
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
allocs, frees,
|
|
||||||
"ADT leg alloc/free mismatch (allocs={allocs} frees={frees} live={live})"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// mir.4 soundness, recur-arg leg: a Str loop binder whose `recur`
|
/// mir.4 soundness, recur-arg leg: a Str loop binder whose `recur`
|
||||||
/// rebinds it to a fresh static LITERAL each iteration. The seed-only
|
/// rebinds it to a fresh static LITERAL each iteration. The seed-only
|
||||||
/// promotion would miss this — the recur literal "reset" must also be
|
/// promotion would miss this — the recur literal "reset" must also be
|
||||||
|
|||||||
@@ -6366,48 +6366,6 @@ mod tests {
|
|||||||
assert!(diags.is_empty(), "expected green; got {diags:?}");
|
assert!(diags.is_empty(), "expected green; got {diags:?}");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// a bare `Pattern::Ctor` (e.g. `MkBox x`)
|
|
||||||
/// against a scrutinee of type `lib.Box<Int>` resolves through
|
|
||||||
/// the type-driven lookup keyed on `expected.name`. Previously
|
|
||||||
/// this exercised an imports-fallback; post-canonical-type-form the lookup is
|
|
||||||
/// anchored to the scrutinee's qualified TypeDef directly.
|
|
||||||
#[test]
|
|
||||||
fn cross_module_pat_ctor_typedriven_resolves() {
|
|
||||||
let consumer = Module {
|
|
||||||
schema: SCHEMA.into(),
|
|
||||||
name: "use_lib".into(),
|
|
||||||
kernel: false,
|
|
||||||
imports: vec![Import { module: "lib".into(), alias: None }],
|
|
||||||
defs: vec![fn_def(
|
|
||||||
"open",
|
|
||||||
Type::Fn {
|
|
||||||
params: vec![Type::Con {
|
|
||||||
name: "lib.Box".into(),
|
|
||||||
args: vec![Type::int()],
|
|
||||||
}],
|
|
||||||
ret: Box::new(Type::int()),
|
|
||||||
effects: vec![],
|
|
||||||
param_modes: vec![],
|
|
||||||
ret_mode: ParamMode::Own,
|
|
||||||
},
|
|
||||||
vec!["b"],
|
|
||||||
Term::Match {
|
|
||||||
scrutinee: Box::new(Term::Var { name: "b".into() }),
|
|
||||||
arms: vec![Arm {
|
|
||||||
pat: Pattern::Ctor {
|
|
||||||
ctor: "MkBox".into(),
|
|
||||||
fields: vec![Pattern::Var { name: "x".into() }],
|
|
||||||
},
|
|
||||||
body: Term::Var { name: "x".into() },
|
|
||||||
}],
|
|
||||||
},
|
|
||||||
)],
|
|
||||||
};
|
|
||||||
let ws = cross_module_ws(consumer);
|
|
||||||
let diags = check_workspace(&ws);
|
|
||||||
assert!(diags.is_empty(), "expected green; got {diags:?}");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// a recursive cross-module ADT (`std_list.List a` with a
|
/// a recursive cross-module ADT (`std_list.List a` with a
|
||||||
/// `Cons a (List a)` ctor) round-trips through both `Term::Ctor` synth
|
/// `Cons a (List a)` ctor) round-trips through both `Term::Ctor` synth
|
||||||
/// and pattern-ctor binding without unqualified-field-name unification
|
/// and pattern-ctor binding without unqualified-field-name unification
|
||||||
@@ -8527,51 +8485,6 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// fieldtest B3 (raw-buf, #7): the rendered `param-not-in-restricted-set`
|
|
||||||
/// message must name the *allowed set*, not only the offending type.
|
|
||||||
/// Property per design/models/0007-kernel-extensions.md §4
|
|
||||||
/// ("ParamNotInRestrictedSet names the offending type AND the allowed
|
|
||||||
/// set"): a downstream author rejected for `(con StubT (con Str))`
|
|
||||||
/// under `param-in = {"a": {"Int", "Float"}}` must be told `Int` and
|
|
||||||
/// `Float` are the permitted choices, so the message is actionable.
|
|
||||||
#[test]
|
|
||||||
fn param_in_reject_message_names_allowed_set() {
|
|
||||||
let m: Module = serde_json::from_value(serde_json::json!({
|
|
||||||
"schema": SCHEMA,
|
|
||||||
"name": "k",
|
|
||||||
"defs": [
|
|
||||||
{"kind": "type", "name": "StubT", "vars": ["a"],
|
|
||||||
"ctors": [{"name": "Stub", "fields": [{"k": "var", "name": "a"}]}],
|
|
||||||
"param-in": {"a": ["Int", "Float"]}},
|
|
||||||
{"kind": "fn", "name": "bad",
|
|
||||||
"type": {"k": "fn", "params": [], "param_modes": [],
|
|
||||||
"ret": {"k": "con", "name": "StubT",
|
|
||||||
"args": [{"k": "con", "name": "Str"}]}, "ret_mode": "own",
|
|
||||||
"effects": []},
|
|
||||||
"params": [],
|
|
||||||
"body": {"t": "lit", "lit": {"kind": "unit"}}}
|
|
||||||
]
|
|
||||||
})).unwrap();
|
|
||||||
let mut modules = BTreeMap::new();
|
|
||||||
modules.insert("k".to_string(), m);
|
|
||||||
let ws = Workspace {
|
|
||||||
entry: "k".into(),
|
|
||||||
modules,
|
|
||||||
root_dir: std::path::PathBuf::from("."),
|
|
||||||
registry: ailang_core::workspace::Registry::default(),
|
|
||||||
};
|
|
||||||
let diags = check_workspace(&ws);
|
|
||||||
let d = diags
|
|
||||||
.iter()
|
|
||||||
.find(|d| d.code == "param-not-in-restricted-set")
|
|
||||||
.unwrap_or_else(|| panic!("expected param-not-in-restricted-set, got {diags:?}"));
|
|
||||||
assert!(
|
|
||||||
d.message.contains("Int") && d.message.contains("Float"),
|
|
||||||
"message must name the allowed set {{Int, Float}}, got: {:?}",
|
|
||||||
d.message
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// #52: the allowed set in `param-not-in-restricted-set` must render in
|
/// #52: the allowed set in `param-not-in-restricted-set` must render in
|
||||||
/// AILang type syntax — a brace-wrapped, comma-separated set of bare
|
/// AILang type syntax — a brace-wrapped, comma-separated set of bare
|
||||||
/// type names (`{Float, Int}`) — not a Rust debug list
|
/// type names (`{Float, Int}`) — not a Rust debug list
|
||||||
|
|||||||
@@ -2104,30 +2104,6 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// a fn with `(borrow T)` and a borrowing
|
|
||||||
/// body never fires `over-strict-mode` (the lint is gated on
|
|
||||||
/// `Own`).
|
|
||||||
#[test]
|
|
||||||
fn over_strict_mode_silent_when_param_is_borrow() {
|
|
||||||
// Body shape: literal — no use of the param at all.
|
|
||||||
let m = Module {
|
|
||||||
schema: ailang_core::SCHEMA.into(),
|
|
||||||
name: "t".into(),
|
|
||||||
kernel: false,
|
|
||||||
imports: vec![],
|
|
||||||
defs: vec![fn_with_modes(
|
|
||||||
"f",
|
|
||||||
vec![ParamMode::Borrow],
|
|
||||||
Term::Lit { lit: Literal::Int { value: 0 } },
|
|
||||||
)],
|
|
||||||
};
|
|
||||||
let diags = check_module(&m);
|
|
||||||
assert!(
|
|
||||||
!diags.iter().any(|d| d.code == "over-strict-mode"),
|
|
||||||
"no over-strict-mode expected for borrow-mode param; got {diags:?}"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// a fn with `(own T)` whose body is
|
/// a fn with `(own T)` whose body is
|
||||||
/// `(match p0 ...)` and never consumes `p0` directly nor any of
|
/// `(match p0 ...)` and never consumes `p0` directly nor any of
|
||||||
/// `p0`'s sub-binders DOES fire `over-strict-mode` under the
|
/// `p0`'s sub-binders DOES fire `over-strict-mode` under the
|
||||||
|
|||||||
@@ -2638,40 +2638,6 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// a qualified `InstanceDef.class` referencing a class in
|
|
||||||
/// an imported module is the canonical form post-canonical-class-form and is
|
|
||||||
/// accepted by `validate_canonical_type_names`. Symmetric to the canonical-form rule's
|
|
||||||
/// "qualified Type::Con resolves" positive path.
|
|
||||||
#[test]
|
|
||||||
fn mq1_qualified_instancedef_class_accepted() {
|
|
||||||
let a: Module = serde_json::from_value(serde_json::json!({
|
|
||||||
"schema": crate::SCHEMA,
|
|
||||||
"name": "A",
|
|
||||||
"imports": [],
|
|
||||||
"defs": [{
|
|
||||||
"kind": "class",
|
|
||||||
"name": "Show",
|
|
||||||
"param": "a",
|
|
||||||
"methods": []
|
|
||||||
}]
|
|
||||||
})).unwrap();
|
|
||||||
let b: Module = serde_json::from_value(serde_json::json!({
|
|
||||||
"schema": crate::SCHEMA,
|
|
||||||
"name": "B",
|
|
||||||
"imports": [{ "module": "A" }],
|
|
||||||
"defs": [{
|
|
||||||
"kind": "instance",
|
|
||||||
"class": "A.Show",
|
|
||||||
"type": { "k": "con", "name": "Int" },
|
|
||||||
"methods": []
|
|
||||||
}]
|
|
||||||
})).unwrap();
|
|
||||||
let mut modules = BTreeMap::new();
|
|
||||||
modules.insert("A".to_string(), a);
|
|
||||||
modules.insert("B".to_string(), b);
|
|
||||||
validate_canonical_type_names(&modules, &["prelude"]).expect("qualified A.Show must be accepted");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// pd.1 Task 1: `load_modules_with` returns modules without injecting
|
/// pd.1 Task 1: `load_modules_with` returns modules without injecting
|
||||||
/// prelude. The prelude inject is the caller's responsibility (the
|
/// prelude. The prelude inject is the caller's responsibility (the
|
||||||
/// shim `load_workspace_with` does it in pd.1; surface owns it from
|
/// shim `load_workspace_with` does it in pd.1; surface owns it from
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ enum VariantTag {
|
|||||||
TermReuseAs,
|
TermReuseAs,
|
||||||
TermLoop,
|
TermLoop,
|
||||||
TermRecur,
|
TermRecur,
|
||||||
|
TermNew,
|
||||||
TermIntrinsic,
|
TermIntrinsic,
|
||||||
// Pattern
|
// Pattern
|
||||||
PatternWild,
|
PatternWild,
|
||||||
@@ -97,6 +98,7 @@ const EXPECTED_VARIANTS: &[VariantTag] = &[
|
|||||||
VariantTag::TermReuseAs,
|
VariantTag::TermReuseAs,
|
||||||
VariantTag::TermLoop,
|
VariantTag::TermLoop,
|
||||||
VariantTag::TermRecur,
|
VariantTag::TermRecur,
|
||||||
|
VariantTag::TermNew,
|
||||||
VariantTag::TermIntrinsic,
|
VariantTag::TermIntrinsic,
|
||||||
VariantTag::PatternWild,
|
VariantTag::PatternWild,
|
||||||
VariantTag::PatternVar,
|
VariantTag::PatternVar,
|
||||||
@@ -248,13 +250,9 @@ fn visit_term(t: &Term, observed: &mut HashSet<VariantTag>) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Term::New { args, .. } => {
|
Term::New { args, .. } => {
|
||||||
// prep.2 (kernel-extension-mechanics): functional construction.
|
// Functional construction. Exercised by the `new_*` /
|
||||||
// The variant is intentionally NOT added to `VariantTag` /
|
// `raw_buf_*` fixtures under examples/.
|
||||||
// `EXPECTED_VARIANTS` in prep.2 — no .ail fixture in the
|
observed.insert(VariantTag::TermNew);
|
||||||
// current corpus emits `"t": "new"` (per the iter's
|
|
||||||
// out-of-scope note), so an EXPECTED entry would fail the
|
|
||||||
// coverage check. A future milestone that ships a Term::New
|
|
||||||
// fixture extends both the enum and the expected list.
|
|
||||||
for arg in args {
|
for arg in args {
|
||||||
match arg {
|
match arg {
|
||||||
NewArg::Type(t) => visit_type(t, observed),
|
NewArg::Type(t) => visit_type(t, observed),
|
||||||
|
|||||||
@@ -14,7 +14,8 @@
|
|||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
use ailang_core::ast::{
|
use ailang_core::ast::{
|
||||||
ConstDef, Ctor, Def, FnDef, Literal, NewArg, Pattern, Suppress, Term, Type, TypeDef,
|
ClassDef, ConstDef, Ctor, Def, FnDef, InstanceDef, Literal, NewArg, Pattern, Suppress, Term,
|
||||||
|
Type, TypeDef,
|
||||||
};
|
};
|
||||||
use ailang_core::FORM_A_SPEC;
|
use ailang_core::FORM_A_SPEC;
|
||||||
|
|
||||||
@@ -293,10 +294,27 @@ fn spec_mentions_every_def_kind() {
|
|||||||
drop_iterative: false,
|
drop_iterative: false,
|
||||||
param_in: BTreeMap::new(),
|
param_in: BTreeMap::new(),
|
||||||
};
|
};
|
||||||
|
let class_def = ClassDef {
|
||||||
|
name: "C".into(),
|
||||||
|
param: "a".into(),
|
||||||
|
superclass: None,
|
||||||
|
methods: vec![],
|
||||||
|
doc: None,
|
||||||
|
};
|
||||||
|
let instance_def = InstanceDef {
|
||||||
|
class: "C".into(),
|
||||||
|
type_: Type::int(),
|
||||||
|
methods: vec![],
|
||||||
|
doc: None,
|
||||||
|
};
|
||||||
let exemplars: Vec<(&str, Def)> = vec![
|
let exemplars: Vec<(&str, Def)> = vec![
|
||||||
("(fn ", Def::Fn(fn_def)),
|
("(fn ", Def::Fn(fn_def)),
|
||||||
("(const ", Def::Const(const_def)),
|
("(const ", Def::Const(const_def)),
|
||||||
("(data ", Def::Type(type_def)),
|
("(data ", Def::Type(type_def)),
|
||||||
|
// class/instance gained prose projection in e809f45 (form-a.tidy):
|
||||||
|
// FORM_A_SPEC now carries `(class ` / `(instance ` anchors.
|
||||||
|
("(class ", Def::Class(class_def)),
|
||||||
|
("(instance", Def::Instance(instance_def)),
|
||||||
];
|
];
|
||||||
|
|
||||||
for (anchor, def) in exemplars {
|
for (anchor, def) in exemplars {
|
||||||
@@ -304,12 +322,6 @@ fn spec_mentions_every_def_kind() {
|
|||||||
Def::Fn(_) => "fn",
|
Def::Fn(_) => "fn",
|
||||||
Def::Const(_) => "const",
|
Def::Const(_) => "const",
|
||||||
Def::Type(_) => "type",
|
Def::Type(_) => "type",
|
||||||
// class/instance Def variants exist but are
|
|
||||||
// not yet anchored in the prose-spec block. Once 22b.4
|
|
||||||
// adds prose projection for them, the FORM_A_SPEC text
|
|
||||||
// gains `(class ` / `(instance ` anchors and this match
|
|
||||||
// will be exercised. For 22b.1 the exemplars list above
|
|
||||||
// does not produce these variants.
|
|
||||||
Def::Class(_) => "class",
|
Def::Class(_) => "class",
|
||||||
Def::Instance(_) => "instance",
|
Def::Instance(_) => "instance",
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2238,20 +2238,31 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn doc_wrap_widow_control_skips_when_combined_too_long() {
|
fn doc_wrap_widow_control_skips_when_combined_too_long() {
|
||||||
// If the combined length of (pulled + " " + orphan) would
|
// Widow control pulls the penultimate line's tail word down to
|
||||||
// exceed budget, widow control must NOT fire — preserving
|
// join a 1-word orphan — but only when (pulled + " " + orphan)
|
||||||
// the wrap budget invariant takes priority over cosmetics.
|
// still fits the budget. When it does NOT fit, the budget
|
||||||
// Construct a case: prev-tail = a long word, orphan = a long
|
// invariant wins and the orphan stays a 1-word last line.
|
||||||
// word — combined > 80.
|
//
|
||||||
let pieces = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb \
|
// Construct that case at budget 80: a short head word forces the
|
||||||
cccccccccccccccccccccccccccccccccccccccccc";
|
// penultimate line to hold two words ("tail" + 40×'b'), and the
|
||||||
let lines = wrap_words(pieces, 80);
|
// orphan is a 40×'c' word. Greedy wrap yields
|
||||||
|
// ["tail bbb…"(45), "ccc…"(40)]; the merge candidate is
|
||||||
|
// pulled(40) + 1 + orphan(40) = 81 > 80, so widow control must
|
||||||
|
// skip and the last line keeps its single word.
|
||||||
|
let pieces = format!("tail {} {}", "b".repeat(40), "c".repeat(40));
|
||||||
|
let lines = wrap_words(&pieces, 80);
|
||||||
// Sanity: the wrap produced multiple lines.
|
// Sanity: the wrap produced multiple lines.
|
||||||
assert!(lines.len() >= 2);
|
assert!(lines.len() >= 2);
|
||||||
// No line exceeds budget.
|
// No line exceeds budget.
|
||||||
for l in &lines {
|
for l in &lines {
|
||||||
assert!(l.len() <= 80, "line over budget: {l:?}");
|
assert!(l.len() <= 80, "line over budget: {l:?}");
|
||||||
}
|
}
|
||||||
|
// Widow control did NOT fire: the orphan stays a 1-word last line.
|
||||||
|
assert_eq!(
|
||||||
|
lines.last().unwrap().split_whitespace().count(),
|
||||||
|
1,
|
||||||
|
"widow control should have skipped (merge over budget); got {lines:?}"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- Polish 5: nested match across lines ----
|
// ---- Polish 5: nested match across lines ----
|
||||||
|
|||||||
Reference in New Issue
Block a user