From af207022494e2e8c5e5f9ccc009965dcfebdcc07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Bene=C5=A1?= Date: Fri, 28 Nov 2025 15:03:32 +0100 Subject: [PATCH 01/11] Allow (multi-)pattern matching in 'with val' --- .../shared/src/main/scala/effekt/Parser.scala | 95 ++++++++++++++----- .../src/main/scala/effekt/source/Tree.scala | 2 +- 2 files changed, 74 insertions(+), 23 deletions(-) diff --git a/effekt/shared/src/main/scala/effekt/Parser.scala b/effekt/shared/src/main/scala/effekt/Parser.scala index 053943b4a..2316530cd 100644 --- a/effekt/shared/src/main/scala/effekt/Parser.scala +++ b/effekt/shared/src/main/scala/effekt/Parser.scala @@ -270,40 +270,91 @@ class Parser(tokens: Seq[Token], source: Source) { else ExprStmt(e, stmts(inBraces), span()) }) labelled "statements" - // ATTENTION: here the grammar changed (we added `with val` to disambiguate) - // with val (: )? = ; - // with val ( (: )?...) = + // with val (, )* = ; + // with def = ; // with ; def withStmt(inBraces: Boolean): Stmt = `with` ~> peek.kind match { case `val` => - val params = (`val` ~> peek.kind match { - case `(` => valueParamsOpt() - case _ => List(valueParamOpt()) // TODO copy position - }) - desugarWith(params, Nil, `=` ~> expr(), semi() ~> stmts(inBraces), span()) + consume(`val`) + val patterns = some(matchPattern, `,`) + val call = `=` ~> expr() + val body = semi() ~> stmts(inBraces) + desugarWithPatterns(patterns, call, body, span()) case `def` => val params = (`def` ~> peek.kind match { case `{` => blockParamsOpt() case _ => List(blockParamOpt()) // TODO copy position }) - desugarWith(Nil, params, `=` ~> expr(), semi() ~> stmts(inBraces), span()) + val call = `=` ~> expr() + val body = semi() ~> stmts(inBraces) + val blockLit: BlockLiteral = BlockLiteral(Nil, Nil, params, body, body.span.synthesized) + desugarWith(call, blockLit, span()) + + case _ => + val call = expr() + val body = semi() ~> stmts(inBraces) + val blockLit: BlockLiteral = BlockLiteral(Nil, Nil, Nil, body, body.span.synthesized) + desugarWith(call, blockLit, span()) + } + + // Desugar `with val` with pattern(s) + def desugarWithPatterns(patterns: Many[MatchPattern], call: Term, body: Stmt, withSpan: Span): Stmt = { + // Check if all patterns are simple variable bindings or ignored + val allSimpleVars = patterns.unspan.forall { + case AnyPattern(_, _) => true + case IgnorePattern(_) => true + case _ => false + } + + val blockLit: BlockLiteral = if (allSimpleVars) { + // Simple case: all patterns are just variable names (or ignored) + // Desugar to: call { (x, y, _, ...) => body } + val vparams: List[ValueParam] = patterns.unspan.map { + case AnyPattern(id, span) => ValueParam(id, None, span) + case _ => sys.error("impossible: checked above") + } + BlockLiteral(Nil, vparams, Nil, body, body.span.synthesized) + } else { + // Complex case: at least one pattern needs matching + // Desugar to: call { case pat1, pat2, ... => body } + // This requires one argument per pattern, matching against multiple scrutinees + val patternList = patterns.unspan + val names = List.tabulate(patternList.length) { n => s"__withArg${n}" } + val argSpans = patternList.map(_.span) + + val vparams: List[ValueParam] = names.zip(argSpans).map { (name, span) => + ValueParam(IdDef(name, span.synthesized), None, span.synthesized) + } + val scrutinees = names.zip(argSpans).map { (name, span) => + Var(IdRef(Nil, name, span.synthesized), span.synthesized) + } + + val pattern: MatchPattern = patterns match { + case Many(List(single), _) => single + case Many(ps, span) => MultiPattern(ps, span) + } + + val clause = MatchClause(pattern, Nil, body, Span(source, pattern.span.from, body.span.to, Synthesized)) + val matchExpr = Match(scrutinees, List(clause), None, withSpan.synthesized) + val matchBody = Return(matchExpr, withSpan.synthesized) + BlockLiteral(Nil, vparams, Nil, matchBody, withSpan.synthesized) + } - case _ => desugarWith(Nil, Nil, expr(), semi() ~> stmts(inBraces), span()) + desugarWith(call, blockLit, withSpan) } - def desugarWith(vparams: List[ValueParam], bparams: List[BlockParam], call: Term, body: Stmt, withSpan: Span): Stmt = call match { - case m@MethodCall(receiver, id, tps, vargs, bargs, callSpan) => - Return(MethodCall(receiver, id, tps, vargs, bargs :+ (BlockLiteral(Nil, vparams, bparams, body, body.span.synthesized)), callSpan), withSpan.synthesized) - case c@Call(callee, tps, vargs, bargs, callSpan) => - Return(Call(callee, tps, vargs, bargs :+ (BlockLiteral(Nil, vparams, bparams, body, body.span.synthesized)), callSpan), withSpan.synthesized) - case Var(id, varSpan) => - val tgt = IdTarget(id) - Return(Call(tgt, Nil, Nil, (BlockLiteral(Nil, vparams, bparams, body, body.span.synthesized)) :: Nil, varSpan), withSpan.synthesized) - case Do(id, targs, vargs, bargs, doSpan) => - Return(Do(id, targs, vargs, bargs :+ BlockLiteral(Nil, vparams, bparams, body, body.span.synthesized), doSpan), withSpan.synthesized) - case term => - Return(Call(ExprTarget(term), Nil, Nil, (BlockLiteral(Nil, vparams, bparams, body, body.span.synthesized)) :: Nil, term.span.synthesized), withSpan.synthesized) + def desugarWith(call: Term, blockLit: BlockLiteral, withSpan: Span): Stmt = call match { + case MethodCall(receiver, id, tps, vargs, bargs, callSpan) => + Return(MethodCall(receiver, id, tps, vargs, bargs :+ blockLit, callSpan), withSpan.synthesized) + case Call(callee, tps, vargs, bargs, callSpan) => + Return(Call(callee, tps, vargs, bargs :+ blockLit, callSpan), withSpan.synthesized) + case Var(id, varSpan) => + Return(Call(IdTarget(id), Nil, Nil, blockLit :: Nil, varSpan), withSpan.synthesized) + case Do(id, targs, vargs, bargs, doSpan) => + Return(Do(id, targs, vargs, bargs :+ blockLit, doSpan), withSpan.synthesized) + case term => + Return(Call(ExprTarget(term), Nil, Nil, blockLit :: Nil, term.span.synthesized), withSpan.synthesized) } def maybeSemi(): Unit = if isSemi then semi() diff --git a/effekt/shared/src/main/scala/effekt/source/Tree.scala b/effekt/shared/src/main/scala/effekt/source/Tree.scala index 1d637e778..0682e5010 100644 --- a/effekt/shared/src/main/scala/effekt/source/Tree.scala +++ b/effekt/shared/src/main/scala/effekt/source/Tree.scala @@ -678,7 +678,7 @@ enum MatchPattern extends Tree { * * case a, b => ... * - * Currently should *only* occur in lambda-cases during Parsing + * Currently should *only* occur in lambda-cases & `with` statements during parsing */ case MultiPattern(patterns: List[MatchPattern], span: Span) extends MatchPattern } From dd906f68e96e694f6c14dfc548de3a90d86f3472 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Bene=C5=A1?= Date: Fri, 28 Nov 2025 15:15:58 +0100 Subject: [PATCH 02/11] Fix missing case for ignored patterns --- effekt/shared/src/main/scala/effekt/Parser.scala | 1 + 1 file changed, 1 insertion(+) diff --git a/effekt/shared/src/main/scala/effekt/Parser.scala b/effekt/shared/src/main/scala/effekt/Parser.scala index 2316530cd..f5b1f7c75 100644 --- a/effekt/shared/src/main/scala/effekt/Parser.scala +++ b/effekt/shared/src/main/scala/effekt/Parser.scala @@ -312,6 +312,7 @@ class Parser(tokens: Seq[Token], source: Source) { // Desugar to: call { (x, y, _, ...) => body } val vparams: List[ValueParam] = patterns.unspan.map { case AnyPattern(id, span) => ValueParam(id, None, span) + case IgnorePattern(span) => ValueParam(IdDef(s"__ignored", span.synthesized), None, span) case _ => sys.error("impossible: checked above") } BlockLiteral(Nil, vparams, Nil, body, body.span.synthesized) From 991da4a6a6d4c97ea2210f1ef2acbe71b214c40c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Bene=C5=A1?= Date: Fri, 28 Nov 2025 15:16:27 +0100 Subject: [PATCH 03/11] Add a few pos tests --- examples/pos/with_val_many_args.check | 1 + examples/pos/with_val_many_args.effekt | 8 ++++++++ examples/pos/with_val_many_ignored_args.check | 4 ++++ examples/pos/with_val_many_ignored_args.effekt | 13 +++++++++++++ examples/pos/with_val_pair.check | 4 ++++ examples/pos/with_val_pair.effekt | 6 ++++++ examples/pos/with_val_pair_complex.check | 3 +++ examples/pos/with_val_pair_complex.effekt | 7 +++++++ examples/pos/with_val_pair_explicit.check | 4 ++++ examples/pos/with_val_pair_explicit.effekt | 6 ++++++ examples/pos/with_val_two_args.check | 4 ++++ examples/pos/with_val_two_args.effekt | 6 ++++++ examples/pos/with_val_two_args_complex.check | 3 +++ examples/pos/with_val_two_args_complex.effekt | 7 +++++++ 14 files changed, 76 insertions(+) create mode 100644 examples/pos/with_val_many_args.check create mode 100644 examples/pos/with_val_many_args.effekt create mode 100644 examples/pos/with_val_many_ignored_args.check create mode 100644 examples/pos/with_val_many_ignored_args.effekt create mode 100644 examples/pos/with_val_pair.check create mode 100644 examples/pos/with_val_pair.effekt create mode 100644 examples/pos/with_val_pair_complex.check create mode 100644 examples/pos/with_val_pair_complex.effekt create mode 100644 examples/pos/with_val_pair_explicit.check create mode 100644 examples/pos/with_val_pair_explicit.effekt create mode 100644 examples/pos/with_val_two_args.check create mode 100644 examples/pos/with_val_two_args.effekt create mode 100644 examples/pos/with_val_two_args_complex.check create mode 100644 examples/pos/with_val_two_args_complex.effekt diff --git a/examples/pos/with_val_many_args.check b/examples/pos/with_val_many_args.check new file mode 100644 index 000000000..c401a39fc --- /dev/null +++ b/examples/pos/with_val_many_args.check @@ -0,0 +1 @@ +42, 100; a \ No newline at end of file diff --git a/examples/pos/with_val_many_args.effekt b/examples/pos/with_val_many_args.effekt new file mode 100644 index 000000000..34603f05a --- /dev/null +++ b/examples/pos/with_val_many_args.effekt @@ -0,0 +1,8 @@ +module with_val_two_args + +def callback { f: (Int, Int, String, Char) => Unit }: Unit = f(42, 100, "hello", 'a') + +def main() = { + with val i, j, _, c = callback; + println(show(i) ++ ", " ++ show(j) ++ "; " ++ show(c)) +} \ No newline at end of file diff --git a/examples/pos/with_val_many_ignored_args.check b/examples/pos/with_val_many_ignored_args.check new file mode 100644 index 000000000..c59ed4d66 --- /dev/null +++ b/examples/pos/with_val_many_ignored_args.check @@ -0,0 +1,4 @@ +before +called! +called! +after \ No newline at end of file diff --git a/examples/pos/with_val_many_ignored_args.effekt b/examples/pos/with_val_many_ignored_args.effekt new file mode 100644 index 000000000..e61ebf6b4 --- /dev/null +++ b/examples/pos/with_val_many_ignored_args.effekt @@ -0,0 +1,13 @@ +module with_val_two_args + +def callback { f: (Int, Int, String, Char) => Unit }: Unit = { + println("before") + f(42, 100, "hello", 'a') + f(0, 0, "", '0') + println("after") +} + +def main() = { + with val _, _, _, _ = callback; + println("called!") +} \ No newline at end of file diff --git a/examples/pos/with_val_pair.check b/examples/pos/with_val_pair.check new file mode 100644 index 000000000..6220905b8 --- /dev/null +++ b/examples/pos/with_val_pair.check @@ -0,0 +1,4 @@ +0: a +1: b +2: c +3: d \ No newline at end of file diff --git a/examples/pos/with_val_pair.effekt b/examples/pos/with_val_pair.effekt new file mode 100644 index 000000000..118e649b4 --- /dev/null +++ b/examples/pos/with_val_pair.effekt @@ -0,0 +1,6 @@ +module with_val_pair + +def main() = { + with val (i, x) = [(0, 'a'), (1, 'b'), (2, 'c'), (3, 'd')].foreach; + println(show(i) ++ ": " ++ show(x)) +} \ No newline at end of file diff --git a/examples/pos/with_val_pair_complex.check b/examples/pos/with_val_pair_complex.check new file mode 100644 index 000000000..850b9934c --- /dev/null +++ b/examples/pos/with_val_pair_complex.check @@ -0,0 +1,3 @@ +1=1: a +2=2: b +3=3: c \ No newline at end of file diff --git a/examples/pos/with_val_pair_complex.effekt b/examples/pos/with_val_pair_complex.effekt new file mode 100644 index 000000000..030cefe1d --- /dev/null +++ b/examples/pos/with_val_pair_complex.effekt @@ -0,0 +1,7 @@ +module with_val_pair_complex + +record Pos(i: Int, x: Char) +def main() = { + with val (i, Pos(j, x)) = [(1, Pos(1, 'a')), (2, Pos(2, 'b')), (3, Pos(3, 'c'))].foreach; + println(show(i) ++ "=" ++ show(j) ++ ": " ++ show(x)) +} \ No newline at end of file diff --git a/examples/pos/with_val_pair_explicit.check b/examples/pos/with_val_pair_explicit.check new file mode 100644 index 000000000..6220905b8 --- /dev/null +++ b/examples/pos/with_val_pair_explicit.check @@ -0,0 +1,4 @@ +0: a +1: b +2: c +3: d \ No newline at end of file diff --git a/examples/pos/with_val_pair_explicit.effekt b/examples/pos/with_val_pair_explicit.effekt new file mode 100644 index 000000000..f26856299 --- /dev/null +++ b/examples/pos/with_val_pair_explicit.effekt @@ -0,0 +1,6 @@ +module with_val_pair_explicit + +def main() = { + with val Tuple2(i, x) = [(0, 'a'), (1, 'b'), (2, 'c'), (3, 'd')].foreach; + println(show(i) ++ ": " ++ show(x)) +} \ No newline at end of file diff --git a/examples/pos/with_val_two_args.check b/examples/pos/with_val_two_args.check new file mode 100644 index 000000000..6220905b8 --- /dev/null +++ b/examples/pos/with_val_two_args.check @@ -0,0 +1,4 @@ +0: a +1: b +2: c +3: d \ No newline at end of file diff --git a/examples/pos/with_val_two_args.effekt b/examples/pos/with_val_two_args.effekt new file mode 100644 index 000000000..cda674a37 --- /dev/null +++ b/examples/pos/with_val_two_args.effekt @@ -0,0 +1,6 @@ +module with_val_two_args + +def main() = { + with val i, x = ['a', 'b', 'c', 'd'].foreachIndex; + println(show(i) ++ ": " ++ show(x)) +} \ No newline at end of file diff --git a/examples/pos/with_val_two_args_complex.check b/examples/pos/with_val_two_args_complex.check new file mode 100644 index 000000000..a00684ff8 --- /dev/null +++ b/examples/pos/with_val_two_args_complex.check @@ -0,0 +1,3 @@ +0: a +1: b +2: c \ No newline at end of file diff --git a/examples/pos/with_val_two_args_complex.effekt b/examples/pos/with_val_two_args_complex.effekt new file mode 100644 index 000000000..0128f71b1 --- /dev/null +++ b/examples/pos/with_val_two_args_complex.effekt @@ -0,0 +1,7 @@ +module with_val_two_args_complex + +record Pos(i: Int, x: Char) +def main() = { + with val i, Pos(j, x) = [Pos(1, 'a'), Pos(2, 'b'), Pos(3, 'c')].foreachIndex; + println(show(i) ++ ": " ++ show(x)) +} \ No newline at end of file From 55d2a5f0c58ec2900311b420d5c625b7218a319e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Bene=C5=A1?= Date: Fri, 28 Nov 2025 15:24:54 +0100 Subject: [PATCH 04/11] Update tests --- examples/pos/with_val_many_args.effekt | 5 +++-- examples/pos/with_val_many_ignored_args.effekt | 2 +- examples/pos/withdefstatement.effekt | 2 +- examples/pos/withstatement.effekt | 4 ++-- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/examples/pos/with_val_many_args.effekt b/examples/pos/with_val_many_args.effekt index 34603f05a..0c506782b 100644 --- a/examples/pos/with_val_many_args.effekt +++ b/examples/pos/with_val_many_args.effekt @@ -1,6 +1,7 @@ -module with_val_two_args +module with_val_many_args -def callback { f: (Int, Int, String, Char) => Unit }: Unit = f(42, 100, "hello", 'a') +def callback { f: (Int, Int, String, Char) => Unit }: Unit = + f(42, 100, "hello", 'a') def main() = { with val i, j, _, c = callback; diff --git a/examples/pos/with_val_many_ignored_args.effekt b/examples/pos/with_val_many_ignored_args.effekt index e61ebf6b4..a82e058f8 100644 --- a/examples/pos/with_val_many_ignored_args.effekt +++ b/examples/pos/with_val_many_ignored_args.effekt @@ -1,4 +1,4 @@ -module with_val_two_args +module with_val_many_ignored_args def callback { f: (Int, Int, String, Char) => Unit }: Unit = { println("before") diff --git a/examples/pos/withdefstatement.effekt b/examples/pos/withdefstatement.effekt index ebf580572..e92ec520b 100644 --- a/examples/pos/withdefstatement.effekt +++ b/examples/pos/withdefstatement.effekt @@ -15,7 +15,7 @@ def fresh[T](init: Int) { prog: {Cell} => T }: T = { } def main() = { - with def target = fresh(0) + with def {target} = fresh(0) with def c1 = fresh(0) with def c2 = fresh(0) c1.set(1); diff --git a/examples/pos/withstatement.effekt b/examples/pos/withstatement.effekt index bf164cd6d..bf3c7f5e8 100644 --- a/examples/pos/withstatement.effekt +++ b/examples/pos/withstatement.effekt @@ -11,8 +11,8 @@ def bar { f: (Int, String) => Unit / {}}: Unit = def user(): Unit = { with printer; - with val x: Int = foreach([1,2,3]); - with val (a, b) = bar; + with val x = foreach([1,2,3]); + with val a, b = bar; println(show(x) ++ b); val z = { val x = 2; From 0af38eb6630a5b49c82ba77e9f49357a13ff9965 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Bene=C5=A1?= Date: Sat, 13 Dec 2025 20:32:46 +0100 Subject: [PATCH 05/11] Try to add support for guards and fallback --- .../shared/src/main/scala/effekt/Parser.scala | 24 ++++++++++--------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/effekt/shared/src/main/scala/effekt/Parser.scala b/effekt/shared/src/main/scala/effekt/Parser.scala index f5b1f7c75..d9b3cb237 100644 --- a/effekt/shared/src/main/scala/effekt/Parser.scala +++ b/effekt/shared/src/main/scala/effekt/Parser.scala @@ -270,16 +270,18 @@ class Parser(tokens: Seq[Token], source: Source) { else ExprStmt(e, stmts(inBraces), span()) }) labelled "statements" - // with val (, )* = ; + // with val (, )* (and )* = (else )?; // with def = ; // with ; def withStmt(inBraces: Boolean): Stmt = `with` ~> peek.kind match { case `val` => consume(`val`) val patterns = some(matchPattern, `,`) + val guards = manyWhile(`and` ~> matchGuard(), `and`) val call = `=` ~> expr() + val fallback = when(`else`) { Some(stmt()) } { None } val body = semi() ~> stmts(inBraces) - desugarWithPatterns(patterns, call, body, span()) + desugarWithPatterns(patterns, guards, call, fallback, body, span()) case `def` => val params = (`def` ~> peek.kind match { @@ -298,17 +300,17 @@ class Parser(tokens: Seq[Token], source: Source) { desugarWith(call, blockLit, span()) } - // Desugar `with val` with pattern(s) - def desugarWithPatterns(patterns: Many[MatchPattern], call: Term, body: Stmt, withSpan: Span): Stmt = { - // Check if all patterns are simple variable bindings or ignored - val allSimpleVars = patterns.unspan.forall { + // Desugar `with val` with pattern(s), optional guards, and optional fallback + def desugarWithPatterns(patterns: Many[MatchPattern], guards: List[MatchGuard], call: Term, fallback: Option[Stmt], body: Stmt, withSpan: Span): Stmt = { + // Check if all patterns are simple variable bindings or ignored, with no guards and no fallback + val allSimpleVars = guards.isEmpty && fallback.isEmpty && patterns.unspan.forall { case AnyPattern(_, _) => true case IgnorePattern(_) => true case _ => false } val blockLit: BlockLiteral = if (allSimpleVars) { - // Simple case: all patterns are just variable names (or ignored) + // Simple case: all patterns are just variable names (or ignored), no guards, no fallback // Desugar to: call { (x, y, _, ...) => body } val vparams: List[ValueParam] = patterns.unspan.map { case AnyPattern(id, span) => ValueParam(id, None, span) @@ -317,8 +319,8 @@ class Parser(tokens: Seq[Token], source: Source) { } BlockLiteral(Nil, vparams, Nil, body, body.span.synthesized) } else { - // Complex case: at least one pattern needs matching - // Desugar to: call { case pat1, pat2, ... => body } + // Complex case: at least one pattern needs matching, or there are guards/fallback + // Desugar to: call { case pat1, pat2, ... and guard1 and ... => body; else => fallback } // This requires one argument per pattern, matching against multiple scrutinees val patternList = patterns.unspan val names = List.tabulate(patternList.length) { n => s"__withArg${n}" } @@ -336,8 +338,8 @@ class Parser(tokens: Seq[Token], source: Source) { case Many(ps, span) => MultiPattern(ps, span) } - val clause = MatchClause(pattern, Nil, body, Span(source, pattern.span.from, body.span.to, Synthesized)) - val matchExpr = Match(scrutinees, List(clause), None, withSpan.synthesized) + val clause = MatchClause(pattern, guards, body, Span(source, pattern.span.from, body.span.to, Synthesized)) + val matchExpr = Match(scrutinees, List(clause), fallback, withSpan.synthesized) val matchBody = Return(matchExpr, withSpan.synthesized) BlockLiteral(Nil, vparams, Nil, matchBody, withSpan.synthesized) } From 72b59060c1295a8a4a232f89d88b5f8e264e2ef7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Bene=C5=A1?= Date: Sat, 13 Dec 2025 20:40:37 +0100 Subject: [PATCH 06/11] Add a single example for 'with val x and p and q = foo else bar' --- examples/pos/with_val_and_else.check | 1 + examples/pos/with_val_and_else.effekt | 32 +++++++++++++++++++++++++++ 2 files changed, 33 insertions(+) create mode 100644 examples/pos/with_val_and_else.check create mode 100644 examples/pos/with_val_and_else.effekt diff --git a/examples/pos/with_val_and_else.check b/examples/pos/with_val_and_else.check new file mode 100644 index 000000000..df29faaf2 --- /dev/null +++ b/examples/pos/with_val_and_else.check @@ -0,0 +1 @@ +Some(User(Joe, 42)) \ No newline at end of file diff --git a/examples/pos/with_val_and_else.effekt b/examples/pos/with_val_and_else.effekt new file mode 100644 index 000000000..422f80b2b --- /dev/null +++ b/examples/pos/with_val_and_else.effekt @@ -0,0 +1,32 @@ +module with_val_and_else + +import json +import scanner + +def lookup[V](assoc: List[(String, V)], k: String): Option[V] = assoc match { + case Cons((k2, v), rest) and k == k2 => Some(v) + case Cons(_ , rest) => rest.lookup(k) + case Nil() => None() +} + +record User(name: String, age: Double) + +def processUser { data: { JsonValue => Option[User] } => Option[User] } : Option[User] = { + with val obj + and obj is Dict(fields) + and fields.lookup("user") is Some(userObj) + and userObj is Dict(userFields) + and userFields.lookup("name") is Some(String(name)) + and userFields.lookup("age") is Some(Number(age)) + = data() else None() + + Some(User(name, age)) +} + +def main() = { + with on[WrongFormat].panic; + with feed("""{ "user": { "name": "Joe", "age": 42 } }""") + with scanner[Char] + val data = build { decodeJson() }.second + inspect(processUser { {f} => f(data) }) +} \ No newline at end of file From ccfab6e7ba168c905ae5e1e37d37521acbded72c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Bene=C5=A1?= Date: Sat, 13 Dec 2025 20:47:00 +0100 Subject: [PATCH 07/11] Add a few more examples --- examples/pos/with_val_pair_and_else.check | 1 + examples/pos/with_val_pair_and_else.effekt | 32 +++++++++++++++++++ examples/pos/with_val_two_args_and_else.check | 1 + .../pos/with_val_two_args_and_else.effekt | 32 +++++++++++++++++++ 4 files changed, 66 insertions(+) create mode 100644 examples/pos/with_val_pair_and_else.check create mode 100644 examples/pos/with_val_pair_and_else.effekt create mode 100644 examples/pos/with_val_two_args_and_else.check create mode 100644 examples/pos/with_val_two_args_and_else.effekt diff --git a/examples/pos/with_val_pair_and_else.check b/examples/pos/with_val_pair_and_else.check new file mode 100644 index 000000000..df29faaf2 --- /dev/null +++ b/examples/pos/with_val_pair_and_else.check @@ -0,0 +1 @@ +Some(User(Joe, 42)) \ No newline at end of file diff --git a/examples/pos/with_val_pair_and_else.effekt b/examples/pos/with_val_pair_and_else.effekt new file mode 100644 index 000000000..3bc5e7eb5 --- /dev/null +++ b/examples/pos/with_val_pair_and_else.effekt @@ -0,0 +1,32 @@ +module with_val_pair_and_else + +import json +import scanner + +def lookup[V](assoc: List[(String, V)], k: String): Option[V] = assoc match { + case Cons((k2, v), rest) and k == k2 => Some(v) + case Cons(_ , rest) => rest.lookup(k) + case Nil() => None() +} + +record User(name: String, age: Double) + +def processUser { data: { ((JsonValue, Double)) => Option[User] } => Option[User] } : Option[User] = { + with val (obj, currentYear) + and obj is Dict(fields) + and fields.lookup("user") is Some(userObj) + and userObj is Dict(userFields) + and userFields.lookup("name") is Some(String(name)) + and userFields.lookup("birthYear") is Some(Number(birthYear)) + = data() else None() + + Some(User(name, currentYear - birthYear)) +} + +def main() = { + with on[WrongFormat].panic; + with feed("""{ "user": { "name": "Joe", "birthYear": 1983 } }""") + with scanner[Char] + val data = build { decodeJson() }.second + inspect(processUser { {f} => f((data, 2025.0)) }) +} \ No newline at end of file diff --git a/examples/pos/with_val_two_args_and_else.check b/examples/pos/with_val_two_args_and_else.check new file mode 100644 index 000000000..df29faaf2 --- /dev/null +++ b/examples/pos/with_val_two_args_and_else.check @@ -0,0 +1 @@ +Some(User(Joe, 42)) \ No newline at end of file diff --git a/examples/pos/with_val_two_args_and_else.effekt b/examples/pos/with_val_two_args_and_else.effekt new file mode 100644 index 000000000..e8f422480 --- /dev/null +++ b/examples/pos/with_val_two_args_and_else.effekt @@ -0,0 +1,32 @@ +module with_val_two_args_and_else + +import json +import scanner + +def lookup[V](assoc: List[(String, V)], k: String): Option[V] = assoc match { + case Cons((k2, v), rest) and k == k2 => Some(v) + case Cons(_ , rest) => rest.lookup(k) + case Nil() => None() +} + +record User(name: String, age: Double) + +def processUser { data: { (JsonValue, Double) => Option[User] } => Option[User] } : Option[User] = { + with val obj, currentYear + and obj is Dict(fields) + and fields.lookup("user") is Some(userObj) + and userObj is Dict(userFields) + and userFields.lookup("name") is Some(String(name)) + and userFields.lookup("birthYear") is Some(Number(birthYear)) + = data() else None() + + Some(User(name, currentYear - birthYear)) +} + +def main() = { + with on[WrongFormat].panic; + with feed("""{ "user": { "name": "Joe", "birthYear": 1983 } }""") + with scanner[Char] + val data = build { decodeJson() }.second + inspect(processUser { {f} => f(data, 2025.0) }) +} \ No newline at end of file From 5becde76291ea70fd7bbaa0dbdc30e37e5d52226 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Bene=C5=A1?= Date: Sat, 13 Dec 2025 21:05:07 +0100 Subject: [PATCH 08/11] One more with-val-else example --- examples/pos/with_val_else.check | 2 ++ examples/pos/with_val_else.effekt | 18 ++++++++++++++++++ 2 files changed, 20 insertions(+) create mode 100644 examples/pos/with_val_else.check create mode 100644 examples/pos/with_val_else.effekt diff --git a/examples/pos/with_val_else.check b/examples/pos/with_val_else.check new file mode 100644 index 000000000..d6108ecca --- /dev/null +++ b/examples/pos/with_val_else.check @@ -0,0 +1,2 @@ +Read value: 42 +Read value: 100 \ No newline at end of file diff --git a/examples/pos/with_val_else.effekt b/examples/pos/with_val_else.effekt new file mode 100644 index 000000000..03e14e570 --- /dev/null +++ b/examples/pos/with_val_else.effekt @@ -0,0 +1,18 @@ +module with_val_else + +import stream + +def readOption[T, R] { f: Option[T] => R }: R / read[T] = + f(try { Some(do read()) } with stop { None() }) + +def getValue() = { + with val Some(x) = readOption[Int, String]() else "" + show(x) +} + +def main() = { + with [42, 100].feed; + while (getValue() is v and v != "") { + println("Read value: " ++ v) + } +} \ No newline at end of file From 673276f818d4e2ceb367feac9ec8e1d6c4b6986d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Bene=C5=A1?= Date: Sat, 13 Dec 2025 21:08:15 +0100 Subject: [PATCH 09/11] Remove 'inspect' from examples (oops) --- examples/pos/with_val_and_else.check | 2 +- examples/pos/with_val_and_else.effekt | 7 ++++++- examples/pos/with_val_pair_and_else.check | 2 +- examples/pos/with_val_pair_and_else.effekt | 7 ++++++- examples/pos/with_val_two_args_and_else.check | 2 +- examples/pos/with_val_two_args_and_else.effekt | 6 +++++- 6 files changed, 20 insertions(+), 6 deletions(-) diff --git a/examples/pos/with_val_and_else.check b/examples/pos/with_val_and_else.check index df29faaf2..634c7a7c2 100644 --- a/examples/pos/with_val_and_else.check +++ b/examples/pos/with_val_and_else.check @@ -1 +1 @@ -Some(User(Joe, 42)) \ No newline at end of file +User(name=Joe, age=42) \ No newline at end of file diff --git a/examples/pos/with_val_and_else.effekt b/examples/pos/with_val_and_else.effekt index 422f80b2b..6ba955bce 100644 --- a/examples/pos/with_val_and_else.effekt +++ b/examples/pos/with_val_and_else.effekt @@ -11,6 +11,11 @@ def lookup[V](assoc: List[(String, V)], k: String): Option[V] = assoc match { record User(name: String, age: Double) +def toString(u: Option[User]): String = u match { + case Some(u) => "User(name=" ++ u.name ++ ", age=" ++ u.age.show ++ ")" + case None() => "No user found" +} + def processUser { data: { JsonValue => Option[User] } => Option[User] } : Option[User] = { with val obj and obj is Dict(fields) @@ -28,5 +33,5 @@ def main() = { with feed("""{ "user": { "name": "Joe", "age": 42 } }""") with scanner[Char] val data = build { decodeJson() }.second - inspect(processUser { {f} => f(data) }) + println(processUser { {f} => f(data) }.toString) } \ No newline at end of file diff --git a/examples/pos/with_val_pair_and_else.check b/examples/pos/with_val_pair_and_else.check index df29faaf2..634c7a7c2 100644 --- a/examples/pos/with_val_pair_and_else.check +++ b/examples/pos/with_val_pair_and_else.check @@ -1 +1 @@ -Some(User(Joe, 42)) \ No newline at end of file +User(name=Joe, age=42) \ No newline at end of file diff --git a/examples/pos/with_val_pair_and_else.effekt b/examples/pos/with_val_pair_and_else.effekt index 3bc5e7eb5..07e596251 100644 --- a/examples/pos/with_val_pair_and_else.effekt +++ b/examples/pos/with_val_pair_and_else.effekt @@ -11,6 +11,11 @@ def lookup[V](assoc: List[(String, V)], k: String): Option[V] = assoc match { record User(name: String, age: Double) +def toString(u: Option[User]): String = u match { + case Some(u) => "User(name=" ++ u.name ++ ", age=" ++ u.age.show ++ ")" + case None() => "No user found" +} + def processUser { data: { ((JsonValue, Double)) => Option[User] } => Option[User] } : Option[User] = { with val (obj, currentYear) and obj is Dict(fields) @@ -28,5 +33,5 @@ def main() = { with feed("""{ "user": { "name": "Joe", "birthYear": 1983 } }""") with scanner[Char] val data = build { decodeJson() }.second - inspect(processUser { {f} => f((data, 2025.0)) }) + println(processUser { {f} => f((data, 2025.0)) }.toString) } \ No newline at end of file diff --git a/examples/pos/with_val_two_args_and_else.check b/examples/pos/with_val_two_args_and_else.check index df29faaf2..634c7a7c2 100644 --- a/examples/pos/with_val_two_args_and_else.check +++ b/examples/pos/with_val_two_args_and_else.check @@ -1 +1 @@ -Some(User(Joe, 42)) \ No newline at end of file +User(name=Joe, age=42) \ No newline at end of file diff --git a/examples/pos/with_val_two_args_and_else.effekt b/examples/pos/with_val_two_args_and_else.effekt index e8f422480..b96588cc0 100644 --- a/examples/pos/with_val_two_args_and_else.effekt +++ b/examples/pos/with_val_two_args_and_else.effekt @@ -11,6 +11,10 @@ def lookup[V](assoc: List[(String, V)], k: String): Option[V] = assoc match { record User(name: String, age: Double) +def toString(u: Option[User]): String = u match { + case Some(u) => "User(name=" ++ u.name ++ ", age=" ++ u.age.show ++ ")" + case None() => "No user found" +} def processUser { data: { (JsonValue, Double) => Option[User] } => Option[User] } : Option[User] = { with val obj, currentYear and obj is Dict(fields) @@ -28,5 +32,5 @@ def main() = { with feed("""{ "user": { "name": "Joe", "birthYear": 1983 } }""") with scanner[Char] val data = build { decodeJson() }.second - inspect(processUser { {f} => f(data, 2025.0) }) + println(processUser { {f} => f(data, 2025.0) }.toString) } \ No newline at end of file From d973a0dfca80df4ce9577b80c8adc51a3c370d7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Bene=C5=A1?= Date: Mon, 15 Dec 2025 19:00:40 +0100 Subject: [PATCH 10/11] Ignore new tests in reparser for now --- effekt/jvm/src/test/scala/effekt/core/ReparseTests.scala | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/effekt/jvm/src/test/scala/effekt/core/ReparseTests.scala b/effekt/jvm/src/test/scala/effekt/core/ReparseTests.scala index e3aef4a6f..d73a49cfd 100644 --- a/effekt/jvm/src/test/scala/effekt/core/ReparseTests.scala +++ b/effekt/jvm/src/test/scala/effekt/core/ReparseTests.scala @@ -41,7 +41,12 @@ class ReparseTests extends CoreTests { // FIXME: There is currently a limitation in TestRenamer in that it does not rename captures. // This means, that in this example, captures for State[Int] and State[String] are both printed as "State", // leading to a collapse of the capture set {State_1, State_2} to just {State}. - File("examples/pos/parametrized.effekt") + File("examples/pos/parametrized.effekt"), + // FIXME: Added in #1230, blocked on #1258 (https://github.com/effekt-lang/effekt/pull/1258) + // doesn't work since any program importing `json` fails due to string escapes. + examplesDir / "pos" / "with_val_and_else.effekt", + examplesDir / "pos" / "with_val_and_two_args_and_else.effekt", + examplesDir / "pos" / "with_val_pair_and_else.effekt", ) def positives: Set[File] = Set( From cbc8eca86865ca4af009bebade99bdc6722dc6e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Bene=C5=A1?= Date: Mon, 15 Dec 2025 20:33:55 +0100 Subject: [PATCH 11/11] Fix typo in ignored test name --- effekt/jvm/src/test/scala/effekt/core/ReparseTests.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/effekt/jvm/src/test/scala/effekt/core/ReparseTests.scala b/effekt/jvm/src/test/scala/effekt/core/ReparseTests.scala index d73a49cfd..9eb974ad3 100644 --- a/effekt/jvm/src/test/scala/effekt/core/ReparseTests.scala +++ b/effekt/jvm/src/test/scala/effekt/core/ReparseTests.scala @@ -45,7 +45,7 @@ class ReparseTests extends CoreTests { // FIXME: Added in #1230, blocked on #1258 (https://github.com/effekt-lang/effekt/pull/1258) // doesn't work since any program importing `json` fails due to string escapes. examplesDir / "pos" / "with_val_and_else.effekt", - examplesDir / "pos" / "with_val_and_two_args_and_else.effekt", + examplesDir / "pos" / "with_val_two_args_and_else.effekt", examplesDir / "pos" / "with_val_pair_and_else.effekt", )