Skip to content
Closed
131 changes: 67 additions & 64 deletions cli/src/doctest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -410,78 +410,81 @@ fn doctest_transform(
Ok(cache.get(src_id).unwrap())
}

let mut record_with_doctests =
|mut record_data: RecordData, dyn_fields, pos| -> Result<_, CoreError> {
let mut doc_fields: Vec<(Ident, RichTerm)> = Vec::new();
for (id, field) in &record_data.fields {
if let Some(doc) = &field.metadata.doc {
let arena = Arena::new();
let snippets = nickel_code_blocks(comrak::parse_document(
&arena,
doc,
&ComrakOptions::default(),
));

for (i, snippet) in snippets.iter().enumerate() {
let mut test_term = prepare(cache, &snippet.input, &source_path)?;

if let Expected::Value(s) = &snippet.expected {
// Create the contract `std.contract.Equal <expected>` and apply it to the
// test term.
let expected_term = prepare(cache, s, &source_path)?;
// unwrap: we just parsed it, so it will have a span
let expected_span = expected_term.pos.into_opt().unwrap();

let eq = make::static_access(
RichTerm::from(Term::Var("std".into())),
["contract", "Equal"],
);
let eq = mk_app!(eq, expected_term);
let eq_ty = Type::from(TypeF::Contract(eq));
test_term = Term::Annotated(
TypeAnnotation {
typ: None,
contracts: vec![LabeledType {
typ: eq_ty.clone(),
label: Label {
typ: Rc::new(eq_ty),
span: expected_span,
..Default::default()
},
}],
},
test_term,
)
.into();
}

// Make the test term lazy, so that the tests don't automatically get evaluated
// just by evaluating the record spine.
let test_term = mk_fun!(LocIdent::fresh(), test_term);
let test_id = LocIdent::fresh().ident();
let entry = TestEntry {
expected_error: snippet.expected.error(),
field_name: *id,
test_idx: i,
};
registry.tests.insert(test_id, entry);
doc_fields.push((test_id, test_term));
let mut record_with_doctests = |mut record_data: RecordData,
includes,
dyn_fields,
pos|
-> Result<_, CoreError> {
let mut doc_fields: Vec<(Ident, RichTerm)> = Vec::new();
for (id, field) in &record_data.fields {
if let Some(doc) = &field.metadata.doc {
let arena = Arena::new();
let snippets = nickel_code_blocks(comrak::parse_document(
&arena,
doc,
&ComrakOptions::default(),
));

for (i, snippet) in snippets.iter().enumerate() {
let mut test_term = prepare(cache, &snippet.input, &source_path)?;

if let Expected::Value(s) = &snippet.expected {
// Create the contract `std.contract.Equal <expected>` and apply it to the
// test term.
let expected_term = prepare(cache, s, &source_path)?;
// unwrap: we just parsed it, so it will have a span
let expected_span = expected_term.pos.into_opt().unwrap();

let eq = make::static_access(
RichTerm::from(Term::Var("std".into())),
["contract", "Equal"],
);
let eq = mk_app!(eq, expected_term);
let eq_ty = Type::from(TypeF::Contract(eq));
test_term = Term::Annotated(
TypeAnnotation {
typ: None,
contracts: vec![LabeledType {
typ: eq_ty.clone(),
label: Label {
typ: Rc::new(eq_ty),
span: expected_span,
..Default::default()
},
}],
},
test_term,
)
.into();
}

// Make the test term lazy, so that the tests don't automatically get evaluated
// just by evaluating the record spine.
let test_term = mk_fun!(LocIdent::fresh(), test_term);
let test_id = LocIdent::fresh().ident();
let entry = TestEntry {
expected_error: snippet.expected.error(),
field_name: *id,
test_idx: i,
};
registry.tests.insert(test_id, entry);
doc_fields.push((test_id, test_term));
}
}
for (id, term) in doc_fields {
record_data.fields.insert(id.into(), term.into());
}
Ok(RichTerm::from(Term::RecRecord(record_data, dyn_fields, None)).with_pos(pos))
};
}
for (id, term) in doc_fields {
record_data.fields.insert(id.into(), term.into());
}
Ok(RichTerm::from(Term::RecRecord(record_data, includes, dyn_fields, None)).with_pos(pos))
};

let mut traversal = |rt: RichTerm| -> Result<RichTerm, CoreError> {
let term = match_sharedterm!(match (rt.term) {
Term::RecRecord(record_data, dyn_fields, _deps) => {
record_with_doctests(record_data, dyn_fields, rt.pos)?
Term::RecRecord(record_data, includes, dyn_fields, _deps) => {
record_with_doctests(record_data, includes, dyn_fields, rt.pos)?
}
Term::Record(record_data) => {
record_with_doctests(record_data, Vec::new(), rt.pos)?
record_with_doctests(record_data, Vec::new(), Vec::new(), rt.pos)?
}
_ => rt,
});
Expand Down
3 changes: 2 additions & 1 deletion cli/src/query.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,9 @@ impl From<Field> for QueryResult {
fields.sort();
Some(fields.into_iter().map(LocIdent::ident).collect())
}
Term::RecRecord(record, dyn_fields, ..) if !record.fields.is_empty() => {
Term::RecRecord(record, includes, dyn_fields, ..) if !record.fields.is_empty() => {
let mut fields: Vec<_> = record.fields.keys().map(LocIdent::ident).collect();
fields.extend(includes.iter().map(LocIdent::ident));
fields.sort();
let dynamic = Ident::from("<dynamic>");
fields.extend(dyn_fields.iter().map(|_| dynamic));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# capture = 'stderr'
# command = ['eval']

let foo = {bar.baz = 1} in
{include foo, foo.qux = 2}
6 changes: 6 additions & 0 deletions cli/tests/snapshot/inputs/errors/include_multiple_list.ncl
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# capture = 'stderr'
# command = ['eval']

let foo = {} in
let bar = {} in
{include [foo, bar, foo]}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# capture = 'stderr'
# command = ['eval']

let foo = {} in
{include foo, include foo}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# capture = 'stderr'
# command = ['eval']

let foo = {} in
{include foo, foo = {}}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
source: cli/tests/snapshot/main.rs
expression: err
---
error: multiple declarations for included field `foo`
┌─ [INPUTS_PATH]/errors/include_multiple_composite_path.ncl:5:10
5 │ {include foo, foo.qux = 2}
│ ^^^ --- but also declared here
│ │
│ included here
= Piecewise definitions involving an included field are currently not supported
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
source: cli/tests/snapshot/main.rs
expression: err
---
error: multiple declarations for included field `foo`
┌─ [INPUTS_PATH]/errors/include_multiple_list.ncl:6:11
6 │ {include [foo, bar, foo]}
│ ^^^ --- but also declared here
│ │
│ included here
= Piecewise definitions involving an included field are currently not supported
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
source: cli/tests/snapshot/main.rs
expression: err
---
error: multiple declarations for included field `foo`
┌─ [INPUTS_PATH]/errors/include_multiple_other_include.ncl:5:10
5 │ {include foo, include foo}
│ ^^^ --- but also declared here
│ │
│ included here
= Piecewise definitions involving an included field are currently not supported
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
source: cli/tests/snapshot/main.rs
expression: err
---
error: multiple declarations for included field `foo`
┌─ [INPUTS_PATH]/errors/include_multiple_with_def.ncl:5:10
5 │ {include foo, foo = {}}
│ ^^^ --- but also declared here
│ │
│ included here
= Piecewise definitions involving an included field are currently not supported
29 changes: 27 additions & 2 deletions core/src/bytecode/ast/alloc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ impl Allocable for Record<'_> {}
impl Allocable for record::FieldPathElem<'_> {}
impl Allocable for record::FieldDef<'_> {}
impl Allocable for record::FieldMetadata<'_> {}
impl Allocable for record::Include<'_> {}

impl Allocable for Pattern<'_> {}
impl Allocable for EnumPattern<'_> {}
Expand All @@ -41,6 +42,9 @@ impl Allocable for typ::EnumRows<'_> {}
impl Allocable for typ::EnumRow<'_> {}
impl Allocable for typ::RecordRow<'_> {}

impl Allocable for Ident {}
impl Allocable for LocIdent {}

/// Owns the arenas required to allocate new AST nodes and provide builder methods to create them.
///
/// # Drop and arena allocation
Expand Down Expand Up @@ -204,12 +208,20 @@ impl AstAlloc {
Node::Record(record)
}

pub fn record_data<'ast, Ss, Ds>(&'ast self, field_defs: Ds, open: bool) -> &'ast Record<'ast>
pub fn record_data<'ast, Is, Ds>(
&'ast self,
includes: Is,
field_defs: Ds,
open: bool,
) -> &'ast Record<'ast>
where
Ds: IntoIterator<Item = FieldDef<'ast>>,
Is: IntoIterator<Item = Include<'ast>>,
Ds::IntoIter: ExactSizeIterator,
Is::IntoIter: ExactSizeIterator,
{
self.generic_arena.alloc(Record {
includes: self.generic_arena.alloc_slice_fill_iter(includes),
field_defs: self.generic_arena.alloc_slice_fill_iter(field_defs),
open,
})
Expand Down Expand Up @@ -488,15 +500,28 @@ impl CloneTo for LetMetadata<'_> {
}
}

impl CloneTo for Include<'_> {
type Data<'ast> = Include<'ast>;

fn clone_to<'to>(data: Self::Data<'_>, dest: &'to AstAlloc) -> Self::Data<'to> {
Include {
ident: data.ident,
metadata: FieldMetadata::clone_to(data.metadata, dest),
}
}
}

impl CloneTo for Record<'_> {
type Data<'ast> = Record<'ast>;

fn clone_to<'to>(data: Self::Data<'_>, dest: &'to AstAlloc) -> Self::Data<'to> {
Record {
includes: dest.alloc_many(data.includes.iter().cloned().map(|include| Include::clone_to(include, dest))),
field_defs: dest.alloc_many(
data.field_defs
.iter()
.map(|field_def| FieldDef::clone_to(field_def.clone(), dest)),
.cloned()
.map(|field_def| FieldDef::clone_to(field_def, dest)),
),
open: data.open,
}
Expand Down
20 changes: 17 additions & 3 deletions core/src/bytecode/ast/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
use crate::identifier::Ident;

use super::{
record::{FieldMetadata, FieldPathElem, MergePriority},
record::{FieldMetadata, FieldPathElem, Include, MergePriority},
typ::Type,
*,
};
Expand Down Expand Up @@ -239,6 +239,7 @@ impl<'ast> Field<'ast, Record<'ast>> {
#[derive(Debug, Default)]
pub struct Record<'ast> {
field_defs: Vec<record::FieldDef<'ast>>,
includes: Vec<Include<'ast>>,
open: bool,
}

Expand Down Expand Up @@ -271,6 +272,17 @@ impl<'ast> Record<'ast> {
self
}

/// Adds an `include` expression (define a field by taking it from the outer environment).
pub fn include(mut self, ident: LocIdent) -> Self {
self.include_with_metadata(ident, Default::default())
}

/// Adds an `include` expression with associated metadata.
pub fn include_with_metadata(mut self, ident: LocIdent, metadata: FieldMetadata<'ast>) -> Self {
self.includes.push(Include { ident, metadata });
self
}

/// Start constructing a field at the given path
pub fn path<It, I>(self, path: It) -> Field<'ast, Record<'ast>>
where
Expand Down Expand Up @@ -303,6 +315,7 @@ impl<'ast> Record<'ast> {
alloc
.record(record::Record {
field_defs: alloc.alloc_many(self.field_defs),
includes: alloc.alloc_many(self.includes),
open: self.open,
})
.into()
Expand Down Expand Up @@ -458,6 +471,7 @@ mod tests {
pos: TermPos::None,
}
})),
includes: &[],
open,
})
.into()
Expand Down Expand Up @@ -665,7 +679,7 @@ mod tests {
pos: TermPos::None,
},
]),
open: false,
..Default::default()
})
.into()
),
Expand All @@ -682,7 +696,7 @@ mod tests {
pos: TermPos::None,
}
]),
open: false,
..Default::default()
})
.into()
);
Expand Down
Loading
Loading