feat: add rule scopes
Build and Release Binary / build (push) Successful in 1m3s
Build and Release Binary / release (push) Successful in 1m30s

Signed-off-by: Nikolaos Karaolidis <nick@karaolidis.com>
This commit is contained in:
2026-06-03 18:31:47 +00:00
parent edfb8e39b6
commit 3adeb9d1d7
19 changed files with 770 additions and 147 deletions
+3 -1
View File
@@ -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(),
+35 -2
View File
@@ -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(),
+45 -22
View File
@@ -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(),
+1 -1
View File
@@ -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 }
}]
}
}
+13 -6
View File
@@ -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 }
}
+35 -1
View File
@@ -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(),
+127 -24
View File
@@ -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(&current, &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(&current, &original),
)
.unwrap()
.to_string();
assert!(out.contains("original . x"), "got: {out}");
}
}
+29 -5
View File
@@ -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 {
+136 -1
View File
@@ -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(),
+144 -26
View File
@@ -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, &quote! { 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, &quote! { 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"));
}
+4
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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:
```
+4
View File
@@ -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 }
+3
View File
@@ -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
View File
@@ -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
View File
@@ -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
}