diff --git a/crates/codegen/src/ast/cameras.rs b/crates/codegen/src/ast/cameras.rs index 7be9ec6..e8d3d5a 100644 --- a/crates/codegen/src/ast/cameras.rs +++ b/crates/codegen/src/ast/cameras.rs @@ -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(), diff --git a/crates/codegen/src/ast/dnf.rs b/crates/codegen/src/ast/dnf.rs index a3af9ce..339e697 100644 --- a/crates/codegen/src/ast/dnf.rs +++ b/crates/codegen/src/ast/dnf.rs @@ -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(), diff --git a/crates/codegen/src/ast/grammar.rs b/crates/codegen/src/ast/grammar.rs index c8528d7..37bd58f 100644 --- a/crates/codegen/src/ast/grammar.rs +++ b/crates/codegen/src/ast/grammar.rs @@ -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, } -#[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, } -#[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(), diff --git a/crates/codegen/src/ast/mod.rs b/crates/codegen/src/ast/mod.rs index 51451e8..814f44a 100644 --- a/crates/codegen/src/ast/mod.rs +++ b/crates/codegen/src/ast/mod.rs @@ -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 } }] } } diff --git a/crates/codegen/src/common/renders/camera.rs b/crates/codegen/src/common/renders/camera.rs index 0f6e61f..6fe5236 100644 --- a/crates/codegen/src/common/renders/camera.rs +++ b/crates/codegen/src/common/renders/camera.rs @@ -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 diff --git a/crates/codegen/src/common/simulations/camera.rs b/crates/codegen/src/common/simulations/camera.rs index 356b74f..f82e6ef 100644 --- a/crates/codegen/src/common/simulations/camera.rs +++ b/crates/codegen/src/common/simulations/camera.rs @@ -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 } } diff --git a/crates/codegen/src/schema/alias.rs b/crates/codegen/src/schema/alias.rs index f1df6f5..647b11d 100644 --- a/crates/codegen/src/schema/alias.rs +++ b/crates/codegen/src/schema/alias.rs @@ -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(), diff --git a/crates/codegen/src/schema/grammar.rs b/crates/codegen/src/schema/grammar.rs index 28f7243..a0b5d0a 100644 --- a/crates/codegen/src/schema/grammar.rs +++ b/crates/codegen/src/schema/grammar.rs @@ -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 { 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::>>()?; 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::>>()?; 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 { - 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::>>()?; + 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 { - 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 { 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::>>()?; 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 { 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::>>()?; 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 { 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 { @@ -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}"); + } +} diff --git a/crates/codegen/src/schema/inverse.rs b/crates/codegen/src/schema/inverse.rs index 512e292..d2e415a 100644 --- a/crates/codegen/src/schema/inverse.rs +++ b/crates/codegen/src/schema/inverse.rs @@ -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 { diff --git a/crates/codegen/src/schema/presence.rs b/crates/codegen/src/schema/presence.rs index 43469b1..ea7d4dc 100644 --- a/crates/codegen/src/schema/presence.rs +++ b/crates/codegen/src/schema/presence.rs @@ -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 = 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(), diff --git a/crates/codegen/src/schema/repair.rs b/crates/codegen/src/schema/repair.rs index e743e5b..7dc8753 100644 --- a/crates/codegen/src/schema/repair.rs +++ b/crates/codegen/src/schema/repair.rs @@ -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 { 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::>>()?; @@ -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::>>()?; 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 { 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 { 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::>>()?; Ok(quote! { @@ -214,18 +259,27 @@ fn generate_leaf_attempt( settings: &BTreeMap<&str, SettingInfo<'_>>, leaf: &Leaf, parent: &Lifetime, + has_original: bool, ) -> anyhow::Result { + 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")); } diff --git a/docs/contributors/adding-cameras.md b/docs/contributors/adding-cameras.md index 3d2c375..8c0bb3c 100644 --- a/docs/contributors/adding-cameras.md +++ b/docs/contributors/adding-cameras.md @@ -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. ] } } diff --git a/docs/fml/grammar.md b/docs/fml/grammar.md index 894e5b2..bad6c10 100644 --- a/docs/fml/grammar.md +++ b/docs/fml/grammar.md @@ -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.`. Sketch: +Each leaf becomes a Rust boolean expression over `self.`, or +`original.` 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 diff --git a/docs/fml/rules.md b/docs/fml/rules.md index b475414..f3688f2 100644 --- a/docs/fml/rules.md +++ b/docs/fml/rules.md @@ -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"`. | diff --git a/docs/internals/analyses.md b/docs/internals/analyses.md index 8037de2..9036274 100644 --- a/docs/internals/analyses.md +++ b/docs/internals/analyses.md @@ -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 : 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: ``` diff --git a/docs/internals/codegen.md b/docs/internals/codegen.md index 43b961e..0d9c111 100644 --- a/docs/internals/codegen.md +++ b/docs/internals/codegen.md @@ -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 } diff --git a/docs/internals/runtime.md b/docs/internals/runtime.md index e6b07ee..4cdac2b 100644 --- a/docs/internals/runtime.md +++ b/docs/internals/runtime.md @@ -104,6 +104,9 @@ render-capable camera must override `render`, which the codegen does: send the image, fetch the current `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 diff --git a/fml/camera.cue b/fml/camera.cue index 4a5ee43..57e86aa 100644 --- a/fml/camera.cue +++ b/fml/camera.cue @@ -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"]}}, + ]}, + ] + }, ] } } diff --git a/fml/grammar.cue b/fml/grammar.cue index 69ab6ed..9ee812a 100644 --- a/fml/grammar.cue +++ b/fml/grammar.cue @@ -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 }