From 7a758f833eb6510fa8688e60b5514f9c8c50efb0 Mon Sep 17 00:00:00 2001 From: Yann Hamdaoui Date: Mon, 12 Sep 2022 14:28:09 +0200 Subject: [PATCH 01/11] Add `priority` keyword for metadata in the parser --- src/parser/grammar.lalrpop | 5 +++++ src/parser/lexer.rs | 2 ++ 2 files changed, 7 insertions(+) diff --git a/src/parser/grammar.lalrpop b/src/parser/grammar.lalrpop index 8decf3f6b3..1dfd5339ea 100644 --- a/src/parser/grammar.lalrpop +++ b/src/parser/grammar.lalrpop @@ -104,6 +104,10 @@ AnnotAtom: MetaValue = { priority: MergePriority::Default, ..Default::default() }, + "|" "priority" <"num literal"> => MetaValue { + priority: MergePriority::Default, + ..Default::default() + }, "|" "doc" => MetaValue { doc: Some(strip_indent_doc(s)), ..Default::default() @@ -880,6 +884,7 @@ extern { "default" => Token::Normal(NormalToken::Default), "doc" => Token::Normal(NormalToken::Doc), "optional" => Token::Normal(NormalToken::Optional), + "priority" => Token::Normal(NormalToken::Priority), "hash" => Token::Normal(NormalToken::OpHash), "serialize" => Token::Normal(NormalToken::Serialize), diff --git a/src/parser/lexer.rs b/src/parser/lexer.rs index 9dc25a83f6..041c57a383 100644 --- a/src/parser/lexer.rs +++ b/src/parser/lexer.rs @@ -216,6 +216,8 @@ pub enum NormalToken<'input> { Doc, #[token("optional")] Optional, + #[token("priority")] + Priority, #[token("%hash%")] OpHash, From 288e6fd824496a3aba18a98fcbbe84d5251dd284 Mon Sep 17 00:00:00 2001 From: Yann Hamdaoui Date: Mon, 12 Sep 2022 19:28:56 +0200 Subject: [PATCH 02/11] Implement extended merge priorities (top, bottom, numeral) --- src/eval/tests.rs | 2 +- src/parser/grammar.lalrpop | 9 +- src/parser/uniterm.rs | 2 +- src/pretty.rs | 4 +- src/repl/query_print.rs | 5 +- src/term.rs | 173 ++++++++++++++++++++++++++++++++----- 6 files changed, 165 insertions(+), 30 deletions(-) diff --git a/src/eval/tests.rs b/src/eval/tests.rs index b6554864f5..7c725e857f 100644 --- a/src/eval/tests.rs +++ b/src/eval/tests.rs @@ -102,7 +102,7 @@ fn mk_default(t: RichTerm) -> Term { use crate::term::MergePriority; let mut meta = MetaValue::from(t); - meta.priority = MergePriority::Default; + meta.priority = MergePriority::Bottom; Term::MetaValue(meta) } diff --git a/src/parser/grammar.lalrpop b/src/parser/grammar.lalrpop index 1dfd5339ea..db7ec11010 100644 --- a/src/parser/grammar.lalrpop +++ b/src/parser/grammar.lalrpop @@ -60,7 +60,7 @@ use crate::{ term::{ BinaryOp, RichTerm, Term, UnaryOp, StrChunk, MetaValue, MergePriority, Contract, NAryOp, RecordAttrs, SharedTerm, - make as mk_term}, + NumeralPriority, make as mk_term}, types::{Types, AbsType}, position::TermPos, label::Label, @@ -101,11 +101,12 @@ AnnotAtom: MetaValue = { ..Default::default() }, "|" "default" => MetaValue { - priority: MergePriority::Default, + priority: MergePriority::Bottom, ..Default::default() }, "|" "priority" <"num literal"> => MetaValue { - priority: MergePriority::Default, + // unwrap(): a literal can't be NaN + priority: MergePriority::Numeral(NumeralPriority::try_from(<>).unwrap()), ..Default::default() }, "|" "doc" => MetaValue { @@ -431,7 +432,7 @@ Match: Match = { // A default annotation in a pattern. DefaultAnnot: MetaValue = "?" => MetaValue { - priority: MergePriority::Default, + priority: MergePriority::Bottom, value: Some(t), ..Default::default() }; diff --git a/src/parser/uniterm.rs b/src/parser/uniterm.rs index b7ac187219..35c90ad71f 100644 --- a/src/parser/uniterm.rs +++ b/src/parser/uniterm.rs @@ -211,7 +211,7 @@ impl UniRecord { types: Some(ctrt), contracts, opt: false, - priority: MergePriority::Normal, + priority: MergePriority::Neutral, value: None, }) if contracts.is_empty() => Ok(Types(AbsType::RowExtend( id, diff --git a/src/pretty.rs b/src/pretty.rs index 4528d68434..27b22a712c 100644 --- a/src/pretty.rs +++ b/src/pretty.rs @@ -99,7 +99,7 @@ where }), self.line().clone(), )) - .append(if mv.priority == crate::term::MergePriority::Default { + .append(if mv.priority == crate::term::MergePriority::Bottom { self.line().append(self.text("| default")) } else { self.nil() @@ -207,7 +207,7 @@ where MetaValue { types, contracts, - priority: crate::term::MergePriority::Default, + priority: crate::term::MergePriority::Bottom, value: Some(value), .. } => allocator diff --git a/src/repl/query_print.rs b/src/repl/query_print.rs index 5fe4334852..990ad2b3c2 100644 --- a/src/repl/query_print.rs +++ b/src/repl/query_print.rs @@ -235,7 +235,7 @@ fn write_query_result_( match &meta { MetaValue { - priority: MergePriority::Default, + priority: MergePriority::Bottom, value: Some(t), .. } if selected_attrs.default => { @@ -243,10 +243,11 @@ fn write_query_result_( found = true; } MetaValue { - priority: MergePriority::Normal, + priority: MergePriority::Numeral(n), value: Some(t), .. } if selected_attrs.value => { + renderer.write_metadata(out, "priority", &format!("{}", n))?; renderer.write_metadata(out, "value", &t.as_ref().shallow_repr())?; found = true; } diff --git a/src/term.rs b/src/term.rs index 5f98a5bc41..78abcde204 100644 --- a/src/term.rs +++ b/src/term.rs @@ -16,20 +16,28 @@ //! contracts, default values, documentation, etc. They bring such usually external object down to //! the term level, and together with [crate::eval::merge], they allow for flexible and modular //! definitions of contracts, record and metadata all together. -use crate::destruct::Destruct; -use crate::error::ParseError; -use crate::identifier::Ident; -use crate::label::Label; -use crate::match_sharedterm; -use crate::position::TermPos; -use crate::types::{AbsType, Types, UnboundTypeVariableError}; +use crate::{ + destruct::Destruct, + error::ParseError, + identifier::Ident, + label::Label, + match_sharedterm, + position::TermPos, + types::{AbsType, Types, UnboundTypeVariableError}, +}; + use codespan::FileId; + use serde::{Deserialize, Serialize}; -use std::collections::{HashMap, HashSet}; -use std::ffi::OsString; -use std::fmt; -use std::ops::Deref; -use std::rc::Rc; + +use std::{ + cmp::{Ordering, PartialOrd}, + collections::{HashMap, HashSet}, + ffi::OsString, + fmt, + ops::Deref, + rc::Rc, +}; /// The AST of a Nickel expression. /// @@ -287,15 +295,131 @@ pub struct RecordDeps { /// Potential dependencies of a single field over the sibling fields in a recursive record. pub type FieldDeps = Option>>; -#[derive(Debug, Eq, PartialEq, Ord, PartialOrd, Copy, Clone)] +/// A wrapper around f64 which makes `NaN` not representable. As opposed to floats, it is `Eq` and +/// `Ord`. +#[derive(Debug, Copy, Clone, PartialEq, PartialOrd)] +pub struct NumeralPriority(f64); + +/// Error raised when trying to convert a float with `NaN` value to a `NumeralPriority`. +#[derive(Debug, Copy, Clone)] +pub struct PriorityIsNaN; + +// The following impl are ok because `NumeralPriority(NaN)` can't be constructed. +impl Eq for NumeralPriority {} +impl Ord for NumeralPriority { + fn cmp(&self, other: &Self) -> Ordering { + // Ok: NaN is forbidden + self.partial_cmp(other).unwrap() + } +} + +impl NumeralPriority { + pub fn zero() -> Self { + NumeralPriority(0.0) + } +} + +impl TryFrom for NumeralPriority { + type Error = PriorityIsNaN; + + fn try_from(f: f64) -> Result { + if f.is_nan() { + Err(PriorityIsNaN) + } else { + Ok(NumeralPriority(f)) + } + } +} + +impl From for f64 { + fn from(n: NumeralPriority) -> Self { + n.0 + } +} + +impl fmt::Display for NumeralPriority { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + self.0.fmt(f) + } +} + +#[derive(Debug, Copy, Clone)] pub enum MergePriority { - Default, - Normal, + /// The priority of default values that are overridden by everything else. + Bottom, + /// The priority by default, when no priority annotation (`default`, `force`, `priority`) is + /// provided. + /// + /// Act as the value `MergePriority::Numeral(0.0)` with respect to ordering and equality + /// testing. The only way to discriminate this variant is to pattern match on it. + Neutral, + /// A numeral priority. The inner value should never be `NaN`. Comparing a `MergePriority` with + /// a `NaN` value will panic. + Numeral(NumeralPriority), + /// The priority of values that override everything else and can't be overridden. + Top, +} + +impl PartialOrd for MergePriority { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl PartialEq for MergePriority { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (MergePriority::Bottom, MergePriority::Bottom) + | (MergePriority::Neutral, MergePriority::Neutral) + | (MergePriority::Top, MergePriority::Top) => true, + (MergePriority::Numeral(p1), MergePriority::Numeral(p2)) => p1 == p2, + (MergePriority::Neutral, MergePriority::Numeral(p)) + | (MergePriority::Numeral(p), MergePriority::Neutral) + if p == &NumeralPriority::zero() => + { + true + } + _ => false, + } + } +} + +impl Eq for MergePriority {} + +impl Ord for MergePriority { + fn cmp(&self, other: &Self) -> Ordering { + match (self, other) { + // Equalities + (MergePriority::Bottom, MergePriority::Bottom) + | (MergePriority::Top, MergePriority::Top) + | (MergePriority::Neutral, MergePriority::Neutral) => Ordering::Equal, + (MergePriority::Numeral(p1), MergePriority::Numeral(p2)) => p1.cmp(p2), + + // Top and bottom. + (MergePriority::Bottom, _) | (_, MergePriority::Top) => Ordering::Less, + (MergePriority::Top, _) | (_, MergePriority::Bottom) => Ordering::Greater, + + // Neutral and numeral. + (MergePriority::Neutral, MergePriority::Numeral(n)) => NumeralPriority::zero().cmp(n), + (MergePriority::Numeral(n), MergePriority::Neutral) => n.cmp(&NumeralPriority::zero()), + } + } } impl Default for MergePriority { fn default() -> Self { - Self::Normal + Self::Neutral + } +} + +impl fmt::Display for MergePriority { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + MergePriority::Bottom => write!(f, "default"), + MergePriority::Neutral => write!(f, "{}", NumeralPriority::zero()), + MergePriority::Numeral(p) => write!(f, "{}", p), + MergePriority::Top => write!(f, "force"), + } } } @@ -335,7 +459,7 @@ impl MetaValue { } /// Flatten two nested metavalues into one, combining their metadata. If data that can't be - /// combined (typically, the documentation or the type annotation) are set by both metavalues, + /// combined (typically, the documentation, the type annotation or the priority) are set by both metavalues, /// outer's one are kept. /// /// Note that no environment management such as closurization takes place, because this @@ -345,7 +469,7 @@ impl MetaValue { /// #Preconditions /// /// - `outer.value` is assumed to be `inner`. While `flatten` may still work fine if this - /// condition is not fullfilled, the value of the final metavalue is set to be `inner`'s one, + /// condition is not fulfilled, the value of the final metavalue is set to be `inner`'s one, /// and `outer`'s one is dropped. pub fn flatten(mut outer: MetaValue, mut inner: MetaValue) -> MetaValue { // Keep the outermost value for non-mergeable information, such as documentation, type annotation, @@ -364,12 +488,21 @@ impl MetaValue { outer.contracts.extend(inner.contracts.into_iter()); + let priority = match (outer.priority, inner.priority) { + // Neutral corresponds to the case where no priority was specified. In that case, the + // other priority takes precedence. + (MergePriority::Neutral, p) | (p, MergePriority::Neutral) => p, + // Otherwise, we keep the maximum of both priorities, as we would do when merging + // values. + (p1, p2) => std::cmp::max(p1, p2), + }; + MetaValue { doc: outer.doc.or(inner.doc), types: outer.types.or(inner.types), contracts: outer.contracts, opt: outer.opt || inner.opt, - priority: std::cmp::min(outer.priority, inner.priority), + priority, value: inner.value, } } @@ -541,7 +674,7 @@ impl Term { content.push_str("contract,"); } - let value_label = if meta.priority == MergePriority::Default { + let value_label = if meta.priority == MergePriority::Bottom { "default" } else { "value" From 912f6fd9e0a1400e30d3962124c5a2fc54b3027e Mon Sep 17 00:00:00 2001 From: Yann Hamdaoui Date: Tue, 13 Sep 2022 14:52:17 +0200 Subject: [PATCH 03/11] Mention priorities in the non-mergeable error message --- src/error.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/error.rs b/src/error.rs index 24afc4a31c..3ac62f61c8 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1116,7 +1116,10 @@ impl ToDiagnostic for EvalError { vec![Diagnostic::error() .with_message("non mergeable terms") - .with_labels(labels)] + .with_labels(labels) + .with_notes(vec![String::from( + "Both values have the same merge priority but they can't be combined", + )])] } EvalError::UnboundIdentifier(ident, span_opt) => vec![Diagnostic::error() .with_message("unbound identifier") From 27d1b5765de9ffa19c15e85be56c2c3d58c98285 Mon Sep 17 00:00:00 2001 From: Yann Hamdaoui Date: Tue, 13 Sep 2022 15:39:03 +0200 Subject: [PATCH 04/11] Add `force` keyword for top merge priority --- src/parser/grammar.lalrpop | 9 +++++++-- src/parser/lexer.rs | 4 +++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/parser/grammar.lalrpop b/src/parser/grammar.lalrpop index db7ec11010..e2a29d0ab8 100644 --- a/src/parser/grammar.lalrpop +++ b/src/parser/grammar.lalrpop @@ -104,6 +104,10 @@ AnnotAtom: MetaValue = { priority: MergePriority::Bottom, ..Default::default() }, + "|" "force" => MetaValue { + priority: MergePriority::Top, + ..Default::default() + }, "|" "priority" <"num literal"> => MetaValue { // unwrap(): a literal can't be NaN priority: MergePriority::Numeral(NumeralPriority::try_from(<>).unwrap()), @@ -537,7 +541,7 @@ UOp: UnaryOp = { "record_map" => UnaryOp::RecordMap(), "seq" => UnaryOp::Seq(), "deep_seq" => UnaryOp::DeepSeq(None), - "force" => UnaryOp::Force(None), + "op force" => UnaryOp::Force(None), "head" => UnaryOp::ArrayHead(), "tail" => UnaryOp::ArrayTail(), "length" => UnaryOp::ArrayLength(), @@ -854,7 +858,7 @@ extern { "typeof" => Token::Normal(NormalToken::Typeof), "assume" => Token::Normal(NormalToken::Assume), "array_lazy_assume" => Token::Normal(NormalToken::ArrayLazyAssume), - "force" => Token::Normal(NormalToken::Force), + "op force" => Token::Normal(NormalToken::OpForce), "blame" => Token::Normal(NormalToken::Blame), "chng_pol" => Token::Normal(NormalToken::ChangePol), "polarity" => Token::Normal(NormalToken::Polarity), @@ -883,6 +887,7 @@ extern { "elem_at" => Token::Normal(NormalToken::ElemAt), "merge" => Token::Normal(NormalToken::Merge), "default" => Token::Normal(NormalToken::Default), + "force" => Token::Normal(NormalToken::Force), "doc" => Token::Normal(NormalToken::Doc), "optional" => Token::Normal(NormalToken::Optional), "priority" => Token::Normal(NormalToken::Priority), diff --git a/src/parser/lexer.rs b/src/parser/lexer.rs index 041c57a383..543c085023 100644 --- a/src/parser/lexer.rs +++ b/src/parser/lexer.rs @@ -186,7 +186,7 @@ pub enum NormalToken<'input> { #[token("%deep_seq%")] DeepSeq, #[token("%force%")] - Force, + OpForce, #[token("%head%")] Head, #[token("%tail%")] @@ -218,6 +218,8 @@ pub enum NormalToken<'input> { Optional, #[token("priority")] Priority, + #[token("force")] + Force, #[token("%hash%")] OpHash, From 18d0a4f7c0d4dad03a00c554ad0be5b7fa5d59ed Mon Sep 17 00:00:00 2001 From: Yann Hamdaoui Date: Tue, 13 Sep 2022 15:46:58 +0200 Subject: [PATCH 05/11] Enables parsing negative merge priorities --- src/parser/grammar.lalrpop | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/parser/grammar.lalrpop b/src/parser/grammar.lalrpop index e2a29d0ab8..b27c3d87c9 100644 --- a/src/parser/grammar.lalrpop +++ b/src/parser/grammar.lalrpop @@ -108,7 +108,7 @@ AnnotAtom: MetaValue = { priority: MergePriority::Top, ..Default::default() }, - "|" "priority" <"num literal"> => MetaValue { + "|" "priority" => MetaValue { // unwrap(): a literal can't be NaN priority: MergePriority::Numeral(NumeralPriority::try_from(<>).unwrap()), ..Default::default() @@ -787,6 +787,14 @@ TypeAtom: Types = { }, } +SignedNumLiteral: f64 = => { + if sign.is_some() { + -value + } else { + value + } +}; + extern { type Location = usize; type Error = ParseError; From 3f5b7743de754f6cf9eb67eaa59774817fc227e5 Mon Sep 17 00:00:00 2001 From: Yann Hamdaoui Date: Tue, 13 Sep 2022 15:32:25 +0200 Subject: [PATCH 06/11] Adds tests for extended merge priorities --- tests/pass.rs | 5 +++++ tests/pass/priorities.ncl | 41 +++++++++++++++++++++++++++++++++++++++ tests/records_fail.rs | 16 +++++++++++++++ 3 files changed, 62 insertions(+) create mode 100644 tests/pass/priorities.ncl diff --git a/tests/pass.rs b/tests/pass.rs index 4d0cfb66fc..498db4c16c 100644 --- a/tests/pass.rs +++ b/tests/pass.rs @@ -127,3 +127,8 @@ fn recursive_let() { fn quote_in_indentifier() { check_file("quote_in_identifier.ncl") } + +#[test] +fn priorities() { + check_file("priorities.ncl") +} diff --git a/tests/pass/priorities.ncl b/tests/pass/priorities.ncl new file mode 100644 index 0000000000..81c7084973 --- /dev/null +++ b/tests/pass/priorities.ncl @@ -0,0 +1,41 @@ +let {Assert, ..} = import "testlib.ncl" in + +let block1 = { + foo | default = 1, + bar = 1, + baz | force = 1, + x | priority 10 = 1, + y | priority -5 = 1, + z | priority 0 = 1, + d | default = 1, +} in + +let block2 = { + foo | priority -10 = 2, + bar | priority 10 = 2, + baz = 2, + x | priority 11 = 2, + y = 2, + z | priority 10 = 2, + +} in + +let block3 = { + foo | priority -10.1 = 3, + bar | default = 3, + baz | priority 1000 = 3, + x | priority 12 = 3, + y | priority -1 = 3, + z | priority 50 = 3, +} in + +block1 & block2 & block3 +== { + foo = 2, + bar = 2, + baz = 1, + x = 3, + y = 2, + z = 3, + d = 1, +} | Assert diff --git a/tests/records_fail.rs b/tests/records_fail.rs index 81b17cbb42..147da5cd5f 100644 --- a/tests/records_fail.rs +++ b/tests/records_fail.rs @@ -42,6 +42,22 @@ fn non_mergeable_piecewise() { ); } +#[test] +fn non_mergeable_prio() { + assert_matches!( + eval("({a.b | priority 0 = 1, a = {b = 2}}).a.b"), + Err(Error::EvalError(EvalError::MergeIncompatibleArgs(..))) + ); + assert_matches!( + eval("({a | force = false} & {a | force = true}).a"), + Err(Error::EvalError(EvalError::MergeIncompatibleArgs(..))) + ); + assert_matches!( + eval("({foo.bar | priority -10 = false, foo.bar | priority -10 = true}).foo.bar"), + Err(Error::EvalError(EvalError::MergeIncompatibleArgs(..))) + ); +} + #[test] fn dynamic_not_recursive() { assert_matches!( From d5de17b7b28b4dbfc2fcb1087a77cb727334c24d Mon Sep 17 00:00:00 2001 From: Yann Hamdaoui Date: Tue, 13 Sep 2022 16:33:30 +0200 Subject: [PATCH 07/11] Update manual to include merge priorities --- doc/manual/merging.md | 64 ++++++++++++++++++++++++++++++++++++------- doc/manual/syntax.md | 22 ++++++++++++--- 2 files changed, 72 insertions(+), 14 deletions(-) diff --git a/doc/manual/merging.md b/doc/manual/merging.md index 63975c2a76..9248321c3e 100644 --- a/doc/manual/merging.md +++ b/doc/manual/merging.md @@ -254,16 +254,64 @@ final record: ## Merging record with metadata Metadata can be attached to values thanks to the `|` operator. Metadata -currently includes contract annotations, default value, and documentation. We -describe in this section how metadata interacts with merging. +currently includes contract annotations, default value, merge priority, and +documentation. We describe in this section how metadata interacts with merging. -### Default values +### Merge priorities + +Priorities are specified using the `priority` annotation, followed by a number +literal. There are also two other special priorities, the bottom priority, specified +using the `default` annotation, and the top priority, specified using the +`force` annotation. + +Priorities dictate which values take precedence over other values. By default, +values are given the priority `0`. Values with the same priority are recursively +merged as specified in this document, which can mean failure if the values can't +be meaningfully merged: + +```text +nickel>{foo | default = 1} & {foo = 2} +error: non mergeable terms + ┌─ repl-input-1:1:8 + │ +1 │ {foo = 1} & {foo = 2} + │ ^ ^ with this expression + │ │ + │ cannot merge this expression + │ + = Both values have the same merge priority but they can't be combined +``` + +On the other hand, if the priorities differ, the value with highest priority +simply erases the other in the final result: + +```text +nickel>{foo | priority 1 = 1} & {foo = 2} +{ foo = 1 } + +nickel>{foo | priority -1 = 1} & {foo = 2} +{ foo = 2 } +``` + +The priorities are ordered in the following way: + +- bottom is the lowest priority +- numeral priorities are ordered as usual numbers (priorities can be any valid Nickel + number, including fractions and negative values) +- top is the highest priority + +#### Default values A `default` annotation can be used to provide a base value, but let it be overridable through merging. For example, `{foo | default = 1} & {foo = 2}` -evaluates to `{foo = 2}`. Without the default value, this merge would have -failed with a `non mergeable fields` error, because merging being symmetric, it -doesn't know how to combine `1` and `2` in a generic and meaningful way. +evaluates to `{foo = 2}`. A default value is just a special case of a priority, +being the lowest possible one. + +#### Forcing values + +Dually, values with the `force` annotation are given the highest priority. Such +a value can never be overriden, and will either take precedence over another +value or be tentatively merged if the other value is forcing as well. #### Specification @@ -284,10 +332,6 @@ same on both side: } ``` -Currently, there are only two priorities, `normal` (by default, when nothing is -specified) and the `default` one, with `default < normal`. We plan to add more -in the future (see [RFC001](https://github.com/tweag/nickel/blob/c21cf280dc610821fceed4c2caafedb60ce7177c/rfcs/001-overriding.md#priorities)). - #### Example Let us stick to our firewall example. Thanks to default values, we set the most diff --git a/doc/manual/syntax.md b/doc/manual/syntax.md index 9c2b920075..ebd5aa1df1 100644 --- a/doc/manual/syntax.md +++ b/doc/manual/syntax.md @@ -12,7 +12,7 @@ alphanumeric characters, `_` (underscores) or `'` (single quotes). For example, ## Simple values -There are four basic kind of values in Nickel : +There are four basic kinds of values in Nickel : 1. numeric values 2. boolean values @@ -532,9 +532,14 @@ Adding documentation can be done with `| doc < string >`. Examples: true ``` -Record contracts can set default values using the `default` metadata: It is -noted as `| default = < default value >`. This is especially useful when merging -records (more about this in the dedicated document about merge). +Metadata can also set merge priorities using the following annotations: + +- `default` gives the lowest priority (default values) +- `priority NN`, where `NN` is a number literal, gives a numeral priority +- `force` gives the highest priority + +If there is no priority specified, `priority 0` is given by default. See more +about this in the dedicated section on merging. Examples: @@ -552,4 +557,13 @@ Examples: > {foo | default = 1, bar = foo + 1} & {foo = 2} { foo = 2, bar = 3 } + +> {foo | force = 1, bar = foo + 1} & {foo = 2} +{ bar = 2, foo = 1 } + +> {foo | priority 10 = 1} & {foo | priority 8 = 2} & {foo = 3} +{ foo = 1 } + +> {foo | priority -1 = 1} & {foo = 2} +{ foo = 2 } ``` From 669277b8a5c5e47c71c370837036a2e8fdfbda8e Mon Sep 17 00:00:00 2001 From: Yann Hamdaoui Date: Tue, 13 Sep 2022 16:41:37 +0200 Subject: [PATCH 08/11] Add an example using merge priorities --- examples/merge-priorities/README.md | 12 ++++++++++++ examples/merge-priorities/main.ncl | 20 ++++++++++++++++++++ examples/merge-priorities/security.ncl | 8 ++++++++ examples/merge-priorities/server.ncl | 5 +++++ 4 files changed, 45 insertions(+) create mode 100644 examples/merge-priorities/README.md create mode 100644 examples/merge-priorities/main.ncl create mode 100644 examples/merge-priorities/security.ncl create mode 100644 examples/merge-priorities/server.ncl diff --git a/examples/merge-priorities/README.md b/examples/merge-priorities/README.md new file mode 100644 index 0000000000..3951f2f174 --- /dev/null +++ b/examples/merge-priorities/README.md @@ -0,0 +1,12 @@ +# Merge priorities + +This example is a variant of the merge example that makes use of merge +priorities. The code to run lies in `main.ncl`. The default value +`firewall.enabled` defined in `security.ncl` is overwritten in the final +configuration. + +## Run + +```console +nickel -f main.ncl export +``` diff --git a/examples/merge-priorities/main.ncl b/examples/merge-priorities/main.ncl new file mode 100644 index 0000000000..bd44cb10e7 --- /dev/null +++ b/examples/merge-priorities/main.ncl @@ -0,0 +1,20 @@ +# Merge several blocks into one final configuration. In a real world case, one +# would also want contracts to validate the shape of the data. +let server = import "server.ncl" in +let security = import "security.ncl" in +# Disabling firewall in the final result +server & security & { + #As opposed to the simple merge example, this would now fail + #firewall.enabled = false + + firewall.open_ports | priority 10 = [80], + firewall.type = "superiptables", + server.host.ip = "89.22.11.01", + + # this will only be selected if no values with higher priority (or no priority + # annotation, which is the same as priority 0) is ever defined + # because there's a definite value below, this won't be selected. + server.host.name | priority -1 = "hello-world.backup.com", +} & { + server.host.name = "hello-world.main.com", +} diff --git a/examples/merge-priorities/security.ncl b/examples/merge-priorities/security.ncl new file mode 100644 index 0000000000..ffcc5ad0ac --- /dev/null +++ b/examples/merge-priorities/security.ncl @@ -0,0 +1,8 @@ +{ + server.host.options | priority 10 = "TLS", + + # force make it impossible to override this value with false + firewall.enabled | force = true, + firewall.type | default = "iptables", + firewall.open_ports | priority 5 = [21, 80, 443], +} diff --git a/examples/merge-priorities/server.ncl b/examples/merge-priorities/server.ncl new file mode 100644 index 0000000000..9474686a88 --- /dev/null +++ b/examples/merge-priorities/server.ncl @@ -0,0 +1,5 @@ +{ + server.host.ip | default = "182.168.1.1", + server.host.port | default = 80, + server.host.name | default = "hello-world.net", +} From 834d78dce5f595e00847d94bd43f6b38861c054b Mon Sep 17 00:00:00 2001 From: Yann Hamdaoui Date: Wed, 14 Sep 2022 15:22:49 +0200 Subject: [PATCH 09/11] Small improvement of documentation of `term` --- src/term.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/term.rs b/src/term.rs index 78abcde204..a17c6c8de5 100644 --- a/src/term.rs +++ b/src/term.rs @@ -353,8 +353,7 @@ pub enum MergePriority { /// Act as the value `MergePriority::Numeral(0.0)` with respect to ordering and equality /// testing. The only way to discriminate this variant is to pattern match on it. Neutral, - /// A numeral priority. The inner value should never be `NaN`. Comparing a `MergePriority` with - /// a `NaN` value will panic. + /// A numeral priority. Numeral(NumeralPriority), /// The priority of values that override everything else and can't be overridden. Top, From dadf2ffed11b14d387eb50dbe6c66b10eaaa1bfb Mon Sep 17 00:00:00 2001 From: Yann Hamdaoui Date: Fri, 16 Sep 2022 16:40:25 +0200 Subject: [PATCH 10/11] Small fixes in the merging manual section --- doc/manual/merging.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/manual/merging.md b/doc/manual/merging.md index 9248321c3e..a5f4ac1b81 100644 --- a/doc/manual/merging.md +++ b/doc/manual/merging.md @@ -270,7 +270,7 @@ merged as specified in this document, which can mean failure if the values can't be meaningfully merged: ```text -nickel>{foo | default = 1} & {foo = 2} +nickel> {foo = 1} & {foo = 2} error: non mergeable terms ┌─ repl-input-1:1:8 │ @@ -286,10 +286,10 @@ On the other hand, if the priorities differ, the value with highest priority simply erases the other in the final result: ```text -nickel>{foo | priority 1 = 1} & {foo = 2} +nickel> {foo | priority 1 = 1} & {foo = 2} { foo = 1 } -nickel>{foo | priority -1 = 1} & {foo = 2} +nickel> {foo | priority -1 = 1} & {foo = 2} { foo = 2 } ``` From 9c7e7b5334581b941d91c3bf8c8cdee2fa69c7b4 Mon Sep 17 00:00:00 2001 From: Yann Hamdaoui Date: Fri, 16 Sep 2022 16:43:09 +0200 Subject: [PATCH 11/11] Cosmetic fix of merge example with priorities Co-authored-by: francois-caddet --- examples/merge-priorities/main.ncl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/merge-priorities/main.ncl b/examples/merge-priorities/main.ncl index bd44cb10e7..ac87ec9c0d 100644 --- a/examples/merge-priorities/main.ncl +++ b/examples/merge-priorities/main.ncl @@ -4,8 +4,8 @@ let server = import "server.ncl" in let security = import "security.ncl" in # Disabling firewall in the final result server & security & { - #As opposed to the simple merge example, this would now fail - #firewall.enabled = false + # As opposed to the simple merge example, this would now fail + # firewall.enabled = false firewall.open_ports | priority 10 = [80], firewall.type = "superiptables",