feat: add rule scopes
Signed-off-by: Nikolaos Karaolidis <nick@karaolidis.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
use serde::Deserialize;
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::ast::{LeafEquals, LeafPresent, Predicate};
|
||||
use crate::ast::{LeafEquals, LeafPresent, Predicate, Scope};
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
@@ -149,11 +149,13 @@ impl From<&Assignment> for Predicate {
|
||||
match &a.effect {
|
||||
AssignmentEffect::Set(v) => LeafEquals {
|
||||
r#ref: a.r#ref.clone(),
|
||||
scope: Scope::Current,
|
||||
equals: v.clone(),
|
||||
}
|
||||
.into(),
|
||||
AssignmentEffect::Clear => LeafPresent {
|
||||
r#ref: a.r#ref.clone(),
|
||||
scope: Scope::Current,
|
||||
present: false,
|
||||
}
|
||||
.into(),
|
||||
|
||||
@@ -11,7 +11,7 @@ use anyhow::bail;
|
||||
use crate::{
|
||||
ast::{
|
||||
Assignment, AssignmentEffect, LeafBetween, LeafEquals, LeafGt, LeafGte, LeafIn, LeafLt,
|
||||
LeafLte, LeafPresent, PredAll, PredAny, PredNot, Predicate,
|
||||
LeafLte, LeafPresent, PredAll, PredAny, PredNot, Predicate, Scope,
|
||||
},
|
||||
util::multiset,
|
||||
};
|
||||
@@ -49,10 +49,28 @@ impl Leaf {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn scope(&self) -> Scope {
|
||||
match self {
|
||||
Self::Present(p) => p.scope,
|
||||
Self::Equals(p) | Self::NotEquals(p) => p.scope,
|
||||
Self::In(p) | Self::NotIn(p) => p.scope,
|
||||
Self::Between(p) | Self::NotBetween(p) => p.scope,
|
||||
Self::LessThan(p) | Self::NotLessThan(p) => p.scope,
|
||||
Self::LessThanOrEqual(p) | Self::NotLessThanOrEqual(p) => p.scope,
|
||||
Self::GreaterThan(p) | Self::NotGreaterThan(p) => p.scope,
|
||||
Self::GreaterThanOrEqual(p) | Self::NotGreaterThanOrEqual(p) => p.scope,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn negated(self) -> Self {
|
||||
match self {
|
||||
Self::Present(LeafPresent { r#ref, present }) => Self::Present(LeafPresent {
|
||||
Self::Present(LeafPresent {
|
||||
r#ref,
|
||||
scope,
|
||||
present,
|
||||
}) => Self::Present(LeafPresent {
|
||||
r#ref,
|
||||
scope,
|
||||
present: !present,
|
||||
}),
|
||||
Self::Equals(l) => Self::NotEquals(l),
|
||||
@@ -78,10 +96,12 @@ impl From<&Assignment> for Leaf {
|
||||
match &a.effect {
|
||||
AssignmentEffect::Set(v) => Leaf::Equals(LeafEquals {
|
||||
r#ref: a.r#ref.clone(),
|
||||
scope: Scope::Current,
|
||||
equals: v.clone(),
|
||||
}),
|
||||
AssignmentEffect::Clear => Leaf::Present(LeafPresent {
|
||||
r#ref: a.r#ref.clone(),
|
||||
scope: Scope::Current,
|
||||
present: false,
|
||||
}),
|
||||
}
|
||||
@@ -399,6 +419,7 @@ mod tests {
|
||||
fn lp(name: &str, present: bool) -> Leaf {
|
||||
Leaf::Present(LeafPresent {
|
||||
r#ref: name.into(),
|
||||
scope: Scope::Current,
|
||||
present,
|
||||
})
|
||||
}
|
||||
@@ -406,6 +427,7 @@ mod tests {
|
||||
fn le(name: &str, v: serde_json::Value) -> Leaf {
|
||||
Leaf::Equals(LeafEquals {
|
||||
r#ref: name.into(),
|
||||
scope: Scope::Current,
|
||||
equals: v,
|
||||
})
|
||||
}
|
||||
@@ -446,6 +468,7 @@ mod tests {
|
||||
not: Box::new(
|
||||
LeafEquals {
|
||||
r#ref: "a".into(),
|
||||
scope: Scope::Current,
|
||||
equals: json!(1),
|
||||
}
|
||||
.into(),
|
||||
@@ -463,6 +486,7 @@ mod tests {
|
||||
not: Box::new(
|
||||
LeafPresent {
|
||||
r#ref: "a".into(),
|
||||
scope: Scope::Current,
|
||||
present: true,
|
||||
}
|
||||
.into(),
|
||||
@@ -479,6 +503,7 @@ mod tests {
|
||||
not: Box::new(
|
||||
LeafEquals {
|
||||
r#ref: "a".into(),
|
||||
scope: Scope::Current,
|
||||
equals: json!(1),
|
||||
}
|
||||
.into(),
|
||||
@@ -496,6 +521,7 @@ mod tests {
|
||||
all: vec![
|
||||
LeafPresent {
|
||||
r#ref: "A".into(),
|
||||
scope: Scope::Current,
|
||||
present: true,
|
||||
}
|
||||
.into(),
|
||||
@@ -503,11 +529,13 @@ mod tests {
|
||||
any: vec![
|
||||
LeafEquals {
|
||||
r#ref: "B".into(),
|
||||
scope: Scope::Current,
|
||||
equals: json!(1),
|
||||
}
|
||||
.into(),
|
||||
LeafEquals {
|
||||
r#ref: "C".into(),
|
||||
scope: Scope::Current,
|
||||
equals: json!(2),
|
||||
}
|
||||
.into(),
|
||||
@@ -547,6 +575,7 @@ mod tests {
|
||||
all: vec![
|
||||
LeafEquals {
|
||||
r#ref: "A".into(),
|
||||
scope: Scope::Current,
|
||||
equals: json!(1),
|
||||
}
|
||||
.into(),
|
||||
@@ -554,11 +583,13 @@ mod tests {
|
||||
any: vec![
|
||||
LeafPresent {
|
||||
r#ref: "B".into(),
|
||||
scope: Scope::Current,
|
||||
present: true,
|
||||
}
|
||||
.into(),
|
||||
LeafPresent {
|
||||
r#ref: "C".into(),
|
||||
scope: Scope::Current,
|
||||
present: false,
|
||||
}
|
||||
.into(),
|
||||
@@ -581,11 +612,13 @@ mod tests {
|
||||
all: vec![
|
||||
LeafEquals {
|
||||
r#ref: "A".into(),
|
||||
scope: Scope::Current,
|
||||
equals: json!(1),
|
||||
}
|
||||
.into(),
|
||||
LeafPresent {
|
||||
r#ref: "B".into(),
|
||||
scope: Scope::Current,
|
||||
present: true,
|
||||
}
|
||||
.into(),
|
||||
|
||||
@@ -11,6 +11,14 @@ use serde_json::Value;
|
||||
|
||||
use crate::util::multiset;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Eq, Hash)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum Scope {
|
||||
#[default]
|
||||
Current,
|
||||
Original,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Hash)]
|
||||
#[serde(untagged, deny_unknown_fields)]
|
||||
pub enum Predicate {
|
||||
@@ -134,61 +142,69 @@ pub struct PredNot {
|
||||
pub not: Box<Predicate>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Hash)]
|
||||
#[derive(Clone, Debug, Default, Deserialize, PartialEq, Eq, Hash)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct LeafEquals {
|
||||
pub r#ref: String,
|
||||
pub scope: Scope,
|
||||
pub equals: Value,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Hash)]
|
||||
#[derive(Clone, Debug, Default, Deserialize, PartialEq, Eq, Hash)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct LeafIn {
|
||||
pub r#ref: String,
|
||||
pub scope: Scope,
|
||||
#[serde(rename = "in")]
|
||||
pub values: Vec<Value>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Hash)]
|
||||
#[derive(Clone, Debug, Default, Deserialize, PartialEq, Eq, Hash)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct LeafBetween {
|
||||
pub r#ref: String,
|
||||
pub scope: Scope,
|
||||
pub min: Value,
|
||||
pub max: Value,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Hash)]
|
||||
#[derive(Clone, Debug, Default, Deserialize, PartialEq, Eq, Hash)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct LeafLt {
|
||||
pub r#ref: String,
|
||||
pub scope: Scope,
|
||||
pub lt: Value,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Hash)]
|
||||
#[derive(Clone, Debug, Default, Deserialize, PartialEq, Eq, Hash)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct LeafLte {
|
||||
pub r#ref: String,
|
||||
pub scope: Scope,
|
||||
pub lte: Value,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Hash)]
|
||||
#[derive(Clone, Debug, Default, Deserialize, PartialEq, Eq, Hash)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct LeafGt {
|
||||
pub r#ref: String,
|
||||
pub scope: Scope,
|
||||
pub gt: Value,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Hash)]
|
||||
#[derive(Clone, Debug, Default, Deserialize, PartialEq, Eq, Hash)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct LeafGte {
|
||||
pub r#ref: String,
|
||||
pub scope: Scope,
|
||||
pub gte: Value,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Hash)]
|
||||
#[derive(Clone, Debug, Default, Deserialize, PartialEq, Eq, Hash)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct LeafPresent {
|
||||
pub r#ref: String,
|
||||
pub scope: Scope,
|
||||
pub present: bool,
|
||||
}
|
||||
|
||||
@@ -267,8 +283,8 @@ mod tests {
|
||||
fn nested_logic_round_trips() {
|
||||
let pred = parse(
|
||||
r#"{ "all": [
|
||||
{ "ref": "x", "equals": 5 },
|
||||
{ "not": { "ref": "y", "present": true } }
|
||||
{ "ref": "x", "scope": "current", "equals": 5 },
|
||||
{ "not": { "ref": "y", "scope": "current", "present": true } }
|
||||
] }"#,
|
||||
);
|
||||
assert!(matches!(pred, Predicate::All(_)));
|
||||
@@ -288,7 +304,8 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn not_is_recursively_boxed() {
|
||||
let pred = parse(r#"{ "not": { "not": { "ref": "x", "present": true } } }"#);
|
||||
let pred =
|
||||
parse(r#"{ "not": { "not": { "ref": "x", "scope": "current", "present": true } } }"#);
|
||||
let Predicate::Not(outer) = pred else {
|
||||
panic!()
|
||||
};
|
||||
@@ -301,32 +318,32 @@ mod tests {
|
||||
#[test]
|
||||
fn comparator_leaves_disambiguate_by_key_name() {
|
||||
assert!(matches!(
|
||||
parse(r#"{ "ref": "x", "lt": 5 }"#),
|
||||
parse(r#"{ "ref": "x", "scope": "current", "lt": 5 }"#),
|
||||
Predicate::LessThan(_)
|
||||
));
|
||||
assert!(matches!(
|
||||
parse(r#"{ "ref": "x", "lte": 5 }"#),
|
||||
parse(r#"{ "ref": "x", "scope": "current", "lte": 5 }"#),
|
||||
Predicate::LessThanOrEqual(_)
|
||||
));
|
||||
assert!(matches!(
|
||||
parse(r#"{ "ref": "x", "gt": 5 }"#),
|
||||
parse(r#"{ "ref": "x", "scope": "current", "gt": 5 }"#),
|
||||
Predicate::GreaterThan(_)
|
||||
));
|
||||
assert!(matches!(
|
||||
parse(r#"{ "ref": "x", "gte": 5 }"#),
|
||||
parse(r#"{ "ref": "x", "scope": "current", "gte": 5 }"#),
|
||||
Predicate::GreaterThanOrEqual(_)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn equals_accepts_any_json_scalar() {
|
||||
let eq: Predicate = parse(r#"{ "ref": "x", "equals": "hello" }"#);
|
||||
let eq: Predicate = parse(r#"{ "ref": "x", "scope": "current", "equals": "hello" }"#);
|
||||
let Predicate::Equals(leaf) = eq else {
|
||||
panic!()
|
||||
};
|
||||
assert!(leaf.equals.is_string());
|
||||
|
||||
let eq: Predicate = parse(r#"{ "ref": "x", "equals": true }"#);
|
||||
let eq: Predicate = parse(r#"{ "ref": "x", "scope": "current", "equals": true }"#);
|
||||
let Predicate::Equals(leaf) = eq else {
|
||||
panic!()
|
||||
};
|
||||
@@ -335,16 +352,16 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn between_requires_both_bounds() {
|
||||
parse_err(r#"{ "ref": "x", "min": 5 }"#);
|
||||
parse_err(r#"{ "ref": "x", "max": 5 }"#);
|
||||
let bt = parse(r#"{ "ref": "x", "min": 0, "max": 10 }"#);
|
||||
parse_err(r#"{ "ref": "x", "scope": "current", "min": 5 }"#);
|
||||
parse_err(r#"{ "ref": "x", "scope": "current", "max": 5 }"#);
|
||||
let bt = parse(r#"{ "ref": "x", "scope": "current", "min": 0, "max": 10 }"#);
|
||||
assert!(matches!(bt, Predicate::Between(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn present_accepts_both_polarities() {
|
||||
let t = parse(r#"{ "ref": "x", "present": true }"#);
|
||||
let f = parse(r#"{ "ref": "x", "present": false }"#);
|
||||
let t = parse(r#"{ "ref": "x", "scope": "current", "present": true }"#);
|
||||
let f = parse(r#"{ "ref": "x", "scope": "current", "present": false }"#);
|
||||
let Predicate::Present(t) = t else { panic!() };
|
||||
let Predicate::Present(f) = f else { panic!() };
|
||||
assert!(t.present);
|
||||
@@ -368,16 +385,19 @@ mod tests {
|
||||
let mut out = BTreeSet::new();
|
||||
Predicate::from(LeafEquals {
|
||||
r#ref: "A".into(),
|
||||
scope: Scope::Current,
|
||||
equals: json!("x"),
|
||||
})
|
||||
.refs(&mut out);
|
||||
Predicate::from(LeafPresent {
|
||||
r#ref: "B".into(),
|
||||
scope: Scope::Current,
|
||||
present: true,
|
||||
})
|
||||
.refs(&mut out);
|
||||
Predicate::from(LeafIn {
|
||||
r#ref: "C".into(),
|
||||
scope: Scope::Current,
|
||||
values: vec![json!(1), json!(2)],
|
||||
})
|
||||
.refs(&mut out);
|
||||
@@ -393,6 +413,7 @@ mod tests {
|
||||
not: Box::new(
|
||||
LeafEquals {
|
||||
r#ref: "A".into(),
|
||||
scope: Scope::Current,
|
||||
equals: json!("x"),
|
||||
}
|
||||
.into(),
|
||||
@@ -403,11 +424,13 @@ mod tests {
|
||||
any: vec![
|
||||
LeafPresent {
|
||||
r#ref: "B".into(),
|
||||
scope: Scope::Current,
|
||||
present: true,
|
||||
}
|
||||
.into(),
|
||||
LeafEquals {
|
||||
r#ref: "C".into(),
|
||||
scope: Scope::Current,
|
||||
equals: json!(1),
|
||||
}
|
||||
.into(),
|
||||
|
||||
@@ -67,7 +67,7 @@ mod tests {
|
||||
"settings": [{ "id": "img", "ref": "image_size" }],
|
||||
"rules": [{
|
||||
"message": "demo rule",
|
||||
"when": { "ref": "img", "present": true }
|
||||
"when": { "ref": "img", "scope": "current", "present": true }
|
||||
}]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ use crate::{
|
||||
schema::{
|
||||
alias::{NormalizedRule, NormalizedTransformation},
|
||||
grammar::{
|
||||
SettingInfo, build_settings, generate_apply_transformations, generate_dnf,
|
||||
Scopes, SettingInfo, build_settings, generate_apply_transformations, generate_dnf,
|
||||
generate_emit_warnings_and_infos,
|
||||
},
|
||||
inverse::generate_inverses,
|
||||
@@ -154,8 +154,14 @@ fn generate_inherent_impl(
|
||||
let profile_code_lit = Literal::u32_suffixed(profile_code);
|
||||
|
||||
let apply_transformations = generate_apply_transformations(settings, &render.transformations)?;
|
||||
let warnings_infos = generate_emit_warnings_and_infos(settings, effective_rules)?;
|
||||
let solve = generate_solve(settings, effective_rules)?;
|
||||
let self_acc = quote! { self };
|
||||
let original_acc = quote! { original };
|
||||
let warnings_infos = generate_emit_warnings_and_infos(
|
||||
settings,
|
||||
effective_rules,
|
||||
Scopes::with_original(&self_acc, &original_acc),
|
||||
)?;
|
||||
let solve = generate_solve(settings, effective_rules, true)?;
|
||||
let try_update_from =
|
||||
generate_try_update_from(settings, &render.fields, renders_path, struct_ident)?;
|
||||
|
||||
@@ -200,6 +206,7 @@ fn generate_try_update_from(
|
||||
&mut self,
|
||||
partial: #renders_path::RenderBase,
|
||||
) -> ::anyhow::Result<()> {
|
||||
let original = self.clone();
|
||||
let mut partial_profile: #struct_ident = #struct_ident {
|
||||
#( #init_fields )*
|
||||
};
|
||||
@@ -211,8 +218,8 @@ fn generate_try_update_from(
|
||||
#( #merge_assigns )*
|
||||
candidate.apply_transformations();
|
||||
|
||||
candidate.solve(&pin)?;
|
||||
candidate.emit_warnings_and_infos()?;
|
||||
candidate.solve(&pin, &original)?;
|
||||
candidate.emit_warnings_and_infos(&original)?;
|
||||
|
||||
*self = candidate;
|
||||
Ok(())
|
||||
@@ -414,7 +421,7 @@ fn generate_convert_one(
|
||||
|
||||
if let Some(condition) = presence_conditions.get(field.id()) {
|
||||
let profile_accessor = quote! { profile };
|
||||
let cond = generate_dnf(settings, condition, &profile_accessor)?;
|
||||
let cond = generate_dnf(settings, condition, Scopes::new(&profile_accessor))?;
|
||||
Ok(quote! {
|
||||
if #cond {
|
||||
#convert
|
||||
|
||||
@@ -10,7 +10,7 @@ use crate::{
|
||||
schema::{
|
||||
alias::{NormalizedRule, NormalizedTransformation},
|
||||
grammar::{
|
||||
SettingInfo, build_settings, generate_apply_transformations, generate_dnf,
|
||||
Scopes, SettingInfo, build_settings, generate_apply_transformations, generate_dnf,
|
||||
generate_emit_warnings_and_infos,
|
||||
},
|
||||
presence::PresenceDag,
|
||||
@@ -167,8 +167,10 @@ fn generate_inherent_impl(
|
||||
|
||||
let apply_transformations =
|
||||
generate_apply_transformations(settings, &simulation.transformations)?;
|
||||
let warnings_infos = generate_emit_warnings_and_infos(settings, effective_rules)?;
|
||||
let solve = generate_solve(settings, effective_rules)?;
|
||||
let self_acc = quote! { self };
|
||||
let warnings_infos =
|
||||
generate_emit_warnings_and_infos(settings, effective_rules, Scopes::new(&self_acc))?;
|
||||
let solve = generate_solve(settings, effective_rules, false)?;
|
||||
let try_update_from = generate_try_update_from(settings, &simulation.settings, struct_ident)?;
|
||||
let name = generate_name(settings, options_path);
|
||||
|
||||
@@ -439,7 +441,7 @@ fn generate_try_pull(
|
||||
};
|
||||
|
||||
let body = if let Some(dnf) = presence_conditions.get(id) {
|
||||
let cond = generate_dnf(settings, dnf, &staging_accessor)?;
|
||||
let cond = generate_dnf(settings, dnf, Scopes::new(&staging_accessor))?;
|
||||
quote! {
|
||||
if #cond { #read_call } else { None }
|
||||
}
|
||||
|
||||
@@ -68,7 +68,7 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::ast::{
|
||||
Assignment, AssignmentEffect, LeafEquals, LeafPresent, PredAll, PredNot, Predicate, Rule,
|
||||
Severity,
|
||||
Scope, Severity,
|
||||
};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
@@ -110,6 +110,7 @@ mod tests {
|
||||
fn le(name: &str, v: Value) -> Leaf {
|
||||
Leaf::Equals(LeafEquals {
|
||||
r#ref: name.into(),
|
||||
scope: Scope::Current,
|
||||
equals: v,
|
||||
})
|
||||
}
|
||||
@@ -117,6 +118,7 @@ mod tests {
|
||||
fn lp(name: &str, present: bool) -> Leaf {
|
||||
Leaf::Present(LeafPresent {
|
||||
r#ref: name.into(),
|
||||
scope: Scope::Current,
|
||||
present,
|
||||
})
|
||||
}
|
||||
@@ -126,6 +128,7 @@ mod tests {
|
||||
let ts = vec![alias_t(
|
||||
LeafEquals {
|
||||
r#ref: "dr".into(),
|
||||
scope: Scope::Current,
|
||||
equals: json!("hdr800_plus"),
|
||||
}
|
||||
.into(),
|
||||
@@ -134,6 +137,7 @@ mod tests {
|
||||
let rules = vec![rule(
|
||||
LeafEquals {
|
||||
r#ref: "dr".into(),
|
||||
scope: Scope::Current,
|
||||
equals: json!("hdr800_plus"),
|
||||
}
|
||||
.into(),
|
||||
@@ -151,6 +155,7 @@ mod tests {
|
||||
let ts = vec![alias_t(
|
||||
LeafEquals {
|
||||
r#ref: "dr".into(),
|
||||
scope: Scope::Current,
|
||||
equals: json!("hdr800_plus"),
|
||||
}
|
||||
.into(),
|
||||
@@ -159,6 +164,7 @@ mod tests {
|
||||
let rules = vec![rule(
|
||||
LeafEquals {
|
||||
r#ref: "film_simulation".into(),
|
||||
scope: Scope::Current,
|
||||
equals: json!("provia"),
|
||||
}
|
||||
.into(),
|
||||
@@ -176,6 +182,7 @@ mod tests {
|
||||
when: Some(
|
||||
LeafEquals {
|
||||
r#ref: "wb".into(),
|
||||
scope: Scope::Current,
|
||||
equals: json!("as_shot"),
|
||||
}
|
||||
.into(),
|
||||
@@ -188,6 +195,7 @@ mod tests {
|
||||
let rules = vec![rule(
|
||||
LeafEquals {
|
||||
r#ref: "wb".into(),
|
||||
scope: Scope::Current,
|
||||
equals: json!("as_shot"),
|
||||
}
|
||||
.into(),
|
||||
@@ -202,6 +210,7 @@ mod tests {
|
||||
when: Some(
|
||||
LeafEquals {
|
||||
r#ref: "wb".into(),
|
||||
scope: Scope::Current,
|
||||
equals: json!("as_shot"),
|
||||
}
|
||||
.into(),
|
||||
@@ -220,6 +229,7 @@ mod tests {
|
||||
let rules = vec![rule(
|
||||
LeafEquals {
|
||||
r#ref: "wb".into(),
|
||||
scope: Scope::Current,
|
||||
equals: json!("as_shot"),
|
||||
}
|
||||
.into(),
|
||||
@@ -237,6 +247,7 @@ mod tests {
|
||||
alias_t(
|
||||
LeafEquals {
|
||||
r#ref: "dr".into(),
|
||||
scope: Scope::Current,
|
||||
equals: json!("hdr800_plus"),
|
||||
}
|
||||
.into(),
|
||||
@@ -245,6 +256,7 @@ mod tests {
|
||||
alias_t(
|
||||
LeafEquals {
|
||||
r#ref: "dr".into(),
|
||||
scope: Scope::Current,
|
||||
equals: json!("hdr800_plus"),
|
||||
}
|
||||
.into(),
|
||||
@@ -254,6 +266,7 @@ mod tests {
|
||||
let rules = vec![rule(
|
||||
LeafEquals {
|
||||
r#ref: "dr".into(),
|
||||
scope: Scope::Current,
|
||||
equals: json!("hdr800_plus"),
|
||||
}
|
||||
.into(),
|
||||
@@ -269,6 +282,7 @@ mod tests {
|
||||
let ts = vec![alias_t(
|
||||
LeafEquals {
|
||||
r#ref: "dr".into(),
|
||||
scope: Scope::Current,
|
||||
equals: json!("hdr800_plus"),
|
||||
}
|
||||
.into(),
|
||||
@@ -279,6 +293,7 @@ mod tests {
|
||||
all: vec![
|
||||
LeafEquals {
|
||||
r#ref: "dr".into(),
|
||||
scope: Scope::Current,
|
||||
equals: json!("hdr800_plus"),
|
||||
}
|
||||
.into(),
|
||||
@@ -286,6 +301,7 @@ mod tests {
|
||||
not: Box::new(
|
||||
LeafEquals {
|
||||
r#ref: "foo".into(),
|
||||
scope: Scope::Current,
|
||||
equals: json!("bar"),
|
||||
}
|
||||
.into(),
|
||||
@@ -310,11 +326,13 @@ mod tests {
|
||||
all: vec![
|
||||
LeafEquals {
|
||||
r#ref: "a".into(),
|
||||
scope: Scope::Current,
|
||||
equals: json!(1),
|
||||
}
|
||||
.into(),
|
||||
LeafPresent {
|
||||
r#ref: "b".into(),
|
||||
scope: Scope::Current,
|
||||
present: true,
|
||||
}
|
||||
.into(),
|
||||
@@ -328,11 +346,13 @@ mod tests {
|
||||
all: vec![
|
||||
LeafEquals {
|
||||
r#ref: "a".into(),
|
||||
scope: Scope::Current,
|
||||
equals: json!(1),
|
||||
}
|
||||
.into(),
|
||||
LeafPresent {
|
||||
r#ref: "b".into(),
|
||||
scope: Scope::Current,
|
||||
present: true,
|
||||
}
|
||||
.into(),
|
||||
@@ -354,11 +374,13 @@ mod tests {
|
||||
all: vec![
|
||||
LeafEquals {
|
||||
r#ref: "a".into(),
|
||||
scope: Scope::Current,
|
||||
equals: json!(1),
|
||||
}
|
||||
.into(),
|
||||
LeafPresent {
|
||||
r#ref: "b".into(),
|
||||
scope: Scope::Current,
|
||||
present: true,
|
||||
}
|
||||
.into(),
|
||||
@@ -372,11 +394,13 @@ mod tests {
|
||||
all: vec![
|
||||
LeafPresent {
|
||||
r#ref: "b".into(),
|
||||
scope: Scope::Current,
|
||||
present: true,
|
||||
}
|
||||
.into(),
|
||||
LeafEquals {
|
||||
r#ref: "a".into(),
|
||||
scope: Scope::Current,
|
||||
equals: json!(1),
|
||||
}
|
||||
.into(),
|
||||
@@ -393,6 +417,7 @@ mod tests {
|
||||
let ts = vec![alias_t(
|
||||
LeafPresent {
|
||||
r#ref: "legacy_field".into(),
|
||||
scope: Scope::Current,
|
||||
present: true,
|
||||
}
|
||||
.into(),
|
||||
@@ -403,6 +428,7 @@ mod tests {
|
||||
not: Box::new(
|
||||
LeafPresent {
|
||||
r#ref: "legacy_field".into(),
|
||||
scope: Scope::Current,
|
||||
present: true,
|
||||
}
|
||||
.into(),
|
||||
@@ -420,6 +446,7 @@ mod tests {
|
||||
alias_t(
|
||||
LeafEquals {
|
||||
r#ref: "a".into(),
|
||||
scope: Scope::Current,
|
||||
equals: json!("x"),
|
||||
}
|
||||
.into(),
|
||||
@@ -428,6 +455,7 @@ mod tests {
|
||||
alias_t(
|
||||
LeafEquals {
|
||||
r#ref: "b".into(),
|
||||
scope: Scope::Current,
|
||||
equals: json!("y"),
|
||||
}
|
||||
.into(),
|
||||
@@ -437,6 +465,7 @@ mod tests {
|
||||
let rules = vec![rule(
|
||||
LeafEquals {
|
||||
r#ref: "a".into(),
|
||||
scope: Scope::Current,
|
||||
equals: json!("x"),
|
||||
}
|
||||
.into(),
|
||||
@@ -452,11 +481,13 @@ mod tests {
|
||||
all: vec![
|
||||
LeafEquals {
|
||||
r#ref: "a".into(),
|
||||
scope: Scope::Current,
|
||||
equals: json!(1),
|
||||
}
|
||||
.into(),
|
||||
LeafEquals {
|
||||
r#ref: "b".into(),
|
||||
scope: Scope::Current,
|
||||
equals: json!(2),
|
||||
}
|
||||
.into(),
|
||||
@@ -470,16 +501,19 @@ mod tests {
|
||||
all: vec![
|
||||
LeafEquals {
|
||||
r#ref: "a".into(),
|
||||
scope: Scope::Current,
|
||||
equals: json!(1),
|
||||
}
|
||||
.into(),
|
||||
LeafEquals {
|
||||
r#ref: "b".into(),
|
||||
scope: Scope::Current,
|
||||
equals: json!(2),
|
||||
}
|
||||
.into(),
|
||||
LeafEquals {
|
||||
r#ref: "c".into(),
|
||||
scope: Scope::Current,
|
||||
equals: json!(3),
|
||||
}
|
||||
.into(),
|
||||
|
||||
@@ -7,7 +7,7 @@ use serde_json::Value;
|
||||
|
||||
use crate::{
|
||||
ast::{
|
||||
Assignment, AssignmentEffect, Conjunction, Dnf, Field, FujiOption, Leaf, Predicate,
|
||||
Assignment, AssignmentEffect, Conjunction, Dnf, Field, FujiOption, Leaf, Predicate, Scope,
|
||||
Setting, Severity, SpecKind, Transformation,
|
||||
},
|
||||
common::options,
|
||||
@@ -15,6 +15,40 @@ use crate::{
|
||||
util::ident::safe_upper_camel_case_ident,
|
||||
};
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct Scopes<'a> {
|
||||
pub current: &'a TokenStream,
|
||||
pub original: Option<&'a TokenStream>,
|
||||
}
|
||||
|
||||
impl<'a> Scopes<'a> {
|
||||
pub fn new(accessor: &'a TokenStream) -> Self {
|
||||
Self {
|
||||
current: accessor,
|
||||
original: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_original(current: &'a TokenStream, original: &'a TokenStream) -> Self {
|
||||
Self {
|
||||
current,
|
||||
original: Some(original),
|
||||
}
|
||||
}
|
||||
|
||||
fn pick(&self, scope: Scope) -> &'a TokenStream {
|
||||
match scope {
|
||||
Scope::Current => self.current,
|
||||
Scope::Original => match self.original {
|
||||
Some(o) => o,
|
||||
None => {
|
||||
unreachable!("original-scope leaf reached a context with no original accessor")
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait OptionLike {
|
||||
fn id(&self) -> &str;
|
||||
fn option_ref(&self) -> Option<&str>;
|
||||
@@ -127,7 +161,7 @@ pub fn build_settings<'a, F: OptionLike + 'a>(
|
||||
pub fn generate_predicate(
|
||||
settings: &BTreeMap<&str, SettingInfo<'_>>,
|
||||
pred: &Predicate,
|
||||
accessor: &TokenStream,
|
||||
scopes: Scopes<'_>,
|
||||
) -> anyhow::Result<TokenStream> {
|
||||
Ok(match pred {
|
||||
Predicate::Bool(b) => {
|
||||
@@ -141,12 +175,12 @@ pub fn generate_predicate(
|
||||
if p.all.is_empty() {
|
||||
quote! { true }
|
||||
} else if p.all.len() == 1 {
|
||||
generate_predicate(settings, &p.all[0], accessor)?
|
||||
generate_predicate(settings, &p.all[0], scopes)?
|
||||
} else {
|
||||
let parts = p
|
||||
.all
|
||||
.iter()
|
||||
.map(|c| generate_predicate(settings, c, accessor))
|
||||
.map(|c| generate_predicate(settings, c, scopes))
|
||||
.collect::<anyhow::Result<Vec<_>>>()?;
|
||||
quote! { #( ( #parts ) )&&* }
|
||||
}
|
||||
@@ -155,18 +189,18 @@ pub fn generate_predicate(
|
||||
if p.any.is_empty() {
|
||||
quote! { false }
|
||||
} else if p.any.len() == 1 {
|
||||
generate_predicate(settings, &p.any[0], accessor)?
|
||||
generate_predicate(settings, &p.any[0], scopes)?
|
||||
} else {
|
||||
let parts = p
|
||||
.any
|
||||
.iter()
|
||||
.map(|c| generate_predicate(settings, c, accessor))
|
||||
.map(|c| generate_predicate(settings, c, scopes))
|
||||
.collect::<anyhow::Result<Vec<_>>>()?;
|
||||
quote! { #( ( #parts ) )||* }
|
||||
}
|
||||
}
|
||||
Predicate::Not(p) => {
|
||||
let inner = generate_predicate(settings, &p.not, accessor)?;
|
||||
let inner = generate_predicate(settings, &p.not, scopes)?;
|
||||
quote! { !( #inner ) }
|
||||
}
|
||||
Predicate::Present(p) => {
|
||||
@@ -174,6 +208,7 @@ pub fn generate_predicate(
|
||||
.get(p.r#ref.as_str())
|
||||
.expect("ref validated during cue export");
|
||||
let field = info.field_ident();
|
||||
let accessor = scopes.pick(p.scope);
|
||||
if p.present {
|
||||
quote! { #accessor.#field.is_some() }
|
||||
} else {
|
||||
@@ -184,13 +219,14 @@ pub fn generate_predicate(
|
||||
let info = settings
|
||||
.get(p.r#ref.as_str())
|
||||
.expect("ref validated during cue export");
|
||||
generate_compare(info, accessor, &p.equals, CompareOp::Eq)?
|
||||
generate_compare(info, scopes.pick(p.scope), &p.equals, CompareOp::Eq)?
|
||||
}
|
||||
Predicate::In(p) => {
|
||||
let info = settings
|
||||
.get(p.r#ref.as_str())
|
||||
.expect("ref validated during cue export");
|
||||
let field = info.field_ident();
|
||||
let accessor = scopes.pick(p.scope);
|
||||
let arms = p
|
||||
.values
|
||||
.iter()
|
||||
@@ -222,6 +258,7 @@ pub fn generate_predicate(
|
||||
.get(p.r#ref.as_str())
|
||||
.expect("ref validated during cue export");
|
||||
let field = info.field_ident();
|
||||
let accessor = scopes.pick(p.scope);
|
||||
let lo = generate_value_expr(info, &p.min)?;
|
||||
let hi = generate_value_expr(info, &p.max)?;
|
||||
match info.kind {
|
||||
@@ -242,7 +279,7 @@ pub fn generate_predicate(
|
||||
settings
|
||||
.get(p.r#ref.as_str())
|
||||
.expect("ref validated during cue export"),
|
||||
accessor,
|
||||
scopes.pick(p.scope),
|
||||
&p.lt,
|
||||
CompareOp::Lt,
|
||||
)?,
|
||||
@@ -250,7 +287,7 @@ pub fn generate_predicate(
|
||||
settings
|
||||
.get(p.r#ref.as_str())
|
||||
.expect("ref validated during cue export"),
|
||||
accessor,
|
||||
scopes.pick(p.scope),
|
||||
&p.lte,
|
||||
CompareOp::Lte,
|
||||
)?,
|
||||
@@ -258,7 +295,7 @@ pub fn generate_predicate(
|
||||
settings
|
||||
.get(p.r#ref.as_str())
|
||||
.expect("ref validated during cue export"),
|
||||
accessor,
|
||||
scopes.pick(p.scope),
|
||||
&p.gt,
|
||||
CompareOp::Gt,
|
||||
)?,
|
||||
@@ -266,7 +303,7 @@ pub fn generate_predicate(
|
||||
settings
|
||||
.get(p.r#ref.as_str())
|
||||
.expect("ref validated during cue export"),
|
||||
accessor,
|
||||
scopes.pick(p.scope),
|
||||
&p.gte,
|
||||
CompareOp::Gte,
|
||||
)?,
|
||||
@@ -304,7 +341,8 @@ pub fn generate_transformation(
|
||||
let body = quote! { #( #assignments )* };
|
||||
Ok(match &transformation.when {
|
||||
Some(pred) => {
|
||||
let cond = generate_predicate(settings, pred, accessor)?;
|
||||
let scopes = Scopes::new(accessor);
|
||||
let cond = generate_predicate(settings, pred, scopes)?;
|
||||
quote! { if #cond { #body } }
|
||||
}
|
||||
None => body,
|
||||
@@ -330,15 +368,20 @@ pub fn generate_apply_transformations(
|
||||
pub fn generate_emit_warnings_and_infos(
|
||||
settings: &BTreeMap<&str, SettingInfo<'_>>,
|
||||
rules: &[NormalizedRule],
|
||||
scopes: Scopes<'_>,
|
||||
) -> anyhow::Result<TokenStream> {
|
||||
let accessor = quote! { self };
|
||||
let parts = rules
|
||||
.iter()
|
||||
.filter(|r| !matches!(r.severity, Severity::Error))
|
||||
.map(|r| generate_normalized_rule(settings, r, &accessor))
|
||||
.map(|r| generate_normalized_rule(settings, r, scopes))
|
||||
.collect::<anyhow::Result<Vec<_>>>()?;
|
||||
let original_param = match scopes.original {
|
||||
Some(_) => quote! { , original: &Self },
|
||||
None => quote! {},
|
||||
};
|
||||
Ok(quote! {
|
||||
fn emit_warnings_and_infos(&self) -> ::anyhow::Result<()> {
|
||||
#[allow(unused_variables)]
|
||||
fn emit_warnings_and_infos(&self #original_param) -> ::anyhow::Result<()> {
|
||||
#( #parts )*
|
||||
Ok(())
|
||||
}
|
||||
@@ -348,9 +391,9 @@ pub fn generate_emit_warnings_and_infos(
|
||||
pub fn generate_normalized_rule(
|
||||
settings: &BTreeMap<&str, SettingInfo<'_>>,
|
||||
rule: &NormalizedRule,
|
||||
accessor: &TokenStream,
|
||||
scopes: Scopes<'_>,
|
||||
) -> anyhow::Result<TokenStream> {
|
||||
let cond = generate_dnf(settings, &rule.when, accessor)?;
|
||||
let cond = generate_dnf(settings, &rule.when, scopes)?;
|
||||
Ok({
|
||||
let severity = rule.severity;
|
||||
let message: &str = &rule.message;
|
||||
@@ -366,7 +409,7 @@ pub fn generate_normalized_rule(
|
||||
pub fn generate_dnf(
|
||||
settings: &BTreeMap<&str, SettingInfo<'_>>,
|
||||
dnf: &Dnf,
|
||||
accessor: &TokenStream,
|
||||
scopes: Scopes<'_>,
|
||||
) -> anyhow::Result<TokenStream> {
|
||||
if dnf.is_contradiction() {
|
||||
return Ok(quote! { false });
|
||||
@@ -376,7 +419,7 @@ pub fn generate_dnf(
|
||||
}
|
||||
let parts = dnf
|
||||
.iter()
|
||||
.map(|c| generate_conjunction(settings, c, accessor))
|
||||
.map(|c| generate_conjunction(settings, c, scopes))
|
||||
.collect::<anyhow::Result<Vec<_>>>()?;
|
||||
Ok(if parts.len() == 1 {
|
||||
parts
|
||||
@@ -391,14 +434,14 @@ pub fn generate_dnf(
|
||||
pub fn generate_conjunction(
|
||||
settings: &BTreeMap<&str, SettingInfo<'_>>,
|
||||
conj: &Conjunction,
|
||||
accessor: &TokenStream,
|
||||
scopes: Scopes<'_>,
|
||||
) -> anyhow::Result<TokenStream> {
|
||||
if conj.0.is_empty() {
|
||||
return Ok(quote! { true });
|
||||
}
|
||||
let parts = conj
|
||||
.iter()
|
||||
.map(|l| generate_leaf(settings, l, accessor))
|
||||
.map(|l| generate_leaf(settings, l, scopes))
|
||||
.collect::<anyhow::Result<Vec<_>>>()?;
|
||||
Ok(if parts.len() == 1 {
|
||||
parts
|
||||
@@ -413,10 +456,10 @@ pub fn generate_conjunction(
|
||||
pub fn generate_leaf(
|
||||
settings: &BTreeMap<&str, SettingInfo<'_>>,
|
||||
leaf: &Leaf,
|
||||
accessor: &TokenStream,
|
||||
scopes: Scopes<'_>,
|
||||
) -> anyhow::Result<TokenStream> {
|
||||
let pred: Predicate = leaf.clone().into();
|
||||
generate_predicate(settings, &pred, accessor)
|
||||
generate_predicate(settings, &pred, scopes)
|
||||
}
|
||||
|
||||
pub fn generate_value_expr(info: &SettingInfo<'_>, value: &Value) -> anyhow::Result<TokenStream> {
|
||||
@@ -495,3 +538,63 @@ fn generate_compare(
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use serde_json::json;
|
||||
|
||||
use super::*;
|
||||
use crate::ast::{LeafEquals, Scope};
|
||||
|
||||
fn integer_info(id: &'static str) -> SettingInfo<'static> {
|
||||
SettingInfo {
|
||||
id,
|
||||
kind: SpecKind::Integer,
|
||||
option: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn settings() -> BTreeMap<&'static str, SettingInfo<'static>> {
|
||||
let mut settings = BTreeMap::new();
|
||||
let _ = settings.insert("x", integer_info("x"));
|
||||
settings
|
||||
}
|
||||
|
||||
fn eq_leaf(scope: Scope) -> Predicate {
|
||||
LeafEquals {
|
||||
r#ref: "x".into(),
|
||||
scope,
|
||||
equals: json!(1),
|
||||
}
|
||||
.into()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn current_scope_reads_the_current_accessor() {
|
||||
let current = quote! { self };
|
||||
let original = quote! { original };
|
||||
let out = generate_predicate(
|
||||
&settings(),
|
||||
&eq_leaf(Scope::Current),
|
||||
Scopes::with_original(¤t, &original),
|
||||
)
|
||||
.unwrap()
|
||||
.to_string();
|
||||
assert!(out.contains("self . x"), "got: {out}");
|
||||
assert!(!out.contains("original . x"), "got: {out}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn original_scope_reads_the_original_accessor() {
|
||||
let current = quote! { self };
|
||||
let original = quote! { original };
|
||||
let out = generate_predicate(
|
||||
&settings(),
|
||||
&eq_leaf(Scope::Original),
|
||||
Scopes::with_original(¤t, &original),
|
||||
)
|
||||
.unwrap()
|
||||
.to_string();
|
||||
assert!(out.contains("original . x"), "got: {out}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,13 +5,16 @@ use proc_macro2::TokenStream;
|
||||
use quote::quote;
|
||||
|
||||
use crate::{
|
||||
ast::{Assignment, AssignmentEffect, PredAll, Predicate, Transformation},
|
||||
schema::grammar::{SettingInfo, generate_assignment, generate_predicate},
|
||||
ast::{Assignment, AssignmentEffect, PredAll, Predicate, Scope, Transformation},
|
||||
schema::grammar::{Scopes, SettingInfo, generate_assignment, generate_predicate},
|
||||
};
|
||||
|
||||
impl Transformation {
|
||||
pub fn is_invertible(&self, all: &[Self]) -> bool {
|
||||
if !matches!(self.when, Some(Predicate::Equals(_))) {
|
||||
let Some(Predicate::Equals(when_leaf)) = self.when.as_ref() else {
|
||||
return false;
|
||||
};
|
||||
if when_leaf.scope != Scope::Current {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -83,7 +86,7 @@ fn generate_one_inverse(
|
||||
} else {
|
||||
PredAll { all: set_leaves }.into()
|
||||
};
|
||||
let condition = generate_predicate(settings, &match_pred, accessor)?;
|
||||
let condition = generate_predicate(settings, &match_pred, Scopes::new(accessor))?;
|
||||
|
||||
let Some(Predicate::Equals(when_leaf)) = t.when.as_ref() else {
|
||||
bail!("generate_one_inverse called on non-alias transformation");
|
||||
@@ -123,7 +126,7 @@ fn pattern_matches(a: &[(&str, &serde_json::Value)], b: &[(&str, &serde_json::Va
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::ast::{Assignment, AssignmentEffect, LeafEquals};
|
||||
use crate::ast::{Assignment, AssignmentEffect, LeafEquals, Scope};
|
||||
use serde_json::json;
|
||||
|
||||
fn alias_t(
|
||||
@@ -135,6 +138,7 @@ mod tests {
|
||||
when: Some(
|
||||
LeafEquals {
|
||||
r#ref: trigger_field.into(),
|
||||
scope: Scope::Current,
|
||||
equals: trigger_v,
|
||||
}
|
||||
.into(),
|
||||
@@ -169,6 +173,26 @@ mod tests {
|
||||
assert!(!all[1].is_invertible(&all));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn original_scope_when_blocks_invertibility() {
|
||||
let t = Transformation {
|
||||
when: Some(
|
||||
LeafEquals {
|
||||
r#ref: "dr".into(),
|
||||
scope: Scope::Original,
|
||||
equals: json!("hdr800_plus"),
|
||||
}
|
||||
.into(),
|
||||
),
|
||||
apply: vec![Assignment {
|
||||
r#ref: "dr".into(),
|
||||
effect: AssignmentEffect::Set(json!("hdr800")),
|
||||
}],
|
||||
};
|
||||
let all = vec![t];
|
||||
assert!(!all[0].is_invertible(&all));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unconditional_transformation_not_invertible() {
|
||||
let t = Transformation {
|
||||
|
||||
@@ -3,7 +3,7 @@ use std::collections::{BTreeMap, BTreeSet};
|
||||
use anyhow::bail;
|
||||
|
||||
use crate::{
|
||||
ast::{Conjunction, Dnf, Leaf, LeafPresent, PredAll, Predicate, Severity},
|
||||
ast::{Conjunction, Dnf, Leaf, LeafPresent, PredAll, Predicate, Scope, Severity},
|
||||
schema::alias::NormalizedRule,
|
||||
};
|
||||
|
||||
@@ -55,6 +55,9 @@ impl PresenceDag {
|
||||
let mut other_clauses: Vec<Leaf> = Vec::new();
|
||||
|
||||
for lit in &conj.0 {
|
||||
if lit.scope() == Scope::Original {
|
||||
continue;
|
||||
}
|
||||
match lit {
|
||||
Leaf::Present(p) => {
|
||||
if p.present {
|
||||
@@ -73,6 +76,7 @@ impl PresenceDag {
|
||||
.chain(false_anchors.iter().map(|r| {
|
||||
Leaf::Present(LeafPresent {
|
||||
r#ref: r.clone(),
|
||||
scope: Scope::Current,
|
||||
present: false,
|
||||
})
|
||||
}))
|
||||
@@ -200,11 +204,13 @@ mod tests {
|
||||
all: vec![
|
||||
LeafPresent {
|
||||
r#ref: "A".into(),
|
||||
scope: Scope::Current,
|
||||
present: true,
|
||||
}
|
||||
.into(),
|
||||
LeafEquals {
|
||||
r#ref: "B".into(),
|
||||
scope: Scope::Current,
|
||||
equals: json!("x"),
|
||||
}
|
||||
.into(),
|
||||
@@ -218,6 +224,65 @@ mod tests {
|
||||
assert_eq!(info.edges, [edge("B", "A")].into_iter().collect());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn original_scope_leaf_is_dropped_from_presence_dag() {
|
||||
let rules = vec![rule(
|
||||
PredAll {
|
||||
all: vec![
|
||||
LeafEquals {
|
||||
r#ref: "A".into(),
|
||||
scope: Scope::Original,
|
||||
equals: json!("x"),
|
||||
}
|
||||
.into(),
|
||||
LeafPresent {
|
||||
r#ref: "B".into(),
|
||||
scope: Scope::Current,
|
||||
present: true,
|
||||
}
|
||||
.into(),
|
||||
],
|
||||
}
|
||||
.into(),
|
||||
)];
|
||||
let info = collect_raw(&rules).unwrap();
|
||||
assert!(info.conditions.is_empty());
|
||||
assert!(info.edges.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn original_scope_leaf_does_not_gate_alongside_a_current_clause() {
|
||||
let rules = vec![rule(
|
||||
PredAll {
|
||||
all: vec![
|
||||
LeafEquals {
|
||||
r#ref: "A".into(),
|
||||
scope: Scope::Original,
|
||||
equals: json!("x"),
|
||||
}
|
||||
.into(),
|
||||
LeafPresent {
|
||||
r#ref: "B".into(),
|
||||
scope: Scope::Current,
|
||||
present: true,
|
||||
}
|
||||
.into(),
|
||||
LeafEquals {
|
||||
r#ref: "C".into(),
|
||||
scope: Scope::Current,
|
||||
equals: json!("y"),
|
||||
}
|
||||
.into(),
|
||||
],
|
||||
}
|
||||
.into(),
|
||||
)];
|
||||
let info = collect_raw(&rules).unwrap();
|
||||
let cond = info.conditions.get("B").expect("B condition");
|
||||
assert_gate_is_single_negated_value_leaf(cond);
|
||||
assert_eq!(info.edges, [edge("C", "B")].into_iter().collect());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn present_false_rule_does_not_negate() {
|
||||
let rules = vec![rule(
|
||||
@@ -225,11 +290,13 @@ mod tests {
|
||||
all: vec![
|
||||
LeafPresent {
|
||||
r#ref: "A".into(),
|
||||
scope: Scope::Current,
|
||||
present: false,
|
||||
}
|
||||
.into(),
|
||||
LeafEquals {
|
||||
r#ref: "B".into(),
|
||||
scope: Scope::Current,
|
||||
equals: json!("x"),
|
||||
}
|
||||
.into(),
|
||||
@@ -254,11 +321,13 @@ mod tests {
|
||||
all: vec![
|
||||
LeafPresent {
|
||||
r#ref: "A".into(),
|
||||
scope: Scope::Current,
|
||||
present: true,
|
||||
}
|
||||
.into(),
|
||||
LeafEquals {
|
||||
r#ref: "B".into(),
|
||||
scope: Scope::Current,
|
||||
equals: json!("x"),
|
||||
}
|
||||
.into(),
|
||||
@@ -275,6 +344,7 @@ mod tests {
|
||||
let rules = vec![rule(
|
||||
LeafPresent {
|
||||
r#ref: "A".into(),
|
||||
scope: Scope::Current,
|
||||
present: true,
|
||||
}
|
||||
.into(),
|
||||
@@ -288,6 +358,7 @@ mod tests {
|
||||
not: Box::new(
|
||||
LeafEquals {
|
||||
r#ref: "A".into(),
|
||||
scope: Scope::Current,
|
||||
equals: json!("x"),
|
||||
}
|
||||
.into(),
|
||||
@@ -303,11 +374,13 @@ mod tests {
|
||||
any: vec![
|
||||
LeafPresent {
|
||||
r#ref: "A".into(),
|
||||
scope: Scope::Current,
|
||||
present: true,
|
||||
}
|
||||
.into(),
|
||||
LeafEquals {
|
||||
r#ref: "B".into(),
|
||||
scope: Scope::Current,
|
||||
equals: json!(1),
|
||||
}
|
||||
.into(),
|
||||
@@ -328,11 +401,13 @@ mod tests {
|
||||
all: vec![
|
||||
LeafPresent {
|
||||
r#ref: "A".into(),
|
||||
scope: Scope::Current,
|
||||
present: true,
|
||||
}
|
||||
.into(),
|
||||
LeafEquals {
|
||||
r#ref: "B".into(),
|
||||
scope: Scope::Current,
|
||||
equals: json!("x"),
|
||||
}
|
||||
.into(),
|
||||
@@ -343,11 +418,13 @@ mod tests {
|
||||
all: vec![
|
||||
LeafPresent {
|
||||
r#ref: "C".into(),
|
||||
scope: Scope::Current,
|
||||
present: true,
|
||||
}
|
||||
.into(),
|
||||
LeafEquals {
|
||||
r#ref: "D".into(),
|
||||
scope: Scope::Current,
|
||||
equals: json!("y"),
|
||||
}
|
||||
.into(),
|
||||
@@ -376,6 +453,7 @@ mod tests {
|
||||
any: vec![
|
||||
LeafPresent {
|
||||
r#ref: "A".into(),
|
||||
scope: Scope::Current,
|
||||
present: false,
|
||||
}
|
||||
.into(),
|
||||
@@ -383,6 +461,7 @@ mod tests {
|
||||
not: Box::new(
|
||||
LeafEquals {
|
||||
r#ref: "B".into(),
|
||||
scope: Scope::Current,
|
||||
equals: json!("x"),
|
||||
}
|
||||
.into(),
|
||||
@@ -410,6 +489,7 @@ mod tests {
|
||||
not: Box::new(
|
||||
LeafPresent {
|
||||
r#ref: "A".into(),
|
||||
scope: Scope::Current,
|
||||
present: false,
|
||||
}
|
||||
.into(),
|
||||
@@ -418,6 +498,7 @@ mod tests {
|
||||
.into(),
|
||||
LeafEquals {
|
||||
r#ref: "B".into(),
|
||||
scope: Scope::Current,
|
||||
equals: json!("x"),
|
||||
}
|
||||
.into(),
|
||||
@@ -457,6 +538,7 @@ mod tests {
|
||||
all: vec![
|
||||
LeafPresent {
|
||||
r#ref: "A".into(),
|
||||
scope: Scope::Current,
|
||||
present: true,
|
||||
}
|
||||
.into(),
|
||||
@@ -464,11 +546,13 @@ mod tests {
|
||||
any: vec![
|
||||
LeafEquals {
|
||||
r#ref: "B".into(),
|
||||
scope: Scope::Current,
|
||||
equals: json!("x"),
|
||||
}
|
||||
.into(),
|
||||
LeafEquals {
|
||||
r#ref: "C".into(),
|
||||
scope: Scope::Current,
|
||||
equals: json!("y"),
|
||||
}
|
||||
.into(),
|
||||
@@ -486,11 +570,13 @@ mod tests {
|
||||
all: vec![
|
||||
LeafPresent {
|
||||
r#ref: "A".into(),
|
||||
scope: Scope::Current,
|
||||
present: true,
|
||||
}
|
||||
.into(),
|
||||
LeafEquals {
|
||||
r#ref: "B".into(),
|
||||
scope: Scope::Current,
|
||||
equals: json!("x"),
|
||||
}
|
||||
.into(),
|
||||
@@ -501,11 +587,13 @@ mod tests {
|
||||
all: vec![
|
||||
LeafPresent {
|
||||
r#ref: "A".into(),
|
||||
scope: Scope::Current,
|
||||
present: true,
|
||||
}
|
||||
.into(),
|
||||
LeafEquals {
|
||||
r#ref: "C".into(),
|
||||
scope: Scope::Current,
|
||||
equals: json!("y"),
|
||||
}
|
||||
.into(),
|
||||
@@ -533,11 +621,13 @@ mod tests {
|
||||
all: vec![
|
||||
LeafEquals {
|
||||
r#ref: "A".into(),
|
||||
scope: Scope::Current,
|
||||
equals: json!("x"),
|
||||
}
|
||||
.into(),
|
||||
LeafEquals {
|
||||
r#ref: "B".into(),
|
||||
scope: Scope::Current,
|
||||
equals: json!("y"),
|
||||
}
|
||||
.into(),
|
||||
@@ -557,11 +647,13 @@ mod tests {
|
||||
all: vec![
|
||||
LeafPresent {
|
||||
r#ref: "A".into(),
|
||||
scope: Scope::Current,
|
||||
present: true,
|
||||
}
|
||||
.into(),
|
||||
LeafPresent {
|
||||
r#ref: "B".into(),
|
||||
scope: Scope::Current,
|
||||
present: false,
|
||||
}
|
||||
.into(),
|
||||
@@ -590,11 +682,13 @@ mod tests {
|
||||
all: vec![
|
||||
LeafPresent {
|
||||
r#ref: "A".into(),
|
||||
scope: Scope::Current,
|
||||
present: false,
|
||||
}
|
||||
.into(),
|
||||
LeafPresent {
|
||||
r#ref: "B".into(),
|
||||
scope: Scope::Current,
|
||||
present: true,
|
||||
}
|
||||
.into(),
|
||||
@@ -615,11 +709,13 @@ mod tests {
|
||||
all: vec![
|
||||
LeafPresent {
|
||||
r#ref: "A".into(),
|
||||
scope: Scope::Current,
|
||||
present: false,
|
||||
}
|
||||
.into(),
|
||||
LeafEquals {
|
||||
r#ref: "B".into(),
|
||||
scope: Scope::Current,
|
||||
equals: json!("x"),
|
||||
}
|
||||
.into(),
|
||||
@@ -641,11 +737,13 @@ mod tests {
|
||||
any: vec![
|
||||
LeafPresent {
|
||||
r#ref: "A".into(),
|
||||
scope: Scope::Current,
|
||||
present: true,
|
||||
}
|
||||
.into(),
|
||||
LeafPresent {
|
||||
r#ref: "B".into(),
|
||||
scope: Scope::Current,
|
||||
present: true,
|
||||
}
|
||||
.into(),
|
||||
@@ -654,6 +752,7 @@ mod tests {
|
||||
.into(),
|
||||
LeafEquals {
|
||||
r#ref: "C".into(),
|
||||
scope: Scope::Current,
|
||||
equals: json!("x"),
|
||||
}
|
||||
.into(),
|
||||
@@ -680,16 +779,19 @@ mod tests {
|
||||
all: vec![
|
||||
LeafPresent {
|
||||
r#ref: "A".into(),
|
||||
scope: Scope::Current,
|
||||
present: true,
|
||||
}
|
||||
.into(),
|
||||
LeafPresent {
|
||||
r#ref: "B".into(),
|
||||
scope: Scope::Current,
|
||||
present: true,
|
||||
}
|
||||
.into(),
|
||||
LeafEquals {
|
||||
r#ref: "C".into(),
|
||||
scope: Scope::Current,
|
||||
equals: json!("x"),
|
||||
}
|
||||
.into(),
|
||||
@@ -714,11 +816,13 @@ mod tests {
|
||||
all: vec![
|
||||
LeafPresent {
|
||||
r#ref: "A".into(),
|
||||
scope: Scope::Current,
|
||||
present: true,
|
||||
}
|
||||
.into(),
|
||||
LeafEquals {
|
||||
r#ref: "B".into(),
|
||||
scope: Scope::Current,
|
||||
equals: json!("x"),
|
||||
}
|
||||
.into(),
|
||||
@@ -731,11 +835,13 @@ mod tests {
|
||||
all: vec![
|
||||
LeafPresent {
|
||||
r#ref: "A".into(),
|
||||
scope: Scope::Current,
|
||||
present: true,
|
||||
}
|
||||
.into(),
|
||||
LeafEquals {
|
||||
r#ref: "C".into(),
|
||||
scope: Scope::Current,
|
||||
equals: json!("y"),
|
||||
}
|
||||
.into(),
|
||||
@@ -769,11 +875,13 @@ mod tests {
|
||||
all: vec![
|
||||
LeafPresent {
|
||||
r#ref: "A".into(),
|
||||
scope: Scope::Current,
|
||||
present: true,
|
||||
}
|
||||
.into(),
|
||||
LeafEquals {
|
||||
r#ref: "B".into(),
|
||||
scope: Scope::Current,
|
||||
equals: json!("x"),
|
||||
}
|
||||
.into(),
|
||||
@@ -792,11 +900,13 @@ mod tests {
|
||||
all: vec![
|
||||
LeafPresent {
|
||||
r#ref: "A".into(),
|
||||
scope: Scope::Current,
|
||||
present: true,
|
||||
}
|
||||
.into(),
|
||||
LeafEquals {
|
||||
r#ref: "A".into(),
|
||||
scope: Scope::Current,
|
||||
equals: json!("x"),
|
||||
}
|
||||
.into(),
|
||||
@@ -815,6 +925,7 @@ mod tests {
|
||||
all: vec![
|
||||
LeafPresent {
|
||||
r#ref: "A".into(),
|
||||
scope: Scope::Current,
|
||||
present: true,
|
||||
}
|
||||
.into(),
|
||||
@@ -824,11 +935,13 @@ mod tests {
|
||||
any: vec![
|
||||
LeafEquals {
|
||||
r#ref: "A".into(),
|
||||
scope: Scope::Current,
|
||||
equals: json!("x"),
|
||||
}
|
||||
.into(),
|
||||
LeafEquals {
|
||||
r#ref: "B".into(),
|
||||
scope: Scope::Current,
|
||||
equals: json!("y"),
|
||||
}
|
||||
.into(),
|
||||
@@ -853,6 +966,7 @@ mod tests {
|
||||
all: vec![
|
||||
LeafPresent {
|
||||
r#ref: "A".into(),
|
||||
scope: Scope::Current,
|
||||
present: true,
|
||||
}
|
||||
.into(),
|
||||
@@ -862,11 +976,13 @@ mod tests {
|
||||
any: vec![
|
||||
LeafEquals {
|
||||
r#ref: "B".into(),
|
||||
scope: Scope::Current,
|
||||
equals: json!("x"),
|
||||
}
|
||||
.into(),
|
||||
LeafEquals {
|
||||
r#ref: "C".into(),
|
||||
scope: Scope::Current,
|
||||
equals: json!("y"),
|
||||
}
|
||||
.into(),
|
||||
@@ -895,11 +1011,13 @@ mod tests {
|
||||
all: vec![
|
||||
LeafPresent {
|
||||
r#ref: "A".into(),
|
||||
scope: Scope::Current,
|
||||
present: true,
|
||||
}
|
||||
.into(),
|
||||
LeafEquals {
|
||||
r#ref: "B".into(),
|
||||
scope: Scope::Current,
|
||||
equals: json!("x"),
|
||||
}
|
||||
.into(),
|
||||
@@ -912,11 +1030,13 @@ mod tests {
|
||||
all: vec![
|
||||
LeafPresent {
|
||||
r#ref: "A".into(),
|
||||
scope: Scope::Current,
|
||||
present: true,
|
||||
}
|
||||
.into(),
|
||||
LeafEquals {
|
||||
r#ref: "B".into(),
|
||||
scope: Scope::Current,
|
||||
equals: json!("y"),
|
||||
}
|
||||
.into(),
|
||||
@@ -937,11 +1057,13 @@ mod tests {
|
||||
all: vec![
|
||||
LeafPresent {
|
||||
r#ref: "A".into(),
|
||||
scope: Scope::Current,
|
||||
present: true,
|
||||
}
|
||||
.into(),
|
||||
LeafEquals {
|
||||
r#ref: "B".into(),
|
||||
scope: Scope::Current,
|
||||
equals: json!("x"),
|
||||
}
|
||||
.into(),
|
||||
@@ -954,11 +1076,13 @@ mod tests {
|
||||
all: vec![
|
||||
LeafPresent {
|
||||
r#ref: "B".into(),
|
||||
scope: Scope::Current,
|
||||
present: true,
|
||||
}
|
||||
.into(),
|
||||
LeafEquals {
|
||||
r#ref: "A".into(),
|
||||
scope: Scope::Current,
|
||||
equals: json!("y"),
|
||||
}
|
||||
.into(),
|
||||
@@ -991,11 +1115,13 @@ mod tests {
|
||||
all: vec![
|
||||
LeafPresent {
|
||||
r#ref: "B".into(),
|
||||
scope: Scope::Current,
|
||||
present: true,
|
||||
}
|
||||
.into(),
|
||||
LeafEquals {
|
||||
r#ref: "A".into(),
|
||||
scope: Scope::Current,
|
||||
equals: json!("x"),
|
||||
}
|
||||
.into(),
|
||||
@@ -1008,11 +1134,13 @@ mod tests {
|
||||
all: vec![
|
||||
LeafPresent {
|
||||
r#ref: "C".into(),
|
||||
scope: Scope::Current,
|
||||
present: true,
|
||||
}
|
||||
.into(),
|
||||
LeafEquals {
|
||||
r#ref: "B".into(),
|
||||
scope: Scope::Current,
|
||||
equals: json!("y"),
|
||||
}
|
||||
.into(),
|
||||
@@ -1046,11 +1174,13 @@ mod tests {
|
||||
any: vec![
|
||||
LeafPresent {
|
||||
r#ref: "monochromatic_color_temperature".into(),
|
||||
scope: Scope::Current,
|
||||
present: true,
|
||||
}
|
||||
.into(),
|
||||
LeafPresent {
|
||||
r#ref: "monochromatic_color_tint".into(),
|
||||
scope: Scope::Current,
|
||||
present: true,
|
||||
}
|
||||
.into(),
|
||||
@@ -1061,6 +1191,7 @@ mod tests {
|
||||
not: Box::new(
|
||||
LeafIn {
|
||||
r#ref: "film_simulation".into(),
|
||||
scope: Scope::Current,
|
||||
values: vec![json!("monochrome"), json!("acros")],
|
||||
}
|
||||
.into(),
|
||||
@@ -1076,6 +1207,7 @@ mod tests {
|
||||
all: vec![
|
||||
LeafPresent {
|
||||
r#ref: "white_balance_temperature".into(),
|
||||
scope: Scope::Current,
|
||||
present: true,
|
||||
}
|
||||
.into(),
|
||||
@@ -1083,6 +1215,7 @@ mod tests {
|
||||
not: Box::new(
|
||||
LeafEquals {
|
||||
r#ref: "white_balance".into(),
|
||||
scope: Scope::Current,
|
||||
equals: json!("temperature"),
|
||||
}
|
||||
.into(),
|
||||
@@ -1098,6 +1231,7 @@ mod tests {
|
||||
all: vec![
|
||||
LeafPresent {
|
||||
r#ref: "dynamic_range".into(),
|
||||
scope: Scope::Current,
|
||||
present: true,
|
||||
}
|
||||
.into(),
|
||||
@@ -1105,6 +1239,7 @@ mod tests {
|
||||
not: Box::new(
|
||||
LeafEquals {
|
||||
r#ref: "dynamic_range_priority".into(),
|
||||
scope: Scope::Current,
|
||||
equals: json!("off"),
|
||||
}
|
||||
.into(),
|
||||
|
||||
@@ -5,16 +5,17 @@ use quote::{format_ident, quote};
|
||||
use syn::Lifetime;
|
||||
|
||||
use crate::{
|
||||
ast::{Conjunction, Dnf, Leaf, Severity},
|
||||
ast::{Conjunction, Dnf, Leaf, Scope, Severity},
|
||||
schema::{
|
||||
alias::NormalizedRule,
|
||||
grammar::{SettingInfo, generate_conjunction, generate_dnf, generate_value_expr},
|
||||
grammar::{Scopes, SettingInfo, generate_conjunction, generate_dnf, generate_value_expr},
|
||||
},
|
||||
};
|
||||
|
||||
pub fn generate_solve(
|
||||
settings: &BTreeMap<&str, SettingInfo<'_>>,
|
||||
rules: &[NormalizedRule],
|
||||
has_original: bool,
|
||||
) -> anyhow::Result<TokenStream> {
|
||||
let error_rules: Vec<&NormalizedRule> = rules
|
||||
.iter()
|
||||
@@ -24,13 +25,38 @@ pub fn generate_solve(
|
||||
|
||||
let self_acc = quote! { self };
|
||||
let state_acc = quote! { state };
|
||||
let original_acc = quote! { original };
|
||||
|
||||
let original_param = if has_original {
|
||||
quote! { , original: &Self }
|
||||
} else {
|
||||
TokenStream::new()
|
||||
};
|
||||
let original_arg = if has_original {
|
||||
quote! { , original }
|
||||
} else {
|
||||
TokenStream::new()
|
||||
};
|
||||
let original_acc_opt: Option<&TokenStream> = if has_original {
|
||||
Some(&original_acc)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let seed_scopes = Scopes {
|
||||
current: &self_acc,
|
||||
original: original_acc_opt,
|
||||
};
|
||||
let break_scopes = Scopes {
|
||||
current: &state_acc,
|
||||
original: original_acc_opt,
|
||||
};
|
||||
|
||||
let seeds = error_rules
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, r)| {
|
||||
let i_lit = Literal::usize_suffixed(i);
|
||||
let pred = generate_dnf(settings, &r.when, &self_acc)?;
|
||||
let pred = generate_dnf(settings, &r.when, seed_scopes)?;
|
||||
Ok(quote! { ok[#i_lit] = !( #pred ); })
|
||||
})
|
||||
.collect::<anyhow::Result<Vec<_>>>()?;
|
||||
@@ -44,7 +70,7 @@ pub fn generate_solve(
|
||||
let msg = &r.message;
|
||||
quote! {
|
||||
if !ok[#i_lit] {
|
||||
if !self.#fn_name(pin, &ok) {
|
||||
if !self.#fn_name(pin, &ok #original_arg) {
|
||||
::anyhow::bail!(#msg);
|
||||
}
|
||||
ok[#i_lit] = true;
|
||||
@@ -60,12 +86,14 @@ pub fn generate_solve(
|
||||
let fn_name = format_ident!("try_repair_rule_{}", i);
|
||||
let i_lit = Literal::usize_suffixed(i);
|
||||
let mut counter = 0usize;
|
||||
let walk = generate_dnf_walk(settings, &r.when, &mut counter)?;
|
||||
let walk = generate_dnf_walk(settings, &r.when, &mut counter, has_original)?;
|
||||
Ok(quote! {
|
||||
#[allow(unused_variables)]
|
||||
fn #fn_name(
|
||||
&mut self,
|
||||
pin: &::std::collections::HashSet<&'static str>,
|
||||
ok: &[bool; #n_lit],
|
||||
ok: &[bool; #n_lit]
|
||||
#original_param,
|
||||
) -> bool {
|
||||
let current: usize = #i_lit;
|
||||
#walk
|
||||
@@ -79,7 +107,7 @@ pub fn generate_solve(
|
||||
.enumerate()
|
||||
.map(|(i, r)| {
|
||||
let i_lit = Literal::usize_suffixed(i);
|
||||
let pred = generate_dnf(settings, &r.when, &state_acc)?;
|
||||
let pred = generate_dnf(settings, &r.when, break_scopes)?;
|
||||
Ok(quote! {
|
||||
if ok[#i_lit] && current != #i_lit && ( #pred ) { return true; }
|
||||
})
|
||||
@@ -87,10 +115,11 @@ pub fn generate_solve(
|
||||
.collect::<anyhow::Result<Vec<_>>>()?;
|
||||
|
||||
Ok(quote! {
|
||||
#[allow(unused_assignments)]
|
||||
#[allow(unused_assignments, unused_variables)]
|
||||
pub fn solve(
|
||||
&mut self,
|
||||
pin: &::std::collections::HashSet<&'static str>,
|
||||
pin: &::std::collections::HashSet<&'static str>
|
||||
#original_param,
|
||||
) -> ::anyhow::Result<()> {
|
||||
let mut ok: [bool; #n_lit] = [false; #n_lit];
|
||||
#( #seeds )*
|
||||
@@ -100,10 +129,12 @@ pub fn generate_solve(
|
||||
|
||||
#( #repair_fns )*
|
||||
|
||||
#[allow(unused_variables)]
|
||||
fn re_fires_other_ok(
|
||||
state: &Self,
|
||||
ok: &[bool; #n_lit],
|
||||
current: usize,
|
||||
current: usize
|
||||
#original_param,
|
||||
) -> bool {
|
||||
#( #breakage_arms )*
|
||||
false
|
||||
@@ -138,6 +169,7 @@ fn generate_dnf_walk(
|
||||
settings: &BTreeMap<&str, SettingInfo<'_>>,
|
||||
dnf: &Dnf,
|
||||
counter: &mut usize,
|
||||
has_original: bool,
|
||||
) -> anyhow::Result<TokenStream> {
|
||||
if dnf.is_contradiction() {
|
||||
return Ok(quote! { true });
|
||||
@@ -151,13 +183,19 @@ fn generate_dnf_walk(
|
||||
let outer = walk_label(depth);
|
||||
let inner = inner_label(depth);
|
||||
|
||||
let dnf_eval = generate_dnf(settings, dnf, "e! { self })?;
|
||||
let self_acc = quote! { self };
|
||||
let original_acc = quote! { original };
|
||||
let scopes = Scopes {
|
||||
current: &self_acc,
|
||||
original: has_original.then_some(&original_acc),
|
||||
};
|
||||
let dnf_eval = generate_dnf(settings, dnf, scopes)?;
|
||||
|
||||
let steps = dnf
|
||||
.0
|
||||
.iter()
|
||||
.map(|conj| {
|
||||
let sub = generate_conjunction_walk(settings, conj, counter)?;
|
||||
let sub = generate_conjunction_walk(settings, conj, counter, has_original)?;
|
||||
Ok(quote! {
|
||||
{
|
||||
let succ: bool = #sub;
|
||||
@@ -185,6 +223,7 @@ fn generate_conjunction_walk(
|
||||
settings: &BTreeMap<&str, SettingInfo<'_>>,
|
||||
conj: &Conjunction,
|
||||
counter: &mut usize,
|
||||
has_original: bool,
|
||||
) -> anyhow::Result<TokenStream> {
|
||||
if conj.is_empty() {
|
||||
return Ok(quote! { false });
|
||||
@@ -194,11 +233,17 @@ fn generate_conjunction_walk(
|
||||
*counter += 1;
|
||||
let label = walk_label(depth);
|
||||
|
||||
let conj_eval = generate_conjunction(settings, conj, "e! { self })?;
|
||||
let self_acc = quote! { self };
|
||||
let original_acc = quote! { original };
|
||||
let scopes = Scopes {
|
||||
current: &self_acc,
|
||||
original: has_original.then_some(&original_acc),
|
||||
};
|
||||
let conj_eval = generate_conjunction(settings, conj, scopes)?;
|
||||
|
||||
let attempts = conj
|
||||
.iter()
|
||||
.map(|leaf| generate_leaf_attempt(settings, leaf, &label))
|
||||
.map(|leaf| generate_leaf_attempt(settings, leaf, &label, has_original))
|
||||
.collect::<anyhow::Result<Vec<_>>>()?;
|
||||
|
||||
Ok(quote! {
|
||||
@@ -214,18 +259,27 @@ fn generate_leaf_attempt(
|
||||
settings: &BTreeMap<&str, SettingInfo<'_>>,
|
||||
leaf: &Leaf,
|
||||
parent: &Lifetime,
|
||||
has_original: bool,
|
||||
) -> anyhow::Result<TokenStream> {
|
||||
if leaf.scope() == Scope::Original {
|
||||
return Ok(TokenStream::new());
|
||||
}
|
||||
let Some((info, mutation)) = leaf_flip(settings, leaf)? else {
|
||||
return Ok(TokenStream::new());
|
||||
};
|
||||
let field_id = info.id;
|
||||
let field_ident = info.field_ident();
|
||||
let original_arg = if has_original {
|
||||
quote! { , original }
|
||||
} else {
|
||||
TokenStream::new()
|
||||
};
|
||||
Ok(quote! {
|
||||
{
|
||||
if !pin.contains(#field_id) {
|
||||
let saved = self.#field_ident.take();
|
||||
#mutation
|
||||
if !Self::re_fires_other_ok(self, ok, current) {
|
||||
if !Self::re_fires_other_ok(self, ok, current #original_arg) {
|
||||
break #parent true;
|
||||
}
|
||||
self.#field_ident = saved;
|
||||
@@ -289,7 +343,7 @@ mod tests {
|
||||
use serde_json::json;
|
||||
|
||||
use super::*;
|
||||
use crate::ast::{LeafEquals, LeafPresent, Predicate};
|
||||
use crate::ast::{LeafEquals, LeafPresent, Predicate, Scope};
|
||||
|
||||
fn integer_info(id: &'static str) -> SettingInfo<'static> {
|
||||
SettingInfo {
|
||||
@@ -318,7 +372,7 @@ mod tests {
|
||||
#[test]
|
||||
fn empty_rule_set_emits_solve_that_does_nothing() {
|
||||
let settings = BTreeMap::new();
|
||||
let out = generate_solve(&settings, &[]).unwrap().to_string();
|
||||
let out = generate_solve(&settings, &[], false).unwrap().to_string();
|
||||
assert!(out.contains("fn solve"));
|
||||
assert!(out.contains("[bool ; 0usize]"));
|
||||
}
|
||||
@@ -330,12 +384,15 @@ mod tests {
|
||||
let rules = vec![nrule_msg(
|
||||
LeafEquals {
|
||||
r#ref: "a".into(),
|
||||
scope: Scope::Current,
|
||||
equals: json!(1),
|
||||
}
|
||||
.into(),
|
||||
"bad a",
|
||||
)];
|
||||
let out = generate_solve(&settings, &rules).unwrap().to_string();
|
||||
let out = generate_solve(&settings, &rules, false)
|
||||
.unwrap()
|
||||
.to_string();
|
||||
assert!(out.contains("try_repair_rule_0"));
|
||||
assert!(out.contains("pin . contains (\"a\")"));
|
||||
assert!(out.contains("re_fires_other_ok"));
|
||||
@@ -353,6 +410,7 @@ mod tests {
|
||||
message: "w".into(),
|
||||
when: Predicate::from(LeafEquals {
|
||||
r#ref: "a".into(),
|
||||
scope: Scope::Current,
|
||||
equals: json!(1),
|
||||
})
|
||||
.into(),
|
||||
@@ -362,12 +420,15 @@ mod tests {
|
||||
message: "i".into(),
|
||||
when: Predicate::from(LeafEquals {
|
||||
r#ref: "a".into(),
|
||||
scope: Scope::Current,
|
||||
equals: json!(2),
|
||||
})
|
||||
.into(),
|
||||
},
|
||||
];
|
||||
let out = generate_solve(&settings, &rules).unwrap().to_string();
|
||||
let out = generate_solve(&settings, &rules, false)
|
||||
.unwrap()
|
||||
.to_string();
|
||||
assert!(!out.contains("try_repair_rule_0"));
|
||||
assert!(out.contains("[bool ; 0usize]"));
|
||||
}
|
||||
@@ -382,11 +443,13 @@ mod tests {
|
||||
all: vec![
|
||||
LeafEquals {
|
||||
r#ref: "a".into(),
|
||||
scope: Scope::Current,
|
||||
equals: json!(1),
|
||||
}
|
||||
.into(),
|
||||
LeafPresent {
|
||||
r#ref: "b".into(),
|
||||
scope: Scope::Current,
|
||||
present: true,
|
||||
}
|
||||
.into(),
|
||||
@@ -394,7 +457,9 @@ mod tests {
|
||||
}
|
||||
.into(),
|
||||
)];
|
||||
let out = generate_solve(&settings, &rules).unwrap().to_string();
|
||||
let out = generate_solve(&settings, &rules, false)
|
||||
.unwrap()
|
||||
.to_string();
|
||||
assert!(out.contains("pin . contains (\"a\")"));
|
||||
assert!(out.contains("pin . contains (\"b\")"));
|
||||
assert!(out.contains("let snap = self . clone ()"));
|
||||
@@ -409,6 +474,7 @@ mod tests {
|
||||
not: Box::new(
|
||||
LeafEquals {
|
||||
r#ref: "a".into(),
|
||||
scope: Scope::Current,
|
||||
equals: json!(1),
|
||||
}
|
||||
.into(),
|
||||
@@ -416,11 +482,59 @@ mod tests {
|
||||
}
|
||||
.into(),
|
||||
)];
|
||||
let out = generate_solve(&settings, &rules).unwrap().to_string();
|
||||
assert!(
|
||||
out.contains("self . a = Some"),
|
||||
"expected NotEquals flip to set field, got: {out}"
|
||||
);
|
||||
let out = generate_solve(&settings, &rules, false)
|
||||
.unwrap()
|
||||
.to_string();
|
||||
assert!(out.contains("self . a = Some"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn original_scope_leaf_is_non_flippable_and_threads_original_param() {
|
||||
let mut settings = BTreeMap::new();
|
||||
let _ = settings.insert("a", integer_info("a"));
|
||||
let _ = settings.insert("b", integer_info("b"));
|
||||
let rules = vec![nrule(
|
||||
crate::ast::PredAll {
|
||||
all: vec![
|
||||
LeafEquals {
|
||||
r#ref: "a".into(),
|
||||
scope: Scope::Original,
|
||||
equals: json!(1),
|
||||
}
|
||||
.into(),
|
||||
LeafPresent {
|
||||
r#ref: "b".into(),
|
||||
scope: Scope::Current,
|
||||
present: true,
|
||||
}
|
||||
.into(),
|
||||
],
|
||||
}
|
||||
.into(),
|
||||
)];
|
||||
let out = generate_solve(&settings, &rules, true).unwrap().to_string();
|
||||
assert!(out.contains("original : & Self"));
|
||||
assert!(out.contains("original . a"));
|
||||
assert!(!out.contains("self . a = None"));
|
||||
assert!(out.contains("self . b = None"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn solve_without_original_emits_no_original_param() {
|
||||
let mut settings = BTreeMap::new();
|
||||
let _ = settings.insert("a", integer_info("a"));
|
||||
let rules = vec![nrule(
|
||||
LeafEquals {
|
||||
r#ref: "a".into(),
|
||||
scope: Scope::Current,
|
||||
equals: json!(1),
|
||||
}
|
||||
.into(),
|
||||
)];
|
||||
let out = generate_solve(&settings, &rules, false)
|
||||
.unwrap()
|
||||
.to_string();
|
||||
assert!(!out.contains("original"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -431,6 +545,7 @@ mod tests {
|
||||
nrule_msg(
|
||||
LeafEquals {
|
||||
r#ref: "a".into(),
|
||||
scope: Scope::Current,
|
||||
equals: json!(1),
|
||||
}
|
||||
.into(),
|
||||
@@ -439,13 +554,16 @@ mod tests {
|
||||
nrule_msg(
|
||||
LeafEquals {
|
||||
r#ref: "a".into(),
|
||||
scope: Scope::Current,
|
||||
equals: json!(2),
|
||||
}
|
||||
.into(),
|
||||
"r1",
|
||||
),
|
||||
];
|
||||
let out = generate_solve(&settings, &rules).unwrap().to_string();
|
||||
let out = generate_solve(&settings, &rules, false)
|
||||
.unwrap()
|
||||
.to_string();
|
||||
assert!(out.contains("current != 0usize"));
|
||||
assert!(out.contains("current != 1usize"));
|
||||
}
|
||||
|
||||
@@ -171,6 +171,10 @@ features: {
|
||||
rules: [
|
||||
// Same shape as simulation rules. Pin allowed values per camera
|
||||
// here too - same reasoning as for simulations.
|
||||
//
|
||||
// Render rules can additionally use `scope: "original"` on a
|
||||
// leaf to refer to the camera-reported value of an option
|
||||
// *before* the user's partial was merged.
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+25
-8
@@ -49,6 +49,17 @@ Some examples:
|
||||
]}
|
||||
```
|
||||
|
||||
### Scope
|
||||
|
||||
Every leaf also carries `scope: "current" | "original"`, defaulting to
|
||||
`"current"`. `"original"` reads from the pre-merge snapshot and is only
|
||||
available in render-block _rules_ (the camera-reported profile before the user's
|
||||
partial). Transformation `when` predicates are restricted to current scope.
|
||||
Assignments never carry scope; you cannot write to the original. See
|
||||
[rules / cross-state rules](rules.md#cross-state-rules) for an
|
||||
example and [analyses / scope](../internals/analyses.md#scope) for the per-pass
|
||||
behaviour.
|
||||
|
||||
### Type Discipline
|
||||
|
||||
`#GrammarBase._ids` carries four typed-ref sets keyed on the option kind:
|
||||
@@ -63,6 +74,10 @@ The leaf type definitions constrain `ref` to the appropriate set, which is why
|
||||
CUE rejects `{ref: "image_size", lt: 5}` at export (`image_size` is an enum, not
|
||||
in `_ids.i`).
|
||||
|
||||
`#GrammarBase` also carries a `_scoped: bool` toggle. Render blocks set it true
|
||||
to admit `scope: "original"`; simulation blocks leave it false, which restricts
|
||||
`scope` to `"current"`.
|
||||
|
||||
For enum `equals` / `in`, CUE also restricts the value side:
|
||||
|
||||
```cue
|
||||
@@ -96,16 +111,18 @@ There is no "merge" assignment; express that as multiple `apply` entries.
|
||||
|
||||
## Compilation
|
||||
|
||||
Each leaf becomes a Rust boolean expression over `self.<field>`. Sketch:
|
||||
Each leaf becomes a Rust boolean expression over `self.<field>`, or
|
||||
`original.<field>` when `scope: "original"`. Sketch:
|
||||
|
||||
```
|
||||
{ref: "x", equals: 5} (integer) -> self.x.is_some_and(|v| i32::from(v) == 5i32)
|
||||
{ref: "x", in: ["a", "b"]} (enum) -> self.x.is_some_and(|v| matches!(v, Path::A | Path::B))
|
||||
{ref: "x", min: -1.0, max: 1.0} (f32) -> self.x.is_some_and(|v| (-1.0..=1.0).contains(&f32::from(v)))
|
||||
{ref: "x", present: true} -> self.x.is_some()
|
||||
{all: [P, Q]} -> (P) && (Q)
|
||||
{any: [P, Q]} -> (P) || (Q)
|
||||
{not: P} -> !(P)
|
||||
{ref: "x", equals: 5} (integer) -> self.x.is_some_and(|v| i32::from(v) == 5i32)
|
||||
{ref: "x", scope: "original", equals: 5} -> original.x.is_some_and(|v| i32::from(v) == 5i32)
|
||||
{ref: "x", in: ["a", "b"]} (enum) -> self.x.is_some_and(|v| matches!(v, Path::A | Path::B))
|
||||
{ref: "x", min: -1.0, max: 1.0} (f32) -> self.x.is_some_and(|v| (-1.0..=1.0).contains(&f32::from(v)))
|
||||
{ref: "x", present: true} -> self.x.is_some()
|
||||
{all: [P, Q]} -> (P) && (Q)
|
||||
{any: [P, Q]} -> (P) || (Q)
|
||||
{not: P} -> !(P)
|
||||
```
|
||||
|
||||
Float equality uses `(v - x).abs() < f32::EPSILON`, not raw `==`. Strings use
|
||||
|
||||
+36
-8
@@ -220,13 +220,41 @@ What this gives you:
|
||||
That's roughly 25 lines of CUE producing several hundred lines of emitted Rust
|
||||
that implements all four things consistently.
|
||||
|
||||
## Cross-State Rules
|
||||
|
||||
Render rules can refer to the pre-merge state via `scope: "original"` - the
|
||||
camera's reported render profile before the user's partial was merged.
|
||||
|
||||
```cue
|
||||
{
|
||||
message: "Dynamic Range cannot exceed the value the image was shot with."
|
||||
when: any: [
|
||||
{all: [
|
||||
{ref: "dynamic_range", scope: "original", equals: "hdr200"},
|
||||
{not: {ref: "dynamic_range", in: ["hdr100", "hdr200"]}},
|
||||
]},
|
||||
{all: [
|
||||
{ref: "dynamic_range", scope: "original", equals: "hdr400"},
|
||||
{not: {ref: "dynamic_range", in: ["hdr100", "hdr200", "hdr400"]}},
|
||||
]},
|
||||
// ... and so on
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
If the only fix would be to change the original, `solve` bails with the rule's
|
||||
message - the camera shot it that way, we can't undo it. See
|
||||
[analyses / scope](../internals/analyses.md#scope) for how each pass handles
|
||||
original-scope leaves.
|
||||
|
||||
## When to Use What
|
||||
|
||||
| Need | Use |
|
||||
| -------------------------------------------- | ------------------------------------------------------------------------------------------- |
|
||||
| "These two values are incompatible." | A rule with `severity: error`. |
|
||||
| "This field doesn't make sense when..." | A rule with the standard `Present(X) && ~cond` shape. The presence-DAG handles read-gating. |
|
||||
| "Allow `compound_value`; flatten to (a, b)." | A transformation with `when: equals` and a multi-`apply`. |
|
||||
| "Set a default for an inline render slot." | An unconditional transformation. |
|
||||
| "Suggest something to the user." | A rule with `severity: warning`. |
|
||||
| "Log when an unusual combination appears." | A rule with `severity: info`. |
|
||||
| Need | Use |
|
||||
| ----------------------------------------------------------- | ------------------------------------------------------------------------------------------- |
|
||||
| "These two values are incompatible." | A rule with `severity: error`. |
|
||||
| "This field doesn't make sense when..." | A rule with the standard `Present(X) && ~cond` shape. The presence-DAG handles read-gating. |
|
||||
| "Allow `compound_value`; flatten to (a, b)." | A transformation with `when: equals` and a multi-`apply`. |
|
||||
| "Set a default for an inline render slot." | An unconditional transformation. |
|
||||
| "Suggest something to the user." | A rule with `severity: warning`. |
|
||||
| "Log when an unusual combination appears." | A rule with `severity: info`. |
|
||||
| "Constrain a render based on what the image was shot with." | A render rule that references `scope: "original"`. |
|
||||
|
||||
+65
-17
@@ -22,6 +22,33 @@ TokenStream (schema/grammar.rs + emitters)
|
||||
Plus a separate pass on transformations for invertibility detection
|
||||
(`schema/inverse.rs`).
|
||||
|
||||
## Scope
|
||||
|
||||
Every `Leaf` carries `scope: Scope` (`Current` or `Original`, defaulting to
|
||||
`Current`). The codegen helpers in
|
||||
[`schema/grammar.rs`](../../crates/codegen/src/schema/grammar.rs) take a
|
||||
`Scopes { current: &TokenStream, original: Option<&TokenStream> }` and route
|
||||
each leaf to the right accessor via `Scopes::pick(scope)`. An `Original` leaf in
|
||||
a context with no original accessor fires `unreachable!()` - CUE typing should
|
||||
keep that path unreachable.
|
||||
|
||||
Bindings differ by call site:
|
||||
|
||||
| Site | `current` | `original` |
|
||||
| ----------------------------------------------- | --------- | ---------- |
|
||||
| `solve` seeds + repair walks | `self` | `original` |
|
||||
| `re_fires_other_ok` | `state` | `original` |
|
||||
| `emit_warnings_and_infos` | `self` | `original` |
|
||||
| `apply_transformations` | `self` | none |
|
||||
| Simulation `try_pull` | `staged` | none |
|
||||
| Render deserialization (`generate_convert_one`) | `profile` | none |
|
||||
|
||||
The render path snapshots `original = self.clone()` before merging the user
|
||||
partial in `try_update_from`; simulation has no original because simulation
|
||||
settings carry no immutable shot-time state. Read deserialization runs over wire
|
||||
bytes alone; there is no "original" yet (it's what the read is producing). The
|
||||
per-pass sections below note where scope changes behaviour.
|
||||
|
||||
## DNF Normalization
|
||||
|
||||
[`ast/dnf.rs`](../../crates/codegen/src/ast/dnf.rs) turns any `Predicate` into a
|
||||
@@ -63,6 +90,9 @@ Conjunction equality is **multiset** equality
|
||||
([`util::multiset`](../../crates/codegen/src/util/multiset.rs)) so `A && B && A`
|
||||
!= `A && B`, but `A && B` == `B && A`. Hashing matches.
|
||||
|
||||
Scope is part of `Leaf::PartialEq` (and `Hash`), so a `Current` leaf and an
|
||||
`Original` leaf compare unequal even if their other fields match.
|
||||
|
||||
## Alias Substitution
|
||||
|
||||
[`schema/alias.rs`](../../crates/codegen/src/schema/alias.rs) normalizes a
|
||||
@@ -108,6 +138,11 @@ Important properties:
|
||||
The resulting `NormalizedRule` carries the normalized DNF, the original
|
||||
severity, and the original message.
|
||||
|
||||
Aliases come from transformations whose `apply` always produces `Current`-scope
|
||||
leaves. Trigger matching uses multiset-subset over `Leaf`, which includes scope
|
||||
in equality, so original-scope literals in a rule conjunction are inert under
|
||||
substitution.
|
||||
|
||||
## The Presence DAG
|
||||
|
||||
[`schema/presence.rs`](../../crates/codegen/src/schema/presence.rs) is the
|
||||
@@ -118,27 +153,30 @@ highest-leverage analysis. From the error rules alone, it derives:
|
||||
|
||||
For each error rule, for each conjunction in its DNF:
|
||||
|
||||
1. Partition the literals into `Present(X, true)` anchors, `Present(X, false)`
|
||||
anchors, and "other" literals.
|
||||
2. Pick a polarity:
|
||||
1. Drop original-scope leaves. They describe a relationship between the
|
||||
pre-merge snapshot and the candidate; the read path has no "original" yet, so
|
||||
they cannot anchor or gate a read.
|
||||
2. Partition the remaining literals into `Present(X, true)` anchors,
|
||||
`Present(X, false)` anchors, and "other" literals.
|
||||
3. Pick a polarity:
|
||||
- If there are any `true` anchors -> polarity is `true`; targets are the
|
||||
true-anchors; false-anchors fold into the gating clauses.
|
||||
- Else if there are only `false` anchors -> polarity is `false`; targets are
|
||||
the false-anchors; gating clauses are as-is.
|
||||
- Else (only "other" literals) - this rule has no presence anchor, so it
|
||||
doesn't contribute to the DAG. It still validates.
|
||||
3. Collect `gating_refs` - every ref mentioned in the gating clauses.
|
||||
4. Reject self-gating: if any target ref appears in `gating_refs`, bail.
|
||||
4. Collect `gating_refs` - every ref mentioned in the gating clauses.
|
||||
5. Reject self-gating: if any target ref appears in `gating_refs`, bail.
|
||||
Deciding whether to read X would require knowing X's value.
|
||||
5. For each (gating_ref, target) pair, add an edge `gating_ref ->
|
||||
6. For each (gating_ref, target) pair, add an edge `gating_ref ->
|
||||
target`
|
||||
(target must be read after gating ref).
|
||||
6. Compute the "gate" as a `Dnf`:
|
||||
7. Compute the "gate" as a `Dnf`:
|
||||
- Polarity true -> gate is the negation of each gating literal, joined as
|
||||
separate disjuncts (`!cond -> skip X`).
|
||||
- Polarity false -> gate is the original gating literals as a single
|
||||
conjunction.
|
||||
7. Append the gate to `contributions[target]`.
|
||||
8. Append the gate to `contributions[target]`.
|
||||
|
||||
After iterating, each target's contributions are combined with `All` (every
|
||||
contributing rule's gate must hold for the read to fire). The resulting
|
||||
@@ -169,7 +207,11 @@ caused by two rules with crossed gating (`A gates B` and `B gates A`).
|
||||
## Repair
|
||||
|
||||
[`schema/repair.rs`](../../crates/codegen/src/schema/repair.rs) emits the
|
||||
`solve(&mut self, pin: &HashSet<&'static str>)` function. The emitted code does:
|
||||
`solve(&mut self, pin: &HashSet<&'static str>)` function. The render path gets
|
||||
an extra `original: &Self` parameter; original-scope leaves in the rule DNFs
|
||||
read from that snapshot. Simulation `solve` keeps the original-free signature.
|
||||
|
||||
The emitted code does:
|
||||
|
||||
```rust
|
||||
let mut ok = [false; N];
|
||||
@@ -188,7 +230,10 @@ every disjunct (a conjunction whose truth implies the rule firing), attempt to
|
||||
break the disjunct by flipping one of its literals. The flip is only attempted
|
||||
if the target field is not `pin`'d - i.e. the user did not explicitly set it.
|
||||
|
||||
For each leaf, the flip is:
|
||||
Original-scope leaves are non-flippable; the walk skips them. If a disjunct has
|
||||
no flippable literal left, repair falls through and the outer loop bails.
|
||||
|
||||
For each current-scope leaf, the flip is:
|
||||
|
||||
| Literal | Flip |
|
||||
| ----------------------------------------------------------------------- | --------------------------------------------- |
|
||||
@@ -234,16 +279,15 @@ runs.
|
||||
|
||||
A transformation `T` is **invertible** iff:
|
||||
|
||||
- `T.when` is a single `Equals` leaf (not compound).
|
||||
- No other transformation in the same list has the same `apply` set-pattern
|
||||
(same `(field, value)` pairs).
|
||||
- `T.when` is a single current-scope `Equals` leaf.
|
||||
- No other transformation in the same list has the same `apply` set-pattern.
|
||||
|
||||
Reasoning:
|
||||
|
||||
- The first constraint makes the "is this pattern present on the wire" check
|
||||
unambiguous to write: a single leaf as the reconstructed `when`.
|
||||
- The second makes it unambiguous to attribute: if two transformations produce
|
||||
the same flat form, you can't tell from the wire which one was applied.
|
||||
- A single Equals leaf makes the "is this pattern on the wire" check unambiguous
|
||||
to write.
|
||||
- A unique `apply` pattern makes it unambiguous to attribute - two
|
||||
transformations producing the same flat form can't be told apart on read.
|
||||
|
||||
For each invertible `T`, the emitted inverse:
|
||||
|
||||
@@ -303,6 +347,10 @@ candidate : <Camera>Simulation
|
||||
*self = candidate
|
||||
```
|
||||
|
||||
The render path is identical in shape but snapshots `original = self.clone()`
|
||||
before the merge and passes it through `solve(&pin, &original)` and
|
||||
`emit_warnings_and_infos(&original)`.
|
||||
|
||||
The `try_pull` (read) path is simpler:
|
||||
|
||||
```
|
||||
|
||||
@@ -131,6 +131,10 @@ the `Simulation` trait impl (`try_pull` / `try_push` talking to PTP),
|
||||
respects "I set X explicitly; repair around it" without needing extra
|
||||
annotations.
|
||||
|
||||
The render path additionally snapshots `original = self.clone()` before the
|
||||
merge and threads it through `solve` and `emit_warnings_and_infos`; see
|
||||
[analyses / scope](analyses.md#scope).
|
||||
|
||||
`try_pull` reads each setting in topological read order (see
|
||||
[analyses / presence DAG](analyses.md#the-presence-dag)); for each field with a
|
||||
derived gate, the read is wrapped in `if gate { read }
|
||||
|
||||
@@ -104,6 +104,9 @@ render-capable camera must override `render`, which the codegen does: send the
|
||||
image, fetch the current `<Camera>RenderProfile`, `try_update_from(partial)`,
|
||||
write back, then `render_image`.
|
||||
|
||||
For the cross-state semantics that `try_update_from` enables, see
|
||||
[fml/rules / cross-state rules](../fml/rules.md#cross-state-rules).
|
||||
|
||||
## PTP and Option Traits
|
||||
|
||||
[`src/lib/ptp/option.rs`](../../src/lib/ptp/option.rs) declares the two
|
||||
|
||||
+29
-2
@@ -81,6 +81,7 @@ cameras: [string]: #Camera
|
||||
}
|
||||
|
||||
#Grammar: #GrammarBase & {
|
||||
_scoped: true
|
||||
_ids: {
|
||||
all: [for _, field in fields {field.id}]
|
||||
i: list.Concat([
|
||||
@@ -93,11 +94,16 @@ cameras: [string]: #Camera
|
||||
}
|
||||
}
|
||||
|
||||
#TransformationGrammar: #GrammarBase & {
|
||||
_scoped: false
|
||||
_ids: #Grammar._ids
|
||||
}
|
||||
|
||||
transformations?: [...#Transformation]
|
||||
|
||||
#Transformation: #TransformationBase & {
|
||||
when?: #Grammar.#Predicate
|
||||
apply: [...#Grammar.#Assignment]
|
||||
when?: #TransformationGrammar.#Predicate
|
||||
apply: [...#TransformationGrammar.#Assignment]
|
||||
}
|
||||
|
||||
rules?: [...#Rule]
|
||||
@@ -423,6 +429,27 @@ cameras: {
|
||||
]},
|
||||
]
|
||||
},
|
||||
{
|
||||
message: "Dynamic Range cannot exceed the value the image was shot with."
|
||||
when: any: [
|
||||
{all: [
|
||||
{ref: "dynamic_range", scope: "original", equals: "hdr100"},
|
||||
{not: {ref: "dynamic_range", in: ["hdr100"]}},
|
||||
]},
|
||||
{all: [
|
||||
{ref: "dynamic_range", scope: "original", equals: "hdr200"},
|
||||
{not: {ref: "dynamic_range", in: ["hdr100", "hdr200"]}},
|
||||
]},
|
||||
{all: [
|
||||
{ref: "dynamic_range", scope: "original", equals: "hdr400"},
|
||||
{not: {ref: "dynamic_range", in: ["hdr100", "hdr200", "hdr400"]}},
|
||||
]},
|
||||
{all: [
|
||||
{ref: "dynamic_range", scope: "original", equals: "hdr800"},
|
||||
{not: {ref: "dynamic_range", in: ["hdr100", "hdr200", "hdr400", "hdr800"]}},
|
||||
]},
|
||||
]
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+30
-19
@@ -1,5 +1,7 @@
|
||||
package fml
|
||||
|
||||
#Scope: "current" | "original"
|
||||
|
||||
#GrammarBase: {
|
||||
_ids: {
|
||||
all: [...string]
|
||||
@@ -9,6 +11,8 @@ package fml
|
||||
e: [...string]
|
||||
}
|
||||
|
||||
_scoped: bool | *false
|
||||
|
||||
#Predicate: #Predicates.#Logic | #Predicates.#Leaf
|
||||
|
||||
#Predicates: {
|
||||
@@ -31,6 +35,13 @@ package fml
|
||||
#Predicates.#PredicatePresent |
|
||||
#Predicates.#PredicateBool
|
||||
|
||||
_LeafScope: {
|
||||
scope: #Scope | *"current"
|
||||
if !_scoped {
|
||||
scope: "current"
|
||||
}
|
||||
}
|
||||
|
||||
#PredicateInteger:
|
||||
#PredicateIntegerEquals |
|
||||
#PredicateIntegerIn |
|
||||
@@ -40,13 +51,13 @@ package fml
|
||||
#PredicateIntegerGreaterThanOrEqual |
|
||||
#PredicateIntegerBetween
|
||||
|
||||
#PredicateIntegerEquals: {ref: or(_ids.i), equals: int}
|
||||
#PredicateIntegerIn: {ref: or(_ids.i), in: [...int]}
|
||||
#PredicateIntegerLessThan: {ref: or(_ids.i), lt: int}
|
||||
#PredicateIntegerLessThanOrEqual: {ref: or(_ids.i), lte: int}
|
||||
#PredicateIntegerGreaterThan: {ref: or(_ids.i), gt: int}
|
||||
#PredicateIntegerGreaterThanOrEqual: {ref: or(_ids.i), gte: int}
|
||||
#PredicateIntegerBetween: {ref: or(_ids.i), min: int, max: int}
|
||||
#PredicateIntegerEquals: _LeafScope & {ref: or(_ids.i), equals: int}
|
||||
#PredicateIntegerIn: _LeafScope & {ref: or(_ids.i), in: [...int]}
|
||||
#PredicateIntegerLessThan: _LeafScope & {ref: or(_ids.i), lt: int}
|
||||
#PredicateIntegerLessThanOrEqual: _LeafScope & {ref: or(_ids.i), lte: int}
|
||||
#PredicateIntegerGreaterThan: _LeafScope & {ref: or(_ids.i), gt: int}
|
||||
#PredicateIntegerGreaterThanOrEqual: _LeafScope & {ref: or(_ids.i), gte: int}
|
||||
#PredicateIntegerBetween: _LeafScope & {ref: or(_ids.i), min: int, max: int}
|
||||
|
||||
#PredicateFloat:
|
||||
#PredicateFloatEquals |
|
||||
@@ -57,32 +68,32 @@ package fml
|
||||
#PredicateFloatGreaterThanOrEqual |
|
||||
#PredicateFloatBetween
|
||||
|
||||
#PredicateFloatEquals: {ref: or(_ids.f), equals: float}
|
||||
#PredicateFloatIn: {ref: or(_ids.f), in: [...float]}
|
||||
#PredicateFloatLessThan: {ref: or(_ids.f), lt: float}
|
||||
#PredicateFloatLessThanOrEqual: {ref: or(_ids.f), lte: float}
|
||||
#PredicateFloatGreaterThan: {ref: or(_ids.f), gt: float}
|
||||
#PredicateFloatGreaterThanOrEqual: {ref: or(_ids.f), gte: float}
|
||||
#PredicateFloatBetween: {ref: or(_ids.f), min: float, max: float}
|
||||
#PredicateFloatEquals: _LeafScope & {ref: or(_ids.f), equals: float}
|
||||
#PredicateFloatIn: _LeafScope & {ref: or(_ids.f), in: [...float]}
|
||||
#PredicateFloatLessThan: _LeafScope & {ref: or(_ids.f), lt: float}
|
||||
#PredicateFloatLessThanOrEqual: _LeafScope & {ref: or(_ids.f), lte: float}
|
||||
#PredicateFloatGreaterThan: _LeafScope & {ref: or(_ids.f), gt: float}
|
||||
#PredicateFloatGreaterThanOrEqual: _LeafScope & {ref: or(_ids.f), gte: float}
|
||||
#PredicateFloatBetween: _LeafScope & {ref: or(_ids.f), min: float, max: float}
|
||||
|
||||
#PredicateString:
|
||||
#PredicateStringEquals |
|
||||
#PredicateStringIn
|
||||
|
||||
#PredicateStringEquals: {ref: or(_ids.s), equals: string}
|
||||
#PredicateStringIn: {ref: or(_ids.s), in: [...string]}
|
||||
#PredicateStringEquals: _LeafScope & {ref: or(_ids.s), equals: string}
|
||||
#PredicateStringIn: _LeafScope & {ref: or(_ids.s), in: [...string]}
|
||||
|
||||
#PredicateEnum:
|
||||
#PredicateEnumEquals |
|
||||
#PredicateEnumIn
|
||||
|
||||
#PredicateEnumEquals: {
|
||||
#PredicateEnumEquals: _LeafScope & {
|
||||
ref: or(_ids.e)
|
||||
equals: or([for v in options[ref].spec.rules.variants {v.id}])
|
||||
}
|
||||
#PredicateEnumIn: {ref: or(_ids.e), in: [...or([for v in options[ref].spec.rules.variants {v.id}])]}
|
||||
#PredicateEnumIn: _LeafScope & {ref: or(_ids.e), in: [...or([for v in options[ref].spec.rules.variants {v.id}])]}
|
||||
|
||||
#PredicatePresent: {ref: or(_ids.all), present: bool}
|
||||
#PredicatePresent: _LeafScope & {ref: or(_ids.all), present: bool}
|
||||
|
||||
#PredicateBool: bool
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user