Skip to content

Commit 646cea6

Browse files
authored
Merge pull request #956 from flyingrobots/feat/bounded-list-worklist
feat(provider): execute bounded list worklist operations
2 parents 6493b93 + a03de45 commit 646cea6

53 files changed

Lines changed: 3627 additions & 52 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎CHANGELOG.md‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,27 @@
77

88
### Added
99

10+
- The provider judgments and runtime paths support bounded list length,
11+
checked access, capacity-preserving append and bound-summing concatenation
12+
under `core.list/v1`. Synthetic tests cover strict domains, capability and
13+
type refusals, deterministic copy charges and one-unit-short budgets.
14+
Append and concat bind each declared operand type to its reconstructed type;
15+
coherently widening concat operands cannot invent capacity or item bounds.
16+
Inline record coordinates from structural alias joins resolve under bounded
17+
parsing in pure, ordered-read and runtime judgments, preserving nominal fields.
18+
Provider judgments and runtime preparation include operand validation depth across helper calls;
19+
capability discovery preserves the canonical artifact nesting allowance.
20+
List-domain proof scans keep ancestor predicate depth separate from descendant
21+
expression depth while preserving work and actual nesting limits.
22+
The checked Wasm component pair is promoted from two matching independent
23+
builds, with refreshed package manifests and identity goldens. Public compiler
24+
witnesses execute non-idempotent worklist folds at bounds 0/1/2/3/5, structural
25+
alias concatenation and stored-read worklists, including nominal elements
26+
and forward, reversed and negated strict bounds on equal-width words.
27+
Independent result oracles check contents, order, duplicate retention and
28+
guarded indexing. Exact/one-unit-short limits, invalid inputs and old-provider
29+
refusals exercise the compiler, separate verifier and runtime boundaries.
30+
1031
- The source-function lowerer, independent verifier and runtime support
1132
bounded list values with element/length checks and deterministic resource
1233
accounting. Ordered-read judgments accept lists and recursive record/branch

‎crates/echo-edict-provider-lowerer/README.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -139,8 +139,8 @@ proposal constructor supports mutations and refuses a `Query`; authored reads
139139
remain a separate bounded observer/optic path and must never be represented as
140140
synthetic mutations.
141141

142-
The refreshed 478,042-byte checked lowerer component has SHA-256
143-
`2a3738e3947529990143e184c0c1f39858e921738e6c6efaaeb4b7da5110626d` and has
142+
The refreshed 543,117-byte checked lowerer component has SHA-256
143+
`873dfec859285f26cadff97a7aa602fd91bcbcba3a3d347918c3658c4575afaa` and has
144144
crossed the reproducible promotion boundary. The pinned Edict host admits its
145145
generated envelope under the owning `generated-artifact` CDDL root, and the
146146
isolated host fixture exercises the exact helper binding, typed codecs, EINT

‎crates/echo-edict-provider-lowerer/src/executable_operation.rs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
mod bounded_read;
66
mod capabilities;
77
mod effect_resources;
8+
mod record_type;
89
mod source_functions;
910

1011
use std::collections::BTreeSet;

