Skip to content

Commit be7d00a

Browse files
committed
feat: Add macro for deriving lenses for record fields
1 parent 4f41a7f commit be7d00a

File tree

2 files changed

+185
-0
lines changed

2 files changed

+185
-0
lines changed

src/lens.rs

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
//! Implementation of the `lens!` macro.
2+
3+
use gluon_codegen::Trace;
4+
5+
use crate::{
6+
base::{
7+
ast::{self, AstClone, SpannedExpr},
8+
pos,
9+
symbol::{Symbol, Symbols},
10+
types,
11+
},
12+
vm::macros::{self, Macro, MacroExpander, MacroFuture},
13+
};
14+
15+
/// Macro for deriving field accessor lenses
16+
///
17+
/// ```ignore
18+
/// let lens = import! std.lens
19+
///
20+
/// type MyRecord = {
21+
/// x: Int,
22+
/// y: String
23+
/// }
24+
///
25+
/// let _x = lens! lens MyRecord x
26+
/// let _y = lens! lens MyRecord y
27+
/// ```
28+
#[derive(Trace)]
29+
#[gluon(crate_name = "vm")]
30+
pub struct DeriveLens;
31+
32+
impl Macro for DeriveLens {
33+
fn expand<'r, 'a: 'r, 'b: 'r, 'c: 'r, 'ast: 'r>(
34+
&self,
35+
_env: &'b mut MacroExpander<'a>,
36+
_symbols: &'c mut Symbols,
37+
arena: &'b mut ast::OwnedArena<'ast, Symbol>,
38+
args: &'b mut [SpannedExpr<'ast, Symbol>],
39+
) -> MacroFuture<'r, 'ast> {
40+
Box::pin(async move {
41+
let (module_arg, typ_arg, field_arg) = match args {
42+
[module, typ, field] => (module, typ, field),
43+
_ => return Err(macros::Error::message(format!("lens! expects 3 arguments"))),
44+
};
45+
46+
let mut symbols = Symbols::new();
47+
48+
let typ = match &typ_arg.value {
49+
ast::Expr::Ident(id) => id.clone(),
50+
_ => {
51+
return Err(macros::Error::message(format!(
52+
"lens! expects an identifier as the second argument"
53+
)))
54+
}
55+
};
56+
57+
let field_ident = match &field_arg.value {
58+
ast::Expr::Ident(id) => id.clone(),
59+
_ => {
60+
return Err(macros::Error::message(format!(
61+
"lens! expects an identifier as the third argument"
62+
)))
63+
}
64+
};
65+
66+
let struct_var = ast::TypedIdent::new(symbols.simple_symbol("s"));
67+
let get_var = ast::TypedIdent::new(symbols.simple_symbol("get"));
68+
let set_var = ast::TypedIdent::new(symbols.simple_symbol("set"));
69+
70+
let span = field_arg.span;
71+
72+
let struct_ast_type = || {
73+
ast::AstType::new_no_loc(
74+
arena.borrow(),
75+
types::Type::Ident(ast::TypedIdent {
76+
name: typ.name.clone(),
77+
typ: Default::default(),
78+
}),
79+
)
80+
};
81+
82+
let hole_type = || ast::AstType::new_no_loc(arena.borrow(), types::Type::Hole);
83+
84+
let func_type = |a, b| {
85+
ast::AstType::new_no_loc(
86+
arena.borrow(),
87+
types::Type::Function(types::ArgType::Explicit, a, b),
88+
)
89+
};
90+
91+
let get_binding = arena.alloc(ast::ValueBinding {
92+
metadata: Default::default(),
93+
name: pos::spanned(span, ast::Pattern::Ident(get_var.clone())),
94+
typ: Some(func_type(struct_ast_type(), hole_type())),
95+
resolved_type: Default::default(),
96+
args: arena.alloc_extend(vec![ast::Argument {
97+
arg_type: types::ArgType::Explicit,
98+
name: pos::spanned(span, struct_var.clone()),
99+
}]),
100+
expr: pos::spanned(
101+
span,
102+
ast::Expr::Projection(
103+
arena.alloc(pos::spanned(span, ast::Expr::Ident(struct_var.clone()))),
104+
field_ident.name.clone(),
105+
field_ident.typ.clone(),
106+
),
107+
),
108+
});
109+
110+
let set_binding = arena.alloc(ast::ValueBinding {
111+
metadata: Default::default(),
112+
name: pos::spanned(span, ast::Pattern::Ident(set_var.clone())),
113+
typ: Some(func_type(
114+
hole_type(),
115+
func_type(struct_ast_type(), struct_ast_type()),
116+
)),
117+
resolved_type: Default::default(),
118+
args: arena.alloc_extend(vec![
119+
ast::Argument {
120+
arg_type: types::ArgType::Explicit,
121+
name: pos::spanned(span, field_ident.clone()),
122+
},
123+
ast::Argument {
124+
arg_type: types::ArgType::Explicit,
125+
name: pos::spanned(span, struct_var.clone()),
126+
},
127+
]),
128+
expr: pos::spanned(
129+
span,
130+
ast::Expr::Record {
131+
typ: Default::default(),
132+
types: &mut [],
133+
exprs: arena.alloc_extend(vec![ast::ExprField {
134+
metadata: Default::default(),
135+
name: pos::spanned(span, field_ident.name.clone()),
136+
value: None,
137+
}]),
138+
base: Some(
139+
arena.alloc(pos::spanned(span, ast::Expr::Ident(struct_var.clone()))),
140+
),
141+
},
142+
),
143+
});
144+
145+
let lens_module = arena.alloc((*module_arg).ast_clone(arena.borrow()));
146+
147+
let make_func = arena.alloc(pos::spanned(
148+
span,
149+
ast::Expr::Projection(
150+
lens_module,
151+
symbols.simple_symbol("make"),
152+
Default::default(),
153+
),
154+
));
155+
156+
let make_expr = arena.alloc(pos::spanned(
157+
span,
158+
ast::Expr::App {
159+
func: make_func,
160+
implicit_args: &mut [],
161+
args: arena.alloc_extend(vec![
162+
pos::spanned(span, ast::Expr::Ident(get_var)),
163+
pos::spanned(span, ast::Expr::Ident(set_var)),
164+
]),
165+
},
166+
));
167+
168+
let result = pos::spanned(
169+
span,
170+
ast::Expr::LetBindings(
171+
ast::ValueBindings::Plain(set_binding),
172+
arena.alloc(pos::spanned(
173+
span,
174+
ast::Expr::LetBindings(ast::ValueBindings::Plain(get_binding), make_expr),
175+
)),
176+
),
177+
);
178+
179+
Ok(result.into())
180+
})
181+
}
182+
}

src/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ pub mod compiler_pipeline;
4949
#[macro_use]
5050
pub mod import;
5151
pub mod lift_io;
52+
pub mod lens;
5253
#[doc(hidden)]
5354
pub mod query;
5455
pub mod std_lib;
@@ -958,6 +959,8 @@ impl VmBuilder {
958959
}
959960

960961
macros.insert(String::from("lift_io"), lift_io::LiftIo);
962+
963+
macros.insert(String::from("lens"), lens::DeriveLens);
961964
}
962965

963966
add_extern_module_with_deps(

0 commit comments

Comments
 (0)