Skip to content

Commit

Permalink
Add support for constant pattern
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 committed Apr 26, 2024
1 parent 13fe12c commit 5c4ebad
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 5c4ebad

Please sign in to comment.