‎crates/echo-edict-provider-lowerer/src/executable_operation/bounded_read/relation/types.rs‎

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,24 @@ impl<'a> Scope<'a> {
172172
canonical_number(max)?,
173173
));
174174
}
175+
if let Some(spec) = coordinate
176+
.strip_prefix("Record<")
177+
.and_then(|s| s.strip_suffix('>'))
178+
{
179+
self.remaining_type_work.set(
180+
self.remaining_type_work
181+
.get()
182+
.checked_sub(spec.len())
183+
.ok_or_else(invalid)?,
184+
);
185+
let mut fields = BTreeMap::new();
186+
for (name, coordinate) in
187+
super::super::super::record_type::fields(spec).map_err(|()| invalid())?
188+
{
189+
fields.insert(name.to_owned(), self.resolve(coordinate, depth + 1)?);
190+
}
191+
return Ok(ReadType::Record(fields));
192+
}
175193
let key = coordinate.strip_prefix(&prefix).unwrap_or(coordinate);
176194
let value = field(field(self.core, "types")?, key)?;
177195
match text(value, "kind")? {
@@ -437,6 +455,30 @@ impl<'a> Scope<'a> {
437455
})
438456
.collect::<Result<Vec<_>, _>>()?;
439457
match (name, actual.as_slice(), declared.as_slice()) {
458+
// Whole-intent checking has already proved the list domains.
459+
("core.list.length", [value], [ty @ ReadType::List(Some(_), _)])
460+
if ty.accepts(value) =>
461+
{
462+
Ok(ReadType::Unsigned(u64::MAX))
463+
}
464+
(
465+
"core.list.at",
466+
[value, ReadType::Unsigned(u64::MAX)],
467+
[ty @ ReadType::List(Some(item), _)],
468+
) if ty.accepts(value) => Ok(*item.clone()),
469+
("core.list.append", [value, entry], [ty @ ReadType::List(Some(item), _)])
470+
if ty == value && item.accepts(entry) =>
471+
{
472+
Ok(ty.clone())
473+
}
474+
(
475+
"core.list.concat",
476+
[left, right],
477+
[a @ ReadType::List(Some(item), n), b @ ReadType::List(Some(other), m)],
478+
) if item == other && a.accepts(left) && b.accepts(right) => Ok(ReadType::List(
479+
Some(item.clone()),
480+
n.checked_add(*m).ok_or_else(invalid)?,
481+
)),
440482
// source_functions::validate already established the independent
441483
// no-overflow and order proofs before this read relation is constructed.
442484
("core.integer.add", [left, right], [expected @ ReadType::Unsigned(_)])
@@ -528,9 +570,11 @@ impl<'a> Scope<'a> {
528570
) {
529571
("==", ReadType::Bool, ReadType::Bool)
530572
| ("==", ReadType::Bytes(_, _), ReadType::Bytes(_, _)) => Ok(()),
531-
("==" | "<=", ReadType::Unsigned(left), ReadType::Unsigned(right)) if left == right => {
532-
Ok(())
533-
}
573+
(
574+
"==" | "<=" | "<" | ">" | ">=",
575+
ReadType::Unsigned(left),
576+
ReadType::Unsigned(right),
577+
) if left == right => Ok(()),
534578
_ => Err(invalid()),
535579
}
536580
}

‎crates/echo-edict-provider-lowerer/src/executable_operation/bounded_read/relation/types/list_tests.rs‎

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,161 @@
33
//! Read type-judgment witnesses; these do not claim public compiler provenance.
44
use super::*;
55

