Skip to content

Commit 95967eb

Browse files
yannhamjneem
andauthored
Add from/to_tag_and_arg (#1939)
* Add from/to_tag_and_arg This commit adds two functions to convert enums to and back from records, as a tag and an optional argument. Such functions are useful to handle enums in a general, dynamic way, while pattern matching requires to know in advance the possible tags. Additionally, we also implement a `map` function, which can be derived from the conversions above. * Update core/stdlib/std.ncl Co-authored-by: jneem <[email protected]> * Update core/stdlib/std.ncl Co-authored-by: jneem <[email protected]> * Update core/stdlib/std.ncl Co-authored-by: jneem <[email protected]> --------- Co-authored-by: jneem <[email protected]>
1 parent 9fe8b98 commit 95967eb

File tree

9 files changed

+186
-14
lines changed

9 files changed

+186
-14
lines changed

core/src/eval/operation.rs

+22-1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
//! On the other hand, the functions `process_unary_operation` and `process_binary_operation`
88
//! receive evaluated operands and implement the actual semantics of operators.
99
use super::{
10+
cache::lazy::Thunk,
1011
merge::{self, MergeMode},
1112
stack::StrAccData,
1213
subst, Cache, Closure, Environment, ImportResolver, VirtualMachine,
@@ -1216,7 +1217,7 @@ impl<R: ImportResolver, C: Cache> VirtualMachine<R, C> {
12161217
))
12171218
}
12181219
}
1219-
UnaryOp::EnumUnwrapVariant => {
1220+
UnaryOp::EnumGetArg => {
12201221
if let Term::EnumVariant { arg, .. } = &*t {
12211222
Ok(Closure {
12221223
body: arg.clone(),
@@ -1226,6 +1227,26 @@ impl<R: ImportResolver, C: Cache> VirtualMachine<R, C> {
12261227
Err(mk_type_error!("enum_unwrap_variant", "Enum variant"))
12271228
}
12281229
}
1230+
UnaryOp::EnumMakeVariant => {
1231+
let Term::Str(tag) = &*t else {
1232+
return Err(mk_type_error!("enum/make_variant", "String"));
1233+
};
1234+
1235+
let (arg_clos, _) = self.stack.pop_arg(&self.cache).ok_or_else(|| {
1236+
EvalError::NotEnoughArgs(2, String::from("enum/make_variant"), pos)
1237+
})?;
1238+
let arg_pos = arg_clos.body.pos;
1239+
let arg = RichTerm::new(Term::Closure(Thunk::new(arg_clos)), arg_pos);
1240+
1241+
Ok(Closure::atomic_closure(RichTerm::new(
1242+
Term::EnumVariant {
1243+
tag: LocIdent::new(tag).with_pos(pos),
1244+
arg,
1245+
attrs: EnumVariantAttrs { closurized: true },
1246+
},
1247+
pos_op_inh,
1248+
)))
1249+
}
12291250
UnaryOp::EnumGetTag => match &*t {
12301251
Term::EnumVariant { tag, .. } | Term::Enum(tag) => Ok(Closure::atomic_closure(
12311252
RichTerm::new(Term::Enum(*tag), pos_op_inh),

core/src/parser/grammar.lalrpop

+4-2
Original file line numberDiff line numberDiff line change
@@ -1123,7 +1123,8 @@ UOp: UnaryOp = {
11231123
})
11241124
}
11251125
},
1126-
"enum/unwrap_variant" => UnaryOp::EnumUnwrapVariant,
1126+
"enum/get_arg" => UnaryOp::EnumGetArg,
1127+
"enum/make_variant" => UnaryOp::EnumMakeVariant,
11271128
"enum/is_variant" => UnaryOp::EnumIsVariant,
11281129
"enum/get_tag" => UnaryOp::EnumGetTag,
11291130
}
@@ -1585,7 +1586,8 @@ extern {
15851586
"label/push_diag" => Token::Normal(NormalToken::LabelPushDiag),
15861587
"array/slice" => Token::Normal(NormalToken::ArraySlice),
15871588
"eval_nix" => Token::Normal(NormalToken::EvalNix),
1588-
"enum/unwrap_variant" => Token::Normal(NormalToken::EnumUnwrapVariant),
1589+
"enum/get_arg" => Token::Normal(NormalToken::EnumGetArg),
1590+
"enum/make_variant" => Token::Normal(NormalToken::EnumMakeVariant),
15891591
"enum/is_variant" => Token::Normal(NormalToken::EnumIsVariant),
15901592
"enum/get_tag" => Token::Normal(NormalToken::EnumGetTag),
15911593
"pattern_branch" => Token::Normal(NormalToken::PatternBranch),

core/src/parser/lexer.rs

+4-2
Original file line numberDiff line numberDiff line change
@@ -335,8 +335,10 @@ pub enum NormalToken<'input> {
335335
NumberFromString,
336336
#[token("%enum/from_string%")]
337337
EnumFromString,
338-
#[token("%enum/unwrap_variant%")]
339-
EnumUnwrapVariant,
338+
#[token("%enum/get_arg%")]
339+
EnumGetArg,
340+
#[token("%enum/make_variant%")]
341+
EnumMakeVariant,
340342
#[token("%enum/is_variant%")]
341343
EnumIsVariant,
342344
#[token("%enum/get_tag%")]

core/src/term/mod.rs

+7-3
Original file line numberDiff line numberDiff line change
@@ -1415,8 +1415,11 @@ pub enum UnaryOp {
14151415
#[cfg(feature = "nix-experimental")]
14161416
EvalNix,
14171417

1418-
/// Unwrap the variant from an enum: `%unwrap_enum_variant% ('Foo t) := t`
1419-
EnumUnwrapVariant,
1418+
/// Retrive the argument from an enum variant: `%enum/get_arg% ('Foo t) := t`
1419+
EnumGetArg,
1420+
/// Create an enum variant from a tag and an argument. This operator is strict in tag and
1421+
/// return a function that can be further applied to an argument.
1422+
EnumMakeVariant,
14201423
/// Return true if the given parameter is an enum variant.
14211424
EnumIsVariant,
14221425
/// Extract the tag from an enum tag or an enum variant.
@@ -1490,7 +1493,8 @@ impl fmt::Display for UnaryOp {
14901493
#[cfg(feature = "nix-experimental")]
14911494
EvalNix => write!(f, "eval_nix"),
14921495

1493-
EnumUnwrapVariant => write!(f, "enum/unwrap_variant"),
1496+
EnumGetArg => write!(f, "enum/get_arg"),
1497+
EnumMakeVariant => write!(f, "enum/make_variant"),
14941498
EnumIsVariant => write!(f, "enum/is_variant"),
14951499
EnumGetTag => write!(f, "enum/get_tag"),
14961500

core/src/term/pattern/compile.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -813,7 +813,7 @@ impl CompilePart for EnumPattern {
813813
if_condition,
814814
make::let_in(
815815
value_id,
816-
make::op1(UnaryOp::EnumUnwrapVariant, Term::Var(value_id)),
816+
make::op1(UnaryOp::EnumGetArg, Term::Var(value_id)),
817817
pat.compile_part(value_id, bindings_id),
818818
),
819819
Term::Null,

core/src/typecheck/operation.rs

+9-2
Original file line numberDiff line numberDiff line change
@@ -243,8 +243,15 @@ pub fn get_uop_type(
243243
// primop.
244244
// This isn't a problem, as this operator is mostly internal and pattern matching should be
245245
// used to destructure enum variants.
246-
UnaryOp::EnumUnwrapVariant => (mk_uniftype::dynamic(), mk_uniftype::dynamic()),
247-
// Same as `EnumUnwrapVariant` just above.
246+
// Dyn -> Dyn
247+
UnaryOp::EnumGetArg => (mk_uniftype::dynamic(), mk_uniftype::dynamic()),
248+
// String -> (Dyn -> Dyn)
249+
UnaryOp::EnumMakeVariant => (
250+
mk_uniftype::str(),
251+
mk_uniftype::arrow(mk_uniftype::dynamic(), mk_uniftype::dynamic()),
252+
),
253+
// Same as `EnumGetArg` just above.
254+
// Dyn -> Dyn
248255
UnaryOp::EnumGetTag => (mk_uniftype::dynamic(), mk_uniftype::dynamic()),
249256
// Note that is_variant breaks parametricity, so it can't get a polymorphic type.
250257
// Dyn -> Bool

core/stdlib/std.ncl

+94-2
Original file line numberDiff line numberDiff line change
@@ -1566,15 +1566,31 @@
15661566

15671567
```nickel
15681568
('foo | std.enum.Tag) =>
1569-
`foo
1569+
'foo
15701570
('FooBar | std.enum.Tag) =>
1571-
`FooBar
1571+
'FooBar
15721572
("tag" | std.enum.Tag) =>
15731573
error
15741574
```
15751575
"%
15761576
= std.contract.from_predicate is_enum_tag,
15771577

1578+
Enum
1579+
| doc m%"
1580+
Enforces that the value is an enum (either a tag or a variant).
1581+
1582+
# Examples
1583+
1584+
```nickel
1585+
('Foo | std.enum.Enum) =>
1586+
'Foo
1587+
('Bar 5 | std.enum.Enum) =>
1588+
'Bar 5
1589+
("tag" | std.enum.Enum) =>
1590+
error
1591+
"%
1592+
= std.contract.from_predicate std.is_enum,
1593+
15781594
TagOrString
15791595
| doc m%%"
15801596
Accepts both enum tags and strings. Strings are automatically
@@ -1670,6 +1686,82 @@
16701686
```
16711687
"%
16721688
= fun value => %enum/is_variant% value,
1689+
1690+
to_tag_and_arg
1691+
| Enum -> { tag | String, arg | optional }
1692+
| doc m%"
1693+
Convert an enum to record with a string tag and an optional argument. If
1694+
the enum is an enum tag, the `arg` field is simply omitted.
1695+
1696+
`std.enum.from_tag_and_arg` provides the inverse transformation,
1697+
reconstructing an enum from a string tag and an argument.
1698+
1699+
# Examples
1700+
1701+
```nickel
1702+
std.enum.to_tag_and_arg ('Foo "arg") =>
1703+
{ tag = "Foo", arg = "arg" }
1704+
std.enum.to_tag_and_arg 'http
1705+
=> { tag = "http" }
1706+
```
1707+
"%
1708+
= fun enum_value =>
1709+
let tag_string = %to_string% (%enum/get_tag% enum_value) in
1710+
if %enum/is_variant% enum_value then
1711+
{
1712+
tag = tag_string,
1713+
arg = %enum/get_arg% enum_value,
1714+
}
1715+
else
1716+
{ tag = tag_string },
1717+
1718+
from_tag_and_arg
1719+
| { tag | String, arg | optional } -> Enum
1720+
| doc m%"
1721+
Create an enum from a string tag and an optional argument. If the `arg`
1722+
field is omitted, a bare enum tag is created.
1723+
1724+
`std.enum.to_tag_and_value` provides the inverse transformation,
1725+
extracting a string tag and an argument from an enum.
1726+
1727+
# Examples
1728+
1729+
```nickel
1730+
std.enum.from_tag_and_arg { tag = "Foo", arg = "arg" }
1731+
=> ('Foo "arg")
1732+
std.enum.from_tag_and_arg { tag = "http" }
1733+
=> 'http
1734+
```
1735+
"%
1736+
= fun enum_data =>
1737+
if %record/has_field% "arg" enum_data then
1738+
%enum/make_variant% enum_data.tag enum_data.arg
1739+
else
1740+
%enum/from_string% enum_data.tag,
1741+
1742+
map
1743+
| (Dyn -> Dyn) -> Enum -> Enum
1744+
| doc m%"
1745+
Maps a function over an enum variant's argument. If the enum doesn't
1746+
have an argument, it is left unchanged.
1747+
1748+
# Examples
1749+
1750+
```nickel
1751+
std.enum.map ((+) 1) ('Foo 42)
1752+
=> 'Foo 43
1753+
std.enum.map f 'Bar
1754+
=> 'Bar
1755+
```
1756+
"%
1757+
= fun f enum_value =>
1758+
if %enum/is_variant% enum_value then
1759+
let tag = (%to_string% (%enum/get_tag% enum_value)) in
1760+
let mapped = f (%enum/get_arg% enum_value) in
1761+
1762+
%enum/make_variant% tag mapped
1763+
else
1764+
enum_value,
16731765
},
16741766

16751767
function = {

core/tests/integration/inputs/adts/enum_primops.ncl

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33

44
[
5-
%enum/unwrap_variant% ('Left (1+1)) == 2,
5+
%enum/get_arg% ('Left (1+1)) == 2,
66
!(%enum/is_variant% 'Right),
77
%enum/is_variant% ('Right 1),
88
%enum/get_tag% 'Right == 'Right,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# test.type = 'pass'
2+
let enum = std.enum in
3+
4+
[
5+
enum.is_enum_tag 'A,
6+
!(enum.is_enum_tag ('A 'arg)),
7+
enum.is_enum_variant ('A 'arg),
8+
!enum.is_enum_variant 'A,
9+
10+
let enum_round_trip = fun enum_value =>
11+
enum_value
12+
|> enum.to_tag_and_arg
13+
|> enum.from_tag_and_arg
14+
|> (==) enum_value
15+
in
16+
[
17+
enum_round_trip 'Foo,
18+
enum_round_trip ('Foo 'arg),
19+
enum_round_trip ('Foo { value = "hello" }),
20+
]
21+
|> std.test.assert_all,
22+
23+
let record_round_trip = fun data =>
24+
data
25+
|> enum.from_tag_and_arg
26+
|> enum.to_tag_and_arg
27+
|> (==) data
28+
in
29+
[
30+
record_round_trip { tag = "Foo" },
31+
record_round_trip { tag = "Foo", arg = "arg" },
32+
record_round_trip { tag = "Foo", arg = { value = "hello" } },
33+
]
34+
|> std.test.assert_all,
35+
36+
'Foo
37+
|> std.enum.map (fun _ => null)
38+
|> (==) 'Foo,
39+
40+
'Foo 2
41+
|> std.enum.map ((*) 2)
42+
|> (==) ('Foo 4),
43+
]
44+
|> std.test.assert_all

0 commit comments

Comments
 (0)