aboutsummaryrefslogtreecommitdiff
path: root/lib/core/control_flow.nom
diff options
context:
space:
mode:
Diffstat (limited to 'lib/core/control_flow.nom')
-rw-r--r--lib/core/control_flow.nom489
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