From 84f7d9a2f22c4576db3098604b4253879bf632c6 Mon Sep 17 00:00:00 2001 From: Tim Fennis Date: Fri, 13 Nov 2026 22:11:08 +0100 Subject: [PATCH] =?UTF-8?q?feat(stdlib):=20add=20panic,=20and=20type=20com?= =?UTF-8?q?binations,=20permutations=20and=20find=5Fsubsequence=20?= =?UTF-8?q?=F0=9F=93=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- ndc_stdlib/src/cmp.rs | 7 ++++ ndc_stdlib/src/list.rs | 39 +++++++++++++------ ndc_stdlib/src/sequence.rs | 10 ++--- ndc_vm/src/iterator.rs | 4 +- .../005_functions/107_panic_never_returns.ndc | 8 ++++ .../programs/008_iterators/005_take.ndc | 2 +- .../603_stdlib_seq/011_combinations.ndc | 8 ++-- ...tions_and_permutations_are_typed_lists.ndc | 4 ++ ..._find_subsequence_has_an_optional_form.ndc | 4 ++ .../bug0022_combinations_lazy_source.ndc | 18 ++++----- .../900_bugs/bug0027_combinations_huge_k.ndc | 2 +- 11 files changed, 72 insertions(+), 34 deletions(-) create mode 100644 tests/functional/programs/005_functions/107_panic_never_returns.ndc create mode 100644 tests/functional/programs/603_stdlib_seq/030_combinations_and_permutations_are_typed_lists.ndc create mode 100644 tests/functional/programs/603_stdlib_seq/031_find_subsequence_has_an_optional_form.ndc diff --git a/ndc_stdlib/src/cmp.rs b/ndc_stdlib/src/cmp.rs index 0f66e369..8d1e2c86 100644 --- a/ndc_stdlib/src/cmp.rs +++ b/ndc_stdlib/src/cmp.rs @@ -16,6 +16,13 @@ mod inner { } } + /// Stops the program with the error `message`. It never returns, so it + /// can end a branch whose other branches give a value. + #[function(signature = "(String) -> Never")] + pub fn panic(message: &str) -> anyhow::Result { + Err(anyhow!(message.to_string())) + } + /// Produces an error if the arguments aren't equal to each other. #[function(signature = "(A, B)")] pub fn assert_eq(left: Value, right: Value) -> anyhow::Result<()> { diff --git a/ndc_stdlib/src/list.rs b/ndc_stdlib/src/list.rs index 6d847b30..eff81f22 100644 --- a/ndc_stdlib/src/list.rs +++ b/ndc_stdlib/src/list.rs @@ -30,18 +30,23 @@ mod inner { list.windows(subsequence.len()).contains(&subsequence) } - /// Returns the starting index of the first occurrence of `subsequence` in `list`, or unit if not found. - #[function(signature = "(List, List) -> Any")] - pub fn find_subsequence(list: &[Value], subsequence: &[Value]) -> Value { - let result = list - .windows(subsequence.len()) - .enumerate() - .find(|(_, seq)| *seq == subsequence) - .map(|(idx, _)| idx); - if let Some(result) = result { - Value::Int(result as i64) - } else { - Value::unit() + /// Returns the starting index of the first occurrence of `subsequence` in `list`, or produces an error if there is none. + #[function(signature = "(List, List) -> Int")] + pub fn find_subsequence(list: &[Value], subsequence: &[Value]) -> anyhow::Result { + subsequence_start(list, subsequence) + .map(|idx| Value::Int(idx as i64)) + .ok_or_else(|| anyhow!("the list doesn't contain the subsequence")) + } + + /// Returns the starting index of the first occurrence of `subsequence` in `list`, or `None` if there is none. + #[function( + name = "find_subsequence?", + signature = "(List, List) -> Option" + )] + pub fn maybe_find_subsequence(list: &[Value], subsequence: &[Value]) -> Value { + match subsequence_start(list, subsequence) { + None => Value::None, + Some(idx) => Value::Object(Rc::new(Object::Some(Value::Int(idx as i64)))), } } @@ -390,3 +395,13 @@ pub mod ops { env.declare_global_fn(native); } } + +/// Where `subsequence` first starts in `list`, if it's in it. +fn subsequence_start(list: &[Value], subsequence: &[Value]) -> Option { + // An empty subsequence starts anywhere, and `windows` can't be empty. + if subsequence.is_empty() { + return Some(0); + } + list.windows(subsequence.len()) + .position(|window| window == subsequence) +} diff --git a/ndc_stdlib/src/sequence.rs b/ndc_stdlib/src/sequence.rs index c6210fe7..6d5c74aa 100644 --- a/ndc_stdlib/src/sequence.rs +++ b/ndc_stdlib/src/sequence.rs @@ -603,8 +603,8 @@ mod inner { default.call(&[]).map_err(|e| anyhow!(e)) } - /// Returns the `k` sized combinations of the given sequence `seq` as a lazy iterator of tuples. - #[function(signature = "(Iterable, Int) -> Iterator")] + /// Returns the `k` sized combinations of the given sequence `seq` as a lazy iterator of lists. + #[function(signature = "(Iterable, Int) -> Iterator>")] pub fn combinations(seq: SeqValue, k: usize) -> anyhow::Result { let iter = CombinationsIter::new(seq, k) .ok_or_else(|| anyhow!("combinations requires a sequence"))?; @@ -618,14 +618,14 @@ mod inner { Ok(Value::iterator(iter.into_shared())) } - /// Returns the `k` sized permutations of the given sequence `seq` as a list of tuples. - #[function(signature = "(Iterable, Int) -> List")] + /// Returns the `k` sized permutations of the given sequence `seq` as a list of lists. + #[function(signature = "(Iterable, Int) -> List>")] pub fn permutations(seq: SeqValue, k: usize) -> anyhow::Result { Ok(Value::list( seq.try_into_iter() .ok_or_else(|| anyhow!("permutations requires a sequence"))? .permutations(k) - .map(Value::tuple) + .map(Value::list) .collect::>(), )) } diff --git a/ndc_vm/src/iterator.rs b/ndc_vm/src/iterator.rs index 5ba66c7c..e2a3e20f 100644 --- a/ndc_vm/src/iterator.rs +++ b/ndc_vm/src/iterator.rs @@ -457,7 +457,7 @@ impl VmIterator for CombinationsIter { if k == 0 { return if self.first { self.first = false; - Some(Value::tuple(vec![])) + Some(Value::list(vec![])) } else { None }; @@ -504,7 +504,7 @@ impl VmIterator for CombinationsIter { } // All indices are in-bounds: direct indexing is safe. - Some(Value::tuple( + Some(Value::list( self.indices .iter() .map(|&i| self.buffer[i].clone()) diff --git a/tests/functional/programs/005_functions/107_panic_never_returns.ndc b/tests/functional/programs/005_functions/107_panic_never_returns.ndc new file mode 100644 index 00000000..7e895a75 --- /dev/null +++ b/tests/functional/programs/005_functions/107_panic_never_returns.ndc @@ -0,0 +1,8 @@ +// `panic` stops the program and never returns, so it can end a branch whose +// other branch gives a value. +// expect-error: no such thing +fn pick(c) { + if c { 1 } else { panic("no such thing") } +} +let n: Int = pick(true); +print(n + pick(false)); diff --git a/tests/functional/programs/008_iterators/005_take.ndc b/tests/functional/programs/008_iterators/005_take.ndc index 918946fc..f51ae21c 100644 --- a/tests/functional/programs/008_iterators/005_take.ndc +++ b/tests/functional/programs/008_iterators/005_take.ndc @@ -11,4 +11,4 @@ assert_eq([1, 2].take(10).list(), [1, 2]); assert_eq([1, 2, 3].take(0).list(), []); // Chained: take from a combinations iterator. -assert_eq(combinations([1, 2, 3, 4], 2).take(3).list(), [(1, 2), (1, 3), (1, 4)]); +assert_eq(combinations([1, 2, 3, 4], 2).take(3).list(), [[1, 2], [1, 3], [1, 4]]); diff --git a/tests/functional/programs/603_stdlib_seq/011_combinations.ndc b/tests/functional/programs/603_stdlib_seq/011_combinations.ndc index 8364eb41..63730ab9 100644 --- a/tests/functional/programs/603_stdlib_seq/011_combinations.ndc +++ b/tests/functional/programs/603_stdlib_seq/011_combinations.ndc @@ -1,18 +1,18 @@ // Basic 2-combinations of a list. -assert_eq(combinations([1, 2, 3], 2).list(), [(1, 2), (1, 3), (2, 3)]); +assert_eq(combinations([1, 2, 3], 2).list(), [[1, 2], [1, 3], [2, 3]]); // k=0 yields exactly one empty tuple. -assert_eq(combinations([1, 2], 0).list(), [()]); +assert_eq(combinations([1, 2], 0).list(), [[]]); // k > length yields nothing. assert_eq(combinations([1, 2], 5).list(), []); // Works on a tuple source. -assert_eq(combinations((10, 20, 30), 2).list(), [(10, 20), (10, 30), (20, 30)]); +assert_eq(combinations((10, 20, 30), 2).list(), [[10, 20], [10, 30], [20, 30]]); // Works on a map source (order is unspecified, test length only). assert_eq(combinations(%{"a": 1, "b": 2, "c": 3}, 2).list().len(), 3); // Lazy: take only the first combination from a large range iterator — the pool // is never fully materialised. -assert_eq(combinations(1..100, 2).take(1).list(), [(1, 2)]); +assert_eq(combinations(1..100, 2).take(1).list(), [[1, 2]]); diff --git a/tests/functional/programs/603_stdlib_seq/030_combinations_and_permutations_are_typed_lists.ndc b/tests/functional/programs/603_stdlib_seq/030_combinations_and_permutations_are_typed_lists.ndc new file mode 100644 index 00000000..fad7ecf3 --- /dev/null +++ b/tests/functional/programs/603_stdlib_seq/030_combinations_and_permutations_are_typed_lists.ndc @@ -0,0 +1,4 @@ +// Combinations and permutations are lists of the elements, so what they hold +// is typed. +// expect-output: [3,4,5] [3,4,3,5,4,5] +print([a + b for [a, b] in [1, 2, 3].combinations(2)], [p.sum() for p in [1, 2, 3].permutations(2)]); diff --git a/tests/functional/programs/603_stdlib_seq/031_find_subsequence_has_an_optional_form.ndc b/tests/functional/programs/603_stdlib_seq/031_find_subsequence_has_an_optional_form.ndc new file mode 100644 index 00000000..9a7f7b4b --- /dev/null +++ b/tests/functional/programs/603_stdlib_seq/031_find_subsequence_has_an_optional_form.ndc @@ -0,0 +1,4 @@ +// `find_subsequence` gives where a subsequence starts, and `find_subsequence?` +// gives `None` when it isn't there. +// expect-output: 2 Some(0) None +print([1, 2, 3, 4].find_subsequence([3, 4]), [1, 2].find_subsequence?([1]), [1, 2].find_subsequence?([5])); diff --git a/tests/functional/programs/900_bugs/bug0022_combinations_lazy_source.ndc b/tests/functional/programs/900_bugs/bug0022_combinations_lazy_source.ndc index 788304d0..ee98490b 100644 --- a/tests/functional/programs/900_bugs/bug0022_combinations_lazy_source.ndc +++ b/tests/functional/programs/900_bugs/bug0022_combinations_lazy_source.ndc @@ -6,23 +6,23 @@ // some entirely. Fix: pull `indices[k-1] + 1` before computing pivot. // Eager (list) reference — correct for both pre- and post-fix. -assert_eq([1, 2, 3, 4].combinations(2).list(), [(1, 2), (1, 3), (1, 4), (2, 3), (2, 4), (3, 4)]); +assert_eq([1, 2, 3, 4].combinations(2).list(), [[1, 2], [1, 3], [1, 4], [2, 3], [2, 4], [3, 4]]); // Lazy (iterator) source — this is the path the bug lived in. assert_eq( [1, 2, 3, 4].enumerate().combinations(2).list(), [ - ((0, 1), (1, 2)), - ((0, 1), (2, 3)), - ((0, 1), (3, 4)), - ((1, 2), (2, 3)), - ((1, 2), (3, 4)), - ((2, 3), (3, 4)) + [(0, 1), (1, 2)], + [(0, 1), (2, 3)], + [(0, 1), (3, 4)], + [(1, 2), (2, 3)], + [(1, 2), (3, 4)], + [(2, 3), (3, 4)] ] ); // Larger k — exercise the multi-pull-per-pivot path. let triples = [10, 20, 30, 40, 50].enumerate().combinations(3).list(); assert_eq(triples.len, 10); -assert_eq(triples[0], ((0, 10), (1, 20), (2, 30))); -assert_eq(triples[-1], ((2, 30), (3, 40), (4, 50))); +assert_eq(triples[0], [(0, 10), (1, 20), (2, 30)]); +assert_eq(triples[-1], [(2, 30), (3, 40), (4, 50)]); diff --git a/tests/functional/programs/900_bugs/bug0027_combinations_huge_k.ndc b/tests/functional/programs/900_bugs/bug0027_combinations_huge_k.ndc index 44fb00d0..778dc9c4 100644 --- a/tests/functional/programs/900_bugs/bug0027_combinations_huge_k.ndc +++ b/tests/functional/programs/900_bugs/bug0027_combinations_huge_k.ndc @@ -20,4 +20,4 @@ assert_eq(lazy.list(), []); assert_eq(lazy.list(), []); // Valid combinations are still produced correctly. -assert_eq([1, 2, 3, 4].combinations(2).list(), [(1, 2), (1, 3), (1, 4), (2, 3), (2, 4), (3, 4)]); +assert_eq([1, 2, 3, 4].combinations(2).list(), [[1, 2], [1, 3], [1, 4], [2, 3], [2, 4], [3, 4]]);