1
1
mirror of https://github.com/tweag/nickel.git synced 2024-09-11 11:47:03 +03:00

Change switch to match

This commit changes the keyword of the switch branching construct to a
match, to make room for future more advanced features like pattern
matching.

Also, the match is now curried, meaning it doesn't have to be applied to
an argument and `match {..cases..}` can be used directly as a function.
This commit is contained in:
Yann Hamdaoui 2022-12-05 18:12:54 +01:00
parent 760a611fb8
commit 2518b2ce3e
No known key found for this signature in database
GPG Key ID: 96305DE11214ABE6
26 changed files with 287 additions and 240 deletions

View File

@ -211,11 +211,11 @@ The following type constructors are available:
```nickel
let protocol : [| `http, `ftp, `sftp |] = `http in
(switch {
(protocol |> match {
`http => 1,
`ftp => 2,
`sftp => 3
} protocol) : Num
}) : Num
```
- **Arrow (function)**: `S -> T`. A function taking arguments of type `S` and

View File

@ -79,7 +79,7 @@ rather than data. Take the following example:
```nickel
let record = {
protocol | default = `Http,
port | default = switch protocol {
port | default = protocol |> match {
`Http => 80,
`Ftp => 21,
_ => 8181,

View File

@ -216,8 +216,8 @@ look-ahead to distinguish).
With the first release in mind, we propose to just disable support for enum
altogether. This is a handy but hardly fundamental feature, and this lets us
more time to find a good syntax replacement. We can also disable `switch`
temporarily, as currently its only usage is for enums.
more time to find a good syntax replacement. We can also disable `switch` (edit
from 05.12.22: now `match`) temporarily, as currently its only usage is for enums.
### Translation

View File

@ -386,40 +386,7 @@ impl<R: ImportResolver, C: Cache> VirtualMachine<R, C> {
env,
}
}
Term::Switch(exp, cases, default) => {
self.set_mode(EvalMode::UnwrapMeta);
let has_default = default.is_some();
if let Some(t) = default {
self.stack.push_arg(
Closure {
body: t.clone(),
env: env.clone(),
},
pos,
);
}
self.stack.push_arg(
Closure {
body: RichTerm::new(
Term::Record(RecordData::with_fields(cases.clone())),
pos,
),
env: env.clone(),
},
pos,
);
Closure {
body: RichTerm::new(
Term::Op1(UnaryOp::Switch(has_default), exp.clone()),
pos,
),
env,
}
}
Term::Op1(op, t) => {
self.set_mode(op.eval_mode());
@ -686,6 +653,64 @@ impl<R: ImportResolver, C: Cache> VirtualMachine<R, C> {
return Ok((RichTerm::new(Term::Fun(*x, t.clone()), pos), env));
}
}
// A match expression acts as a function (in Nickel, a match expression corresponds
// to the cases, and doesn't include the examined value).
//
// The behavior is the same as for a function: we look for an argument on the
// stack, and proceed to the evaluation of the match, or stop here otherwise. If
// found (let's call it `arg`), we evaluate `%match% arg cases default`, where
// `%match%` is the primitive operation `UnaryOp::Match` taking care of forcing the
// argument `arg` and doing the actual matching operation.
Term::Match { cases, default } => {
if let Some((arg, pos_app)) = self.stack.pop_arg(&self.cache) {
// Setting the stack to be as if we would have evaluated an application
// `_ cases default`, where `default` is optional, and `_` is not relevant.
let has_default = default.is_some();
if let Some(t) = default {
self.stack.push_arg(
Closure {
body: t.clone(),
env: env.clone(),
},
pos,
);
}
self.stack.push_arg(
Closure {
body: RichTerm::new(
Term::Record(RecordData::with_fields(cases.clone())),
pos,
),
env: env.clone(),
},
pos,
);
// Now evaluating `%match% arg`, the left-most part of the application `%match%
// arg cases default`, which is in fact a primop application.
self.stack.push_op_cont(
OperationCont::Op1(UnaryOp::Match { has_default }, pos_app),
self.call_stack.len(),
pos,
);
arg
} else {
return Ok((
RichTerm::new(
Term::Match {
cases: cases.clone(),
default: default.clone(),
},
pos,
),
env,
));
}
}
// Otherwise, this is either an ill-formed application, or we are done
t => {
if let Some((arg, pos_app)) = self.stack.pop_arg(&self.cache) {
@ -850,7 +875,7 @@ pub fn subst<C: Cache>(
RichTerm::new(Term::App(t1, t2), pos)
}
Term::Switch(t, cases, default) => {
Term::Match {cases, default} => {
let default =
default.map(|d| subst(cache, d, initial_env, env));
let cases = cases
@ -862,9 +887,8 @@ pub fn subst<C: Cache>(
)
})
.collect();
let t = subst(cache, t, initial_env, env);
RichTerm::new(Term::Switch(t, cases, default), pos)
RichTerm::new(Term::Match {cases, default}, pos)
}
Term::Op1(op, t) => {
let t = subst(cache, t, initial_env, env);

View File

@ -199,7 +199,7 @@ impl<R: ImportResolver, C: Cache> VirtualMachine<R, C> {
Term::Bool(_) => "Bool",
Term::Str(_) => "Str",
Term::Enum(_) => "Enum",
Term::Fun(..) => "Fun",
Term::Fun(..) | Term::Match { .. } => "Fun",
Term::Array(..) => "Array",
Term::Record(..) | Term::RecRecord(..) => "Record",
Term::Lbl(..) => "Lbl",
@ -304,17 +304,17 @@ impl<R: ImportResolver, C: Cache> VirtualMachine<R, C> {
))
}
}
UnaryOp::Switch(has_default) => {
UnaryOp::Match { has_default } => {
let (cases_closure, ..) = self
.stack
.pop_arg(&self.cache)
.expect("missing arg for switch");
.expect("missing arg for match");
let default = if has_default {
Some(
self.stack
.pop_arg(&self.cache)
.map(|(clos, ..)| clos)
.expect("missing default case for switch"),
.expect("missing default case for match"),
)
} else {
None
@ -331,7 +331,7 @@ impl<R: ImportResolver, C: Cache> VirtualMachine<R, C> {
let mut cases = match cases_term.into_owned() {
Term::Record(r) => r.fields,
_ => panic!("invalid argument for switch"),
_ => panic!("invalid argument for match"),
};
cases
@ -345,7 +345,7 @@ impl<R: ImportResolver, C: Cache> VirtualMachine<R, C> {
// ? We should have a dedicated error for unmatched pattern
EvalError::TypeError(
String::from("Enum"),
String::from("switch"),
String::from("match"),
arg_pos,
RichTerm {
term: t,
@ -357,7 +357,7 @@ impl<R: ImportResolver, C: Cache> VirtualMachine<R, C> {
} else {
Err(EvalError::TypeError(
String::from("Enum"),
String::from("switch"),
String::from("match"),
arg_pos,
RichTerm { term: t, pos },
))
@ -1077,7 +1077,7 @@ impl<R: ImportResolver, C: Cache> VirtualMachine<R, C> {
))
}
}
UnaryOp::StrMatch() => {
UnaryOp::StrFind() => {
if let Term::Str(s) = &*t {
let re = regex::Regex::new(s)
.map_err(|err| EvalError::Other(err.to_string(), pos_op))?;
@ -1087,7 +1087,7 @@ impl<R: ImportResolver, C: Cache> VirtualMachine<R, C> {
param,
RichTerm::new(
Term::Op1(
UnaryOp::StrMatchCompiled(re.into()),
UnaryOp::StrFindCompiled(re.into()),
RichTerm::new(Term::Var(param), pos_op_inh),
),
pos_op_inh,
@ -1119,7 +1119,7 @@ impl<R: ImportResolver, C: Cache> VirtualMachine<R, C> {
))
}
}
UnaryOp::StrMatchCompiled(regex) => {
UnaryOp::StrFindCompiled(regex) => {
if let Term::Str(s) = &*t {
let capt = regex.captures(s);
let result = if let Some(capt) = capt {
@ -1551,7 +1551,7 @@ impl<R: ImportResolver, C: Cache> VirtualMachine<R, C> {
);
match *t1 {
Term::Fun(..) => Ok(Closure {
Term::Fun(..) | Term::Match { .. } => Ok(Closure {
body: RichTerm {
term: t1,
pos: pos1,

View File

@ -364,10 +364,10 @@ fn substitution() {
parse("let x = 1 in if true then 1 + (if false then 1 else \"Glob2\") else false").unwrap()
);
let t = parse("switch {`x => [1, glob1], `y => loc2, `z => {id = true, other = glob3}} loc1")
let t = parse("match{`x => [1, glob1], `y => loc2, `z => {id = true, other = glob3}} loc1")
.unwrap();
assert_eq!(
subst(&eval_cache, t, &initial_env, &env),
parse("switch {`x => [1, 1], `y => (if false then 1 else \"Glob2\"), `z => {id = true, other = false}} true").unwrap()
parse("match {`x => [1, 1], `y => (if false then 1 else \"Glob2\"), `z => {id = true, other = false}} true").unwrap()
);
}

View File

@ -236,28 +236,6 @@ UniTerm: UniTerm = {
UniTerm::from(rt)
},
"switch" "{" <cases: (SwitchCase ",")*> <last: SwitchCase?> "}"
<exp: Term> => {
let mut acc = HashMap::with_capacity(cases.len());
let mut default = None;
for case in cases.into_iter().map(|x| x.0).chain(last.into_iter()) {
match case {
SwitchCase::Normal(id, t) => acc.insert(id, t),
// If there are multiple default cases, the last one silently
// erases the others. We should have a dedicated error for that
SwitchCase::Default(t) => default.replace(t),
};
}
UniTerm::from(
Term::Switch(
exp,
acc,
default,
)
)
},
"if" <cond: Term> "then" <t1: Term> "else" <t2: Term> =>
UniTerm::from(mk_app!(Term::Op1(UnaryOp::Ite(), cond), t1, t2)),
<l: @L> <t: !> <r: @R> => {
@ -300,6 +278,26 @@ Applicative: UniTerm = {
=> UniTerm::from(mk_term::op2(op, t1, t2)),
NOpPre<AsTerm<RecordOperand>>,
RecordOperand,
"match" "{" <cases: (MatchCase ",")*> <last: MatchCase?> "}" => {
let mut acc = HashMap::with_capacity(cases.len());
let mut default = None;
for case in cases.into_iter().map(|x| x.0).chain(last.into_iter()) {
match case {
MatchCase::Normal(id, t) => acc.insert(id, t),
// If there are multiple default cases, the last one silently
// erases the others. We should have a dedicated error for that
MatchCase::Default(t) => default.replace(t),
};
}
UniTerm::from(
Term::Match {
cases: acc,
default,
}
)
},
};
// The parametrized array type.
@ -580,15 +578,15 @@ UOp: UnaryOp = {
"num_from" => UnaryOp::NumFromStr(),
"enum_from" => UnaryOp::EnumFromStr(),
"str_is_match" => UnaryOp::StrIsMatch(),
"str_match" => UnaryOp::StrMatch(),
"str_find" => UnaryOp::StrFind(),
"rec_force_op" => UnaryOp::RecForce(),
"rec_default_op" => UnaryOp::RecDefault(),
"record_empty_with_tail" => UnaryOp::RecordEmptyWithTail(),
};
SwitchCase: SwitchCase = {
<id: EnumTag> "=>" <t: Term> => SwitchCase::Normal(id, t),
"_" "=>" <t: Term> => SwitchCase::Default(<>),
MatchCase: MatchCase = {
<id: EnumTag> "=>" <t: Term> => MatchCase::Normal(id, t),
"_" "=>" <t: Term> => MatchCase::Default(<>),
}
// Infix operators by precedence levels. Lowest levels take precedence over
@ -845,7 +843,7 @@ extern {
"in" => Token::Normal(NormalToken::In),
"let" => Token::Normal(NormalToken::Let),
"rec" => Token::Normal(NormalToken::Rec),
"switch" => Token::Normal(NormalToken::Switch),
"match" => Token::Normal(NormalToken::Match),
"null" => Token::Normal(NormalToken::Null),
"true" => Token::Normal(NormalToken::True),
@ -951,7 +949,7 @@ extern {
"str_replace" => Token::Normal(NormalToken::StrReplace),
"str_replace_regex" => Token::Normal(NormalToken::StrReplaceRegex),
"str_is_match" => Token::Normal(NormalToken::StrIsMatch),
"str_match" => Token::Normal(NormalToken::StrMatch),
"str_find" => Token::Normal(NormalToken::StrFind),
"str_length" => Token::Normal(NormalToken::StrLength),
"str_substr" => Token::Normal(NormalToken::StrSubstr),
"str_from" => Token::Normal(NormalToken::ToStr),

View File

@ -88,8 +88,8 @@ pub enum NormalToken<'input> {
Let,
#[token("rec")]
Rec,
#[token("switch")]
Switch,
#[token("match")]
Match,
#[token("null")]
Null,
@ -276,8 +276,8 @@ pub enum NormalToken<'input> {
StrReplaceRegex,
#[token("%str_is_match%")]
StrIsMatch,
#[token("%str_match%")]
StrMatch,
#[token("%str_find%")]
StrFind,
#[token("%str_length%")]
StrLength,
#[token("%str_substr%")]
@ -319,7 +319,7 @@ pub enum NormalToken<'input> {
pub const KEYWORDS: &[&str] = &[
"Dyn", "Num", "Bool", "Str", "Array", "if", "then", "else", "forall", "in", "let", "rec",
"switch", "null", "true", "false", "fun", "import", "merge", "default", "doc", "optional",
"match", "null", "true", "false", "fun", "import", "merge", "default", "doc", "optional",
"priority", "force",
];

View File

@ -5,7 +5,7 @@ use crate::parser::error::ParseError as InternalParseError;
use crate::term::make as mk_term;
use crate::term::Term::*;
use crate::term::{record, BinaryOp, RichTerm, StrChunk, UnaryOp};
use crate::{mk_app, mk_switch};
use crate::{mk_app, mk_match};
use assert_matches::assert_matches;
use codespan::Files;
@ -177,14 +177,20 @@ fn enum_terms() {
Enum(Ident::from("this works!")).into(),
),
(
"switch with raw tags",
"switch { `foo => true, `bar => false, _ => 456, } 123",
mk_switch!(Num(123.), ("foo", Bool(true)), ("bar", Bool(false)) ; Num(456.)),
"match with raw tags",
"match { `foo => true, `bar => false, _ => 456, } 123",
mk_app!(
mk_match!(("foo", Bool(true)), ("bar", Bool(false)) ; Num(456.)),
Num(123.)
),
),
(
"switch with string tags",
"switch { `\"one:two\" => true, `\"three four\" => false, _ => 13 } 1",
mk_switch!(Num(1.), ("one:two", Bool(true)), ("three four", Bool(false)) ; Num(13.)),
"match with string tags",
"match { `\"one:two\" => true, `\"three four\" => false, _ => 13 } 1",
mk_app!(
mk_match!(("one:two", Bool(true)), ("three four", Bool(false)) ; Num(13.)),
Num(1.)
),
),
];

View File

@ -33,7 +33,7 @@ pub enum StringKind {
/// Distinguish between a normal case `id => exp` and a default case `_ => exp`.
#[derive(Clone, Debug)]
pub enum SwitchCase {
pub enum MatchCase {
Normal(Ident, RichTerm),
Default(RichTerm),
}

View File

@ -581,40 +581,35 @@ where
.append(allocator.line())
.group()
.braces(),
Switch(tst, cases, def) => allocator
.text("switch")
.append(allocator.space())
.append(
allocator
.intersperse(
sorted_map(cases).iter().map(|&(id, t)| {
allocator
.text("`")
.append(allocator.quote_if_needed(id))
.append(allocator.space())
.append(allocator.text("=>"))
.append(allocator.space())
.append(t.to_owned().pretty(allocator))
.append(allocator.text(","))
}),
allocator.line(),
)
.append(def.clone().map_or(allocator.nil(), |d| {
Match { cases, default } => allocator.text("match").append(allocator.space()).append(
allocator
.intersperse(
sorted_map(cases).iter().map(|&(id, t)| {
allocator
.line()
.append(allocator.text("_"))
.text("`")
.append(allocator.quote_if_needed(id))
.append(allocator.space())
.append(allocator.text("=>"))
.append(allocator.space())
.append(d.pretty(allocator))
}))
.nest(2)
.append(allocator.line_())
.braces()
.group(),
)
.append(allocator.space())
.append(allocator.atom(tst)),
.append(t.to_owned().pretty(allocator))
.append(allocator.text(","))
}),
allocator.line(),
)
.append(default.clone().map_or(allocator.nil(), |d| {
allocator
.line()
.append(allocator.text("_"))
.append(allocator.space())
.append(allocator.text("=>"))
.append(allocator.space())
.append(d.pretty(allocator))
}))
.nest(2)
.append(allocator.line_())
.braces()
.group(),
),
Array(fields, _) => allocator
// NOTE: the Array attributes are ignored here.
.line()

View File

@ -108,14 +108,15 @@ pub enum Term {
Vec<(RichTerm, RichTerm)>, /* field whose name is defined by interpolation */
Option<RecordDeps>, /* dependency tracking between fields. None before the free var pass */
),
/// A switch construct. The evaluation is done by the corresponding unary operator, but we
/// still need this one for typechecking.
/// A match construct. Correspond only to the match cases: this expression is still to be
/// applied to an argument to match on. Once applied, the evaluation is done by the
/// corresponding primitive operator. Still, we need this construct for typechecking and being
/// able to handle yet unapplied match expressions.
#[serde(skip)]
Switch(
RichTerm, /* tested expression */
HashMap<Ident, RichTerm>, /* cases */
Option<RichTerm>, /* default */
),
Match {
cases: HashMap<Ident, RichTerm>,
default: Option<RichTerm>,
},
/// An array.
#[serde(serialize_with = "crate::serialize::serialize_array")]
@ -830,7 +831,6 @@ impl<E> StrChunk<E> {
}
impl Term {
//#[cfg(test)]
/// Recursively apply a function to all `Term`s contained in a `RichTerm`.
pub fn apply_to_rich_terms<F>(&mut self, func: F)
where
@ -839,14 +839,16 @@ impl Term {
use self::Term::*;
match self {
Null | ParseError(_) => (),
Switch(ref mut t, ref mut cases, ref mut def) => {
Match {
ref mut cases,
ref mut default,
} => {
cases.iter_mut().for_each(|c| {
let (_, t) = c;
func(t);
});
func(t);
if let Some(def) = def {
func(def)
if let Some(default) = default {
func(default)
}
}
Record(ref mut r) => {
@ -908,6 +910,7 @@ impl Term {
Term::Num(_) => Some("Num"),
Term::Str(_) => Some("Str"),
Term::Fun(_, _) | Term::FunPattern(_, _, _) => Some("Fun"),
Term::Match { .. } => Some("MatchExpression"),
Term::Lbl(_) => Some("Label"),
Term::Enum(_) => Some("Enum"),
Term::Record(..) | Term::RecRecord(..) => Some("Record"),
@ -919,7 +922,6 @@ impl Term {
| Term::LetPattern(..)
| Term::App(_, _)
| Term::Var(_)
| Term::Switch(..)
| Term::Op1(_, _)
| Term::Op2(_, _, _)
| Term::OpN(..)
@ -952,6 +954,7 @@ impl Term {
format!("\"{}\"", chunks_str.join(""))
}
Term::Fun(_, _) | Term::FunPattern(_, _, _) => String::from("<func>"),
Term::Match { .. } => String::from("<func (match expr)>"),
Term::Lbl(_) => String::from("<label>"),
Term::Enum(id) => {
let re = regex::Regex::new("_?[a-zA-Z][_a-zA-Z0-9]*").unwrap();
@ -994,7 +997,6 @@ impl Term {
Term::Let(..)
| Term::LetPattern(..)
| Term::App(_, _)
| Term::Switch(..)
| Term::Op1(_, _)
| Term::Op2(_, _, _)
| Term::OpN(..)
@ -1039,6 +1041,8 @@ impl Term {
| Term::Num(_)
| Term::Str(_)
| Term::Fun(_, _)
// match expressions are function
| Term::Match {..}
| Term::Lbl(_)
| Term::Enum(_)
| Term::Record(..)
@ -1049,7 +1053,6 @@ impl Term {
| Term::FunPattern(..)
| Term::App(_, _)
| Term::Var(_)
| Term::Switch(..)
| Term::Op1(_, _)
| Term::Op2(_, _, _)
| Term::OpN(..)
@ -1088,7 +1091,7 @@ impl Term {
| Term::Fun(_, _)
| Term::FunPattern(_, _, _)
| Term::App(_, _)
| Term::Switch(..)
| Term::Match { .. }
| Term::Var(_)
| Term::Op1(_, _)
| Term::Op2(_, _, _)
@ -1117,14 +1120,25 @@ impl Term {
| Term::RecRecord(..)
| Term::Array(..)
| Term::Var(..)
| Term::Op1(..)
| Term::SealingKey(..) => true,
| Term::SealingKey(..)
// Those special cases aren't really atoms, but mustn't be parenthesized because they
// are really functions taking additional non-strict arguments and printed as "partial"
// infix operators.
//
// For example, `Op1(BoolOr, Var("x"))` is currently printed as `x ||`. Such operators
// must never parenthesized, such as in `(x ||)`.
//
// We might want a more robust mechanism for pretty printing such operators.
| Term::Op1(UnaryOp::BoolAnd(), _)
| Term::Op1(UnaryOp::BoolOr(), _)
=> true,
Term::Let(..)
| Term::Switch(..)
| Term::Match { .. }
| Term::LetPattern(..)
| Term::Fun(..)
| Term::FunPattern(..)
| Term::App(..)
| Term::Op1(..)
| Term::Op2(..)
| Term::OpN(..)
| Term::Sealed(..)
@ -1215,8 +1229,8 @@ pub enum UnaryOp {
/// `embed c x` will have enum type `a | b | c`. It only affects typechecking as at runtime
/// `embed someId` act like the identity.
Embed(Ident),
/// A switch block. Used to match on a enumeration.
Switch(bool /* presence of a default case */), //HashMap<Ident, CapturedTerm>, Option<CapturedTerm>),
/// Evaluate a match block applied to an argument.
Match { has_default: bool },
/// Static access to a record field.
///
@ -1318,17 +1332,17 @@ pub enum UnaryOp {
/// Transform a string to an enum.
EnumFromStr(),
/// Test if a regex matches a string.
/// Like [`UnaryOp::StrMatch`], this is a unary operator because we would like a way to share the
/// Like [`UnaryOp::StrFind`], this is a unary operator because we would like a way to share the
/// same "compiled regex" for many matching calls. This is done by returning functions
/// wrapping [`UnaryOp::StrIsMatchCompiled`] and [`UnaryOp::StrMatchCompiled`]
/// wrapping [`UnaryOp::StrIsMatchCompiled`] and [`UnaryOp::StrFindCompiled`]
StrIsMatch(),
/// Match a regex on a string, and returns the captured groups together, the index of the
/// match, etc.
StrMatch(),
StrFind(),
/// Version of [`UnaryOp::StrIsMatch`] which remembers the compiled regex.
StrIsMatchCompiled(CompiledRegex),
/// Version of [`UnaryOp::StrMatch`] which remembers the compiled regex.
StrMatchCompiled(CompiledRegex),
/// Version of [`UnaryOp::StrFind`] which remembers the compiled regex.
StrFindCompiled(CompiledRegex),
/// Force full evaluation of a term and return it.
///
/// This was added in the context of [`BinaryOp::ArrayLazyAssume`],
@ -1707,10 +1721,10 @@ impl RichTerm {
pos,
)
},
Term::Switch(t, cases, default) => {
Term::Match { cases, default } => {
// The annotation on `map_res` use Result's corresponding trait to convert from
// Iterator<Result> to a Result<Iterator>
let cases_res: Result<HashMap<Ident, RichTerm>, E> = cases
let cases_result : Result<HashMap<Ident, RichTerm>, E> = cases
.into_iter()
// For the conversion to work, note that we need a Result<(Ident,RichTerm), E>
.map(|(id, t)| t.traverse(f, state, order).map(|t_ok| (id, t_ok)))
@ -1718,10 +1732,8 @@ impl RichTerm {
let default = default.map(|t| t.traverse(f, state, order)).transpose()?;
let t = t.traverse(f, state, order)?;
RichTerm::new(
Term::Switch(t, cases_res?, default),
Term::Match {cases: cases_result?, default },
pos,
)
},
@ -2033,22 +2045,22 @@ pub mod make {
/// `mk_switch!(format, ("Json", json_case), ("Yaml", yaml_case) ; def)` corresponds to
/// ``switch { `Json => json_case, `Yaml => yaml_case, _ => def} format``.
#[macro_export]
macro_rules! mk_switch {
( $exp:expr, $( ($id:expr, $body:expr) ),* ; $default:expr ) => {
macro_rules! mk_match {
( $( ($id:expr, $body:expr) ),* ; $default:expr ) => {
{
let mut map = std::collections::HashMap::new();
let mut cases = std::collections::HashMap::new();
$(
map.insert($id.into(), $body.into());
cases.insert($id.into(), $body.into());
)*
$crate::term::RichTerm::from($crate::term::Term::Switch($crate::term::RichTerm::from($exp), map, Some($crate::term::RichTerm::from($default))))
$crate::term::RichTerm::from($crate::term::Term::Match {cases, default: Some($crate::term::RichTerm::from($default)) })
}
};
( $exp:expr, $( ($id:expr, $body:expr) ),*) => {
let mut map = std::collections::HashMap::new();
( $( ($id:expr, $body:expr) ),*) => {
let mut cases = std::collections::HashMap::new();
$(
map.insert($id.into(), $body.into());
cases.insert($id.into(), $body.into());
)*
$crate::term::RichTerm::from($crate::term::Term::Switch($crate::term::RichTerm::from($exp), map, None))
$crate::term::RichTerm::from($crate::term::Term::Match {cases, default: None})
};
}

View File

@ -88,8 +88,7 @@ impl CollectFreeVars for RichTerm {
t1.collect_free_vars(free_vars);
t2.collect_free_vars(free_vars);
}
Term::Switch(t, cases, default) => {
t.collect_free_vars(free_vars);
Term::Match { cases, default } => {
for t in cases.values_mut().chain(default.iter_mut()) {
t.collect_free_vars(free_vars);
}

View File

@ -181,7 +181,9 @@ pub fn should_share(t: &Term) -> bool {
| Term::SealingKey(_)
| Term::Var(_)
| Term::Enum(_)
| Term::Fun(_, _) => false,
| Term::Fun(_, _)
// match acts like a function, and is a WHNF
| Term::Match {..} => false,
_ => true,
}
}

View File

@ -32,7 +32,7 @@
//!
//! ## Equality on terms
//!
//! The terms inside a type may be arbitrarily complex. Primops applications, `switch`, and the
//! The terms inside a type may be arbitrarily complex. Primops applications, `match`, and the
//! like are quite unlikely to appear inside an annotation (they surely appear inside contract
//! definitions). We don't want to compare functions syntactically either. The spirit of this
//! implementation is to equate aliases or simple constructs that may appear inlined inside an

View File

@ -869,12 +869,10 @@ fn walk<L: Linearizer>(
walk(state, ctxt.clone(), lin, linearizer.scope(), e)?;
walk(state, ctxt, lin, linearizer, t)
}
Term::Switch(exp, cases, default) => {
Term::Match {cases, default} => {
cases.values().chain(default.iter()).try_for_each(|case| {
walk(state, ctxt.clone(), lin, linearizer.scope(), case)
})?;
walk(state, ctxt, lin, linearizer, exp)
})
}
Term::RecRecord(record, dynamic, ..) => {
for (id, field) in record.fields.iter() {
@ -1182,10 +1180,14 @@ fn type_check_<L: Linearizer>(
type_check_(state, ctxt.clone(), lin, linearizer.scope(), e, arr)?;
type_check_(state, ctxt, lin, linearizer, t, src)
}
Term::Switch(exp, cases, default) => {
Term::Match { cases, default } => {
// Currently, if it has a default value, we typecheck the whole thing as
// taking ANY enum, since it's more permissive and there's no loss of information
let res = state.table.fresh_type_uvar();
// taking ANY enum, since it's more permissive and there's no loss of information.
// A match expression is a special kind of function. Thus it's typed as `a -> b`, where
// `a` is a enum type determined by the matched tags and `b` is the type of each match
// arm.
let return_type = state.table.fresh_type_uvar();
for case in cases.values() {
type_check_(
@ -1194,13 +1196,20 @@ fn type_check_<L: Linearizer>(
lin,
linearizer.scope(),
case,
res.clone(),
return_type.clone(),
)?;
}
let erows = match default {
Some(t) => {
type_check_(state, ctxt.clone(), lin, linearizer.scope(), t, res.clone())?;
type_check_(
state,
ctxt.clone(),
lin,
linearizer.scope(),
t,
return_type.clone(),
)?;
state.table.fresh_erows_uvar()
}
None => cases.iter().try_fold(
@ -1211,8 +1220,13 @@ fn type_check_<L: Linearizer>(
)?,
};
unify(state, &ctxt, ty, res).map_err(|err| err.into_typecheck_err(state, rt.pos))?;
type_check_(state, ctxt, lin, linearizer, exp, mk_uty_enum!(; erows))
// `arg_type` represents the type of arguments that the match expression can be applied
// to.
let arg_type = mk_uty_enum!(; erows);
// we unify the expected type of the match expression with `arg_type -> return_type`
unify(state, &ctxt, ty, mk_uty_arrow!(arg_type, return_type))
.map_err(|err| err.into_typecheck_err(state, rt.pos))
}
Term::Var(x) => {
let x_ty = ctxt

View File

@ -54,8 +54,8 @@ pub fn get_uop_type(
codomain.constrain_fresh_erows_var(state, row_var_id);
(domain, codomain)
}
// This should not happen, as Switch() is only produced during evaluation.
UnaryOp::Switch(_) => panic!("cannot typecheck Switch()"),
// This should not happen, as a match primop is only produced during evaluation.
UnaryOp::Match { .. } => panic!("cannot typecheck match primop"),
// Dyn -> Dyn
UnaryOp::ChangePolarity() | UnaryOp::GoDom() | UnaryOp::GoCodom() | UnaryOp::GoArray() => {
(mk_uniftype::dynamic(), mk_uniftype::dynamic())
@ -166,13 +166,13 @@ pub fn get_uop_type(
mk_uniftype::str(),
mk_uty_arrow!(mk_uniftype::str(), mk_uniftype::bool()),
),
// Str -> Str -> {match: Str, index: Num, groups: Array Str}
UnaryOp::StrMatch() => (
// Str -> Str -> {matched: Str, index: Num, groups: Array Str}
UnaryOp::StrFind() => (
mk_uniftype::str(),
mk_uty_arrow!(
mk_uniftype::str(),
mk_uty_record!(
("match", TypeF::Str),
("matched", TypeF::Str),
("index", TypeF::Num),
("groups", mk_uniftype::array(TypeF::Str))
)
@ -180,8 +180,8 @@ pub fn get_uop_type(
),
// Str -> Bool
UnaryOp::StrIsMatchCompiled(_) => (mk_uniftype::str(), mk_uniftype::bool()),
// Str -> {match: Str, index: Num, groups: Array Str}
UnaryOp::StrMatchCompiled(_) => (
// Str -> {matched: Str, index: Num, groups: Array Str}
UnaryOp::StrFindCompiled(_) => (
mk_uniftype::str(),
mk_uty_record!(
("match", TypeF::Str),
@ -398,7 +398,7 @@ pub fn get_nop_type(
],
mk_uniftype::dynamic(),
),
// This should not happen, as Switch() is only produced during evaluation.
// This should not happen, as MergeContract() is only produced during evaluation.
NAryOp::MergeContract() => panic!("cannot typecheck MergeContract()"),
})
}

View File

@ -703,7 +703,7 @@ impl EnumRows {
let case_body = if has_tail {
mk_term::var(value_arg)
}
// Otherwise, we build a switch with all the tags as cases, which just returns the
// Otherwise, we build a match with all the tags as cases, which just returns the
// original argument, and a default case that blames.
//
// For example, for an enum type [| `foo, `bar, `baz |], the `case` function looks
@ -711,7 +711,7 @@ impl EnumRows {
//
// ```
// fun l x =>
// switch {
// match {
// `foo => x,
// `bar => x,
// `baz => x,
@ -719,11 +719,13 @@ impl EnumRows {
// } x
// ```
else {
RichTerm::from(Term::Switch(
mk_term::var(value_arg),
cases,
Some(mk_app!(contract::enum_fail(), mk_term::var(label_arg))),
))
mk_app!(
Term::Match {
cases,
default: Some(mk_app!(contract::enum_fail(), mk_term::var(label_arg))),
},
mk_term::var(value_arg)
)
};
let case = mk_fun!(label_arg, value_arg, case_body);

View File

@ -349,24 +349,24 @@
"%
= fun regex => %str_is_match% regex,
match : Str -> Str -> {match: Str, index: Num, groups: Array Str}
find : Str -> Str -> {matched: Str, index: Num, groups: Array Str}
| doc m%"
`match regex str` matches `str` given `regex`. Results in the part of `str` that matched, the index of the
`find regex str` matches `str` given `regex`. Results in the part of `str` that matched, the index of the
first character that was part of the match in `str`, and a arrays of all capture groups if any.
For example:
```nickel
match "^(\\d).*(\\d).*(\\d).*$" "5 apples, 6 pears and 0 grapes" =>
{ match = "5 apples, 6 pears and 0 grapes", index = 0, groups = [ "5", "6", "0" ] }
match "3" "01234" =>
{ match = "3", index = 3, groups = [ ] }
find "^(\\d).*(\\d).*(\\d).*$" "5 apples, 6 pears and 0 grapes" =>
{ matched = "5 apples, 6 pears and 0 grapes", index = 0, groups = [ "5", "6", "0" ] }
find "3" "01234" =>
{ matched = "3", index = 3, groups = [ ] }
```
Note that this function may perform better by sharing its partial application between multiple calls,
because in this case the underlying regular expression will only be compiled once (See the documentation
of `string.is_match` for more details).
"%
= fun regex => %str_match% regex,
= fun regex => %str_find% regex,
length : Str -> Num
| doc m%"

View File

@ -277,12 +277,11 @@ fn records_contracts_closed() {
assert_raise_blame!("let Contract = {a | Num} & {b | Num} in ({a=1, b=2, c=3} | Contract)");
}
// #[test]
// fn enum_complex() {
// eval(
// "let f : <foo, bar> -> Num =
// fun x => switch { `foo => 1, `bar => 2, } x in
// f `boo",
// )
// .unwrap_err();
// }
#[test]
fn enum_complex() {
eval(
"let f : [| `foo, `bar |] -> Num = match { `foo => 1, `bar => 2, } in
f `boo",
)
.unwrap_err();
}

View File

@ -21,7 +21,5 @@ let {check, Assert, ..} = import "testlib.ncl" in
# others_precedence
((fun x => x | Assert) true),
(let AssertOk = fun l t => if t == `Ok then t else %blame% l in
switch {`Ok => true, `Err => false} `Ok | AssertOk),
]
|> check

View File

@ -31,7 +31,7 @@ let {check, ..} = import "testlib.ncl" in
1/4 + 1/4 - 1/4 + 1/4 >= 1/2 == true,
1/4 + 1/4 - 1/4 + 1/4 < 1/2 == false,
# This test checks that the terms of a switch are closured
let x = 3 in (switch { `foo => 1, _ => x} (3 + 2)) == 3,
# This test checks that the terms of a match are closured
let x = 3 in ((3 + 2) |> match { `foo => 1, _ => x}) == 3,
]
|> check

View File

@ -37,19 +37,19 @@ let {check, Assert, ..} = import "testlib.ncl" in
# enums_complex
let f : forall r. [| `foo, `bar ; r |] -> Num =
fun x => switch { `foo => 1, `bar => 2, _ => 3, } x in
match { `foo => 1, `bar => 2, _ => 3, } in
f `bar == 2,
let f : forall r. [| `foo, `bar ; r |] -> Num =
fun x => switch { `foo => 1, `bar => 2, _ => 3, } x in
match { `foo => 1, `bar => 2, _ => 3, } in
f `boo == 3,
let f : forall r. [| `foo, `"bar:baz" ; r |] -> Num =
fun x => switch { `foo => 1, `"bar:baz" => 2, _ => 3, } x in
fun x => match { `foo => 1, `"bar:baz" => 2, _ => 3, } x in
f `"bar:baz" == 2,
let f : forall r. [| `foo, `"bar:baz" ; r |] -> Num =
fun x => switch { `foo => 1, `"bar:baz" => 2, _ => 3, } x in
fun x => match { `foo => 1, `"bar:baz" => 2, _ => 3, } x in
f `"boo,grr" == 3,
# enums_applied

View File

@ -21,9 +21,9 @@ let assertDeserInv = fun x =>
assertSerInv {val = ["a", 3, []]},
assertSerInv {a.foo.bar = "2", b = false, c = [{d = "e"}, {d = "f"}]},
# regression test for a previously missing `#[serde(skip)]` on the `Switch`
# regression test for a previously missing `#[serde(skip)]` on the `Match`
# variant of `term::Term`. That was causing a list with the right size to be
# serialized as a `Switch` instead of an array. This test checks that lists of
# serialized as a `Match` instead of an array. This test checks that lists of
# various sizes don't get misinterpreted again as a term construct that misses a
# `#[serde(skip)]` annotations
{

View File

@ -54,25 +54,25 @@ let typecheck = [
(`blo : [|`bla, `blo |]),
(`bla : forall r. [|`bla ; r |]),
(`bla : forall r. [|`bla, `blo ; r |]),
((switch {`bla => 3} `bla) : Num),
((switch {`bla => 3, _ => 2} `blo) : Num),
((`bla |> match {`bla => 3}) : Num),
((`blo |> match {`bla => 3, _ => 2}) : Num),
# enums_complex
((fun x => switch {`bla => 1, `ble => 2} x) : [|`bla, `ble |] -> Num),
((fun x => switch {`bla => 1, `ble => 2, `bli => 4} (%embed% bli x))
((fun x => x |> match {`bla => 1, `ble => 2}) : [|`bla, `ble |] -> Num),
((fun x => %embed% bli x |> match {`bla => 1, `ble => 2, `bli => 4})
: [|`bla, `ble |] -> Num),
((fun x =>
(switch {`bla => 3, `bli => 2} x)
+ (switch {`bli => 6, `bla => 20} x))
(x |> match {`bla => 3, `bli => 2})
+ (x |> match {`bli => 6, `bla => 20}))
`bla
: Num),
let f : forall r. [|`blo, `ble ; r |] -> Num = fun x =>
switch {`blo => 1, `ble => 2, _ => 3} x in
(f `bli : Num),
let f : forall r. [|`blo, `ble ; r |] -> Num =
match {`blo => 1, `ble => 2, _ => 3} in
(f `bli : Num),
let f : forall r. (forall p. [|`blo, `ble ; r |] -> [|`bla, `bli ; p |]) =
fun x => switch {`blo => `bla, `ble => `bli, _ => `bla} x in
match {`blo => `bla, `ble => `bli, _ => `bla} in
f `bli,
# recursive let bindings

View File

@ -79,30 +79,28 @@ fn simple_forall() {
#[test]
fn enum_simple() {
assert_typecheck_fails!("`foo : [| `bar |]");
assert_typecheck_fails!("switch { `foo => 3} `bar : Num");
assert_typecheck_fails!("switch { `foo => 3, `bar => true} `bar : Num");
assert_typecheck_fails!("match { `foo => 3} `bar : Num");
assert_typecheck_fails!("match { `foo => 3, `bar => true} `bar : Num");
}
#[test]
fn enum_complex() {
assert_typecheck_fails!(
"(fun x => switch {`bla => 1, `ble => 2, `bli => 4} x) : [| `bla, `ble |] -> Num"
);
assert_typecheck_fails!("(match {`bla => 1, `ble => 2, `bli => 4}) : [| `bla, `ble |] -> Num");
// TODO typecheck this, I'm not sure how to do it with row variables
// LATER NOTE: this requires row subtyping, not easy
assert_typecheck_fails!(
"(fun x =>
(switch {`bla => 3, `bli => 2} x) +
(switch {`bla => 6, `blo => 20} x)) `bla : Num"
(x |> match {`bla => 3, `bli => 2}) +
(x |> match {`bla => 6, `blo => 20})) `bla : Num"
);
assert_typecheck_fails!(
"let f : forall r. [| `blo, `ble ; r |] -> Num =
fun x => (switch {`blo => 1, `ble => 2, `bli => 3} x) in
match {`blo => 1, `ble => 2, `bli => 3} in
f"
);
assert_typecheck_fails!(
"let f : forall r. (forall p. [| `blo, `ble ; r |] -> [| `bla, `bli ; p |]) =
fun x => (switch {`blo => `bla, `ble => `bli, _ => `blo} x) in
match {`blo => `bla, `ble => `bli, _ => `blo} in
f `bli"
);
}