diff --git a/CHANGELOG.md b/CHANGELOG.md index 9d97cd9..5028a37 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `Set` stdlib - `Option.force_msg` - Loading namespaces into the current scope (e.g. `using Pair`) +- Assign patterns to variables (e.g. `let x::(t = y::_) = [1, 2, 3, 4]` where `t == [2, 3, 4]`) ### Changed ### Removed diff --git a/docs/sophia_features.md b/docs/sophia_features.md index d6d062b..263eb12 100644 --- a/docs/sophia_features.md +++ b/docs/sophia_features.md @@ -471,6 +471,17 @@ function get_left(Both(x, _)) = Some(x) ``` +Sophia also supports the assignment of patterns to variables: +```sophia +function f(x) = switch(x) + h1::(t = h2::_) => (h1 + h2)::t // same as `h1::h2::k => (h1 + h2)::h2::k` + _ => x + +function g(p : int * option(int)) : int = + let (a, (o = Some(b))) = p // o is equal to Pair.snd(p) + b +``` + *NOTE: Data types cannot currently be recursive.* ## Lists diff --git a/src/aeso_ast_infer_types.erl b/src/aeso_ast_infer_types.erl index a0c07fa..1e6baf8 100644 --- a/src/aeso_ast_infer_types.erl +++ b/src/aeso_ast_infer_types.erl @@ -1703,6 +1703,9 @@ infer_expr(Env, {lam, Attrs, Args, Body}) -> infer_case(Env, Attrs, {tuple, Attrs, ArgPatterns}, {tuple_t, Attrs, ArgTypes}, Body, ResultType), NewArgs = [{arg, As, NewPat, NewT} || {typed, As, NewPat, NewT} <- NewArgPatterns], {typed, Attrs, {lam, Attrs, NewArgs, NewBody}, {fun_t, Attrs, [], ArgTypes, ResultType}}; +infer_expr(Env, {letpat, Attrs, Id, Pattern}) -> + NewPattern = {typed, _, _, PatType} = infer_expr(Env, Pattern), + {typed, Attrs, {letpat, Attrs, check_expr(Env, Id, PatType), NewPattern}, PatType}; infer_expr(Env, Let = {letval, Attrs, _, _}) -> type_error({missing_body_for_let, Attrs}), infer_expr(Env, {block, Attrs, [Let, abort_expr(Attrs, "missing body")]}); @@ -1918,6 +1921,8 @@ free_vars({record, _, Fields}) -> free_vars([E || {field, _, _, E} <- Fields]); free_vars({typed, _, A, _}) -> free_vars(A); +free_vars({letpat, _, Id, Pat}) -> + free_vars(Id) ++ free_vars(Pat); free_vars(L) when is_list(L) -> [V || Elem <- L, V <- free_vars(Elem)]. diff --git a/src/aeso_ast_to_fcode.erl b/src/aeso_ast_to_fcode.erl index 17e83df..128fe9b 100644 --- a/src/aeso_ast_to_fcode.erl +++ b/src/aeso_ast_to_fcode.erl @@ -96,7 +96,8 @@ | nil | {'::', var_name(), var_name()} | {con, arities(), tag(), [var_name()]} - | {tuple, [var_name()]}. + | {tuple, [var_name()]} + | {assign, var_name(), var_name()}. -type ftype() :: integer | boolean @@ -875,7 +876,8 @@ alts_to_fcode(Env, Type, X, Alts) -> | {string, binary()} | nil | {'::', fpat(), fpat()} | {tuple, [fpat()]} - | {con, arities(), tag(), [fpat()]}. + | {con, arities(), tag(), [fpat()]} + | {assign, fpat(), fpat()}. %% %% Invariant: the number of variables matches the number of patterns in each falt. -spec split_tree(env(), [{var_name(), ftype()}], [falt()]) -> fsplit(). @@ -975,6 +977,8 @@ split_pat({'::', P, Q}) -> {{'::', fresh_name(), fresh_name()}, [P, Q]}; split_pat({con, As, I, Pats}) -> Xs = [fresh_name() || _ <- Pats], {{con, As, I, Xs}, Pats}; +split_pat({assign, X = {var, _}, P}) -> + {{assign, fresh_name(), fresh_name()}, [X, P]}; split_pat({tuple, Pats}) -> Xs = [fresh_name() || _ <- Pats], {{tuple, Xs}, Pats}. @@ -985,6 +989,7 @@ split_vars({int, _}, integer) -> []; split_vars({string, _}, string) -> []; split_vars(nil, {list, _}) -> []; split_vars({'::', X, Xs}, {list, T}) -> [{X, T}, {Xs, {list, T}}]; +split_vars({assign, X, P}, T) -> [{X, T}, {P, T}]; split_vars({con, _, I, Xs}, {variant, Cons}) -> lists:zip(Xs, lists:nth(I + 1, Cons)); split_vars({tuple, Xs}, {tuple, Ts}) -> @@ -1040,6 +1045,8 @@ pat_to_fcode(Env, {record_t, Fields}, {record, _, FieldPats}) -> end end, make_tuple([pat_to_fcode(Env, FieldPat(Field)) || Field <- Fields]); +pat_to_fcode(Env, _Type, {letpat, _, Id = {typed, _, {id, _, _}, _}, Pattern}) -> + {assign, pat_to_fcode(Env, Id), pat_to_fcode(Env, Pattern)}; pat_to_fcode(_Env, Type, Pat) -> error({todo, Pat, ':', Type}). @@ -1530,6 +1537,7 @@ match_pat(L, {lit, L}) -> []; match_pat(nil, nil) -> []; match_pat({'::', X, Y}, {op, '::', [A, B]}) -> [{X, A}, {Y, B}]; match_pat({var, X}, E) -> [{X, E}]; +match_pat({assign, X, P}, E) -> [{X, E}, {P, E}]; match_pat(_, _) -> false. constructor_form(Env, Expr) -> @@ -1765,6 +1773,7 @@ pat_vars(nil) -> []; pat_vars({'::', P, Q}) -> pat_vars(P) ++ pat_vars(Q); pat_vars({tuple, Ps}) -> pat_vars(Ps); pat_vars({con, _, _, Ps}) -> pat_vars(Ps); +pat_vars({assign, X, P}) -> pat_vars(X) ++ pat_vars(P); pat_vars(Ps) when is_list(Ps) -> [X || P <- Ps, X <- pat_vars(P)]. -spec fsplit_pat_vars(fsplit_pat()) -> [var_name()]. @@ -1985,7 +1994,11 @@ rename_spat(Ren, {con, Ar, C, Xs}) -> {{con, Ar, C, Zs}, Ren1}; rename_spat(Ren, {tuple, Xs}) -> {Zs, Ren1} = rename_bindings(Ren, Xs), - {{tuple, Zs}, Ren1}. + {{tuple, Zs}, Ren1}; +rename_spat(Ren, {assign, X, P}) -> + {X1, Ren1} = rename_binding(Ren, X), + {P1, Ren2} = rename_binding(Ren1, P), + {{assign, X1, P1}, Ren2}. rename_split(Ren, {split, Type, X, Cases}) -> {split, Type, rename_var(Ren, X), [rename_case(Ren, C) || C <- Cases]}; diff --git a/src/aeso_parser.erl b/src/aeso_parser.erl index 80002ca..c33e47c 100644 --- a/src/aeso_parser.erl +++ b/src/aeso_parser.erl @@ -224,6 +224,9 @@ arg() -> choice( ?RULE(id(), {arg, get_ann(_1), _1, type_wildcard(get_ann(_1))}), ?RULE(id(), tok(':'), type(), {arg, get_ann(_1), _1, _3})). +letpat() -> + ?RULE(keyword('('), id(), tok('='), pattern(), tok(')'), {letpat, get_ann(_1), _2, _4}). + %% -- Types ------------------------------------------------------------------ type_vars() -> paren_list(tvar()). @@ -328,6 +331,7 @@ exprAtom() -> , ?RULE(keyword('['), Expr, token('|'), comma_sep(comprehension_exp()), tok(']'), list_comp_e(_1, _2, _4)) , ?RULE(tok('['), Expr, binop('..'), Expr, tok(']'), _3(_2, _4)) , ?RULE(keyword('('), comma_sep(Expr), tok(')'), tuple_e(_1, _2)) + , letpat() ]) end). @@ -588,6 +592,8 @@ tuple_e(Ann, Exprs) -> {tuple, Ann, Exprs}. list_comp_e(Ann, Expr, Binds) -> {list_comp, Ann, Expr, Binds}. -spec parse_pattern(aeso_syntax:expr()) -> aeso_parse_lib:parser(aeso_syntax:pat()). +parse_pattern({letpat, Ann, Id, Pat}) -> + {letpat, Ann, Id, parse_pattern(Pat)}; parse_pattern({app, Ann, Con = {'::', _}, Es}) -> {app, Ann, Con, lists:map(fun parse_pattern/1, Es)}; parse_pattern({app, Ann, {'-', _}, [{int, _, N}]}) -> diff --git a/src/aeso_pretty.erl b/src/aeso_pretty.erl index 6b43b4c..9be659a 100644 --- a/src/aeso_pretty.erl +++ b/src/aeso_pretty.erl @@ -299,6 +299,8 @@ tuple_type(Factors) -> ]). -spec expr_p(integer(), aeso_syntax:arg_expr()) -> doc(). +expr_p(P, {letpat, _, Id, Pat}) -> + paren(P > 100, follow(hsep(expr(Id), text("=")), expr(Pat))); expr_p(P, {named_arg, _, Name, E}) -> paren(P > 100, follow(hsep(expr(Name), text("=")), expr(E))); expr_p(P, {lam, _, Args, E}) -> diff --git a/src/aeso_syntax.erl b/src/aeso_syntax.erl index 8148a1a..1873d8e 100644 --- a/src/aeso_syntax.erl +++ b/src/aeso_syntax.erl @@ -58,6 +58,7 @@ -type letval() :: {letval, ann(), pat(), expr()}. -type letfun() :: {letfun, ann(), id(), [pat()], type(), expr()}. +-type letpat() :: {letpat, ann(), id(), pat()}. -type fundecl() :: {fun_decl, ann(), id(), type()}. -type letbind() @@ -122,7 +123,8 @@ | {block, ann(), [stmt()]} | {op(), ann()} | id() | qid() | con() | qcon() - | constant(). + | constant() + | letpat(). -type record_or_map() :: record | map | record_or_map_error. @@ -156,6 +158,7 @@ | {list, ann(), [pat()]} | {typed, ann(), pat(), type()} | {record, ann(), [field(pat())]} + | letpat() | constant() | con() | id(). diff --git a/src/aeso_syntax_utils.erl b/src/aeso_syntax_utils.erl index deef257..1110ac3 100644 --- a/src/aeso_syntax_utils.erl +++ b/src/aeso_syntax_utils.erl @@ -88,6 +88,7 @@ fold(Alg = #alg{zero = Zero, plus = Plus, scoped = Scoped}, Fun, K, X) -> {map_get, _, A, B} -> Expr([A, B]); {map_get, _, A, B, C} -> Expr([A, B, C]); {block, _, Ss} -> Expr(Ss); + {letpat, _, X, P} -> Plus(BindExpr(X), Expr(P)); %% field() {field, _, LV, E} -> Expr([LV, E]); {field, _, LV, _, E} -> Expr([LV, E]); diff --git a/test/aeso_compiler_tests.erl b/test/aeso_compiler_tests.erl index 72b10cc..1af71da 100644 --- a/test/aeso_compiler_tests.erl +++ b/test/aeso_compiler_tests.erl @@ -201,6 +201,7 @@ compilable_contracts() -> "create", "child_contract_init_bug", "using_namespace", + "assign_patterns", "test" % Custom general-purpose test file. Keep it last on the list. ]. @@ -236,6 +237,7 @@ failing_contracts() -> , ?PARSE_ERROR(vsemi, [<>]) , ?PARSE_ERROR(vclose, [<>]) , ?PARSE_ERROR(indent_fail, [<>]) + , ?PARSE_ERROR(assign_pattern_to_pattern, [<>]) %% Type errors , ?TYPE_ERROR(name_clash, diff --git a/test/contracts/assign_pattern_to_pattern.aes b/test/contracts/assign_pattern_to_pattern.aes new file mode 100644 index 0000000..7882f92 --- /dev/null +++ b/test/contracts/assign_pattern_to_pattern.aes @@ -0,0 +1,4 @@ +contract AssignPatternToPattern = + entrypoint f() = + let x::(t::z = y) = [1, 2, 3] + (x + t)::y \ No newline at end of file diff --git a/test/contracts/assign_patterns.aes b/test/contracts/assign_patterns.aes new file mode 100644 index 0000000..2d9a784 --- /dev/null +++ b/test/contracts/assign_patterns.aes @@ -0,0 +1,16 @@ +include "List.aes" + +contract AssignPatterns = + + entrypoint test() = foo([1, 0, 2], (2, Some(3)), Some([4, 5])) + + entrypoint foo(xs : list(int), p : int * option(int), some : option(list(int))) = + let x::(t = y::_) = xs + let z::_ = t + + let (a, (o = Some(b))) = p + + let Some((f = g::_)) = some + g + List.get(1, f) + + x + y + z + a + b