diff options
| author | Bruce Hill <bruce@bruce-hill.com> | 2024-11-02 20:22:19 -0400 |
|---|---|---|
| committer | Bruce Hill <bruce@bruce-hill.com> | 2024-11-02 20:22:19 -0400 |
| commit | 0b7a0dd043a4c7ccfc924d618508d1edc0115e2f (patch) | |
| tree | 6e1942840ab7e1e10bed111d8d5a012eacdf8b9b | |
| parent | 985011aed89706e9a4b06e6c6f3239d53ac8e6e8 (diff) | |
Change reducers to use (OP: ...) syntax and return an optional value
| -rw-r--r-- | ast.c | 4 | ||||
| -rw-r--r-- | ast.h | 2 | ||||
| -rw-r--r-- | compile.c | 37 | ||||
| -rw-r--r-- | docs/operators.md | 16 | ||||
| -rw-r--r-- | docs/reductions.md | 81 | ||||
| -rw-r--r-- | parse.c | 15 | ||||
| -rw-r--r-- | test/integers.tm | 8 | ||||
| -rw-r--r-- | test/iterators.tm | 4 | ||||
| -rw-r--r-- | test/minmax.tm | 2 | ||||
| -rw-r--r-- | test/reductions.tm | 29 | ||||
| -rw-r--r-- | typecheck.c | 16 |
11 files changed, 96 insertions, 118 deletions
@@ -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)) @@ -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; @@ -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() -``` @@ -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: |
