mstlo-python 0.2.1__tar.gz → 0.2.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {mstlo_python-0.2.1 → mstlo_python-0.2.2}/Cargo.lock +3 -3
- {mstlo_python-0.2.1 → mstlo_python-0.2.2}/PKG-INFO +1 -1
- {mstlo_python-0.2.1 → mstlo_python-0.2.2}/mstlo/Cargo.toml +2 -2
- {mstlo_python-0.2.1 → mstlo_python-0.2.2}/mstlo/mstlo-macros/Cargo.toml +1 -1
- {mstlo_python-0.2.1 → mstlo_python-0.2.2}/mstlo/src/core.rs +0 -9
- {mstlo_python-0.2.1 → mstlo_python-0.2.2}/mstlo/src/operators/binary_operators.rs +135 -101
- {mstlo_python-0.2.1 → mstlo_python-0.2.2}/mstlo/src/operators/not_operator.rs +0 -5
- {mstlo_python-0.2.1 → mstlo_python-0.2.2}/mstlo/src/operators/unary_temporal_operators.rs +24 -100
- {mstlo_python-0.2.1 → mstlo_python-0.2.2}/mstlo/src/operators/until_operator.rs +10 -29
- {mstlo_python-0.2.1 → mstlo_python-0.2.2}/mstlo/src/ring_buffer.rs +1 -1
- {mstlo_python-0.2.1 → mstlo_python-0.2.2}/mstlo/tests/fixtures/oracles.rs +11 -9
- {mstlo_python-0.2.1 → mstlo_python-0.2.2}/mstlo/tests/monitor_dense_time_test.rs +19 -5
- {mstlo_python-0.2.1 → mstlo_python-0.2.2}/mstlo/tests/monitor_multi_signal_test.rs +65 -0
- {mstlo_python-0.2.1 → mstlo_python-0.2.2}/mstlo-python/Cargo.toml +1 -1
- {mstlo_python-0.2.1 → mstlo_python-0.2.2}/Cargo.toml +0 -0
- {mstlo_python-0.2.1 → mstlo_python-0.2.2}/README.md +0 -0
- {mstlo_python-0.2.1 → mstlo_python-0.2.2}/mstlo/.gitignore +0 -0
- {mstlo_python-0.2.1 → mstlo_python-0.2.2}/mstlo/Cargo.lock +0 -0
- {mstlo_python-0.2.1 → mstlo_python-0.2.2}/mstlo/LICENSE +0 -0
- {mstlo_python-0.2.1 → mstlo_python-0.2.2}/mstlo/README.md +0 -0
- {mstlo_python-0.2.1 → mstlo_python-0.2.2}/mstlo/benches/ab_benchmark.rs +0 -0
- {mstlo_python-0.2.1 → mstlo_python-0.2.2}/mstlo/benches/incubator_benchmark.rs +0 -0
- {mstlo_python-0.2.1 → mstlo_python-0.2.2}/mstlo/benches/memory_profile_benchmark.rs +0 -0
- {mstlo_python-0.2.1 → mstlo_python-0.2.2}/mstlo/benches/monitor_benchmark.rs +0 -0
- {mstlo_python-0.2.1 → mstlo_python-0.2.2}/mstlo/benches/paper_benchmark.rs +0 -0
- {mstlo_python-0.2.1 → mstlo_python-0.2.2}/mstlo/examples/batch_update.rs +0 -0
- {mstlo_python-0.2.1 → mstlo_python-0.2.2}/mstlo/examples/csv_example.rs +0 -0
- {mstlo_python-0.2.1 → mstlo_python-0.2.2}/mstlo/examples/data/property.stl +0 -0
- {mstlo_python-0.2.1 → mstlo_python-0.2.2}/mstlo/examples/data/trace.csv +0 -0
- {mstlo_python-0.2.1 → mstlo_python-0.2.2}/mstlo/examples/intro_example.rs +0 -0
- {mstlo_python-0.2.1 → mstlo_python-0.2.2}/mstlo/examples/simple_example.rs +0 -0
- {mstlo_python-0.2.1 → mstlo_python-0.2.2}/mstlo/examples/variables_example.rs +0 -0
- {mstlo_python-0.2.1 → mstlo_python-0.2.2}/mstlo/mstlo-macros/Cargo.lock +0 -0
- {mstlo_python-0.2.1 → mstlo_python-0.2.2}/mstlo/mstlo-macros/LICENSE +0 -0
- {mstlo_python-0.2.1 → mstlo_python-0.2.2}/mstlo/mstlo-macros/README.md +0 -0
- {mstlo_python-0.2.1 → mstlo_python-0.2.2}/mstlo/mstlo-macros/src/lib.rs +0 -0
- {mstlo_python-0.2.1 → mstlo_python-0.2.2}/mstlo/src/formula_definition.rs +0 -0
- {mstlo_python-0.2.1 → mstlo_python-0.2.2}/mstlo/src/formulas.rs +0 -0
- {mstlo_python-0.2.1 → mstlo_python-0.2.2}/mstlo/src/interner.rs +0 -0
- {mstlo_python-0.2.1 → mstlo_python-0.2.2}/mstlo/src/lib.rs +0 -0
- {mstlo_python-0.2.1 → mstlo_python-0.2.2}/mstlo/src/monitor.rs +0 -0
- {mstlo_python-0.2.1 → mstlo_python-0.2.2}/mstlo/src/naive_operators.rs +0 -0
- {mstlo_python-0.2.1 → mstlo_python-0.2.2}/mstlo/src/operators/atomic_operators.rs +0 -0
- {mstlo_python-0.2.1 → mstlo_python-0.2.2}/mstlo/src/operators/mod.rs +0 -0
- {mstlo_python-0.2.1 → mstlo_python-0.2.2}/mstlo/src/parser.rs +0 -0
- {mstlo_python-0.2.1 → mstlo_python-0.2.2}/mstlo/src/synchronizer.rs +0 -0
- {mstlo_python-0.2.1 → mstlo_python-0.2.2}/mstlo/tests/common/mod.rs +0 -0
- {mstlo_python-0.2.1 → mstlo_python-0.2.2}/mstlo/tests/fixtures/formulas.rs +0 -0
- {mstlo_python-0.2.1 → mstlo_python-0.2.2}/mstlo/tests/fixtures/mod.rs +0 -0
- {mstlo_python-0.2.1 → mstlo_python-0.2.2}/mstlo/tests/fixtures/signals.rs +0 -0
- {mstlo_python-0.2.1 → mstlo_python-0.2.2}/mstlo/tests/linear_interpolation_test.rs +0 -0
- {mstlo_python-0.2.1 → mstlo_python-0.2.2}/mstlo/tests/macros_test.rs +0 -0
- {mstlo_python-0.2.1 → mstlo_python-0.2.2}/mstlo/tests/monitor_build_fail_test.rs +0 -0
- {mstlo_python-0.2.1 → mstlo_python-0.2.2}/mstlo/tests/monitor_rosi_test.rs +0 -0
- {mstlo_python-0.2.1 → mstlo_python-0.2.2}/mstlo/tests/monitor_test.rs +0 -0
- {mstlo_python-0.2.1 → mstlo_python-0.2.2}/mstlo/tests/parser_parity_test.rs +0 -0
- {mstlo_python-0.2.1 → mstlo_python-0.2.2}/mstlo-python/.gitignore +0 -0
- {mstlo_python-0.2.1 → mstlo_python-0.2.2}/mstlo-python/README.md +0 -0
- {mstlo_python-0.2.1 → mstlo_python-0.2.2}/mstlo-python/docs/index.md +0 -0
- {mstlo_python-0.2.1 → mstlo_python-0.2.2}/mstlo-python/docs/reference.md +0 -0
- {mstlo_python-0.2.1 → mstlo_python-0.2.2}/mstlo-python/examples/batch_update.py +0 -0
- {mstlo_python-0.2.1 → mstlo_python-0.2.2}/mstlo-python/examples/csv_example.py +0 -0
- {mstlo_python-0.2.1 → mstlo_python-0.2.2}/mstlo-python/examples/data/property.stl +0 -0
- {mstlo_python-0.2.1 → mstlo_python-0.2.2}/mstlo-python/examples/data/trace.csv +0 -0
- {mstlo_python-0.2.1 → mstlo_python-0.2.2}/mstlo-python/examples/semantics-comparison.ipynb +0 -0
- {mstlo_python-0.2.1 → mstlo_python-0.2.2}/mstlo-python/examples/simple-example.py +0 -0
- {mstlo_python-0.2.1 → mstlo_python-0.2.2}/mstlo-python/examples/variables_example.py +0 -0
- {mstlo_python-0.2.1 → mstlo_python-0.2.2}/mstlo-python/mkdocs.yml +0 -0
- {mstlo_python-0.2.1 → mstlo_python-0.2.2}/mstlo-python/src/lib.rs +0 -0
- {mstlo_python-0.2.1 → mstlo_python-0.2.2}/mstlo-python/tests/__init__.py +0 -0
- {mstlo_python-0.2.1 → mstlo_python-0.2.2}/mstlo-python/tests/test_mstlo_python.py +0 -0
- {mstlo_python-0.2.1 → mstlo_python-0.2.2}/pyproject.toml +0 -0
- {mstlo_python-0.2.1 → mstlo_python-0.2.2}/python/mstlo_python/__init__.py +0 -0
- {mstlo_python-0.2.1 → mstlo_python-0.2.2}/python/mstlo_python/mstlo_python.pyi +0 -0
|
@@ -594,7 +594,7 @@ dependencies = [
|
|
|
594
594
|
|
|
595
595
|
[[package]]
|
|
596
596
|
name = "mstlo"
|
|
597
|
-
version = "0.2.
|
|
597
|
+
version = "0.2.2"
|
|
598
598
|
dependencies = [
|
|
599
599
|
"criterion",
|
|
600
600
|
"csv",
|
|
@@ -612,7 +612,7 @@ dependencies = [
|
|
|
612
612
|
|
|
613
613
|
[[package]]
|
|
614
614
|
name = "mstlo-macros"
|
|
615
|
-
version = "0.2.
|
|
615
|
+
version = "0.2.2"
|
|
616
616
|
dependencies = [
|
|
617
617
|
"proc-macro2",
|
|
618
618
|
"quote",
|
|
@@ -621,7 +621,7 @@ dependencies = [
|
|
|
621
621
|
|
|
622
622
|
[[package]]
|
|
623
623
|
name = "mstlo-python"
|
|
624
|
-
version = "0.2.
|
|
624
|
+
version = "0.2.2"
|
|
625
625
|
dependencies = [
|
|
626
626
|
"mstlo",
|
|
627
627
|
"pyo3",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[package]
|
|
2
2
|
name = "mstlo"
|
|
3
|
-
version = "0.2.
|
|
3
|
+
version = "0.2.2"
|
|
4
4
|
edition = "2024"
|
|
5
5
|
description = "A Rust library for online monitoring of Signal Temporal Logic (STL) specifications."
|
|
6
6
|
license-file = "LICENSE"
|
|
@@ -13,7 +13,7 @@ categories = ["science", "mathematics"]
|
|
|
13
13
|
|
|
14
14
|
[dependencies]
|
|
15
15
|
dyn-clone = "1.0.20"
|
|
16
|
-
mstlo-macros = { version = "0.2.
|
|
16
|
+
mstlo-macros = { version = "0.2.2", path = "mstlo-macros" }
|
|
17
17
|
|
|
18
18
|
[dev-dependencies]
|
|
19
19
|
proptest = "1.11.0"
|
|
@@ -68,15 +68,6 @@ pub trait StlOperatorTrait<T: Clone>: DynClone + Display + SignalIdentifier {
|
|
|
68
68
|
/// The default implementation is a no-op, suitable for stateless operators like `Atomic`.
|
|
69
69
|
fn reset(&mut self) {}
|
|
70
70
|
|
|
71
|
-
/// The timestamp through which this operator's output stream is gap-free, or `None`
|
|
72
|
-
/// if it has no gaps.
|
|
73
|
-
///
|
|
74
|
-
/// Eager `And`/`Or` can emit ahead of an earlier breakpoint that arrives later.
|
|
75
|
-
/// Consumers only close windows up to this bound.
|
|
76
|
-
fn known_through(&self) -> Option<Duration> {
|
|
77
|
-
None
|
|
78
|
-
}
|
|
79
|
-
|
|
80
71
|
/// Returns estimated total memory (stack + heap) in bytes consumed by this
|
|
81
72
|
/// operator and all its children recursively.
|
|
82
73
|
///
|
|
@@ -10,7 +10,7 @@ use crate::core::{
|
|
|
10
10
|
Reach, RobustnessSemantics, SignalIdentifier, StlOperatorAndSignalIdentifier, StlOperatorTrait,
|
|
11
11
|
};
|
|
12
12
|
use crate::ring_buffer::{RingBufferTrait, Step, guarded_prune};
|
|
13
|
-
use std::collections::
|
|
13
|
+
use std::collections::HashSet;
|
|
14
14
|
use std::fmt::{Debug, Display};
|
|
15
15
|
use std::time::Duration;
|
|
16
16
|
|
|
@@ -19,16 +19,47 @@ fn joint_frontier(left: Option<Duration>, right: Option<Duration>) -> Option<Dur
|
|
|
19
19
|
Some(left?.min(right?))
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
///
|
|
23
|
-
///
|
|
22
|
+
/// How far eager output is settled: the joint frontier, extended for as long as the
|
|
23
|
+
/// leading operand holds `short_circuit_val` without interruption from there on.
|
|
24
24
|
///
|
|
25
|
-
///
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
25
|
+
/// The lagging operand cannot change the output over that stretch.
|
|
26
|
+
fn eager_known_through<C, Y>(
|
|
27
|
+
left: (&C, Option<Duration>),
|
|
28
|
+
right: (&C, Option<Duration>),
|
|
29
|
+
short_circuit_val: Y,
|
|
30
|
+
) -> Option<Duration>
|
|
31
|
+
where
|
|
32
|
+
C: RingBufferTrait<Value = Y>,
|
|
33
|
+
Y: PartialEq,
|
|
34
|
+
{
|
|
35
|
+
let (cache, frontier) = if left.1 > right.1 { left } else { right };
|
|
36
|
+
// Until the lagging operand reports at all, the stretch starts at the leading
|
|
37
|
+
// operand's first entry: nothing is pruned before then, and the output is undefined
|
|
38
|
+
// earlier.
|
|
39
|
+
let joint = joint_frontier(left.1, right.1).or_else(|| {
|
|
40
|
+
cache
|
|
41
|
+
.iter()
|
|
42
|
+
.next()
|
|
43
|
+
.filter(|entry| entry.value == short_circuit_val)
|
|
44
|
+
.map(|entry| entry.timestamp)
|
|
45
|
+
})?;
|
|
46
|
+
let held_at_joint = cache
|
|
47
|
+
.partition_point(|entry| entry.timestamp <= joint)
|
|
48
|
+
.saturating_sub(1);
|
|
49
|
+
|
|
50
|
+
let mut known = joint;
|
|
51
|
+
let mut covered = joint;
|
|
52
|
+
for entry in cache.iter().skip(held_at_joint) {
|
|
53
|
+
if entry.timestamp > covered
|
|
54
|
+
|| Some(entry.timestamp) > frontier
|
|
55
|
+
|| entry.value != short_circuit_val
|
|
56
|
+
{
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
known = known.max(entry.timestamp);
|
|
60
|
+
covered = entry.held_until;
|
|
31
61
|
}
|
|
62
|
+
Some(known)
|
|
32
63
|
}
|
|
33
64
|
|
|
34
65
|
/// Whether `ts` lies inside the region both operands have reported on.
|
|
@@ -38,14 +69,13 @@ fn within(joint: Option<Duration>, ts: Duration) -> bool {
|
|
|
38
69
|
|
|
39
70
|
/// Settles the emission watermark for eager mode after a batch of output.
|
|
40
71
|
///
|
|
41
|
-
/// Advances it to the newest output at or below the joint frontier.
|
|
42
|
-
/// short-circuited past that frontier
|
|
43
|
-
///
|
|
72
|
+
/// Advances it to the newest output at or below the joint frontier. Outputs
|
|
73
|
+
/// short-circuited past that frontier do not move it, since the lagging operand still has
|
|
74
|
+
/// to be read there.
|
|
44
75
|
fn settle_eager_watermark<Y>(
|
|
45
76
|
output: &[Step<Y>],
|
|
46
77
|
joint: Option<Duration>,
|
|
47
78
|
last_eval_time: &mut Option<Duration>,
|
|
48
|
-
answered_beyond_joint: &mut VecDeque<Duration>,
|
|
49
79
|
) {
|
|
50
80
|
if let Some(eval_time) = output
|
|
51
81
|
.iter()
|
|
@@ -54,18 +84,6 @@ fn settle_eager_watermark<Y>(
|
|
|
54
84
|
{
|
|
55
85
|
*last_eval_time = Some(eval_time);
|
|
56
86
|
}
|
|
57
|
-
|
|
58
|
-
answered_beyond_joint.extend(
|
|
59
|
-
output
|
|
60
|
-
.iter()
|
|
61
|
-
.map(|step| step.timestamp)
|
|
62
|
-
.filter(|ts| !within(joint, *ts)),
|
|
63
|
-
);
|
|
64
|
-
|
|
65
|
-
// Timestamps the watermark has caught up with no longer need tracking.
|
|
66
|
-
if let Some(last) = *last_eval_time {
|
|
67
|
-
answered_beyond_joint.retain(|ts| *ts > last);
|
|
68
|
-
}
|
|
69
87
|
}
|
|
70
88
|
|
|
71
89
|
/// One side of a binary operator: the cache of what that operand has emitted.
|
|
@@ -73,14 +91,22 @@ struct Operand<'a, C> {
|
|
|
73
91
|
cache: &'a C,
|
|
74
92
|
/// Newest timestamp the operand has produced a value for.
|
|
75
93
|
frontier: Option<Duration>,
|
|
76
|
-
///
|
|
77
|
-
|
|
94
|
+
/// Newest timestamp the operand has produced a *final* value for. Under RoSI a
|
|
95
|
+
/// non-final interval may still be refined, so a held value past this mark is
|
|
96
|
+
/// re-masked (see [`read_operand`]).
|
|
97
|
+
settled: Option<Duration>,
|
|
98
|
+
/// The operand's [`StlOperatorTrait::reach`], used to mask RoSI bounds.
|
|
99
|
+
reach: Reach,
|
|
78
100
|
}
|
|
79
101
|
|
|
80
102
|
/// Reads the operand's value at `ts` from its cache under zero-order hold.
|
|
81
103
|
///
|
|
82
|
-
/// `None` past the operand's frontier. Under RoSI, a
|
|
83
|
-
///
|
|
104
|
+
/// `None` past the operand's frontier. Under RoSI, a value held from an earlier entry
|
|
105
|
+
/// (`entry.timestamp < ts`) is only trusted where the operand has settled; past that
|
|
106
|
+
/// mark its bounds whose [`Reach`] lag has not elapsed are widened to
|
|
107
|
+
/// [`RobustnessSemantics::unknown`]. A fresh emission at `ts`, or one within the settled
|
|
108
|
+
/// region, is used as-is, so a settled bound (e.g. a positive `F` lower bound read by an
|
|
109
|
+
/// `or`) is never discarded.
|
|
84
110
|
///
|
|
85
111
|
/// `newest` is the operand's newest entry at or before `ts`, tracked by the caller's walk.
|
|
86
112
|
fn read_operand<C, Y, const IS_ROSI: bool>(
|
|
@@ -92,19 +118,12 @@ where
|
|
|
92
118
|
C: RingBufferTrait<Value = Y>,
|
|
93
119
|
Y: RobustnessSemantics + Copy,
|
|
94
120
|
{
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
&& operand
|
|
100
|
-
.frontier
|
|
101
|
-
.is_some_and(|bound| ts + operand.lookahead > bound)
|
|
102
|
-
{
|
|
103
|
-
return Some(Y::unknown());
|
|
121
|
+
let frontier = operand.frontier.filter(|bound| ts <= *bound)?;
|
|
122
|
+
let entry = newest.filter(|entry| entry.held_until > ts)?;
|
|
123
|
+
if IS_ROSI && entry.timestamp < ts && operand.settled.is_none_or(|settled| ts > settled) {
|
|
124
|
+
return Some(operand.reach.mask(entry.value, ts, frontier));
|
|
104
125
|
}
|
|
105
|
-
|
|
106
|
-
.filter(|entry| entry.held_until > ts)
|
|
107
|
-
.map(|entry| entry.value)
|
|
126
|
+
Some(entry.value)
|
|
108
127
|
}
|
|
109
128
|
|
|
110
129
|
/// A unified binary processor that handles Delayed, Eager, and Refinable (RoSI) semantics correctly.
|
|
@@ -113,7 +132,8 @@ where
|
|
|
113
132
|
/// operand under zero-order hold.
|
|
114
133
|
///
|
|
115
134
|
/// A timestamp is answered once both operands are known there. Eager mode may also
|
|
116
|
-
/// short-circuit on one operand alone past the joint frontier,
|
|
135
|
+
/// short-circuit on one operand alone past the joint frontier, but only as far as
|
|
136
|
+
/// [`eager_known_through`], so output is always emitted in timestamp order.
|
|
117
137
|
fn process_binary<C, Y, F, const IS_EAGER: bool, const IS_ROSI: bool>(
|
|
118
138
|
left: &Operand<'_, C>,
|
|
119
139
|
right: &Operand<'_, C>,
|
|
@@ -137,6 +157,16 @@ where
|
|
|
137
157
|
let Some(horizon) = horizon else {
|
|
138
158
|
return output_robustness;
|
|
139
159
|
};
|
|
160
|
+
// Eager only short-circuits past the joint frontier over this stretch, so the lagging
|
|
161
|
+
// operand can never deliver a breakpoint that changes an earlier output.
|
|
162
|
+
let settled = match short_circuit_val {
|
|
163
|
+
Some(value) if IS_EAGER && !IS_ROSI => eager_known_through(
|
|
164
|
+
(left.cache, left.frontier),
|
|
165
|
+
(right.cache, right.frontier),
|
|
166
|
+
value,
|
|
167
|
+
),
|
|
168
|
+
_ => None,
|
|
169
|
+
};
|
|
140
170
|
|
|
141
171
|
// Skip breakpoints already answered. RoSI passes `None`, since it refines past outputs.
|
|
142
172
|
let l_skip = start_after.map_or(0, |ts| {
|
|
@@ -182,7 +212,7 @@ where
|
|
|
182
212
|
(Some(_), None) | (None, Some(_)) if joint.is_some_and(|frontier| ts <= frontier) => {}
|
|
183
213
|
// Eager past the joint frontier: short-circuit if possible, otherwise wait.
|
|
184
214
|
(Some(value), None) | (None, Some(value)) => {
|
|
185
|
-
if short_circuit_val != Some(value) {
|
|
215
|
+
if short_circuit_val != Some(value) || settled.is_none_or(|bound| ts > bound) {
|
|
186
216
|
break;
|
|
187
217
|
}
|
|
188
218
|
output_robustness.push(Step::new("output", value, ts));
|
|
@@ -204,12 +234,14 @@ pub struct And<T, C, Y, const IS_EAGER: bool, const IS_ROSI: bool> {
|
|
|
204
234
|
left_cache: C,
|
|
205
235
|
right_cache: C,
|
|
206
236
|
last_eval_time: Option<Duration>,
|
|
207
|
-
/// Timestamps eager answered ahead of the joint frontier; see
|
|
208
|
-
/// [`settle_eager_watermark`].
|
|
209
|
-
answered_beyond_joint: VecDeque<Duration>,
|
|
210
237
|
/// Newest timestamp each operand has produced a value for.
|
|
211
238
|
left_frontier: Option<Duration>,
|
|
212
239
|
right_frontier: Option<Duration>,
|
|
240
|
+
/// Newest timestamp each operand has produced a *final* value for.
|
|
241
|
+
left_settled: Option<Duration>,
|
|
242
|
+
right_settled: Option<Duration>,
|
|
243
|
+
/// Eager only: see [`eager_known_through`].
|
|
244
|
+
known: Option<Duration>,
|
|
213
245
|
left_signals_set: HashSet<&'static str>,
|
|
214
246
|
right_signals_set: HashSet<&'static str>,
|
|
215
247
|
max_lookahead: Duration,
|
|
@@ -237,9 +269,11 @@ impl<T, C, Y, const IS_EAGER: bool, const IS_ROSI: bool> And<T, C, Y, IS_EAGER,
|
|
|
237
269
|
left_cache: left_cache.unwrap_or_else(|| C::new()),
|
|
238
270
|
right_cache: right_cache.unwrap_or_else(|| C::new()),
|
|
239
271
|
last_eval_time: None,
|
|
240
|
-
answered_beyond_joint: VecDeque::new(),
|
|
241
272
|
left_frontier: None,
|
|
242
273
|
right_frontier: None,
|
|
274
|
+
left_settled: None,
|
|
275
|
+
right_settled: None,
|
|
276
|
+
known: None,
|
|
243
277
|
left_signals_set: HashSet::new(),
|
|
244
278
|
right_signals_set: HashSet::new(),
|
|
245
279
|
max_lookahead,
|
|
@@ -264,19 +298,10 @@ where
|
|
|
264
298
|
self.left.reach().join(self.right.reach())
|
|
265
299
|
}
|
|
266
300
|
|
|
267
|
-
/// Eager output is gap-free up to the joint frontier. Other modes have no gaps.
|
|
268
|
-
fn known_through(&self) -> Option<Duration> {
|
|
269
|
-
if !IS_EAGER || IS_ROSI {
|
|
270
|
-
return None;
|
|
271
|
-
}
|
|
272
|
-
Some(joint_frontier(self.left_frontier, self.right_frontier).unwrap_or(Duration::ZERO))
|
|
273
|
-
}
|
|
274
|
-
|
|
275
301
|
fn total_size(&self) -> usize {
|
|
276
302
|
std::mem::size_of::<Self>()
|
|
277
303
|
+ self.left_cache.heap_size()
|
|
278
304
|
+ self.right_cache.heap_size()
|
|
279
|
-
+ self.answered_beyond_joint.capacity() * std::mem::size_of::<Duration>()
|
|
280
305
|
+ self.left_signals_set.capacity() * (std::mem::size_of::<&str>() + 1)
|
|
281
306
|
+ self.right_signals_set.capacity() * (std::mem::size_of::<&str>() + 1)
|
|
282
307
|
+ self.left.total_size()
|
|
@@ -287,9 +312,11 @@ where
|
|
|
287
312
|
self.left_cache.clear();
|
|
288
313
|
self.right_cache.clear();
|
|
289
314
|
self.last_eval_time = None;
|
|
290
|
-
self.answered_beyond_joint.clear();
|
|
291
315
|
self.left_frontier = None;
|
|
292
316
|
self.right_frontier = None;
|
|
317
|
+
self.left_settled = None;
|
|
318
|
+
self.right_settled = None;
|
|
319
|
+
self.known = None;
|
|
293
320
|
self.left.reset();
|
|
294
321
|
self.right.reset();
|
|
295
322
|
}
|
|
@@ -324,8 +351,10 @@ where
|
|
|
324
351
|
if let Some(last) = left_updates.last() {
|
|
325
352
|
self.left_frontier = self.left_frontier.max(Some(last.timestamp));
|
|
326
353
|
}
|
|
327
|
-
self.left_frontier = clamp_to_known(self.left_frontier, self.left.known_through());
|
|
328
354
|
for update in &left_updates {
|
|
355
|
+
if update.value.is_final() {
|
|
356
|
+
self.left_settled = self.left_settled.max(Some(update.timestamp));
|
|
357
|
+
}
|
|
329
358
|
if check_relevance(update.timestamp, self.last_eval_time)
|
|
330
359
|
&& !self.left_cache.update_step(update.clone())
|
|
331
360
|
{
|
|
@@ -344,8 +373,10 @@ where
|
|
|
344
373
|
if let Some(last) = right_updates.last() {
|
|
345
374
|
self.right_frontier = self.right_frontier.max(Some(last.timestamp));
|
|
346
375
|
}
|
|
347
|
-
self.right_frontier = clamp_to_known(self.right_frontier, self.right.known_through());
|
|
348
376
|
for update in &right_updates {
|
|
377
|
+
if update.value.is_final() {
|
|
378
|
+
self.right_settled = self.right_settled.max(Some(update.timestamp));
|
|
379
|
+
}
|
|
349
380
|
if check_relevance(update.timestamp, self.last_eval_time)
|
|
350
381
|
&& !self.right_cache.update_step(update.clone())
|
|
351
382
|
{
|
|
@@ -358,12 +389,14 @@ where
|
|
|
358
389
|
&Operand {
|
|
359
390
|
cache: &self.left_cache,
|
|
360
391
|
frontier: self.left_frontier,
|
|
361
|
-
|
|
392
|
+
settled: self.left_settled,
|
|
393
|
+
reach: self.left.reach(),
|
|
362
394
|
},
|
|
363
395
|
&Operand {
|
|
364
396
|
cache: &self.right_cache,
|
|
365
397
|
frontier: self.right_frontier,
|
|
366
|
-
|
|
398
|
+
settled: self.right_settled,
|
|
399
|
+
reach: self.right.reach(),
|
|
367
400
|
},
|
|
368
401
|
if IS_ROSI { None } else { self.last_eval_time },
|
|
369
402
|
Y::and,
|
|
@@ -375,13 +408,17 @@ where
|
|
|
375
408
|
output.retain(|step| step.timestamp > last_time);
|
|
376
409
|
}
|
|
377
410
|
|
|
378
|
-
// Drop repeats of timestamps already short-circuited beyond the joint frontier.
|
|
379
|
-
if IS_EAGER && !IS_ROSI && !self.answered_beyond_joint.is_empty() {
|
|
380
|
-
output.retain(|step| !self.answered_beyond_joint.contains(&step.timestamp));
|
|
381
|
-
}
|
|
382
|
-
|
|
383
411
|
let lookahead = self.get_max_lookahead();
|
|
384
412
|
let joint = joint_frontier(self.left_frontier, self.right_frontier);
|
|
413
|
+
if IS_EAGER && !IS_ROSI {
|
|
414
|
+
// A late breakpoint inside the settled stretch repeats the held value.
|
|
415
|
+
output.retain(|step| self.known.is_none_or(|known| step.timestamp > known));
|
|
416
|
+
self.known = eager_known_through(
|
|
417
|
+
(&self.left_cache, self.left_frontier),
|
|
418
|
+
(&self.right_cache, self.right_frontier),
|
|
419
|
+
Y::atomic_false(),
|
|
420
|
+
);
|
|
421
|
+
}
|
|
385
422
|
|
|
386
423
|
// we protect up to the minimum of the last known timestamps minus lookahead
|
|
387
424
|
// we can safely prune it if both sides have verdict and are beyond lookahead
|
|
@@ -397,12 +434,7 @@ where
|
|
|
397
434
|
self.last_eval_time = Some(eval_time);
|
|
398
435
|
}
|
|
399
436
|
} else if IS_EAGER {
|
|
400
|
-
settle_eager_watermark(
|
|
401
|
-
&output,
|
|
402
|
-
joint,
|
|
403
|
-
&mut self.last_eval_time,
|
|
404
|
-
&mut self.answered_beyond_joint,
|
|
405
|
-
);
|
|
437
|
+
settle_eager_watermark(&output, joint, &mut self.last_eval_time);
|
|
406
438
|
} else {
|
|
407
439
|
// For delayed/bool, we track the *end* because everything before is finalized
|
|
408
440
|
if let Some(eval_time) = output.last().map(|step| step.timestamp) {
|
|
@@ -440,12 +472,14 @@ pub struct Or<T, C, Y, const IS_EAGER: bool, const IS_ROSI: bool> {
|
|
|
440
472
|
left_cache: C,
|
|
441
473
|
right_cache: C,
|
|
442
474
|
last_eval_time: Option<Duration>,
|
|
443
|
-
/// Timestamps eager answered ahead of the joint frontier; see
|
|
444
|
-
/// [`settle_eager_watermark`].
|
|
445
|
-
answered_beyond_joint: VecDeque<Duration>,
|
|
446
475
|
/// Newest timestamp each operand has produced a value for.
|
|
447
476
|
left_frontier: Option<Duration>,
|
|
448
477
|
right_frontier: Option<Duration>,
|
|
478
|
+
/// Newest timestamp each operand has produced a *final* value for.
|
|
479
|
+
left_settled: Option<Duration>,
|
|
480
|
+
right_settled: Option<Duration>,
|
|
481
|
+
/// Eager only: see [`eager_known_through`].
|
|
482
|
+
known: Option<Duration>,
|
|
449
483
|
left_signals_set: HashSet<&'static str>,
|
|
450
484
|
right_signals_set: HashSet<&'static str>,
|
|
451
485
|
max_lookahead: Duration,
|
|
@@ -473,9 +507,11 @@ impl<T, C, Y, const IS_EAGER: bool, const IS_ROSI: bool> Or<T, C, Y, IS_EAGER, I
|
|
|
473
507
|
left_cache: left_cache.unwrap_or_else(|| C::new()),
|
|
474
508
|
right_cache: right_cache.unwrap_or_else(|| C::new()),
|
|
475
509
|
last_eval_time: None,
|
|
476
|
-
answered_beyond_joint: VecDeque::new(),
|
|
477
510
|
left_frontier: None,
|
|
478
511
|
right_frontier: None,
|
|
512
|
+
left_settled: None,
|
|
513
|
+
right_settled: None,
|
|
514
|
+
known: None,
|
|
479
515
|
left_signals_set: HashSet::new(),
|
|
480
516
|
right_signals_set: HashSet::new(),
|
|
481
517
|
max_lookahead,
|
|
@@ -500,19 +536,10 @@ where
|
|
|
500
536
|
self.left.reach().join(self.right.reach())
|
|
501
537
|
}
|
|
502
538
|
|
|
503
|
-
/// Eager output is gap-free up to the joint frontier. Other modes have no gaps.
|
|
504
|
-
fn known_through(&self) -> Option<Duration> {
|
|
505
|
-
if !IS_EAGER || IS_ROSI {
|
|
506
|
-
return None;
|
|
507
|
-
}
|
|
508
|
-
Some(joint_frontier(self.left_frontier, self.right_frontier).unwrap_or(Duration::ZERO))
|
|
509
|
-
}
|
|
510
|
-
|
|
511
539
|
fn total_size(&self) -> usize {
|
|
512
540
|
std::mem::size_of::<Self>()
|
|
513
541
|
+ self.left_cache.heap_size()
|
|
514
542
|
+ self.right_cache.heap_size()
|
|
515
|
-
+ self.answered_beyond_joint.capacity() * std::mem::size_of::<Duration>()
|
|
516
543
|
+ self.left_signals_set.capacity() * (std::mem::size_of::<&str>() + 1)
|
|
517
544
|
+ self.right_signals_set.capacity() * (std::mem::size_of::<&str>() + 1)
|
|
518
545
|
+ self.left.total_size()
|
|
@@ -523,9 +550,11 @@ where
|
|
|
523
550
|
self.left_cache.clear();
|
|
524
551
|
self.right_cache.clear();
|
|
525
552
|
self.last_eval_time = None;
|
|
526
|
-
self.answered_beyond_joint.clear();
|
|
527
553
|
self.left_frontier = None;
|
|
528
554
|
self.right_frontier = None;
|
|
555
|
+
self.left_settled = None;
|
|
556
|
+
self.right_settled = None;
|
|
557
|
+
self.known = None;
|
|
529
558
|
self.left.reset();
|
|
530
559
|
self.right.reset();
|
|
531
560
|
}
|
|
@@ -560,8 +589,10 @@ where
|
|
|
560
589
|
if let Some(last) = left_updates.last() {
|
|
561
590
|
self.left_frontier = self.left_frontier.max(Some(last.timestamp));
|
|
562
591
|
}
|
|
563
|
-
self.left_frontier = clamp_to_known(self.left_frontier, self.left.known_through());
|
|
564
592
|
for update in &left_updates {
|
|
593
|
+
if update.value.is_final() {
|
|
594
|
+
self.left_settled = self.left_settled.max(Some(update.timestamp));
|
|
595
|
+
}
|
|
565
596
|
if check_relevance(update.timestamp, self.last_eval_time)
|
|
566
597
|
&& !self.left_cache.update_step(update.clone())
|
|
567
598
|
{
|
|
@@ -580,8 +611,10 @@ where
|
|
|
580
611
|
if let Some(last) = right_updates.last() {
|
|
581
612
|
self.right_frontier = self.right_frontier.max(Some(last.timestamp));
|
|
582
613
|
}
|
|
583
|
-
self.right_frontier = clamp_to_known(self.right_frontier, self.right.known_through());
|
|
584
614
|
for update in &right_updates {
|
|
615
|
+
if update.value.is_final() {
|
|
616
|
+
self.right_settled = self.right_settled.max(Some(update.timestamp));
|
|
617
|
+
}
|
|
585
618
|
if check_relevance(update.timestamp, self.last_eval_time)
|
|
586
619
|
&& !self.right_cache.update_step(update.clone())
|
|
587
620
|
{
|
|
@@ -594,12 +627,14 @@ where
|
|
|
594
627
|
&Operand {
|
|
595
628
|
cache: &self.left_cache,
|
|
596
629
|
frontier: self.left_frontier,
|
|
597
|
-
|
|
630
|
+
settled: self.left_settled,
|
|
631
|
+
reach: self.left.reach(),
|
|
598
632
|
},
|
|
599
633
|
&Operand {
|
|
600
634
|
cache: &self.right_cache,
|
|
601
635
|
frontier: self.right_frontier,
|
|
602
|
-
|
|
636
|
+
settled: self.right_settled,
|
|
637
|
+
reach: self.right.reach(),
|
|
603
638
|
},
|
|
604
639
|
if IS_ROSI { None } else { self.last_eval_time },
|
|
605
640
|
Y::or,
|
|
@@ -611,13 +646,17 @@ where
|
|
|
611
646
|
output.retain(|step| step.timestamp > last_time);
|
|
612
647
|
}
|
|
613
648
|
|
|
614
|
-
// Drop repeats of timestamps already short-circuited beyond the joint frontier.
|
|
615
|
-
if IS_EAGER && !IS_ROSI && !self.answered_beyond_joint.is_empty() {
|
|
616
|
-
output.retain(|step| !self.answered_beyond_joint.contains(&step.timestamp));
|
|
617
|
-
}
|
|
618
|
-
|
|
619
649
|
let lookahead = self.get_max_lookahead();
|
|
620
650
|
let joint = joint_frontier(self.left_frontier, self.right_frontier);
|
|
651
|
+
if IS_EAGER && !IS_ROSI {
|
|
652
|
+
// A late breakpoint inside the settled stretch repeats the held value.
|
|
653
|
+
output.retain(|step| self.known.is_none_or(|known| step.timestamp > known));
|
|
654
|
+
self.known = eager_known_through(
|
|
655
|
+
(&self.left_cache, self.left_frontier),
|
|
656
|
+
(&self.right_cache, self.right_frontier),
|
|
657
|
+
Y::atomic_true(),
|
|
658
|
+
);
|
|
659
|
+
}
|
|
621
660
|
|
|
622
661
|
// we protect up to the minimum of the last known timestamps minus lookahead
|
|
623
662
|
// we can safely prune it if both sides have verdict and are beyond lookahead
|
|
@@ -633,12 +672,7 @@ where
|
|
|
633
672
|
self.last_eval_time = Some(eval_time);
|
|
634
673
|
}
|
|
635
674
|
} else if IS_EAGER {
|
|
636
|
-
settle_eager_watermark(
|
|
637
|
-
&output,
|
|
638
|
-
joint,
|
|
639
|
-
&mut self.last_eval_time,
|
|
640
|
-
&mut self.answered_beyond_joint,
|
|
641
|
-
);
|
|
675
|
+
settle_eager_watermark(&output, joint, &mut self.last_eval_time);
|
|
642
676
|
} else {
|
|
643
677
|
// For delayed/bool, we track the *end* because everything before is finalized
|
|
644
678
|
if let Some(eval_time) = output.last().map(|step| step.timestamp) {
|
|
@@ -58,11 +58,6 @@ where
|
|
|
58
58
|
self.operand.reset();
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
/// Negation is pointwise, so this stream has exactly the operand's holes.
|
|
62
|
-
fn known_through(&self) -> Option<Duration> {
|
|
63
|
-
self.operand.known_through()
|
|
64
|
-
}
|
|
65
|
-
|
|
66
61
|
/// Updates the child operator and negates each produced value.
|
|
67
62
|
fn update(&mut self, step: &Step<T>) -> Vec<Step<Self::Output>> {
|
|
68
63
|
let operand_updates = self.operand.update(step);
|