Change reducers to use (OP: ...) syntax and return an optional value

This commit is contained in:
Bruce Hill 2024-11-02 20:22:19 -04:00
parent 985011aed8
commit 0b7a0dd043
11 changed files with 96 additions and 118 deletions

4
ast.c
View File

@ -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))

2
ast.h
View File

@ -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;

View File

@ -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: {

View File

@ -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
```

View File

@ -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()
```

15
parse.c
View File

@ -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) {

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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_:abs()) [1, -10, 5]
= -10
>> (_max_: [3, 5, 2, 1, 4])
= 5?
>> (_max_) [Foo(0, 0), Foo(1, 0), Foo(0, 10)]
>> (_max_:abs(): [1, -10, 5])
= -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

View File

@ -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: