Skip to content

Commit

Permalink
Add support for constant pattern (#1897)
Browse files Browse the repository at this point in the history
This commit adds support for constant patterns, that is patterns
matching specific booleans, strings, numbers or the null value.
  • Loading branch information
yannham authored May 2, 2024
1 parent c09b4ca commit 3b01e62
Show file tree
Hide file tree
Showing 27 changed files with 247 additions and 38 deletions.
18 changes: 18 additions & 0 deletions core/src/parser/grammar.lalrpop
Original file line number Diff line number Diff line change
Expand Up @@ -575,6 +575,7 @@ PatternF<F>: Pattern = {
#[inline]
PatternDataF<F>: PatternData = {
RecordPattern => PatternData::Record(<>),
ConstantPattern => PatternData::Constant(<>),
EnumPatternF<F> => PatternData::Enum(<>),
Ident => PatternData::Any(<>),
};
Expand All @@ -586,6 +587,23 @@ Pattern: Pattern = PatternF<"">;
// A pattern restricted to function arguments.
PatternFun: Pattern = PatternF<"function">;

ConstantPattern: ConstantPattern = {
<start: @L> <data: ConstantPatternData> <end: @R> => ConstantPattern {
data,
pos: mk_pos(src_id, start, end)
}
};

ConstantPatternData: ConstantPatternData = {
Bool => ConstantPatternData::Bool(<>),
NumberLiteral => ConstantPatternData::Number(<>),
// We could accept multiline strings here, but it's unlikely that this will
// result in very readable match expressions. For now we restrict ourselves
// to standard string; we can always extend to multiline later if needed
StandardStaticString => ConstantPatternData::String(<>.into()),
"null" => ConstantPatternData::Null,
};

RecordPattern: RecordPattern = {
<start: @L> "{" <mut field_pats: (<FieldPattern> ",")*> <last: LastFieldPat?> "}" <end: @R> =>? {
let tail = match last {
Expand Down
33 changes: 30 additions & 3 deletions core/src/pretty.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,9 @@ use std::fmt;

use crate::identifier::LocIdent;
use crate::parser::lexer::KEYWORDS;
use crate::term::pattern::{EnumPattern, Pattern, PatternData, RecordPattern, RecordPatternTail};
use crate::term::record::RecordData;
use crate::term::{
record::{Field, FieldMetadata},
pattern::*,
record::{Field, FieldMetadata, RecordData},
*,
};
use crate::typ::*;
Expand Down Expand Up @@ -589,6 +588,34 @@ where
PatternData::Any(id) => allocator.as_string(id),
PatternData::Record(rp) => rp.pretty(allocator),
PatternData::Enum(evp) => evp.pretty(allocator),
PatternData::Constant(cp) => cp.pretty(allocator),
}
}
}

impl<'a, D, A> Pretty<'a, D, A> for &ConstantPattern
where
D: NickelAllocatorExt<'a, A>,
D::Doc: Clone,
A: Clone + 'a,
{
fn pretty(self, allocator: &'a D) -> DocBuilder<'a, D, A> {
self.data.pretty(allocator)
}
}

impl<'a, D, A> Pretty<'a, D, A> for &ConstantPatternData
where
D: NickelAllocatorExt<'a, A>,
D::Doc: Clone,
A: Clone + 'a,
{
fn pretty(self, allocator: &'a D) -> DocBuilder<'a, D, A> {
match self {
ConstantPatternData::Bool(b) => allocator.as_string(b),
ConstantPatternData::Number(n) => allocator.as_string(format!("{}", n.to_sci())),
ConstantPatternData::String(s) => allocator.escaped_string(s).double_quotes(),
ConstantPatternData::Null => allocator.text("null"),
}
}
}
Expand Down
15 changes: 14 additions & 1 deletion core/src/term/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ use crate::{
impl_display_from_pretty,
label::{Label, MergeLabel},
match_sharedterm,
position::TermPos,
position::{RawSpan, TermPos},
typ::{Type, UnboundTypeVariableError},
typecheck::eq::{contract_eq, type_eq_noenv},
};
Expand Down Expand Up @@ -621,6 +621,19 @@ pub struct LabeledType {
}

impl LabeledType {
/// Create a labeled type from a type and a span, which are the minimal information required to
/// instantiate the type and the underlying label. All other values are set to the defaults.
pub fn new(typ: Type, span: RawSpan) -> Self {
Self {
typ: typ.clone(),
label: Label {
typ: Rc::new(typ),
span,
..Default::default()
},
}
}

/// Modify the label's `field_name` field.
pub fn with_field_name(self, ident: Option<LocIdent>) -> Self {
LabeledType {
Expand Down
40 changes: 40 additions & 0 deletions core/src/term/pattern/compile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,46 @@ impl CompilePart for PatternData {
}
PatternData::Record(pat) => pat.compile_part(value_id, bindings_id),
PatternData::Enum(pat) => pat.compile_part(value_id, bindings_id),
PatternData::Constant(pat) => pat.compile_part(value_id, bindings_id),
}
}
}

impl CompilePart for ConstantPattern {
fn compile_part(&self, value_id: LocIdent, bindings_id: LocIdent) -> RichTerm {
self.data.compile_part(value_id, bindings_id)
}
}

impl CompilePart for ConstantPatternData {
fn compile_part(&self, value_id: LocIdent, bindings_id: LocIdent) -> RichTerm {
let compile_constant = |nickel_type: &str, value: Term| {
// if %typeof% value_id == '<nickel_type> && value_id == <value> then
// bindings_id
// else
// null

// %typeof% value_id == '<nickel_type>
let type_matches = make::op2(
BinaryOp::Eq(),
make::op1(UnaryOp::Typeof(), Term::Var(value_id)),
Term::Enum(nickel_type.into()),
);

// value_id == <value>
let value_matches = make::op2(BinaryOp::Eq(), Term::Var(value_id), value);

// <type_matches> && <value_matches>
let if_condition = mk_app!(make::op1(UnaryOp::BoolAnd(), type_matches), value_matches);

make::if_then_else(if_condition, Term::Var(bindings_id), Term::Null)
};

match self {
ConstantPatternData::Bool(b) => compile_constant("Bool", Term::Bool(*b)),
ConstantPatternData::Number(n) => compile_constant("Number", Term::Num(n.clone())),
ConstantPatternData::String(s) => compile_constant("String", Term::Str(s.clone())),
ConstantPatternData::Null => compile_constant("Other", Term::Null),
}
}
}
Expand Down
89 changes: 61 additions & 28 deletions core/src/term/pattern/mod.rs
Original file line number Diff line number Diff line change
@@ -1,24 +1,25 @@
//! Pattern matching and destructuring of Nickel values.
use std::collections::{hash_map::Entry, HashMap};

use super::{
record::{Field, RecordAttrs, RecordData},
LabeledType, NickelString, Number, RichTerm, Term, TypeAnnotation,
};

use crate::{
error::EvalError,
identifier::LocIdent,
impl_display_from_pretty,
label::Label,
mk_app,
impl_display_from_pretty, mk_app,
parser::error::ParseError,
position::TermPos,
stdlib::internals,
term::{
record::{Field, RecordAttrs, RecordData},
LabeledType, RichTerm, Term, TypeAnnotation,
},
typ::{Type, TypeF},
};

pub mod compile;

/// A small helper to generate a
#[derive(Debug, PartialEq, Clone)]
pub enum PatternData {
/// A simple pattern consisting of an identifier. Match anything and bind the result to the
Expand All @@ -28,6 +29,8 @@ pub enum PatternData {
Record(RecordPattern),
/// An enum pattern as in `'Foo x` or `'Foo`
Enum(EnumPattern),
/// A constant pattern as in `42` or `true`.
Constant(ConstantPattern),
}

/// A generic pattern, that can appear in a match expression (not yet implemented) or in a
Expand Down Expand Up @@ -102,6 +105,21 @@ pub struct RecordPattern {
pub pos: TermPos,
}

/// A constant pattern, matching a constant value.
#[derive(Debug, PartialEq, Clone)]
pub struct ConstantPattern {
pub data: ConstantPatternData,
pub pos: TermPos,
}

#[derive(Debug, PartialEq, Clone)]
pub enum ConstantPatternData {
Bool(bool),
Number(Number),
String(NickelString),
Null,
}

/// The tail of a record pattern which might capture the rest of the record.
#[derive(Debug, PartialEq, Clone)]
pub enum RecordPatternTail {
Expand Down Expand Up @@ -200,6 +218,7 @@ impl ElaborateContract for PatternData {
PatternData::Any(_) => None,
PatternData::Record(pat) => pat.elaborate_contract(),
PatternData::Enum(pat) => pat.elaborate_contract(),
PatternData::Constant(pat) => pat.elaborate_contract(),
}
}
}
Expand All @@ -210,6 +229,32 @@ impl ElaborateContract for Pattern {
}
}

// Generate the contract `std.contract.Equal <value>` as a labeled type.
fn contract_eq(value: Term, span: crate::position::RawSpan) -> LabeledType {
let contract = mk_app!(internals::stdlib_contract_equal(), value);

let typ = Type {
typ: TypeF::Flat(contract),
pos: span.into(),
};

LabeledType::new(typ, span)
}

impl ElaborateContract for ConstantPattern {
fn elaborate_contract(&self) -> Option<LabeledType> {
// See [^unwrap-span].
let span = self.pos.unwrap();

Some(match &self.data {
ConstantPatternData::Bool(b) => contract_eq(Term::Bool(*b), span),
ConstantPatternData::String(s) => contract_eq(Term::Str(s.clone()), span),
ConstantPatternData::Number(n) => contract_eq(Term::Num(n.clone()), span),
ConstantPatternData::Null => contract_eq(Term::Null, span),
})
}
}

impl ElaborateContract for EnumPattern {
fn elaborate_contract(&self) -> Option<LabeledType> {
// TODO[adts]: it would be better to simply build a type like `[| 'tag arg |]` or `[| 'tag
Expand All @@ -226,18 +271,11 @@ impl ElaborateContract for EnumPattern {
pos: self.pos,
};

Some(LabeledType {
typ: typ.clone(),
label: Label {
typ: typ.into(),
// [^unwrap-span]: We need the position to be defined here. Hopefully,
// contract-generating pattern are pattern used in destructuring, and destructuring
// patterns aren't currently generated by the Nickel interpreter. So we should only
// encounter user-written patterns here, which should have a position.
span: self.pos.unwrap(),
..Default::default()
},
})
// [^unwrap-span]: We need the position to be defined here. Hopefully,
// contract-generating pattern are pattern used in destructuring, and destructuring
// patterns aren't currently generated by the Nickel interpreter. So we should only
// encounter user-written patterns here, which should have a position.
Some(LabeledType::new(typ, self.pos.unwrap()))
}
}

Expand All @@ -261,19 +299,14 @@ impl ElaborateContract for RecordPattern {
pos: self.pos,
};

Some(LabeledType {
typ: typ.clone(),
label: Label {
typ: typ.into(),
// unwrap(): cf [^unwrap-span]
span: self.pos.unwrap(),
..Default::default()
},
})
// unwrap(): see [^unwrap-span]
Some(LabeledType::new(typ, self.pos.unwrap()))
}
}

impl_display_from_pretty!(PatternData);
impl_display_from_pretty!(Pattern);
impl_display_from_pretty!(ConstantPatternData);
impl_display_from_pretty!(ConstantPattern);
impl_display_from_pretty!(RecordPattern);
impl_display_from_pretty!(EnumPattern);
14 changes: 11 additions & 3 deletions core/src/transform/desugar_destructuring.rs
Original file line number Diff line number Diff line change
Expand Up @@ -140,10 +140,18 @@ impl Desugar for PatternData {
PatternData::Any(id) => Term::Let(id, destr, body, LetAttrs::default()),
PatternData::Record(pat) => pat.desugar(destr, body),
PatternData::Enum(pat) => pat.desugar(destr, body),
PatternData::Constant(pat) => pat.desugar(destr, body),
}
}
}

