From 5970e95e0db63918780a6979642042dfb51cf8be Mon Sep 17 00:00:00 2001 From: simontreanor Date: Fri, 31 Jul 2026 17:46:53 +0100 Subject: [PATCH] stdlib: state the cost on every traversing member, and test it The sweep left a handful of members without a complexity: List.range (which builds a list where Seq.range does not), iter, init, replicate and chunkBySize, plus five Seq members whose lazy-or-consuming half was implied rather than said. The test is the durable part. Every member of List, Seq, Set, Map and String must now state a cost, in big-O or in what it consumes, with a four-name exemption list for the genuine constants. Option and Result hold one value and are uniformly O(1), and Format, Decode and the globals are not traversals, so those modules are not held to it, which the test says in its own documentation so the omission reads as a decision. --- src/types/mod.rs | 64 ++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 54 insertions(+), 10 deletions(-) diff --git a/src/types/mod.rs b/src/types/mod.rs index 7edce0f..99506a5 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -677,7 +677,7 @@ pub const MEMBER_DOCS: &[(&str, &str)] = &[ ("List.rev", "A fresh list in the opposite order. O(n)."), ( "List.range", - "The integers from the first bound up to but not including the second.", + "The integers from the first bound up to but not including the second. O(n) — it builds the list, where `Seq.range` does not.", ), ( "List.zip", @@ -740,7 +740,7 @@ pub const MEMBER_DOCS: &[(&str, &str)] = &[ ("List.indexed", "Pair every element with its index. O(n)."), ( "List.iter", - "Run a function for its effect on every element, giving unit. Carries the function's effect.", + "Run a function for its effect on every element, giving unit. O(n), and carries the function's effect.", ), ( "List.exists", @@ -816,9 +816,9 @@ pub const MEMBER_DOCS: &[(&str, &str)] = &[ ), ( "List.init", - "Build a list of n elements from their indices.", + "Build a list of n elements from their indices. O(n).", ), - ("List.replicate", "A list of n copies of one value."), + ("List.replicate", "A list of n copies of one value. O(n)."), ( "List.updateAt", "A fresh list with one index replaced. O(n) — it copies; an index outside the list changes nothing.", @@ -841,7 +841,7 @@ pub const MEMBER_DOCS: &[(&str, &str)] = &[ ), ( "List.chunkBySize", - "Consecutive chunks of the given size, the last one short if it does not divide evenly.", + "Consecutive chunks of the given size, the last one short if it does not divide evenly. O(n).", ), ("Set.isEmpty", "Whether the set has no elements. O(1)."), ( @@ -1079,11 +1079,11 @@ pub const MEMBER_DOCS: &[(&str, &str)] = &[ ), ( "Seq.initInfinite", - "An endless sequence built from the indices 0, 1, 2, … Pair it with `Seq.take`.", + "An endless sequence built from the indices 0, 1, 2, …, lazily. Pair it with `Seq.take`.", ), ( "Seq.unfold", - "Generate from a state until the function answers `None`. The lazy way to build a sequence with no list behind it.", + "Generate from a state until the function answers `None`, lazily. The way to build a sequence with no list behind it.", ), ( "Seq.head", @@ -1129,13 +1129,16 @@ pub const MEMBER_DOCS: &[(&str, &str)] = &[ ), ( "Seq.fold", - "Combine every element into an accumulator. Forces the sequence.", + "Combine every element into an accumulator. Consumes the whole sequence.", ), ( "Seq.toList", - "Force the sequence into a list. Does not terminate on an endless one.", + "Force the sequence into a list. Consumes the whole sequence, and does not terminate on an endless one.", + ), + ( + "Seq.ofList", + "View a list as a sequence. O(1) — no copy, and the result is single-pass.", ), - ("Seq.ofList", "View a list as a sequence."), ( "Seq.range", "The integers from the first bound up to but not including the second, lazily.", @@ -8436,6 +8439,47 @@ mod member_doc_tests { assert!(stale.is_empty(), "documented but not a member: {stale:?}"); } + /// The modules whose members traverse something, so a reader needs the cost. + /// `Option`/`Result` hold one value and are uniformly O(1); `Format`/`Decode` + /// and the globals are not traversals either, so none are held to this. + const COST_BEARING: &[&str] = &["List", "Seq", "Set", "Map", "String"]; + + /// Members of those modules with genuinely nothing to say: a constant, or a + /// conversion whose cost is the number of digits. + const NO_COST: &[&str] = &[ + "Set.empty", + "Map.empty", + "String.fromInt", + "String.fromFloat", + ]; + + #[test] + fn every_traversing_member_states_its_cost() { + // Documenting a collection member exists because a `List` backed by a Python + // list makes cost easy to misjudge, so a new one must say what it costs — in + // big-O, or (for `Seq`) in what it consumes. + let states_cost = |doc: &str| { + doc.contains("O(") + || doc.contains("lazily") + || doc.contains("Consumes") + || doc.contains("Remembers") + }; + let missing: Vec<&str> = MEMBER_DOCS + .iter() + .filter(|(name, _)| { + name.split_once('.') + .is_some_and(|(m, _)| COST_BEARING.contains(&m)) + && !NO_COST.contains(name) + }) + .filter(|(_, doc)| !states_cost(doc)) + .map(|(name, _)| *name) + .collect(); + assert!( + missing.is_empty(), + "members with no stated cost: {missing:?}" + ); + } + #[test] fn every_builtin_member_has_a_signature() { let sigs = prelude_signatures();