#!/usr/bin/env nomsu -V7.0.0
###
    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
")

(else $) compiles to:
    at (this tree) fail ("
        Compile error: This 'else' is not connected to any 'if' or 'unless' condition.
        Hint: You should probably have a ".." in front of the "else", to indicate that it's attached \
        ..to the previous condition.
    ")

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

### 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, contains \(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)

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

### For-each loop (lua's "ipairs()")
(for $var in $iterable $body) compiles to:
    unless $var:
        at (this tree) fail "No var here"
    
    ### This uses Lua's approach of only allowing loop-scoped variables in a loop
    if (($var.type == "Action") and ($var.stub == "1 =")):
        [$key, $value] = [$var.1, $var.3]
    ..else:
        [$key, $value] = [nil, $var]
    
    unless $value:
        at (this tree) fail "No value here"
    
    ### Numeric loop:
    if (($iterable.type == "Action") and (($iterable, get stub) == "1 to")):
        [$start, $stop] = [$iterable.1, $iterable.3]
        $loop =
            Lua ("
                local _start = \($start as lua expr)
                for \($value as lua identifier)=_start,\($stop as lua expr) do
            ")
        
        if $key:
            $loop, add ("
                
                    local \($key as lua identifier) = \($value as lua identifier) - _start + 1;
            ")
        
        go to (loop set)
    
    ### Numeric loop with step:
    if (($iterable.type == "Action") and (($iterable, get stub) == "1 to 2 by")):
        [$start, $stop, $step] = [$iterable.1, $iterable.3, $iterable.5]
        $loop =
            Lua ("
                local _start, _step = \($start as lua expr), \($step as lua expr);
                for \($value as lua identifier)=_start,\($stop as lua expr),_step do
            ")
        
        if $key:
            $loop, add ("
                
                    local \($key as lua identifier) = (\($value as lua identifier) - _start)/_step + 1
            ")
        
        go to (loop set)
    
    ### for $ in (...):
    if $key:
        $loop =
            Lua ("
                for \($key as lua identifier),\($value as lua identifier) in pairs(\
                ..\($iterable as lua expr)) do
            ")
    ..else:
        $loop =
            Lua "for _i,\($value as lua identifier) in _ipairs(\($iterable as lua expr)) do"
    --- (loop set) ---
    
    ### TODO: don't always wrap in block
    $lua =
        Lua ("
            do -- for-loop
                \$loop
                    \;
        ")
    $lua, add ($body as lua)
    if ($body, contains \(do next)):
        $lua, add "\n        ::continue::"
    
    if ($key and ($body, contains \(do next \$key))):
        $lua, add "\n        " (\(---next \$key ---) as lua)
    
    if ($body, contains \(do next \$value)):
        $lua, add "\n        " (\(---next \$value ---) as lua)
    
    $lua, add "\n    end"
    if ($key and ($body, contains \(stop \$key))):
        $lua, add "\n    " (\(---stop \$key ---) as lua)
    
    if ($body, contains \(stop \$value)):
        $lua, add "\n    " (\(---stop \$value ---) as lua)
    
    $lua, add "\nend -- for-loop"
    $lua, remove free vars
        [($value as lua identifier, text), $key and ($key as lua identifier, text)]
    return $lua

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"])

### Numeric range for loops
test:
    assume ([: for $ in (1 to 5): add $] == [1, 2, 3, 4, 5])
    assume ([: for $ in (1 to 5 by 2): add $] == [1, 3, 5])
    assume ([: for $ in (5 to 1): add $] == [])
    $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])

### repeat $n times is a shorthand:
test:
    $x = 0
    repeat 5 times:
        $x += 1
    assume $x == 5
(repeat $n times $body) parses as (for (=lua "_i") in (1 to $n by 1) $body)

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

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 == "Block"):
        at $body fail ("
            Compile error: 'if' expected a Block, but got a \($body.type).
            Hint: Perhaps you forgot to put a ':' after 'if'?
        ")
    
    for $line in $body:
        unless
            (($line.type == "Action") and (#$line >= 2)) and
                $line.(#$line) is "Block" syntax tree
        ..:
            at $line fail ("
                Compile error: Invalid line for the body of an 'if' block.
                Hint: Each line should contain one or more conditional expressions followed by a block, \
                ..or "else" followed by a block.
            ")
        $action = $line.(#$line)
        if (($line.1 == "else") and (#$line == 2)):
            unless $else_allowed:
                at $line fail ("
                    Compile error: You can't have two 'else' blocks.
                    Hint: Merge all of the 'else' blocks together.
                ")
            
            unless (#"\$code" > 0):
                at $line fail ("
                    Compile error: You can't have an 'else' block without a preceding condition.
                    Hint: 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 (#$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 (#"\$code" == 0):
        at $body fail ("
            Compile error: 'if' block has an empty body.
            Hint: 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 == "Block"):
        at $body fail ("
            Compile error: 'if' expected a Block, but got a \($body.type).
            Hint: Perhaps you forgot to put a ':' after the 'is'?
        ")
    
    for $line in $body:
        unless
            (($line.type == "Action") and (#$line >= 2)) and
                $line.(#$line) is "Block" syntax tree
        ..:
            at $line fail ("
                Compile error: Invalid line for 'if' block.
                Hint: Each line should contain expressions followed by a block, or "else" followed by a block.
            ")
        $action = $line.(#$line)
        if (($line.1 == "else") and (#$line == 2)):
            unless $else_allowed:
                at $line fail ("
                    Compile error: You can't have two 'else' blocks.
                    Hint: Merge all of the 'else' blocks together.
                ")
            
            unless (#"\$code" > 0):
                at $line fail ("
                    Compile error: You can't have an 'else' block without a preceding condition.
                    Hint: 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 (#$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 (#"\$code" == 0):
        at $body fail ("
            Compile error: 'if' block has an empty body.
            Hint: 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 $) == "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) = a_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, contains \(do next)):
        $lua, add "\n        ::continue::"
    
    if ($body, contains \(do next \$var)):
        $lua, add "\n        \(\(---next \$var ---) as lua)"
    
    $lua, add "\n    end -- Recursive loop"
    if ($body, contains \(stop \$var)):
        $lua, add "\n        \(\(---stop \$var ---) as lua)"
    $lua, add "\nend -- Recursive scope"
    return $lua