Skip to content

Commit

Permalink
Use the final syntax for ADTs
Browse files Browse the repository at this point in the history
Now that typechecking, pattern matching and core contracts are
implemented for ADTs (enum variants), we switch from the temporary to
the final syntax, which is just to use the same application syntax as
for function application.
  • Loading branch information
yannham committed Feb 20, 2024
1 parent 0b294a2 commit 84e2dba
Show file tree
Hide file tree
Showing 24 changed files with 177 additions and 100 deletions.
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
# capture = 'stderr'
# command = ['eval']
(std.function.id | forall r. [| 'Foo, 'Bar String, 'Qux Dyn; r |] -> [| 'Foo, 'Bar String, 'Qux Dyn; r |]) ('Foo..(5))
(std.function.id | forall r. [| 'Foo, 'Bar String, 'Qux Dyn; r |] -> [| 'Foo, 'Bar String, 'Qux Dyn; r |]) ('Foo 5)
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,6 @@ let f | forall r. [| ; r |] -> [| 'Foo Number; r |] = fun tag =>
if true then
tag
else
'Foo..(1)
'Foo 1
in
(f ('Foo..("hello")) : _)
(f ('Foo "hello") : _)
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ error: contract broken by the caller
shape mismatch for 'Foo
┌─ [INPUTS_PATH]/errors/enum_contract_shape_mismatch_rev.ncl:3:30
3 │ (std.function.id | forall r. [| 'Foo, 'Bar String, 'Qux Dyn; r |] -> [| 'Foo, 'Bar String, 'Qux Dyn; r |]) ('Foo..(5))
------------------------------------ ----------- evaluated to this expression
3 │ (std.function.id | forall r. [| 'Foo, 'Bar String, 'Qux Dyn; r |] -> [| 'Foo, 'Bar String, 'Qux Dyn; r |]) ('Foo 5)
------------------------------------ -------- evaluated to this expression
│ │
expected type of the argument provided by the caller
Expand All @@ -17,13 +17,13 @@ error: contract broken by the caller
note:
┌─ [INPUTS_PATH]/errors/enum_contract_shape_mismatch_rev.ncl:3:1
3 │ (std.function.id | forall r. [| 'Foo, 'Bar String, 'Qux Dyn; r |] -> [| 'Foo, 'Bar String, 'Qux Dyn; r |]) ('Foo..(5))
---------------------------------------------------------------------------------------------------------------------- (1) calling <func>
3 │ (std.function.id | forall r. [| 'Foo, 'Bar String, 'Qux Dyn; r |] -> [| 'Foo, 'Bar String, 'Qux Dyn; r |]) ('Foo 5)
------------------------------------------------------------------------------------------------------------------- (1) calling <func>

note:
┌─ [INPUTS_PATH]/errors/enum_contract_shape_mismatch_rev.ncl:3:2
3 │ (std.function.id | forall r. [| 'Foo, 'Bar String, 'Qux Dyn; r |] -> [| 'Foo, 'Bar String, 'Qux Dyn; r |]) ('Foo..(5))
3 │ (std.function.id | forall r. [| 'Foo, 'Bar String, 'Qux Dyn; r |] -> [| 'Foo, 'Bar String, 'Qux Dyn; r |]) ('Foo 5)
│ --------------- (2) calling <func>


Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ expression: err
error: multiple enum row declarations
┌─ [INPUTS_PATH]/errors/enum_forall_constraints_typecheck.ncl:9:4
9 │ (f ('Foo..("hello")) : _)
│ ^^^^^^^^^^^^^^^^^ this expression
9 │ (f ('Foo "hello") : _)
│ ^^^^^^^^^^^^^^ this expression
= Found an expression with the row `'Foo _a`
= But this row appears inside another enum type, which already has a diffent declaration for the tag `Foo`
Expand Down
97 changes: 76 additions & 21 deletions core/src/parser/grammar.lalrpop
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,7 @@ UniTerm: UniTerm = {

Ok(UniTerm::from(mk_let(recursive.is_some(), pat, t1, t2, mk_span(src_id, l, r))?))
},
<l: @L> "fun" <pats: Pattern+> "=>" <t: Term> <r: @R> => {
<l: @L> "fun" <pats: PatternFun+> "=>" <t: Term> <r: @R> => {
let pos = mk_pos(src_id, l, r);
let rt = pats.into_iter().rev().fold(t, |t, assgn| RichTerm {
term: SharedTerm::new(mk_fun(assgn, t)),
Expand Down Expand Up @@ -283,10 +283,26 @@ Forall: Type =
// also includes previous levels).
Applicative: UniTerm = {
Atom,
<EnumVariant> => <>.into(),
AsUniTerm<WithPos<TypeArray>>,
<t1: AsTerm<Applicative>> <t2: AsTerm<Atom>> =>
UniTerm::from(mk_app!(t1, t2)),
<t1: AsTerm<Applicative>> <t2: AsTerm<Atom>> => {
// We special case the application of an enum tag here. In principle, an
// enum variant applied to an argument is of different nature than a
// function application. However, for convenience, we made the syntax
// the same. So we now have to detect cases like `'Foo {x=1}` and
// convert that to a proper enum variant.
let term = if let Term::Enum(tag) = t1.as_ref() {
Term::EnumVariant {
tag: *tag,
arg: t2,
attrs: EnumVariantAttrs::default(),
}
}
else {
Term::App(t1, t2)
};

UniTerm::from(term)
},
<op: UOp> <t: AsTerm<Atom>> => UniTerm::from(mk_term::op1(op, t)),
<op: BOpPre> <t1: AsTerm<Atom>> <t2: AsTerm<Atom>>
=> UniTerm::from(mk_term::op2(op, t1, t2)),
Expand Down Expand Up @@ -515,16 +531,39 @@ FieldPathElem: FieldPathElem = {
<WithPos<StrChunks>> => FieldPathElem::Expr(<>),
};

// Last field of a pattern
LastFieldPat: LastPattern<FieldPattern> = {
FieldPattern => LastPattern::Normal(Box::new(<>)),
".." <Ident?> => LastPattern::Ellipsis(<>),
};

// The right hand side of an `=` inside a destructuring pattern.
// A pattern.
//
// The PatternF, PatternDataF and EnumPatternF rules are parametrized by a
// (string) flag (using LALRPOP's undocumented conditional macros). The idea is
// that those rules have two flavours: the most general one, which allow
// patterns to be unrestricted, and a version for function arguments.
//
// The issue is the following: before the introduction of enum variants,
// functions have been allowed to match on several arguments using a sequence of
// patterns. For example, `fun {x} {y} z => x + y + z`. With variants, we've
// added the following pattern form: `'SomeTag argument`. Now, something like
// `fun 'SomeTag 'SomeArg => ...` is ambiguous: are we matching on a single
// argument that we expect to be `('SomeTag 'SomeArg)`, or on two separate
// arguments that are bare enum tags, as in `fun ('SomeTag) ('SomeArg)`?
//
// To avoid ambiguity, we force the top-level argument patterns of a function to
// use a parenthesized version for enum variants. Thus `fun 'Foo 'Bar => ...` is
// always interpreted as `fun ('Foo) ('Bar) => ...`. The other interpretation
// can be written as `fun ('Foo 'Bar) => ...`.
//
// We allow parenthesized enum variants pattern in general pattern as well, not
// only for consistency, but because they also make nested enum variant patterns
// more readable: `'Foo ('Bar 5)` vs `'Foo 'Bar 5`. In fact, we also force
// nested enum patterns to be parenthesized, and forbid the latter, for better
// readability. In practice, this means that the argument pattern of an enum
// variant pattern has the same restriction as a function argument pattern.
//
// The flavour parameter `F` can either be `"function"`, which is disabling the
// non-parenthesized enum variant rule, or any other string for the general
// flavour. In practice we use "".
#[inline]
Pattern: Pattern = {
<l: @L> <alias:(<Ident> "@")?> <data: PatternData> <r: @R> => {
PatternF<F>: Pattern = {
<l: @L> <alias:(<Ident> "@")?> <data: PatternDataF<F>> <r: @R> => {
Pattern {
alias,
data,
Expand All @@ -534,12 +573,19 @@ Pattern: Pattern = {
};

#[inline]
PatternData: PatternData = {
PatternDataF<F>: PatternData = {
RecordPattern => PatternData::Record(<>),
EnumPattern => PatternData::Enum(<>),
EnumPatternF<F> => PatternData::Enum(<>),
Ident => PatternData::Any(<>),
};

// A general pattern.
#[inline]
Pattern: Pattern = PatternF<"">;

// A pattern restricted to function arguments.
PatternFun: Pattern = PatternF<"function">;

RecordPattern: RecordPattern = {
<start: @L> "{" <mut field_pats: (<FieldPattern> ",")*> <last: LastFieldPat?> "}" <end: @R> =>? {
let tail = match last {
Expand Down Expand Up @@ -567,13 +613,20 @@ RecordPattern: RecordPattern = {
},
};

EnumPattern: EnumPattern = {
EnumPatternF<F>: EnumPattern = {
<start: @L> <tag: EnumTag> <end: @R> => EnumPattern {
tag,
pattern: None,
pos: mk_pos(src_id, start, end),
},
<start: @L> <tag: EnumTag> ".." "(" <pattern: Pattern> ")" <end: @R> => EnumPattern {
// See documentation of PatternF to see why we use the "function" variant
// here.
<start: @L> <tag: EnumTag> <pattern: PatternF<"function">> <end: @R> if F != "function" => EnumPattern {
tag,
pattern: Some(Box::new(pattern)),
pos: mk_pos(src_id, start, end),
},
<start: @L> "(" <tag: EnumTag> <pattern: PatternF<"function">> ")" <end: @R> => EnumPattern {
tag,
pattern: Some(Box::new(pattern)),
pos: mk_pos(src_id, start, end),
Expand Down Expand Up @@ -611,6 +664,12 @@ FieldPattern: FieldPattern = {
},
};

// Last field of a pattern
LastFieldPat: LastPattern<FieldPattern> = {
FieldPattern => LastPattern::Normal(Box::new(<>)),
".." <Ident?> => LastPattern::Ellipsis(<>),
};

// A default annotation in a pattern.
DefaultAnnot: RichTerm = "?" <t: Term> => t;

Expand Down Expand Up @@ -756,10 +815,6 @@ EnumTag: LocIdent = {
<StringEnumTag> => <>.into(),
};

EnumVariant: RichTerm =
<tag: EnumTag> ".." "(" <arg: Term> ")" =>
RichTerm::from(Term::EnumVariant { tag, arg, attrs: Default::default() });

ChunkLiteralPart: ChunkLiteralPart = {
"str literal" => ChunkLiteralPart::Str(<>),
"multstr literal" => ChunkLiteralPart::Str(<>),
Expand Down
32 changes: 27 additions & 5 deletions core/src/pretty.rs
Original file line number Diff line number Diff line change
Expand Up @@ -400,6 +400,22 @@ where
fn type_part(&'a self, typ: &Type) -> DocBuilder<'a, Self, A> {
typ.pretty(self).parens_if(needs_parens_in_type_pos(typ))
}

/// Pretty printing of a restricted patterns that requires enum variant patterns to be
/// parenthesized (typically function pattern arguments). The only difference with a general
/// pattern is that for a function, a top-level enum variant pattern with an enum tag as an
/// argument such as `'Foo 'Bar` must be parenthesized, because `fun 'Foo 'Bar => ...` is
/// parsed as a function of two arguments, which are bare enum tags `'Foo` and `'Bar`. We must
/// print `fun ('Foo 'Bar) => ..` instead.
fn pat_with_parens(&'a self, pattern: &Pattern) -> DocBuilder<'a, Self, A> {
pattern.pretty(self).parens_if(matches!(
pattern.data,
PatternData::Enum(EnumPattern {
pattern: Some(_),
..
})
))
}
}

trait NickelDocBuilderExt<'a, D, A> {
Expand Down Expand Up @@ -589,7 +605,11 @@ where
"'",
ident_quoted(&self.tag),
if let Some(ref arg_pat) = self.pattern {
docs![allocator, "..(", &**arg_pat, ")"]
docs![
allocator,
allocator.line(),
allocator.pat_with_parens(&**arg_pat)
]
} else {
allocator.nil()
}
Expand Down Expand Up @@ -719,7 +739,7 @@ where
let mut params = vec![];
let mut rt = self;
while let FunPattern(pat, t) = rt {
params.push(pat.pretty(allocator));
params.push(allocator.pat_with_parens(pat));
rt = t.as_ref();
}
docs![
Expand Down Expand Up @@ -835,9 +855,11 @@ where
EnumVariant { tag, arg, attrs: _ } => allocator
.text("'")
.append(allocator.text(ident_quoted(tag)))
.append("..(")
.append(arg.pretty(allocator))
.append(")"),
.append(
docs![allocator, allocator.line(), allocator.atom(arg)]
.nest(2)
.group(),
),
Record(record_data) => allocator.record(record_data, &[]),
RecRecord(record_data, dyn_fields, _) => allocator.record(record_data, dyn_fields),
Match(data) => docs![
Expand Down
6 changes: 3 additions & 3 deletions core/tests/integration/inputs/adts/enum_primops.ncl
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@
let {check, ..} = import "../lib/assert.ncl" in

[
%enum_unwrap_variant% ('Left..(1+1)) == 2,
%enum_unwrap_variant% ('Left (1+1)) == 2,
!(%enum_is_variant% 'Right),
%enum_is_variant% ('Right..(1)),
%enum_is_variant% ('Right 1),
%enum_get_tag% 'Right == 'Right,
%enum_get_tag% ('Right..("stuff")) == 'Right,
%enum_get_tag% ('Right "stuff") == 'Right,
]
|> check
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
#
# [test.metadata]
# error = 'EvalError::BlameError'
'Foo..(5) | [| 'Foo String, 'Bar Number, 'Barg |]
'Foo 5 | [| 'Foo String, 'Bar Number, 'Barg |]
8 changes: 4 additions & 4 deletions core/tests/integration/inputs/core/eq.ncl
Original file line number Diff line number Diff line change
Expand Up @@ -79,9 +79,9 @@ let {check, ..} = import "../lib/assert.ncl" in
),

# ADTs
'Left..(1+1) == 'Left..(2),
'Left..(1) != 'Left..(2),
'Left..(2) != 'Right..(2),
'Up..([1,2,3]) == 'Up..([0+1,1+1,2+1]),
'Left (1+1) == 'Left 2,
'Left 1 != 'Left 2,
'Left 2 != 'Right 2,
'Up [1,2,3] == 'Up [0+1,1+1,2+1],
]
|> check
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@
#
# [test.metadata]
# error = 'EvalError::BlameError'
let 'Foo = 'Foo..(5) in
let 'Foo = 'Foo 5 in
true
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@
#
# [test.metadata]
# error = 'EvalError::BlameError'
let 'Foo..(x) = 'Foo in
let 'Foo x = 'Foo in
x
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# test.type = 'pass'
(
let y = 'Foo..(1 + 1) in
let 'Foo..(x) = y in
let y = 'Foo (1 + 1) in
let 'Foo x = y in
x + 1 == 3
) : _
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@
#
# [test.metadata.expectation]
# ident = 'Bar'
(let y : [| 'Foo Number, 'Bar String |] = 'Foo..(5) in let 'Foo..(x) = y in x) : _
(let y : [| 'Foo Number, 'Bar String |] = 'Foo 5 in let 'Foo x = y in x) : _
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@
#
# [test.metadata.expectation]
# ident = 'Bar'
(let 'Foo..(x) = 'Bar..(5) in x) : _
(let 'Foo x = 'Bar 5 in x) : _
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@
#
# [test.metadata]
# error = 'EvalError::BlameError'
let 'Foo..(x) = 'Bar..(5) in
let 'Foo x = 'Bar 5 in
x
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@
#
# [test.metadata]
# error = 'EvalError::BlameError'
let 'Foo..(x) = {bar = 1} in
let 'Foo x = {bar = 1} in
x
Loading

0 comments on commit 84e2dba

Please sign in to comment.