aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBruce Hill <bruce@bruce-hill.com>2024-11-02 20:22:19 -0400
committerBruce Hill <bruce@bruce-hill.com>2024-11-02 20:22:19 -0400
commit0b7a0dd043a4c7ccfc924d618508d1edc0115e2f (patch)
tree6e1942840ab7e1e10bed111d8d5a012eacdf8b9b
parent985011aed89706e9a4b06e6c6f3239d53ac8e6e8 (diff)
Change reducers to use (OP: ...) syntax and return an optional value
-rw-r--r--ast.c4
-rw-r--r--ast.h2
-rw-r--r--compile.c37
-rw-r--r--docs/operators.md16
-rw-r--r--docs/reductions.md81
-rw-r--r--parse.c15
-rw-r--r--test/integers.tm8
-rw-r--r--test/iterators.tm4
-rw-r--r--test/minmax.tm2
-rw-r--r--test/reductions.tm29
-rw-r--r--typecheck.c16
11 files changed, 96 insertions, 118 deletions
diff --git a/ast.c b/ast.c
index 0593ccbf..8eca3147 100644
--- a/ast.c
+++ b/ast.c
@@ -139,8 +139,8 @@ CORD ast_to_xml(ast_t *ast)
T(While, "<While>%r%r</While>", optional_tagged("condition", data.condition), optional_tagged("body", data.body))
T(If, "<If>%r%r%r</If>", optional_tagged("condition", data.condition), optional_tagged("body", data.body), optional_tagged("else", data.else_body))
T(When, "<When><subject>%r</subject>%r%r</When>", ast_to_xml(data.subject), when_clauses_to_xml(data.clauses), optional_tagged("else", data.else_body))
- T(Reduction, "<Reduction>%r%r%r</Reduction>", optional_tagged("iterable", data.iter),
- optional_tagged("combination", data.combination), optional_tagged("fallback", data.fallback))
+ T(Reduction, "<Reduction>%r%r</Reduction>", optional_tagged("iterable", data.iter),
+ optional_tagged("combination", data.combination))
T(Skip, "<Skip>%r</Skip>", data.target)
T(Stop, "<Stop>%r</Stop>", data.target)
T(PrintStatement, "<PrintStatement>%r</PrintStatement>", ast_list_to_xml(data.to_print))
diff --git a/ast.h b/ast.h
index 1631b0f2..5d7f87d2 100644
--- a/ast.h
+++ b/ast.h
@@ -264,7 +264,7 @@ struct ast_s {
ast_t *else_body;
} When;
struct {
- ast_t *iter, *combination, *fallback;
+ ast_t *iter, *combination;
} Reduction;
struct {
const char *target;
diff --git a/compile.c b/compile.c
index 2d08f598..ff3cdf72 100644
--- a/compile.c
+++ b/compile.c
@@ -1061,7 +1061,7 @@ CORD compile_statement(env_t *env, ast_t *ast)
// If we're iterating over a comprehension, that's actually just doing
// one loop, we don't need to compile the comprehension as an array
- // comprehension. This is a common case for reducers like `(+) i*2 for i in 5`
+ // comprehension. This is a common case for reducers like `(+: i*2 for i in 5)`
// or `(and) x:is_good() for x in xs`
if (for_->iter->tag == Comprehension) {
auto comp = Match(for_->iter, Comprehension);
@@ -3151,7 +3151,8 @@ CORD compile(env_t *env, ast_t *ast)
}
case Reduction: {
auto reduction = Match(ast, Reduction);
- type_t *t = get_type(env, ast);
+ type_t *optional_t = get_type(env, ast);
+ type_t *t = Match(optional_t, OptionalType)->type;
CORD code = CORD_all(
"({ // Reduction:\n",
compile_declaration(t, "reduction"), ";\n"
@@ -3166,12 +3167,22 @@ CORD compile(env_t *env, ast_t *ast)
// For the special case of (or)/(and), we need to early out if we can:
CORD early_out = CORD_EMPTY;
- if (t->tag == BoolType && reduction->combination->tag == BinaryOp) {
+ if (reduction->combination->tag == BinaryOp) {
auto binop = Match(reduction->combination, BinaryOp);
- if (binop->op == BINOP_AND)
+ if (t->tag != BoolType && (binop->op == BINOP_EQ || binop->op == BINOP_NE
+ || binop->op == BINOP_LT || binop->op == BINOP_LE
+ || binop->op == BINOP_GT || binop->op == BINOP_GE))
+ code_err(ast, "Reductions are not supported for this type of infix operator");
+ else if ((t->tag != IntType || Match(t, IntType)->bits != TYPE_IBITS32) && binop->op == BINOP_CMP)
+ code_err(ast, "<> reductions are only supported for Int32 values");
+ else if (t->tag == BoolType && binop->op == BINOP_AND)
early_out = "if (!reduction) break;";
- else if (binop->op == BINOP_OR)
+ else if (t->tag == BoolType && binop->op == BINOP_OR)
early_out = "if (reduction) break;";
+ else if (t->tag == OptionalType && binop->op == BINOP_AND)
+ early_out = CORD_all("if (", check_null(t, "reduction"), ") break;");
+ else if (t->tag == OptionalType && binop->op == BINOP_OR)
+ early_out = CORD_all("if (!", check_null(t, "reduction"), ") break;");
}
body->__data.InlineCCode.code = CORD_all(
@@ -3183,21 +3194,7 @@ CORD compile(env_t *env, ast_t *ast)
early_out,
"}\n");
- CORD empty_handling;
- if (reduction->fallback) {
- type_t *fallback_type = get_type(scope, reduction->fallback);
- if (fallback_type->tag == AbortType || fallback_type->tag == ReturnType) {
- empty_handling = CORD_all("if (!has_value) ", compile_statement(env, reduction->fallback), "\n");
- } else {
- empty_handling = CORD_all("if (!has_value) reduction = ", compile(env, reduction->fallback), ";\n");
- }
- } else {
- empty_handling = CORD_asprintf("if (!has_value) fail_source(%r, %ld, %ld, \"This collection was empty!\");\n",
- CORD_quoted(ast->file->filename),
- (long)(reduction->iter->start - reduction->iter->file->text),
- (long)(reduction->iter->end - reduction->iter->file->text));
- }
- code = CORD_all(code, compile_statement(scope, loop), "\n", empty_handling, "reduction;})");
+ code = CORD_all(code, compile_statement(scope, loop), "\nhas_value ? ", promote_to_optional(t, "reduction"), " : ", compile_null(t), ";})");
return code;
}
case FieldAccess: {
diff --git a/docs/operators.md b/docs/operators.md
index 85e53242..4afa3ad3 100644
--- a/docs/operators.md
+++ b/docs/operators.md
@@ -50,13 +50,13 @@ reducers in action:
```tomo
>> nums := [10, 20, 30]
->> (+) nums
+>> (+: nums)!
= 60
->> (or) n > 15 for n in nums
+>> (or: n > 15 for n in nums)!
= yes
>> texts := ["one", "two", "three"]
->> (++) texts
+>> (++: texts)!
= "onetwothree"
```
@@ -75,7 +75,7 @@ if you use a reducer on something that has no values:
```tomo
>> nums := [:Int]
->> (+) nums
+>> (+: nums)!
Error: this collection was empty!
```
@@ -84,7 +84,7 @@ If you want to handle this case, you can either wrap it in a conditional
statement or you can provide a fallback option with `else` like this:
```tomo
->> (+) nums else: 0
+>> (+: nums) or 0
= 0
```
@@ -103,10 +103,10 @@ struct Foo(x,y:Int):
>> foos := [Foo(1, 2), Foo(-10, 20)]
->> (+).x foos
+>> (+.x: foos)
= -9
// Shorthand for:
->> (+) f.x for f in foos
+>> (+: f.x for f in foos)
= -9
>> (or):is_even() foos
@@ -114,7 +114,7 @@ struct Foo(x,y:Int):
// Shorthand for:
>> (or) f:is_even() for f in foos
->> (+).x:abs() foos
+>> (+.x:abs(): foos)
= 11
```
diff --git a/docs/reductions.md b/docs/reductions.md
index 143a59ad..b4e78624 100644
--- a/docs/reductions.md
+++ b/docs/reductions.md
@@ -1,14 +1,35 @@
# Reductions
In Tomo, reductions are a way to express the idea of folding or reducing a
-collection of values down to a single value. Reductions use an infix operator
-surrounded by parentheses, followed by a collection:
+collection of values down to a single value. Reductions use a parenthesized
+infix operator followed by a colon, followed by a collection:
```tomo
nums := [10, 20, 30]
-sum := (+) nums
+sum := (+: nums)
>> sum
-= 60
+= 60?
+```
+
+Reductions return an optional value which will be a null value if the thing
+being iterated over has no values. In such cases, the reduction is undefined.
+As with all optionals, you can use either the postfix `!` operator to perform
+a runtime check and error if there's a null value, or you can use `or` to
+provide a fallback value:
+
+```tomo
+nums := [:Int]
+sum := (+: nums)
+
+>> sum
+= !Int
+
+>> sum or 0
+= 0
+
+>> nums = [10, 20]
+>> (+: nums)!
+= 30
```
Reductions can be used as an alternative to generic functions like `sum()`,
@@ -17,19 +38,19 @@ Reductions can be used as an alternative to generic functions like `sum()`,
```tomo
# Sum:
->> (+) [10, 20, 30]
+>> (+: [10, 20, 30])!
= 60
# Product:
->> (*) [2, 3, 4]
+>> (*: [2, 3, 4])!
= 24
# Any:
->> (or) [no, yes, no]
+>> (or: [no, yes, no])!
= yes
# All:
->> (and) [no, yes, no]
+>> (and: [no, yes, no])!
= no
```
@@ -40,11 +61,11 @@ a collection using the `_min_` and `_max_` infix operators.
```tomo
# Get the maximum value:
->> (_max_) [10, 30, 20]
+>> (_max_: [10, 30, 20])!
= 30
# Get the minimum value:
->> (_min_) [10, 30, 20]
+>> (_min_: [10, 30, 20])!
= 10
```
@@ -55,11 +76,11 @@ maximum value _according to some feature_.
```tomo
# Get the longest text:
->> (_max_.length) ["z", "aaaaa", "mmm"]
+>> (_max_.length: ["z", "aaaaa", "mmm"])!
= "aaaaa"
# Get the number with the biggest absolute value:
->> (_max_:abs()) [1, -2, 3, -4]
+>> (_max_:abs(): [1, -2, 3, -4])!
= -4
```
@@ -71,42 +92,10 @@ while filtering out values or while applying a transformation:
```tomo
# Sum the lengths of these texts:
->> (+) t.length for t in ["a", "bc", "def"]
+>> (+: t.length for t in ["a", "bc", "def"])!
= 6
# Sum the primes between 1-100:
->> (+) i for i in 100 if i:is_prime()
+>> (+: i for i in 100 if i:is_prime())!
= 1060
```
-
-## Empty Collection Behavior
-
-If a collection has no members, the default behavior for a reduction is to
-create a runtime error and halt the program with an informative error message.
-If you instead want to provide a default fallback value, you can use `else:` to
-give one:
-
-```tomo
-empty := [:Int]
->> (+) empty else: -1
-= -1
-
->> (+) empty
-# Error: empty iterable!
-```
-
-You can also provide your own call to `fail()` or `exit()` with a custom error
-message, or a short-circuiting control flow statement (`return`, `stop`,
-`skip`) like this:
-
-```tomo
->> (_max_) things else: exit("No things!")
-
-for nums in num_arrays:
- product := (*) nums else: skip
- do_thing(product)
-
-func remove_best(things:[Thing]):
- best := (_max_.score) things else: return
- best:remove()
-```
diff --git a/parse.c b/parse.c
index afaebfa4..2ea8cd78 100644
--- a/parse.c
+++ b/parse.c
@@ -966,7 +966,7 @@ PARSER(parse_reduction) {
const char *start = pos;
if (!match(&pos, "(")) return NULL;
- spaces(&pos);
+ whitespace(&pos);
const char *combo_start = pos;
binop_e op = match_binary_operator(&pos);
if (op == BINOP_UNKNOWN) return NULL;
@@ -997,8 +997,8 @@ PARSER(parse_reduction) {
combination = NewAST(ctx->file, combo_start, pos, BinaryOp, .op=op, .lhs=lhs, .rhs=rhs);
}
- spaces(&pos);
- if (!match(&pos, ")")) return NULL;
+ whitespace(&pos);
+ if (!match(&pos, ":")) return NULL;
ast_t *iter = optional(ctx, &pos, parse_extended_expr);
if (!iter) return NULL;
@@ -1009,13 +1009,10 @@ PARSER(parse_reduction) {
suffixed = parse_comprehension_suffix(ctx, iter);
}
- ast_t *fallback = NULL;
- if (match_word(&pos, "else")) {
- expect_str(ctx, pos-strlen("else"), &pos, ":", "I expected a ':' for this 'else'");
- fallback = expect(ctx, pos-4, &pos, parse_expr, "I couldn't parse the expression after this 'else'");
- }
+ whitespace(&pos);
+ expect_closing(ctx, &pos, ")", "I wasn't able to parse the rest of this reduction");
- return NewAST(ctx->file, start, pos, Reduction, .iter=iter, .combination=combination, .fallback=fallback);
+ return NewAST(ctx->file, start, pos, Reduction, .iter=iter, .combination=combination);
}
ast_t *parse_index_suffix(parse_ctx_t *ctx, ast_t *lhs) {
diff --git a/test/integers.tm b/test/integers.tm
index eb0c3fed..8d34dfc0 100644
--- a/test/integers.tm
+++ b/test/integers.tm
@@ -89,7 +89,7 @@ func main():
= 11
>> 11:prev_prime()
= 7
- >> (and) p:is_prime() for p in [
+ >> (and: p:is_prime() for p in [
2, 3, 5, 7,
137372146048179869781170214707,
811418847921670560768224995279,
@@ -101,15 +101,15 @@ func main():
548605069630614185274710840981,
121475876690852432982324195553,
771958616175795150904761471637,
- ]
+ ])!
= yes
- >> (or) p:is_prime() for p in [
+ >> (or: p:is_prime() for p in [
-1, 0, 1, 4, 6,
137372146048179869781170214707*2,
811418847921670560768224995279*3,
292590241572454328697048860273*754893741683930091960170890717,
- ]
+ ])!
= no
>> Int(yes)
diff --git a/test/iterators.tm b/test/iterators.tm
index fca91b2d..d6bcfa13 100644
--- a/test/iterators.tm
+++ b/test/iterators.tm
@@ -19,7 +19,7 @@ func range(first:Int, last:Int -> func(->Int?)):
func main():
values := ["A", "B", "C", "D"]
- >> ((++) "($(foo.x)$(foo.y))" for foo in pairwise(values))
+ >> (++: "($(foo.x)$(foo.y))" for foo in pairwise(values))!
= "(AB)(BC)(CD)"
>> ["$(foo.x)$(foo.y)" for foo in pairwise(values)]
= ["AB", "BC", "CD"]
@@ -34,5 +34,5 @@ func main():
>> [i for i in range(5, 10)]
= [5, 6, 7, 8, 9, 10]
- >> (+) range(5, 10)
+ >> (+: range(5, 10))!
= 45
diff --git a/test/minmax.tm b/test/minmax.tm
index fa0e8bd8..a5dd984e 100644
--- a/test/minmax.tm
+++ b/test/minmax.tm
@@ -23,5 +23,5 @@ func main():
>> foos := [Foo(5, 1), Foo(5, 99), Foo(-999, -999)]
- >> (_max_) foos
+ >> (_max_: foos)!
= Foo(x=5, y=99)
diff --git a/test/reductions.tm b/test/reductions.tm
index 47f1612a..7bfe212a 100644
--- a/test/reductions.tm
+++ b/test/reductions.tm
@@ -1,25 +1,34 @@
struct Foo(x,y:Int)
func main():
- >> (+) [10, 20, 30]
+ >> (+: [10, 20, 30])
+ = 60?
+
+ >> (+: [:Int])
+ = !Int
+
+ >> (+: [10, 20, 30]) or 0
= 60
- >> (_max_) [3, 5, 2, 1, 4]
- = 5
+ >> (+: [:Int]) or 0
+ = 0
+
+ >> (_max_: [3, 5, 2, 1, 4])
+ = 5?
- >> (_max_:abs()) [1, -10, 5]
- = -10
+ >> (_max_:abs(): [1, -10, 5])
+ = -10?
- >> (_max_) [Foo(0, 0), Foo(1, 0), Foo(0, 10)]
+ >> (_max_: [Foo(0, 0), Foo(1, 0), Foo(0, 10)])!
= Foo(x=1, y=0)
- >> (_max_.y) [Foo(0, 0), Foo(1, 0), Foo(0, 10)]
+ >> (_max_.y: [Foo(0, 0), Foo(1, 0), Foo(0, 10)])!
= Foo(x=0, y=10)
- >> (_max_.y:abs()) [Foo(0, 0), Foo(1, 0), Foo(0, 10), Foo(0, -999)]
+ >> (_max_.y:abs(): [Foo(0, 0), Foo(1, 0), Foo(0, 10), Foo(0, -999)])!
= Foo(x=0, y=-999)
!! (or) and (and) have early out behavior:
- >> (or) i == 3 for i in 9999999999999999999999999999
+ >> (or: i == 3 for i in 9999999999999999999999999999)!
= yes
- >> (and) i < 10 for i in 9999999999999999999999999999
+ >> (and: i < 10 for i in 9999999999999999999999999999)!
= no
diff --git a/typecheck.c b/typecheck.c
index 9818ad33..bc3c7982 100644
--- a/typecheck.c
+++ b/typecheck.c
@@ -1054,21 +1054,7 @@ type_t *get_type(env_t *env, ast_t *ast)
default: code_err(reduction->iter, "I don't know how to do a reduction over %T values", iter_t);
}
- env_t *scope = fresh_scope(env);
- set_binding(scope, "$reduction", new(binding_t, .type=value_t));
- set_binding(scope, "$iter_value", new(binding_t, .type=value_t));
- type_t *t = get_type(scope, reduction->combination);
- if (!reduction->fallback)
- return t;
- type_t *fallback_t = get_type(env, reduction->fallback);
- if (fallback_t->tag == AbortType || fallback_t->tag == ReturnType)
- return t;
- else if (can_promote(fallback_t, t))
- return t;
- else if (can_promote(t, fallback_t))
- return fallback_t;
- else
- return NULL;
+ return Type(OptionalType, .type=value_t);
}
case UpdateAssign: