#!/usr/bin/env nomsu -V7.0.0
###
    This file contains definitions of operators like "+" and "and".
    
use "core/metaprogramming"

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

test:
    assume (all [1 < 2, 2 > 1, 1 <= 2, 2 >= 1, 1 == 1, 1 != 2])

### Comparison Operators
($x < $y) compiles to "(\($x as lua expr) < \($y as lua expr))"
($x > $y) compiles to "(\($x as lua expr) > \($y as lua expr))"
($x <= $y) compiles to "(\($x as lua expr) <= \($y as lua expr))"
($x >= $y) compiles to "(\($x as lua expr) >= \($y as lua expr))"
($a == $b) compiles to "(\($a as lua expr) == \($b as lua expr))"
[$a not= $b, $a != $b] all compile to "(\($a as lua expr) ~= \($b as lua expr))"

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

test:
    $x = 10
    assume ($x == 10)
    [$x, $y] = [10, 20]
    unless (($x == 10) and ($y == 20)):
        fail "mutli-assignment failed."
    [$x, $y] = [$y, $x]
    unless (($y == 10) and ($x == 20)):
        fail "swapping vars failed."
    $vals = [4, 5]
    [$x, $y] = (unpack $vals)
    unless (($x == 4) and ($y == 5)):
        fail "unpacking failed"

