Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Let blocks #2010

Merged
merged 19 commits into from
Sep 10, 2024
Merged
9 changes: 5 additions & 4 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ sha-1 = "0.10.0"
sha2 = "0.10.6"
similar = "2.2.1"
simple-counter = "0.1.0"
smallvec = "1.13.2"
strip-ansi-escapes = "0.2.0"
termimad = "0.23.1"
test-generator = "0.3.1"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# capture = 'stderr'
# command = ['eval']
let
{ x, y } = { x = 1, y = 2 },
x = 3
in
x
7 changes: 7 additions & 0 deletions cli/tests/snapshot/inputs/errors/let_block_not_rec.ncl
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# capture = 'stderr'
# command = ['eval']
let
a = 2,
b = a
in
b
7 changes: 7 additions & 0 deletions cli/tests/snapshot/inputs/errors/let_rec_block_with_pat.ncl
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# capture = 'stderr'
# command = ['eval']
let rec
a = 2,
{ b } = { b = 1 }
in
b
8 changes: 8 additions & 0 deletions cli/tests/snapshot/inputs/eval/let_block_rec.ncl
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# command = ['eval']
# capture = 'stdout'
let rec
b = a,
a = 2,
c = b
in
c + b
jneem marked this conversation as resolved.
Show resolved Hide resolved
10 changes: 10 additions & 0 deletions cli/tests/snapshot/inputs/pretty/let_block.ncl
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# capture = 'stdout'
# command = ['pprint-ast']
let rec
fib = fun n =>
if n == 0 || n == 1 then
1
else
fib (n - 1) + fib (n - 2),
fib2 = fun n => fib (fib n)
in fib2 3
6 changes: 6 additions & 0 deletions cli/tests/snapshot/inputs/pretty/let_block_with_pat.ncl
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# capture = 'stdout'
# command = ['pprint-ast']
let
a = 1,
d@{ b, c } = { b = "b", c = 2 },
in a + d.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
source: cli/tests/snapshot/main.rs
expression: err
---
error: duplicated binding `x` in let block
┌─ [INPUTS_PATH]/errors/let_block_duplicate_identifier.ncl:5:3
4 │ { x, y } = { x = 1, y = 2 },
│ - previous binding here
5 │ x = 3
│ ^ duplicated binding here
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
source: cli/tests/snapshot/main.rs
expression: err
---
error: unbound identifier `a`
┌─ [INPUTS_PATH]/errors/let_block_not_rec.ncl:5:7
5 │ b = a
│ ^ this identifier is unbound
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
source: cli/tests/snapshot/main.rs
expression: err
---
error: recursive destructuring is not supported
┌─ [INPUTS_PATH]/errors/let_rec_block_with_pat.ncl:3:5
3 │ let rec
│ ^^^
= A destructuring let-binding can't be recursive. Try removing the `rec` from `let rec`.
= You can reference other fields of a record recursively from within a field, so you might not need the recursive let.
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
source: cli/tests/snapshot/main.rs
expression: out
---
4
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
source: cli/tests/snapshot/main.rs
expression: out
---
let rec fib
= fun n => if (n == 0) || (n == 1) then 1 else (fib (n - 1)) + (fib (n - 2)),
fib2
= fun n => fib (fib n)
in
fib2 3
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
source: cli/tests/snapshot/main.rs
expression: out
---
let a = 1, d @ { b, c, } = { b = "b", c = 2, } in a + d.c
1 change: 1 addition & 0 deletions core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ codespan.workspace = true
codespan-reporting.workspace = true
cxx = { workspace = true, optional = true }
logos.workspace = true
smallvec.workspace = true
serde = { workspace = true, features = ["derive"] }
serde_json.workspace = true
serde_yaml.workspace = true
Expand Down
26 changes: 26 additions & 0 deletions core/src/error/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -520,6 +520,8 @@ pub enum ParseError {
/// A recursive let pattern was encountered. They are not currently supported because we
/// decided it was too involved to implement them.
RecursiveLetPattern(RawSpan),
/// Let blocks can currently only contain plain bindings, not pattern bindings.
PatternInLetBlock(RawSpan),
/// A type variable is used in ways that imply it has muiltiple different kinds.
///
/// This can happen in several situations, for example:
Expand Down Expand Up @@ -561,6 +563,13 @@ pub enum ParseError {
/// The previous instance of the duplicated identifier.
prev_ident: LocIdent,
},
/// A duplicate binding was encountered in a let block.
DuplicateIdentInLetBlock {
/// The duplicate identifier.
ident: LocIdent,
/// The previous instance of the duplicated identifier.
prev_ident: LocIdent,
},
/// There was an attempt to use a feature that hasn't been enabled.
DisabledFeature { feature: String, span: RawSpan },
/// A term was used as a contract in type position, but this term has no chance to make any
Expand Down Expand Up @@ -773,6 +782,7 @@ impl ParseError {
InternalParseError::RecursiveLetPattern(pos) => {
ParseError::RecursiveLetPattern(pos)
}
InternalParseError::PatternInLetBlock(pos) => ParseError::PatternInLetBlock(pos),
InternalParseError::TypeVariableKindMismatch { ty_var, span } => {
ParseError::TypeVariableKindMismatch { ty_var, span }
}
Expand All @@ -786,6 +796,9 @@ impl ParseError {
InternalParseError::DuplicateIdentInRecordPattern { ident, prev_ident } => {
ParseError::DuplicateIdentInRecordPattern { ident, prev_ident }
}
InternalParseError::DuplicateIdentInLetBlock { ident, prev_ident } => {
ParseError::DuplicateIdentInLetBlock { ident, prev_ident }
}
InternalParseError::DisabledFeature { feature, span } => {
ParseError::DisabledFeature { feature, span }
}
Expand Down Expand Up @@ -1993,6 +2006,10 @@ impl IntoDiagnostics<FileId> for ParseError {
from within a field, so you might not need the recursive let."
.into(),
]),
ParseError::PatternInLetBlock(span) => Diagnostic::error()
.with_message("destructuring patterns are not currently permitted in let blocks")
.with_labels(vec![primary(&span)])
.with_notes(vec!["Try re-writing your let block as nested `let ... in` expressions.".into()]),
ParseError::TypeVariableKindMismatch { ty_var, span } => Diagnostic::error()
.with_message(format!(
"the type variable `{ty_var}` is used in conflicting ways"
Expand Down Expand Up @@ -2049,6 +2066,15 @@ impl IntoDiagnostics<FileId> for ParseError {
secondary(&prev_ident.pos.unwrap()).with_message("previous binding here"),
primary(&ident.pos.unwrap()).with_message("duplicated binding here"),
]),
ParseError::DuplicateIdentInLetBlock { ident, prev_ident } => Diagnostic::error()
.with_message(format!(
"duplicated binding `{}` in let block",
ident.label()
))
.with_labels(vec![
secondary(&prev_ident.pos.unwrap()).with_message("previous binding here"),
primary(&ident.pos.unwrap()).with_message("duplicated binding here"),
]),
ParseError::DisabledFeature { feature, span } => Diagnostic::error()
.with_message("interpreter compiled without required features")
.with_labels(vec![primary(&span).with_message(format!(
Expand Down
39 changes: 24 additions & 15 deletions core/src/eval/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -569,21 +569,30 @@ impl<R: ImportResolver, C: Cache> VirtualMachine<R, C> {
);
Closure { body: t1, env }
}
Term::Let(x, bound, body, LetAttrs { binding_type, rec }) => {
let bound_closure: Closure = Closure {
body: bound,
env: env.clone(),
};
Term::Let(bindings, body, LetAttrs { binding_type, rec }) => {
let mut indices = Vec::new();
let init_env = env.clone();

for (x, bound) in bindings {
let bound_closure: Closure = Closure {
body: bound,
env: init_env.clone(),
};

let idx = self.cache.add(bound_closure, binding_type);
let idx = self.cache.add(bound_closure, binding_type.clone());

// Patch the environment with the (x <- closure) binding
if rec {
indices.push(idx.clone());
}

// Patch the environment with the (x <- closure) binding
if rec {
self.cache
.patch(idx.clone(), |cl| cl.env.insert(x.ident(), idx.clone()));
env.insert(x.ident(), idx);
}

env.insert(x.ident(), idx);
let names = env.iter_elems().map(|(k, _v)| k).collect::<Vec<_>>();
for idx in indices {
self.cache.patch(idx, |cl| cl.env = env.clone());
}

Closure { body, env }
}
Expand Down Expand Up @@ -1171,11 +1180,11 @@ pub fn subst<C: Cache>(

RichTerm::new(Term::EnumVariant { tag, arg, attrs }, pos)
}
Term::Let(id, t1, t2, attrs) => {
let t1 = subst(cache, t1, initial_env, env);
let t2 = subst(cache, t2, initial_env, env);
Term::Let(bindings, body, attrs) => {
let bindings = bindings.into_iter().map(|(key, val)| (key, subst(cache, val, initial_env, env))).collect();
let body = subst(cache, body, initial_env, env);

RichTerm::new(Term::Let(id, t1, t2, attrs), pos)
RichTerm::new(Term::Let(bindings, body, attrs), pos)
}
p @ Term::LetPattern(..) => panic!(
"Pattern {p:?} has not been transformed before evaluation"
Expand Down
10 changes: 5 additions & 5 deletions core/src/eval/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ fn simple_app() {

#[test]
fn simple_let() {
let t = mk_term::let_in("x", mk_term::integer(5), mk_term::var("x"));
let t = mk_term::let_one_in("x", mk_term::integer(5), mk_term::var("x"));
assert_eq!(Ok(Term::Num(Number::from(5))), eval_no_import(t));
}

Expand Down Expand Up @@ -152,7 +152,7 @@ fn imports() {
R: ImportResolver,
{
resolve_imports(
mk_term::let_in(var, mk_term::import(import), body),
mk_term::let_one_in(var, mk_term::import(import), body),
vm.import_resolver_mut(),
)
.map(|resolve_result| resolve_result.transformed_term)
Expand Down Expand Up @@ -267,7 +267,7 @@ fn initial_env() {
),
);

let t = mk_term::let_in("x", mk_term::integer(2), mk_term::var("x"));
let t = mk_term::let_one_in("x", mk_term::integer(2), mk_term::var("x"));
assert_eq!(
VirtualMachine::new_with_cache(DummyResolver {}, eval_cache.clone(), std::io::sink())
.with_initial_env(initial_env.clone())
Expand All @@ -276,7 +276,7 @@ fn initial_env() {
Ok(mk_term::integer(2))
);

let t = mk_term::let_in("x", mk_term::integer(2), mk_term::var("g"));
let t = mk_term::let_one_in("x", mk_term::integer(2), mk_term::var("g"));
assert_eq!(
VirtualMachine::new_with_cache(DummyResolver {}, eval_cache.clone(), std::io::sink())
.with_initial_env(initial_env.clone())
Expand All @@ -286,7 +286,7 @@ fn initial_env() {
);

// Shadowing of the initial environment
let t = mk_term::let_in("g", mk_term::integer(2), mk_term::var("g"));
let t = mk_term::let_one_in("g", mk_term::integer(2), mk_term::var("g"));
assert_eq!(
VirtualMachine::new_with_cache(DummyResolver {}, eval_cache.clone(), std::io::sink())
.with_initial_env(initial_env.clone())
Expand Down
9 changes: 9 additions & 0 deletions core/src/parser/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,13 +99,22 @@ pub enum ParseError {
/// A recursive let pattern was encountered. They are not currently supported because we
/// decided it was too involved to implement them.
RecursiveLetPattern(RawSpan),
/// Let blocks can currently only contain plain bindings, not pattern bindings.
PatternInLetBlock(RawSpan),
/// A duplicate binding was encountered in a record destructuring pattern.
DuplicateIdentInRecordPattern {
/// The duplicate identifier.
ident: LocIdent,
/// The previous instance of the duplicated identifier.
prev_ident: LocIdent,
},
/// A duplicate binding was encountered in a let block.
DuplicateIdentInLetBlock {
/// The duplicate identifier.
ident: LocIdent,
/// The previous instance of the duplicated identifier.
prev_ident: LocIdent,
},
/// A type variable is used in ways that imply it has multiple different kinds.
///
/// This can happen in several situations, for example:
Expand Down
19 changes: 11 additions & 8 deletions core/src/parser/grammar.lalrpop
Original file line number Diff line number Diff line change
Expand Up @@ -221,19 +221,22 @@ pub ExtendedTerm: ExtendedTerm = {
Term => ExtendedTerm::RichTerm(<>),
};

LetBinding: LetBinding = {
<pattern:Pattern> <annot:LetAnnot<FixedType>?> "=" <value:Term> => {
LetBinding { pattern, annot, value }
}
}

// A general uniterm. The root of the grammar.
UniTerm: UniTerm = {
InfixExpr,
AnnotatedInfixExpr,
AsUniTerm<Forall>,
"let" <l: @L> <recursive:"rec"?> <r: @R> <pat:Pattern> <ann: LetAnnot<FixedType>?>
"=" <mut t1: Term>
"in" <t2: Term> =>? {
if let Some(ann) = ann {
t1 = ann.annotation.attach_term(t1);
}

Ok(UniTerm::from(mk_let(recursive.is_some(), pat, t1, t2, mk_span(src_id, l, r))?))
"let" <l: @L> <recursive:"rec"?> <r: @R>
<mut bindings:(<LetBinding> ",")*> <last:LetBinding> ","?
"in" <body: Term> =>? {
bindings.push(last);
Ok(UniTerm::from(mk_let(recursive.is_some(), bindings, body, mk_span(src_id, l, r))?))
},
<l: @L> "fun" <pats: PatternFun+> "=>" <t: Term> <r: @R> => {
let pos = mk_pos(src_id, l, r);
Expand Down
Loading