Change reducers to use (OP: ...) syntax and return an optional value
This commit is contained in:
parent
985011aed8
commit
0b7a0dd043
4
ast.c
4
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))
|
||||
|
2
ast.h
2
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;
|
||||
|
37
compile.c
37
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: {
|
||||
|
@ -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
|
||||
```
|
||||
|
||||
|
@ -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
15
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) {
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
16
typecheck.c
16
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:
|
||||
|
Loading…
Reference in New Issue
Block a user