impl Desugar for ConstantPattern {
fn desugar(self, destr: RichTerm, body: RichTerm) -> Term {
// See [^seq-patterns]
mk_app!(mk_term::op1(UnaryOp::Seq(), destr), body).into()
}
}

impl Desugar for FieldPattern {
// For a field pattern, we assume that the `destr` argument is the whole record being
// destructured. We extract the field from `destr`, or use the default value is the field isn't
Expand Down Expand Up @@ -194,9 +202,9 @@ impl Desugar for EnumPattern {
let extracted = mk_term::op1(UnaryOp::EnumUnwrapVariant(), destr.clone());
arg_pat.desugar(extracted, body)
}
// If the pattern doesn't bind any argument, it's transparent, and we just proceed with the
// body. However, because of lazyness, the associated contract will never be checked,
// because body doesn't depend on `destr`.
// [^seq-patterns]: If the pattern doesn't bind any argument, it's transparent, and we just
// proceed with the body. However, because of lazyness, the associated contract will never
// be checked, because body doesn't depend on `destr`.
//
// For patterns that bind variables, it's reasonable to keep them lazy: that is, in `let
// 'Foo x = destr in body`, `destr` is checked to be an enum only when `x` is evaluated.
Expand Down
2 changes: 2 additions & 0 deletions core/src/transform/free_vars.rs
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,8 @@ impl RemoveBindings for PatternData {
PatternData::Enum(enum_variant_pat) => {
enum_variant_pat.remove_bindings(working_set);
}
// A constant pattern doesn't bind any variable.
PatternData::Constant(_) => (),
}
}
}
Expand Down
Loading

0 comments on commit 3b01e62

Please sign in to comment.