6+
#[test]
7+
fn ordered_list_guards_accept_strict_word_comparisons() {
8+
let core = core();
9+
let operation = |name: &str, args| {
10+
m([
11+
("kind", t("call")),
12+
("callee", t(name)),
13+
("typeArgs", Value::Array(vec![t("Numbers")])),
14+
("args", Value::Array(args)),
15+
])
16+
};
17+
let length = operation("core.list.length", vec![call("numbers", vec![])]);
18+
let access = operation("core.list.at", vec![call("numbers", vec![]), integer(0)]);
19+
let conditional = |op: &str, index: Value, reversed: bool, negated: bool| {
20+
let (left, right) = if reversed {
21+
(length.clone(), index)
22+
} else {
23+
(index, length.clone())
24+
};
25+
m([
26+
("kind", t("if")),
27+
(
28+
"predicate",
29+
m([
30+
("kind", t("compare")),
31+
("op", t(op)),
32+
("left", left),
33+
("right", right),
34+
]),
35+
),
36+
("then", if negated { integer(0) } else { access.clone() }),
37+
("else", if negated { access.clone() } else { integer(0) }),
38+
])
39+
};
40+
let wrong_width = m([
41+
("kind", t("const")),
42+
(
43+
"value",
44+
m([
45+
("kind", t("int")),
46+
("width", t("U8")),
47+
("value", Value::Integer(0)),
48+
]),
49+
),
50+
]);
51+
let mut admitted = Vec::new();
52+
for (op, reversed, negated) in [
53+
("<", false, false),
54+
("<=", true, true),
55+
(">", true, false),
56+
(">=", false, true),
57+
] {
58+
admitted.push(
59+
check(
60+
&core,
61+
&conditional(op, integer(0), reversed, negated),
62+
"U64",
63+
)
64+
.is_ok(),
65+
);
66+
assert_refusal(check(
67+
&core,
68+
&conditional(op, wrong_width.clone(), reversed, negated),
69+
"U64",
70+
));
71+
}
72+
assert_eq!(
73+
admitted,
74+
vec![true, true, true, true],
75+
"all compiler-supported strict guard orientations"
76+
);
77+
}
78+
79+
#[test]
80+
fn ordered_list_operations_preserve_types_after_domain_validation() {
81+
let core = core();
82+
let operation = |name: &str, types: &[&str], args| {
83+
m([
84+
("kind", t("call")),
85+
("callee", t(name)),
86+
(
87+
"typeArgs",
88+
Value::Array(types.iter().map(|s| t(s)).collect()),
89+
),
90+
("args", Value::Array(args)),
91+
])
92+
};
93+
let numbers = || call("numbers", vec![]);
94+
// This type relation runs after the separate whole-intent domain audit.
95+
for (value, expected) in [
96+
(
97+
operation("core.list.length", &["Numbers"], vec![numbers()]),
98+
"U64",
99+
),
100+
(
101+
operation("core.list.at", &["Numbers"], vec![numbers(), integer(0)]),
102+
"U64",
103+
),
104+
(
105+
operation(
106+
"core.list.append",
107+
&["Numbers"],
108+
vec![numbers(), integer(7)],
109+
),
110+
"Numbers",
111+
),
112+
(
113+
operation(
114+
"core.list.concat",
115+
&["Numbers", "Numbers"],
116+
vec![numbers(), numbers()],
117+
),
118+
"List<U64,max=4>",
119+
),
120+
] {
121+
assert!(check(&core, &value, expected).is_ok(), "{expected}");
122+
}
123+
for (value, expected) in [
124+
(
125+
operation(
126+
"core.list.append",
127+
&["List<U64,max=3>"],
128+
vec![numbers(), integer(7)],
129+
),
130+
"List<U64,max=3>",
131+
),
132+
(
133+
operation(
134+
"core.list.concat",
135+
&["Numbers", "Numbers"],
136+
vec![numbers(), numbers()],
137+
),
138+
"List<U64,max=3>",
139+
),
140+
(
141+
operation(
142+
"core.list.at",
143+
&["List<U8,max=2>"],
144+
vec![numbers(), integer(0)],
145+
),
146+
"U8",
147+
),
148+
(
149+
operation(
150+
"core.list.concat",
151+
&["List<U64,max=18446744073709551615>", "Numbers"],
152+
vec![numbers(), numbers()],
153+
),
154+
"Numbers",
155+
),
156+
] {
157+
assert_refusal(check(&core, &value, expected));
158+
}
159+
}
160+
6161
fn t(value: &str) -> Value {
7162
Value::Text(value.into())
8163
}
@@ -47,6 +202,7 @@ fn row(value: Value) -> Value {
47202
fn core() -> Value {
48203
m([
49204
("coordinate", t("example@1")),
205+
("pureFunctions", Value::Array(vec![])),
50206
(
51207
"types",
52208
m([
@@ -255,3 +411,38 @@ fn assert_refusal(result: Result<(), ProviderRefusalV1>) {
255411
"{result:?}"
256412
);
257413
}
414+
415+
#[test]
416+
fn list_inline_record_types_preserve_structural_and_nominal_meaning() {
417+
let core = core();
418+
let judgment = scope(&core);
419+
assert!(judgment.ty("Row").is_ok());
420+
assert!(judgment.ty("Record<payload:Bytes<max=4>>").is_ok());
421+
assert_eq!(
422+
judgment.ty("Row"),
423+
judgment.ty("Record<payload:Bytes<max=4>>")
424+
);
425+
assert!(judgment
426+
.ty("List<Record<a:A,nested:List<Record<x:U8>,max=2>>,max=3>")
427+
.is_ok());
428+
assert!(judgment.ty("Record<a:A>").is_ok());
429+
assert!(judgment.ty("Record<a:B>").is_ok());
430+
assert_ne!(judgment.ty("Record<a:A>"), judgment.ty("Record<a:B>"));
431+
assert!(judgment.ty("Record<>").is_ok());
432+
for invalid in [
433+
"Record<x:U8,x:U8>",
434+
"Record<z:U8,a:U8>",
435+
"Record<x:U8,>",
436+
"Record<:U8>",
437+
"Record<0x:U8>",
438+
"Record<x:>",
439+
"Record<x:List<U8,max=1>>>",
440+
"Record<x:Unknown>",
441+
] {
442+
assert!(judgment.ty(invalid).is_err(), "{invalid}");
443+
}
444+
judgment.remaining_type_work.set(3);
445+
assert!(judgment.ty("Record<long:U8,z:U8>").is_err());
446+
let deep = format!("{}U8{}", "Record<x:".repeat(65), ">".repeat(65));
447+
assert!(scope(&core).ty(&deep).is_err());
448+
}

‎crates/echo-edict-provider-lowerer/src/executable_operation/capabilities.rs‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,10 @@ pub(super) fn validate(core: &Value) -> Result<(), ProviderRefusalV1> {
1616
"required capability is not text",
1717
));
1818
};
19-
if !matches!(name.as_str(), "core.variant/v1" | "core.fold/v1")
20-
|| !supported.insert(name.as_str())
19+
if !matches!(
20+
name.as_str(),
21+
"core.variant/v1" | "core.fold/v1" | "core.list/v1"
22+
) || !supported.insert(name.as_str())
2123
{
2224
return Err(super::super::unsupported_semantics(
2325
SUBJECT,
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
// © James Ross Ω FLYING•ROBOTS <https://github.com/flyingrobots>
3+
//! Lowerer-owned grammar for structural record coordinates. Callers charge bytes
4+
//! before scanning and recursively resolve fields under their existing work bound.
5+
6+
pub(super) fn fields(spec: &str) -> Result<Vec<(&str, &str)>, ()> {
7+
if spec.is_empty() {
8+
return Ok(Vec::new());
9+
}
10+
let mut result = Vec::new();
11+
let mut start = 0;
12+
let mut nesting = 0_usize;
13+
for (index, byte) in spec.bytes().chain(std::iter::once(b',')).enumerate() {
14+
match byte {
15+
b'<' => {
16+
nesting = nesting.checked_add(1).ok_or(())?;
17+
if nesting > 64 {
18+
return Err(());
19+
}
20+
}
21+
b'>' => nesting = nesting.checked_sub(1).ok_or(())?,
22+
b',' if nesting == 0 => {
23+
let (key, coordinate) = spec[start..index].split_once(':').ok_or(())?;
24+
let mut letters = key.bytes();
25+
if !letters
26+
.next()
27+
.is_some_and(|b| b.is_ascii_alphabetic() || b == b'_')
28+
|| !letters.all(|b| b.is_ascii_alphanumeric() || b == b'_')
29+
|| coordinate.is_empty()
30+
|| result.last().is_some_and(|(previous, _)| *previous >= key)
31+
{
32+
return Err(());
33+
}
34+
result.push((key, coordinate));
35+
start = index + 1;
36+
}
37+
_ => {}
38+
}
39+
}
40+
if nesting != 0 {
41+
return Err(());
42+
}
43+
Ok(result)
44+
}

0 commit comments

Comments
 (0)