### Variable assignment operator
($var = $value) compiles to:
    lua> ("
        local lua = LuaCode()
        if \$var.type == "List" then
            for i, \$assignment in ipairs(\$var) do
                if i > 1 then lua:add(", ") end
                local assignment_lua = \($assignment as lua expr)
                lua:add(assignment_lua)
                if \$assignment.type == 'Var' and \$assignment[1].type ~= "MethodCall" then
                    lua:add_free_vars({assignment_lua:text()})
                end
            end
            lua:add(' = ')
            if \$value.type == "List" then
                if #\$value ~= #\$var then
                    compile_error_at(\$value,
                        "This assignment has too "..(#\$value > #\$var and "many" or "few").." values.",
                        "Make sure it has the same number of values on the left and right hand side \
        ..of the '=' operator.")
                end
                for i, \$val in ipairs(\$value) do
                    if i > 1 then lua:add(", ") end
                    local val_lua = \($val as lua expr)
                    lua:add(val_lua)
                end
                lua:add(";")
            else
                lua:add(\($value as lua expr), ';')
            end
        else
            local var_lua = \($var as lua expr)
            lua:add(var_lua)
            if \$var.type == 'Var' and \$var[1].type ~= "MethodCall" then
                lua:add_free_vars({var_lua:text()})
            end
            lua:add(' = ', \($value as lua expr), ';')
        end
        return lua
    ")

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

test:
    [$x, $y] = [1, 2]
    with [$z, $x = 999]:
        assume $z == (nil)
        $z = 999
        unless ($z == 999):
            fail "'with' failed."
        
        unless ($x == 999):
            fail "'with' assignment failed."
    
    unless ($x == 1):
        fail "'with' scoping failed"
    
    unless ($z == (nil)):
        fail "'with' scoping failed"

(with $assignments $body) compiles to:
    lua> ("
        local \$defs = LuaCode()
        for i, \$item in ipairs(\$assignments) do
            if i > 1 then \$defs:add("\\n") end
            local item_lua = \($item as lua)
            if \$item.type == 'Action' and \$item.stub == '1 =' then
                item_lua:remove_free_vars(item_lua.free_vars)
            end
            \$defs:add("local ", item_lua, ";")
        end
    ")
    
    return Lua ("
        do
            \$defs
            \($body as lua)
        end -- 'with' block
    ")

### Math Operators
test:
    unless ((5 wrapped around 2) == 1):
        fail "mod not working"

[$x wrapped around $y, $x mod $y, $x % $y] all compile to
    "((\($x as lua expr)) % (\($y as lua expr)))"

### 3-part chained comparisons
### (uses a lambda to avoid re-evaluating middle value, while still being an expression)
test:
    $calls = 0
    (one) means:
        external:
            $calls = ($calls + 1)
        return 1
    
    unless (0 <= (one) <= 2):
        fail "Three-way chained comparison failed."
    
    unless ($calls == 1):
        fail "Three-way comparison evaluated middle value multiple times"

($x < $y < $z) parses as ((($a $b $c) -> (($a < $b) and ($b < $c))) $x $y $z)
($x <= $y < $z) parses as ((($a $b $c) -> (($a <= $b) and ($b < $c))) $x $y $z)
($x < $y <= $z) parses as ((($a $b $c) -> (($a < $b) and ($b <= $c))) $x $y $z)
($x <= $y <= $z) parses as ((($a $b $c) -> (($a <= $b) and ($b <= $c))) $x $y $z)
($x > $y > $z) parses as ((($a $b $c) -> (($a > $b) and ($b > $c))) $x $y $z)
($x >= $y > $z) parses as ((($a $b $c) -> (($a >= $b) and ($b > $c))) $x $y $z)
($x > $y >= $z) parses as ((($a $b $c) -> (($a > $b) and ($b >= $c))) $x $y $z)
($x >= $y >= $z) parses as ((($a $b $c) -> (($a >= $b) and ($b >= $c))) $x $y $z)

### TODO: optimize for common case where x,y,z are all either variables or number literals
### Boolean Operators
test:
    (barfer) means (fail "short circuiting failed")
    assume (((no) and (barfer)) == (no))
    assume ((no) or (yes))
    assume ((yes) or (barfer))
($x and $y) compiles to "(\($x as lua expr) and \($y as lua expr))"
($x or $y) compiles to "(\($x as lua expr) or \($y as lua expr))"

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

### Bitwise Operators
### These can break if running the precompiled code from another Lua version:
lua> ("
    if \(at compilation $(LUA VERSION)) ~= \$(LUA VERSION) then
        \(
            fail ("
                This code was precompiled with \(at compilation $(LUA VERSION)), but it is being run with \$(LUA VERSION)\
                ... This will cause problems with bitwise operations.
            ")
        )
    end
")

### TODO: implement OR, XOR, AND for multiple operands?
test:
    assume (~(~5) == 5)
    assume ((1 | 4) == 5)
    assume ((1 ~ 3) == 2)
    assume ((1 & 3) == 1)
    assume ((1 << 2) == 4)
    assume ((4 >> 2) == 1)

### Lua 5.3 introduced bit operators like | and &. Use them when possible, otherwise
    fall back to bit.bor(), bit.band(), etc.
lua> "if \((is jit) or ($(LUA API) == "Lua 5.2")) then"
[NOT $, ~$] all compile to "Bit.bnot(\($ as lua expr))"
[$x OR $y, $x | $y] all compile to
    "Bit.bor(\($x as lua expr), \($y as lua expr))"

[$x XOR $y, $x ~ $y] all compile to
    "Bit.bxor(\($x as lua expr), \($y as lua expr))"

[$x AND $y, $x & $y] all compile to
    "Bit.band(\($x as lua expr), \($y as lua expr))"

[$x LSHIFT $shift, $x << $shift] all compile to
    "Bit.lshift(\($x as lua expr), \($shift as lua expr))"

[$x RSHIFT $shift, $x >> $shift] all compile to
    "Bit.rshift(\($x as lua expr), \($shift as lua expr))"

lua> "else"
[NOT $, ~$] all compile to "(~(\($ as lua expr)))"
[$x OR $y, $x | $y] all compile to "(\($x as lua expr) | \($y as lua expr))"
[$x XOR $y, $x ~ $y] all compile to "(\($x as lua expr) ~ \($y as lua expr))"
[$x AND $y, $x & $y] all compile to "(\($x as lua expr) & \($y as lua expr))"
[$x LSHIFT $shift, $x << $shift] all compile to
    "(\($x as lua expr) << \($shift as lua expr))"

[$x RSHIFT $shift, $x >> $shift] all compile to
    "(\($x as lua expr) >> \($shift as lua expr))"

lua> "end"

### Unary operators
test:
    assume ((- 5) == -5)
    assume ((not (yes)) == (no))
-$ compiles to "(-(\($ as lua expr)))"
(not $) compiles to "(not \($ as lua expr))"
test:
    assume ((size of [1, 2, 3]) == 3)
    assume (#[1, 2, 3] == 3)

### Length
[#$list, size of $list] all compile to:
    lua> ("
        local list_lua = \($list as lua expr)
        if list_lua:text() == "..." then
            return LuaCode("select('#', ...)")
        end
        return LuaCode("(#", list_lua, ")")
    ")
($list is empty) parses as (#$list == 0)

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

### Update operators
test:
    $x = 1
    $x += 1
    unless ($x == 2):
        fail "+= failed"
    $x *= 2
    unless ($x == 4):
        fail "*= failed"
    wrap $x around 3
    unless ($x == 1):
        fail "wrap around failed"

($var += $) parses as ($var = (($var or 0) + $))
($var -= $) parses as ($var = (($var or 0) - $))
($var *= $) parses as ($var = (($var or 1) * $))
($var /= $) parses as ($var = ($var / $))
($var ^= $) parses as ($var = ($var ^ $))
($var and= $) parses as ($var = ($var and $))
($var or= $) parses as ($var = ($var or $))
(wrap $var around $) parses as ($var = ($var wrapped around $))