diff options
Diffstat (limited to 'lib/core/control_flow.nom')
| -rw-r--r-- | lib/core/control_flow.nom | 489 |
1 files changed, 489 insertions, 0 deletions
diff --git a/lib/core/control_flow.nom b/lib/core/control_flow.nom new file mode 100644 index 0000000..b0c4f27 --- /dev/null +++ b/lib/core/control_flow.nom @@ -0,0 +1,489 @@ +#!/usr/bin/env nomsu -V6.14 +# + This file contains compile-time actions that define basic control flow structures + like "if" statements and loops. + +use "core/metaprogramming" +use "core/operators" + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +# No-Op +test: + do nothing +(do nothing) compiles to "" + +# Conditionals +test: + if (no): + fail "conditional fail" + +(if $condition $if_body) compiles to (" + if \($condition as lua expr) then + \($if_body as lua) + end +") + +test: + unless (yes): + fail "conditional fail" + +(unless $condition $unless_body) parses as (if (not $condition) $unless_body) +[ + if $condition $if_body else $else_body, unless $condition $else_body else $if_body +] all compile to (" + if \($condition as lua expr) then + \($if_body as lua) + else + \($else_body as lua) + end +") + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +# Conditional expression (ternary operator) +# Note: this uses a function instead of "(condition and if_expr or else_expr)" + because that breaks if $if_expr is falsey, e.g. "x < 5 and false or 99" +test: + assume ((1 if (yes) else 2) == 1) + assume ((1 if (no) else 2) == 2) + +[ + $when_true_expr if $condition else $when_false_expr + $when_true_expr if $condition otherwise $when_false_expr + $when_false_expr unless $condition else $when_true_expr + $when_false_expr unless $condition then $when_true_expr +] all compile to: + # If $when_true_expr is guaranteed to be truthy, we can use Lua's idiomatic + equivalent of a conditional expression: (cond and if_true or if_false) + if {.Text, .List, .Dict, .Number}.($when_true_expr.type): + return + Lua (" + (\($condition as lua expr) and \($when_true_expr as lua expr) or \ + ..\($when_false_expr as lua expr)) + ") + ..else: + # Otherwise, need to do an anonymous inline function (yuck, too bad lua + doesn't have a proper ternary operator!) + To see why this is necessary consider: (random()<.5 and false or 99) + return + Lua (" + ((function() + if \($condition as lua expr) then + return \($when_true_expr as lua expr) + else + return \($when_false_expr as lua expr) + end + end)()) + ") + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +# GOTOs +test: + $i = 0 + --- $loop --- + $i += 1 + unless ($i == 10): + go to $loop + assume ($i == 10) + --- (Loop) --- + $i -= 1 + unless ($i == 0): + go to (Loop) + assume ($i == 0) + +(--- $label ---) compiles to (" + ::label_\( + ($label.stub, as lua id) if ($label.type == "Action") else + $label as lua identifier + ):: +") + +(go to $label) compiles to (" + goto label_\( + ($label.stub, as lua id) if ($label.type == "Action") else + $label as lua identifier + ) +") + +# Basic loop control +(stop $var) compiles to: + if $var: + return (Lua "goto stop_\($var as lua identifier)") + ..else: + return (Lua "break") + +(do next $var) compiles to: + if $var: + return (Lua "goto continue_\($var as lua identifier)") + ..else: + return (Lua "goto continue") + +(---stop $var ---) compiles to "::stop_\($var as lua identifier)::" +(---next $var ---) compiles to "::continue_\($var as lua identifier)::" + +# While loops +test: + $x = 0 + repeat while ($x < 10): $x += 1 + assume ($x == 10) + repeat while ($x < 20): stop + assume ($x == 10) + repeat while ($x < 20): + $x += 1 + if (yes): + do next + fail "Failed to 'do next'" + assume ($x == 20) + +(repeat while $condition $body) compiles to: + $lua = + Lua (" + while \($condition as lua expr) do + \($body as lua) + ") + + if ($body has subtree \(do next)): + $lua, add "\n ::continue::" + + $lua, add "\nend --while-loop" + return $lua + +(repeat $body) parses as (repeat while (yes) $body) +(repeat until $condition $body) parses as (repeat while (not $condition) $body) + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +test: + $nums = [] + for $x in 1 to 5: + $nums, add $x + assume ($nums == [1, 2, 3, 4, 5]) + $nums = [] + for $x in 1 to 5 via 2: + $nums, add $x + assume ($nums == [1, 3, 5]) + $nums = [] + for $outer in 1 to 100: + for $inner in $outer to ($outer + 2): + if ($inner == 2): + $nums, add -2 + do next $inner + $nums, add $inner + if ($inner == 5): + stop $outer + assume ($nums == [1, -2, 3, -2, 3, 4, 3, 4, 5]) + +# Numeric range for loops +[ + for $var in $start to $stop by $step $body + for $var in $start to $stop via $step $body +] all compile to: + # This uses Lua's approach of only allowing loop-scoped variables in a loop + $lua = + Lua (" + for \($var as lua identifier)=\($start as lua expr),\($stop as lua expr),\ + ..\($step as lua expr) do + ") + $lua, add "\n " ($body as lua) + if ($body has subtree \(do next)): + $lua, add "\n ::continue::" + + if ($body has subtree \(do next $var)): + $lua, add "\n " (\(---next $var ---) as lua) + + $lua, add "\nend -- numeric for " ($var as lua identifier) " loop" + if ($body has subtree \(stop $var)): + $lua = + Lua (" + do -- scope for (stop \($var as lua identifier)) + \$lua + \(\(---stop $var ---) as lua) + end -- scope for (stop \($var as lua identifier)) + ") + return $lua + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +(for $var in $start to $stop $body) parses as + for $var in $start to $stop via 1 $body + +test: + $x = 0 + repeat 5 times: + $x += 1 + assume $x == 5 + +(repeat $n times $body) parses as (for (=lua "_XXX_") in 1 to $n $body) +test: + $a = [10, 20, 30, 40, 50] + $b = [] + for $x in $a: + $b, add $x + assume ($a == $b) + $b = [] + for $x in $a: + if ($x == 10): + do next $x + + if ($x == 50): + stop $x + + $b, add $x + assume ($b == [20, 30, 40]) + +# For-each loop (lua's "ipairs()") +(for $var in $iterable at $i $body) compiles to: + # This uses Lua's approach of only allowing loop-scoped variables in a loop + $lua = + Lua (" + for \($i as lua identifier),\($var as lua identifier) in ipairs(\($iterable as lua expr)) do + \; + ") + $lua, add ($body as lua) + if ($body has subtree \(do next)): + $lua, add "\n ::continue::" + + if ($body has subtree \(do next $var)): + $lua, add "\n " (\(---next $var ---) as lua) + + $lua, add "\nend --for \($var as lua identifier) loop" + if ($body has subtree \(stop $var)): + $inner_lua = $lua + $lua = (Lua "do -- scope for stopping for-loop\n ") + $lua, add $inner_lua "\n " + $lua, add (\(---stop $var ---) as lua) + $lua, add "\nend -- end of scope for stopping for-loop" + return $lua + +(for $var in $iterable $body) parses as + for $var in $iterable at (=lua "__") $body + +test: + $d = {.a = 10, .b = 20, .c = 30, .d = 40, .e = 50} + $result = [] + for $k = $v in $d: + if ($k == "a"): + do next $k + + if ($v == 20): + do next $v + + $result, add "\$k = \$v" + assume (($result sorted) == ["c = 30", "d = 40", "e = 50"]) + +# Dict iteration (lua's "pairs()") +[for $key = $value in $iterable $body, for $key $value in $iterable $body] +..all compile to: + $lua = + Lua (" + for \($key as lua identifier),\($value as lua identifier) in pairs(\ + ..\($iterable as lua expr)) do + ") + $lua, add "\n " ($body as lua) + if ($body has subtree \(do next)): + $lua, add "\n ::continue::" + + if ($body has subtree \(do next $key)): + $lua, add "\n " (\(---next $key ---) as lua) + + if ($body has subtree \(do next $value)): + $lua, add "\n " (\(---next $value ---) as lua) + + $lua, add "\nend --foreach-loop" + $stop_labels = (Lua "") + if ($body has subtree \(stop $key)): + $stop_labels, add "\n" (\(---stop $key ---) as lua) + + if ($body has subtree \(stop $value)): + $stop_labels, add "\n" (\(---stop $value ---) as lua) + + if ((size of "\$stop_labels") > 0): + $inner_lua = $lua + $lua = (Lua "do -- scope for stopping for $ = $ loop\n ") + $lua, add $inner_lua $stop_labels "\nend" + + return $lua + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +test: + when: + (1 == 2) (100 < 0): + fail "bad conditional" + (1 == 0) (1 == 1) $not_a_variable.x: do nothing + (1 == 1): + fail "bad conditional" + + (1 == 2): + fail "bad conditional" + + else: + fail "bad conditional" + +# Multi-branch conditional (if..elseif..else) +(when $body) compiles to: + $code = (Lua "") + $clause = "if" + $else_allowed = (yes) + unless ($body.type is "Block"): + compile error at $body "'if' expected a Block, but got a \($body.type)." + "Perhaps you forgot to put a ':' after 'if'?" + + for $line in $body: + unless + (($line.type is "Action") and ((size of $line) >= 2)) and + $line.(size of $line) is "Block" syntax tree + ..: + compile error at $line "Invalid line for the body of an 'if' block." (" + Each line should contain one or more conditional expressions followed by a block, or "else"\ + .. followed by a block. + ") + $action = $line.(size of $line) + if (($line.1 is "else") and ((size of $line) == 2)): + unless $else_allowed: + compile error at $line "You can't have two 'else' blocks." + "Merge all of the 'else' blocks together." + + unless ((size of "\$code") > 0): + compile error at $line + .."You can't have an 'else' block without a preceding condition" (" + If you want the code in this block to always execute, you don't need a conditional block \ + ..around it. Otherwise, make sure the 'else' block comes last. + ") + + $code, add "\nelse\n " ($action as lua) + $else_allowed = (no) + ..else: + $code, add $clause " " + for $i in 1 to ((size of $line) - 1): + if ($i > 1): + $code, add " or " + $code, add ($line.$i as lua expr) + $code, add " then\n " ($action as lua) + $clause = "\nelseif" + + if ((size of "\$code") == 0): + compile error at $body "'if' block has an empty body." + "This means nothing would happen, so the 'if' block should be deleted." + + $code, add "\nend --when" + return $code + +test: + if 5 is: + 1 2 3: + fail "bad switch statement" + + 4 5: + do nothing + + 5 6: + fail "bad switch statement" + + else: + fail "bad switch statement" + +# Switch statement +[if $branch_value is $body, when $branch_value is $body] all compile to: + $code = (Lua "") + $clause = "if" + $else_allowed = (yes) + define mangler + unless ($body.type is "Block"): + compile error at $body "'if' expected a Block, but got a \($body.type)" + "Perhaps you forgot to put a ':' after the 'is'?" + + for $line in $body: + unless + (($line.type is "Action") and ((size of $line) >= 2)) and + $line.(size of $line) is "Block" syntax tree + ..: + compile error at $line "Invalid line for 'if' block." (" + Each line should contain expressions followed by a block, or "else" followed by a block + ") + $action = $line.(size of $line) + if (($line.1 is "else") and ((size of $line) == 2)): + unless $else_allowed: + compile error at $line "You can't have two 'else' blocks." + "Merge all of the 'else' blocks together." + + unless ((size of "\$code") > 0): + compile error at $line + .."You can't have an 'else' block without a preceding condition" (" + If you want the code in this block to always execute, you don't need a conditional block \ + ..around it. Otherwise, make sure the 'else' block comes last. + ") + + $code, add "\nelse\n " ($action as lua) + $else_allowed = (no) + ..else: + $code, add $clause " " + for $i in 1 to ((size of $line) - 1): + if ($i > 1): + $code, add " or " + $code, add "\(mangle "branch value") == " ($line.$i as lua expr) + $code, add " then\n " ($action as lua) + $clause = "\nelseif" + + if ((size of "\$code") == 0): + compile error at $body "'if' block has an empty body." + "This means nothing would happen, so the 'if' block should be deleted." + + $code, add "\nend --when" + return + Lua (" + do --if $ is... + local \(mangle "branch value") = \($branch_value as lua expr) + \$code + end -- if $ is... + ") + +# Do/finally +(do $action) compiles to (" + do + \($action as lua) + end -- do +") + +test: + assume ((result of: return 99) == 99) + +# Inline thunk: +(result of $body) compiles to "\(\(-> $body) as lua)()" +test: + $t = [1, [2, [[3], 4], 5, [[[6]]]]] + $flat = [] + for $ in recursive $t: + if ((lua type of $) is "table"): + for $2 in $: + recurse $ on $2 + ..else: + $flat, add $ + assume (sorted $flat) == [1, 2, 3, 4, 5, 6] + +# Recurion control flow +(recurse $v on $x) compiles to + Lua "table.insert(_stack_\($v as lua expr), \($x as lua expr))" +(for $var in recursive $structure $body) compiles to: + $lua = + Lua (" + do + local _stack_\($var as lua expr) = List{\($structure as lua expr)} + while #_stack_\($var as lua expr) > 0 do + \($var as lua expr) = table.remove(_stack_\($var as lua expr), 1) + \($body as lua) + ") + + if ($body has subtree \(do next)): + $lua, add "\n ::continue::" + + if ($body has subtree \(do next $var)): + $lua, add "\n \(\(---next $var ---) as lua)" + + $lua, add "\n end -- Recursive loop" + if ($body has subtree \(stop $var)): + $lua, add "\n \(\(---stop $var ---) as lua)" + $lua, add "\nend -- Recursive scope" + return $lua |
