From c1c32688a4afc43f6addb99b8b5fa878944a70e3 Mon Sep 17 00:00:00 2001 From: Bruce Hill Date: Mon, 14 Jan 2019 15:42:48 -0800 Subject: Overhaul in progress, mostly working. Moved all the nomsu packages into lib/, including core/*. Changes to how nomsu environments and importing work. --- lib/core/collections.nom | 145 +++++++++++++ lib/core/control_flow.nom | 489 +++++++++++++++++++++++++++++++++++++++++++ lib/core/coroutines.nom | 40 ++++ lib/core/errors.nom | 131 ++++++++++++ lib/core/id.nom | 66 ++++++ lib/core/init.nom | 11 + lib/core/io.nom | 29 +++ lib/core/math.nom | 212 +++++++++++++++++++ lib/core/metaprogramming.nom | 459 ++++++++++++++++++++++++++++++++++++++++ lib/core/operators.nom | 263 +++++++++++++++++++++++ lib/core/text.nom | 78 +++++++ 11 files changed, 1923 insertions(+) create mode 100644 lib/core/collections.nom create mode 100644 lib/core/control_flow.nom create mode 100644 lib/core/coroutines.nom create mode 100644 lib/core/errors.nom create mode 100644 lib/core/id.nom create mode 100644 lib/core/init.nom create mode 100644 lib/core/io.nom create mode 100644 lib/core/math.nom create mode 100644 lib/core/metaprogramming.nom create mode 100644 lib/core/operators.nom create mode 100644 lib/core/text.nom (limited to 'lib/core') diff --git a/lib/core/collections.nom b/lib/core/collections.nom new file mode 100644 index 0000000..4cf54cd --- /dev/null +++ b/lib/core/collections.nom @@ -0,0 +1,145 @@ +#!/usr/bin/env nomsu -V6.14 +# + This file contains code that supports manipulating and using collections like lists + and dictionaries. + +use "core/metaprogramming" +use "core/control_flow" +use "core/operators" + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +# List functionality: +test: + $list = [1, 2, 3, 4, 5] + $visited = {} + for $i = $x in $list: + $visited.$i = (yes) + assume ($visited == {.1, .2, .3, .4, .5}) + $visited = {} + for $x in $list: + $visited.$x = (yes) + assume ($visited == {.1, .2, .3, .4, .5}) + assume (($list, 2 nd to last) == 4) + assume (($list, first) == 1) + assume ($list, has 3) + assume (($list, index of 3) == 3) + assume ((size of $list) == 5) + $list, add 6 + assume (($list, last) == 6) + $list, pop + assume (($list, last) == 5) + $list, remove index 1 + assume (($list, first) == 2) + assume (([1, 2] + [3, 4]) == [1, 2, 3, 4]) + +# Dict functionality +test: + $dict = {.x = 1, .y = 2, .z = 3} + assume (size of $dict) == 3 + assume [: for $k = $v in {.x = 1}: add {.key = $k, .value = $v}] == + [{.key = "x", .value = 1}] + assume ({.x = 1, .y = 1} + {.y = 10, .z = 10}) == {.x = 1, .y = 11, .z = 10} + assume ({.x = 1, .y = 1} | {.y = 10, .z = 10}) == {.x = 1, .y = 1, .z = 10} + assume ({.x = 1, .y = 1} & {.y = 10, .z = 10}) == {.y = 1} + assume ({.x = 1, .y = 1} ~ {.y = 10, .z = 10}) == {.x = 1, .z = 10} + +test: + assume (([[1, 2], [3, 4]] flattened) == [1, 2, 3, 4]) + +externally ($lists flattened) means: + $flat = [] + for $item in recursive $lists: + if ($item is a "List"): + for $ in $item: + recurse $item on $ + ..else: + $flat, add $item + return $flat + +test: + assume ((entries in {.x = 1}) == [{.key = "x", .value = 1}]) + +(entries in $dict) parses as [ + : for $k = $v in $dict: + add {.key = $k, .value = $v} +] + +test: + assume ((keys in {.x = 1}) == ["x"]) + +[keys in $dict, keys of $dict] all parse as [: for $k = $v in $dict: add $k] +test: + assume ((values in {.x = 1}) == [1]) +[values in $dict, values of $dict] all parse as [: for $k = $v in $dict: add $v] + +# Metatable stuff +test: + $t = {} + set $t's metatable to {.__tostring = ($ -> "XXX")} + assume ("\$t" == "XXX") + +(set $dict's metatable to $metatable) compiles to + "setmetatable(\($dict as lua expr), \($metatable as lua expr));" + +[$'s metatable, $'metatable] all compile to "getmetatable(\($ as lua expr))" +test: + assume (({} with fallback $ -> ($ + 1)).10 == 11) + +($dict with fallback $key -> $value) compiles to (" + (function(d) + local mt = {} + for k,v in pairs(getmetatable(d) or {}) do mt[k] = v end + mt.__index = function(self, \($key as lua expr)) + local value = \($value as lua expr) + self[\($key as lua expr)] = value + return value + end + return setmetatable(d, mt) + end)(\($dict as lua expr)) +") + +# Sorting +test: + $x = [3, 1, 2] + sort $x + assume ($x == [1, 2, 3]) + sort $x by $ = (- $) + assume ($x == [3, 2, 1]) + $keys = {.1 = 999, .2 = 0, .3 = 50} + sort $x by $ = $keys.$ + assume ($x == [2, 3, 1]) +(sort $items) compiles to "table.sort(\($items as lua expr));" +[sort $items by $item = $key_expr, sort $items by $item -> $key_expr] +..all parse as + do: + $keys = ({} with fallback $item -> $key_expr) + lua> "table.sort(\$items, function(x,y) return \$keys[x] < \$keys[y] end)" + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +test: + assume ((sorted [3, 1, 2]) == [1, 2, 3]) + +externally [$items sorted, sorted $items] all mean: + $copy = [: for $ in $items: add $] + sort $copy + return $copy + +[$items sorted by $item = $key, $items sorted by $item -> $key] all parse as + result of: + $copy = [: for $ in $items: add $] + sort $copy by $item = $key + return $copy + +test: + assume ((unique [1, 2, 1, 3, 2, 3]) == [1, 2, 3]) + +externally (unique $items) means: + $unique = [] + $seen = {} + for $ in $items: + unless $seen.$: + $unique, add $ + $seen.$ = (yes) + return $unique 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 diff --git a/lib/core/coroutines.nom b/lib/core/coroutines.nom new file mode 100644 index 0000000..6a99f7e --- /dev/null +++ b/lib/core/coroutines.nom @@ -0,0 +1,40 @@ +#!/usr/bin/env nomsu -V6.14 +# + This file defines the code that creates and manipulates coroutines + +use "core/metaprogramming" +use "core/operators" +use "core/control_flow" + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +test: + $co = + ->: + yield 4 + yield 5 + repeat 3 times: + yield 6 + $nums = [] + for $ in coroutine $co: + $nums, add $ + + unless ($nums == [4, 5, 6, 6, 6]): + fail "Coroutine iteration failed" + + $d = {.x = 0} + $co2 = + coroutine: + $d.x += 1 + yield 1 + $d.x += 1 + yield + $d.x += 1 + repeat while ((coroutine status of $co2) != "dead"): resume $co2 + assume $d.x == 3 +(coroutine $body) parses as (coroutine from (-> $body)) +(for $ in coroutine $co $body) compiles to (" + for \($ as lua expr) in coroutine_wrap(\($co as lua expr)) do + \($body as lua) + end +") diff --git a/lib/core/errors.nom b/lib/core/errors.nom new file mode 100644 index 0000000..c878bb7 --- /dev/null +++ b/lib/core/errors.nom @@ -0,0 +1,131 @@ +#!/usr/bin/env nomsu -V6.14 +# + This file contains basic error reporting code + +use "core/metaprogramming" +use "core/operators" +use "core/control_flow" + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +(fail $msg) compiles to "error(\(($msg as lua expr) if $msg else "nil"), 0);" +(assume $condition) compiles to: + lua> (" + local \$assumption = 'Assumption failed: '..tostring((\$condition):get_source_code()) + ") + + return + Lua (" + if not \($condition as lua expr) then + error(\(quote "\$assumption"), 0) + end + ") + +(assume $a == $b) compiles to: + lua> "local \$assumption = 'Assumption failed: '..tostring(\(\($a == $b) as nomsu))" + + define mangler + + return + Lua (" + do + local \(mangle "a"), \(mangle "b") = \($a as lua expr), \($b as lua expr) + if \(mangle "a") ~= \(mangle "b") then + error(\(quote "\$assumption").."\\n"..tostring(\(mangle "a")).." != "..tostring(\ + ..\(mangle "b")), 0) + end + end + ") + +test: + try: fail + $worked = (no) + try: + fail "xx" + ..if it fails with $failure: + $worked = (yes) + ..if it succeeds: + fail "'try' incorrectly ran success case." + assume $failure == "xx" + unless $worked: + fail "'try' failed to recover from failure" + +# Try/except +[ + try $action if it succeeds $success if it fails with $msg $fallback + try $action if it fails with $msg $fallback if it succeeds $success +] all compile to: + $success_lua = ($success as lua) + if ((#"\$success_lua") > 0): + $success_lua, add "\n" + $success_lua, prepend "-- Success:\n" + $success_lua, + add "if not _fell_through then return table.unpack(_result, 2) end" + $fallback_lua = ($fallback as lua) + if ((#"\$fallback_lua") > 0): + $msg_lua = ($msg as lua expr) + if ((#"\$msg_lua") > 0): + $fallback_lua, prepend "\n\$msg_lua = _result[2]\n" + if ($msg_lua, text, is lua id): + $fallback_lua, add free vars [($msg_lua, text)] + $fallback_lua, prepend "-- Failure:" + return + Lua (" + do + local _fell_through = false + local _result = {pcall(function() + \($action as lua) + _fell_through = true + end)} + if _result[1] then + \$success_lua + else + \$fallback_lua + end + end + ") + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +(try $action) parses as + try $action if it succeeds (do nothing) if it fails (do nothing) + +(try $action if it fails $fallback) parses as + try $action if it succeeds (do nothing) if it fails $fallback + +(try $action if it fails with $msg $fallback) parses as + try $action if it succeeds (do nothing) if it fails with $msg $fallback + +(try $action if it succeeds $success) parses as + try $action if it succeeds $success if it fails (do nothing) + +(try $action if it fails $fallback if it succeeds $success) parses as + try $action if it fails with (=lua "") $fallback if it succeeds $success + +(try $action if it succeeds $success if it fails $fallback) parses as + try $action if it succeeds $success if it fails with (=lua "") $fallback + +test: + $success = (no) + try: + do: fail + ..then always: + $success = (yes) + ..if it succeeds: + fail "'try ... then always ...' didn't propagate failure" + + unless $success: + fail "'try ... then always ...' didn't execute the 'always' code" + +(do $action then always $final_action) compiles to (" + do -- do/then always + local _fell_through = false + local _results = {pcall(function() + \($action as lua) + _fell_through = true + end)} + \($final_action as lua) + if not _results[1] then error(_results[2], 0) end + if not _fell_through then return table.unpack(_results, 2) end + end +") diff --git a/lib/core/id.nom b/lib/core/id.nom new file mode 100644 index 0000000..d2427b5 --- /dev/null +++ b/lib/core/id.nom @@ -0,0 +1,66 @@ +#!/usr/bin/env nomsu -V6.14 +# + A simple UUID function based on RFC 4122: http://www.ietf.org/rfc/rfc4122.txt + +use "core/metaprogramming" +use "core/operators" +use "core/math" +use "core/collections" +use "core/control_flow" + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +$NaN_surrogate = {} +$nil_surrogate = {} +$obj_by_id = {} +set $obj_by_id's metatable to {.__mode = "v"} +$id_by_obj = {} +set $id_by_obj's metatable to { + .__mode = "k" + .__index = + for ($self $key): + if ($key == (nil)): + return $self.$nil_surrogate + + if ($key != $key): + return $self.$NaN_surrogate + + --- (retry) --- + $id = (uuid) + if ($obj_by_id.$id != (nil)): go to (retry) + $self.$key = $id + $obj_by_id.$id = $key + return $id +} + +externally (uuid) means: + # Set all the other bits to randomly (or pseudo-randomly) chosen values. + $bytes = [ + # time-low, time-mid, time-high-and-version + randint (2 ^ (4 * 8)), randint (2 ^ (2 * 8)), randint (2 ^ (2 * 8 - 4)) + # clock-seq-and-reserved, clock-seq-low + randint (2 ^ (1 * 8 - 2)), randint (2 ^ (1 * 8)), randint (2 ^ (3 * 8)) + # node + randint (2 ^ (3 * 8)) + ] + + # Set the four most significant bits (bits 12 through 15) of the + # time_hi_and_version field to the 4-bit version number from + # Section 4.1.3. + $bytes.3 += 0x4000 + + # Set the two most significant bits (bits 6 and 7) of the + # clock_seq_hi_and_reserved to zero and one, respectively. + $bytes.4 += 0xC0 + return (=lua "('%08x-%04x-%04x-%02x%02x-%6x%6x'):format(unpack(\$bytes))") + +# For strict identity checking, use ($x's id) == ($y's id) +test: + assume (([] == []) and ((id of []) != (id of []))) + seed random with 0 + $x = [] + assume ((id of $x) == (id of $x)) + seed random with 0 + assume ((id of $x) != (id of [])) + seed random +externally [id of $, $'s id, $'id] all mean $id_by_obj.$ diff --git a/lib/core/init.nom b/lib/core/init.nom new file mode 100644 index 0000000..0c8051d --- /dev/null +++ b/lib/core/init.nom @@ -0,0 +1,11 @@ +# Export everything +export "core/metaprogramming" +export "core/operators" +export "core/control_flow" +export "core/errors" +export "core/collections" +export "core/coroutines" +export "core/math" +export "core/id" +export "core/io" +export "core/text" diff --git a/lib/core/io.nom b/lib/core/io.nom new file mode 100644 index 0000000..7afe889 --- /dev/null +++ b/lib/core/io.nom @@ -0,0 +1,29 @@ +#!/usr/bin/env nomsu -V6.14 +# + This file contains basic input/output code + +use "core/metaprogramming" + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +(say $message) compiles to: + lua> (" + if \$message.type == "Text" then + return LuaCode("say(", \($message as lua expr), ");"); + else + return LuaCode("say(tostring(", \($message as lua expr), "));"); + end + ") + +(say $message inline) compiles to: + lua> (" + if \$message.type == "Text" then + return LuaCode("io.write(", \($message as lua expr), ")"); + else + return LuaCode("io.write(tostring(", \($message as lua expr), "))"); + end + ") + +externally (ask $prompt) means: + $io.write $prompt + return ($io.read()) diff --git a/lib/core/math.nom b/lib/core/math.nom new file mode 100644 index 0000000..685ab1e --- /dev/null +++ b/lib/core/math.nom @@ -0,0 +1,212 @@ +#!/usr/bin/env nomsu -V6.14 +# + This file defines some common math literals and functions + +use "core/metaprogramming" +use "core/text" +use "core/operators" +use "core/control_flow" +use "core/collections" + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +# Literals: +test: + unless (all of [inf, NaN, pi, tau, golden ratio, e]): + fail "math constants failed" + $nan = (NaN) + unless ($nan != $nan): + fail "NaN failed" +[infinity, inf] all compile to "math.huge" +[not a number, NaN, nan] all compile to "(0/0)" +[pi, Pi, PI] all compile to "math.pi" +[tau, Tau, TAU] all compile to "(2*math.pi)" +(golden ratio) compiles to "((1+math.sqrt(5))/2)" +(e) compiles to "math.exp(1)" + +# Functions: +test: + assume (("5" as a number) == 5) +external $($ as a number) = $(tonumber $) +external $($ as number) = $(tonumber $) +test: + unless + all of [ + abs 5, | 5 |, sqrt 5, √ 5, sine 5, cosine 5, tangent 5, arc sine 5, arc cosine 5 + arc tangent 5, arc tangent 5 / 10, hyperbolic sine 5, hyperbolic cosine 5 + hyperbolic tangent 5, e^ 5, ln 5, log 5 base 2, floor 5, ceiling 5, round 5 + ] + ..: + fail "math functions failed" +external [$(absolute value $), $(absolute value of $), $(| $ |), $(abs $)] = + [$math.abs, $math.abs, $math.abs, $math.abs] +external [$(square root $), $(square root of $), $(√ $), $(sqrt $)] = + [$math.sqrt, $math.sqrt, $math.sqrt, $math.sqrt] +external [$(sine $), $(sin $)] = [$math.sin, $math.sin] +external [$(cosine $), $(cos $)] = [$math.cos, $math.cos] +external [$(tangent $), $(tan $)] = [$math.tan, $math.tan] +external [$(arc sine $), $(asin $)] = [$math.asin, $math.asin] +external [$(arc cosine $), $(acos $)] = [$math.acos, $math.acos] +external [$(arc tangent $), $(atan $)] = [$math.atan, $math.atan] +external [$(arc tangent $y / $x), $(atan2 $y $x)] = [$math.atan2, $math.atan2] +external [$(hyperbolic sine $), $(sinh $)] = [$math.sinh, $math.sinh] +external [$(hyperbolic cosine $), $(cosh $)] = [$math.cosh, $math.cosh] +external [$(hyperbolic tangent $), $(tanh $)] = [$math.tanh, $math.tanh] +external [$(e^ $), $(exp $)] = [$math.exp, $math.exp] +external [$(natural log $), $(ln $), $(log $), $(log $ base $)] = [$math.log, $math.log, $math.log, $math.log] +external $(floor $) = $math.floor +external [$(ceiling $), $(ceil $)] = [$math.ceil, $math.ceil] +externally [round $, $ rounded] all mean + floor ($ + 0.5) +test: + unless ((463 to the nearest 100) == 500): fail "rounding failed" + unless ((2.6 to the nearest 0.25) == 2.5): fail "rounding failed" + +externally ($n to the nearest $rounder) means + $rounder * (floor ($n / $rounder + 0.5)) + +# Any/all +externally [all of $items, all $items] all mean: + for $ in $items: + unless $: + return (no) + return (yes) +[not all of $items, not all $items] all parse as (not (all of $items)) +externally [any of $items, any $items] all mean: + for $ in $items: + if $: + return (yes) + return (no) +[none of $items, none $items] all parse as (not (any of $items)) + +# Sum/product +externally [sum of $items, sum $items] all mean: + $total = 0 + for $ in $items: + $total += $ + return $total + +externally [product of $items, product $items] all mean: + $prod = 1 + for $ in $items: + $prod *= $ + return $prod + +externally [avg of $items, average of $items] all mean + (sum of $items) / (size of $items) + +# Min/max +externally [min of $items, smallest of $items, lowest of $items] all mean: + $best = (nil) + for $ in $items: + if (($best == (nil)) or ($ < $best)): $best = $ + return $best + +externally [ + max of $items, biggest of $items, largest of $items, highest of $items +] all mean: + $best = (nil) + for $ in $items: + if (($best == (nil)) or ($ > $best)): $best = $ + return $best + +test: + assume ((min of [3, -4, 1, 2] by $ = ($ * $)) == 1) + assume ((max of [3, -4, 1, 2] by $ = ($ * $)) == -4) + +(min of $items by $item = $value_expr) parses as + result of: + $best = (nil) + $best_key = (nil) + for $item in $items: + $key = $value_expr + if (($best == (nil)) or ($key < $best_key)): + $best = $item + $best_key = $key + return $best + +(max of $items by $item = $value_expr) parses as + result of: + $best = (nil) + $best_key = (nil) + for $item in $items: + $key = $value_expr + if (($best == (nil)) or ($key > $best_key)): + $best = $item + $best_key = $key + return $best + +test: + assume (100 clamped between 0 and 10) == 10 + +externally ($ clamped between $min and $max) means: + when: + ($ < $min): + return $min + + ($ > $max): + return $max + + else: + return $ + +test: + assume (-0.1 smoothed by 2.7) == 0 + assume (0 smoothed by 2.7) == 0 + assume (0.5 smoothed by 2.7) == 0.5 + assume (1 smoothed by 2.7) == 1 + assume (1.1 smoothed by 2.7) == 1 + +externally ($ smoothed by $smoothness) means: + $ = ($ clamped between 0 and 1) + if ($smoothness == 0): return $ + $k = (2 ^ $smoothness) + if ($ < 0.5): + return (0.5 * (2 * $) ^ $k) + ..else: + return (1 - 0.5 * (2 - 2 * $) ^ $k) + +test: + assume (5 to 7 mixed by -1.0) == 5 + assume (5 to 7 mixed by 0.0) == 5 + assume (5 to 7 mixed by 0.5) == 6 + assume (5 to 7 mixed by 1.0) == 7 + assume (5 to 7 mixed by 2.0) == 7 + +externally ($lo to $hi mixed by $amount) means: + $ = ($amount clamped between 0 and 1) + return ((1 - $) * $lo + $ * $hi) + +test: + assume ([0, 1, 11] mixed by 0.0) == 0 + assume ([0, 1, 11] mixed by 0.25) == 0.5 + assume ([0, 1, 11] mixed by 0.5) == 1 + assume ([0, 1, 11] mixed by 0.75) == 6 + assume ([0, 1, 11] mixed by 1.0) == 11 + assume ([99] mixed by 0.5) == 99 + +externally ($nums mixed by $amount) means: + $ = ($amount clamped between 0 and 1) + $i = (1 + ($ * ((#$nums) - 1))) + if ((floor $i) == (#$nums)): + return $nums.(floor $i) + [$lo, $hi] = [$nums.(floor $i), $nums.(floor ($i + 1))] + return ($lo to $hi mixed by ($i mod 1)) + +# Random functions +externally (seed random with $) means: + lua> (" + math.randomseed(\$); + for i=1,20 do math.random(); end + ") +(seed random) parses as (seed random with (=lua "os.time()")) +[random number, random, rand] all compile to "math.random()" +[random int $n, random integer $n, randint $n] all compile to + "math.random(\($n as lua expr))" + +[random from $low to $high, random number from $low to $high, rand $low $high] +..all compile to "math.random(\($low as lua expr), \($high as lua expr))" + +externally [ + random choice from $elements, random choice $elements, random $elements +] all mean (=lua "\$elements[math.random(#\$elements)]") diff --git a/lib/core/metaprogramming.nom b/lib/core/metaprogramming.nom new file mode 100644 index 0000000..936f9bd --- /dev/null +++ b/lib/core/metaprogramming.nom @@ -0,0 +1,459 @@ +#!/usr/bin/env nomsu -V6.14 +# + This File contains actions for making actions and compile-time actions and some helper + functions to make that easier. + +lua> "NOMSU_CORE_VERSION = 14" +lua> "NOMSU_LIB_VERSION = 8" +lua> (" + do + local mangle_index = 0 + function mangler() + local my_mangle_index = mangle_index + mangle_index = mangle_index + 1 + return function(varname) + return (varname..(("\\3%X"):format(my_mangle_index))):as_lua_id() + end + end + end + COMPILE_RULES["define mangler"] = function(\(nomsu environment)) + return LuaCode("local mangle = mangler()") + end +") + +lua> (" + COMPILE_RULES["1 ->"] = function(\(nomsu environment), \$args, \$body) + if \$args and not \$body then \$args, \$body = {}, \$args end + local body_lua = SyntaxTree:is_instance(\$body) and \(nomsu environment):compile(\$body) or \$body + if SyntaxTree:is_instance(\$body) and \$body.type ~= "Block" then body_lua:prepend("\ + ..return ") end + local lua = LuaCode("(function(") + if SyntaxTree:is_instance(\$args) and (\$args.type == "Action" or \$args.type == "MethodCall") then + \$args = \$args:get_args() + elseif SyntaxTree:is_instance(\$args) and \$args.type == "Var" then \$args = {\$args} end + for i, arg in ipairs(\$args) do + local arg_lua = SyntaxTree:is_instance(arg) and \(nomsu environment):compile(arg):text() or arg + if arg_lua == "..." then + if i < #\$args then + compile_error_at(SyntaxTree:is_instance(arg) and arg or nil, + "Extra arguments must come last.", "Try removing any arguments after \ + ..(*extra arguments*)") + end + elseif not arg_lua:is_lua_id() then + compile_error_at(SyntaxTree:is_instance(arg) and arg or nil, + "This does not compile to a Lua identifier, so it can't be used as a function \ + ..argument.", + "This should probably be a Nomsu variable instead (like $x).") + end + lua:add(i > 1 and ", " or "", arg_lua) + body_lua:remove_free_vars({arg_lua}) + end + body_lua:declare_locals() + lua:add(")\\n ", body_lua, "\\nend)") + return lua + end + COMPILE_RULES["->"] = COMPILE_RULES["1 ->"] + COMPILE_RULES["for"] = COMPILE_RULES["1 ->"] +") + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +test: + (five) compiles to "5" + +test: + unless ((five) == 5): + fail "Compile to expression failed." + (loc x) compiles to "local x = 99;" + +test: + lua> "do" + loc x + unless ($x is 99): + fail "Compile to statements with locals failed." + lua> "end" + unless ($x is (nil)): + fail "Failed to properly localize a variable." + + (asdf) compiles to: + $tmp = "" + return (Lua $tmp) + +test: + asdf + unless ($tmp is (nil)): + fail "compile to is leaking variables" + +lua> (" + COMPILE_RULES["1 compiles to"] = function(\(nomsu environment), \$action, \$body) + local \$args = List{"\(nomsu environment)", unpack(\$action:get_args())} + if \$body.type == "Text" then + \$body = SyntaxTree{source=\$body.source, type="Action", "Lua", \$body} + end + return LuaCode("COMPILE_RULES[", \$action:get_stub():as_lua(), + "] = ", \(\($args -> $body) as lua)) + end +") + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +($actions all compile to $body) compiles to: + lua> (" + if \$actions.type ~= "List" then + compile_error(\$actions, "This should be a list of actions.") + end + local lua = \(\($actions.1 compiles to $body) as lua) + local \$args = List{"\(nomsu environment)", unpack(\$actions[1]:get_args())} + local \$compiled_args = List{"\(nomsu environment)"}; + for i=2,#\$args do \$compiled_args[i] = \(nomsu environment):compile(\$args[i]) end + for i=2,#\$actions do + local alias = \$actions[i] + local \$alias_args = List{"\(nomsu environment)", unpack(alias:get_args())} + lua:add("\\nCOMPILE_RULES[", alias:get_stub():as_lua(), "] = ") + if \$alias_args == \$args then + lua:add("COMPILE_RULES[", \$actions[1]:get_stub():as_lua(), "]") + else + lua:add("function(") + local \$compiled_alias_args = List{"\(nomsu environment)"}; + for i=2,#\$alias_args do \$compiled_alias_args[i] = \(nomsu environment):compile(\$alias_args[i]) end + lua:concat_add(\$compiled_alias_args, ", ") + lua:add(") return COMPILE_RULES[", \$actions[1]:get_stub():as_lua(), "](") + lua:concat_add(\$compiled_args, ", ") + lua:add(") end") + end + end + return lua + ") + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +test: + (foo $x) means "outer" + with [$(foo $)]: + (foo $x) means: + $y = ($x + 1) + return $y + + unless ((foo 10) == 11): + fail "Action didn't work." + + unless ($y is (nil)): + fail "Action leaked a local into globals." + + (baz $) parses as (foo $) + assume ((foo 1) == "outer") + +($action means $body) compiles to: + lua> (" + + local lua = LuaCode() + if \$action.type == "MethodCall" then + lua:add(\(nomsu environment):compile(\$action[1]), ".", \$action[2]:get_stub():as_lua_id()) + elseif \$action.type == "Action" then + lua:add(\$action:get_stub():as_lua_id()) + lua:add_free_vars({\$action:get_stub():as_lua_id()}) + else + compile_error_at(\$action, "Expected an action or method call here") + end + lua:add(" = ", \(\($action -> $body) as lua), ";") + return lua + ") + +($actions all mean $body) compiles to: + lua> (" + local lua = \(\($actions.1 means $body) as lua) + local first_def = (\$actions[1].type == "MethodCall" + and LuaCode(\(nomsu environment):compile(\$actions[1][1]), ".", \$actions[1]:get_stub():as_lua_id()) + or LuaCode(\$actions[1]:get_stub():as_lua_id())) + local \$args = List(\$actions[1]:get_args()) + for i=2,#\$actions do + local alias = \$actions[i] + local \$alias_args = List(alias:get_args()) + lua:add("\\n") + if alias.type == "MethodCall" then + lua:add(\(nomsu environment):compile(alias[1]), ".", alias:get_stub():as_lua_id()) + else + lua:add(alias:get_stub():as_lua_id()) + lua:add_free_vars({alias_name}) + end + if \$args == \$alias_args then + lua:add(" = ", first_def, ";") + else + lua:add(" = ", \(\($alias_args -> $actions.1) as lua), ";") + end + end + return lua + ") + +test: + externally (baz1) means: + return "baz1" + externally (baz2) means "baz2" + +test: + assume ((baz1) == "baz1") + assume ((baz2) == "baz2") + +(externally $action means $body) compiles to: + lua> (" + local lua = \(\($action means $body) as lua) + lua:remove_free_vars({\$action:get_stub():as_lua_id()}) + return lua + ") + +(externally $actions all mean $body) compiles to: + lua> (" + local lua = \(\($actions all mean $body) as lua) + lua:remove_free_vars(table.map(\$actions, function(a) return a:get_stub():as_lua_id() end)) + return lua + ") + +test: + (swap $x and $y) parses as + do: + $tmp = $x + $x = $y + $y = $tmp + +test: + [$1, $2] = [1, 2] + swap $1 and $2 + unless (($1 == 2) and ($2 == 1)): + fail "'parse $ as $' failed on 'swap $ and $'" + [$tmp, $tmp2] = [1, 2] + swap $tmp and $tmp2 + unless (($tmp == 2) and ($tmp2 == 1)): + fail "'parse $ as $' variable mangling failed." + +($actions all parse as $body) compiles to: + lua> (" + local replacements = {} + if \$actions.type ~= "List" then + compile_error(\$actions, "This should be a list.") + end + for i,arg in ipairs(\$actions[1]:get_args()) do + replacements[arg[1]] = \(nomsu environment):compile(arg):text() + end + local function make_tree(t) + if SyntaxTree:is_instance(t) and t.type == "Var" then + if replacements[t:as_var()] then + return replacements[t:as_var()] + else + return "SyntaxTree{mangle("..t:as_var():as_lua().."), type="..t.type:as_lua()..", \ + ..source="..tostring(t.source):as_lua().."}" + end + elseif SyntaxTree:is_instance(t) then + local ret = {} + local i = 1 + for k, v in pairs(t) do + if k == i then + ret[#ret+1] = make_tree(t[i]) + i = i + 1 + elseif k == "source" then + ret[#ret+1] = k.."= "..tostring(v):as_lua() + elseif lua_type_of(k) == 'string' and k:is_a_lua_id() then + ret[#ret+1] = k.."= "..make_tree(v) + else + ret[#ret+1] = "["..make_tree(k).."]= "..make_tree(v) + end + end + return "SyntaxTree{"..table.concat(ret, ", ").."}" + elseif lua_type_of(t) == 'number' then + return tostring(t) + else + return t:as_lua() + end + end + local \$new_body = LuaCode:from(\$body.source, + "local mangle = mangler()", + "\\nreturn ", make_tree(\$body)) + local ret = \(\($actions all compile to $new_body) as lua) + return ret + ") + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +[$action parses as $body] all parse as ([$action] all parse as $body) +externally (in (nomsu environment) $tree as lua expr) means: + lua> (" + local tree_lua = \(nomsu environment):compile(\$tree) + if \$tree.type == 'Block' then + tree_lua = LuaCode:from(\$tree.source, '(function()\\n ', tree_lua, '\\nend)()') + elseif \$tree.type == 'MethodCall' and #\$tree > 2 then + compile_error_at(\$tree, "This must be a single value instead of "..(#\$tree - 1).."\ + .. method calls.", + "Replace this with a single method call.") + end + return tree_lua + ") + +# Need to make sure the proper environment is used for compilation (i.e. the caller's environment) +($tree as lua expr) compiles to (\(in \(nomsu environment) $tree as lua expr) as lua) + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +externally [$var as lua identifier, $var as lua id] all mean: + lua> (" + local lua = \($var as lua) + if not lua:text():is_a_lua_id() then + compile_error(\$var, + "This is supposed to be something that compiles to a valid Lua identifier.", + "This should probably be a variable.") + end + return lua + ") + +test: + (num args (*extra arguments*)) means (select "#" (*extra arguments*)) + assume (num args 1 2 3) == 3 + (extra args (*extra arguments*)) means [*extra arguments*] + assume (extra args 1 2 3) == [1, 2, 3] + (third arg (*extra arguments*)) means (select 3 (*extra arguments*)) + assume (third arg 5 6 7 8) == 7 + +(*extra arguments*) compiles to "..." + +($ is syntax tree) compiles to "SyntaxTree:is_instance(\($ as lua expr))" + +externally ($ is $kind syntax tree) means + =lua "SyntaxTree:is_instance(\$) and \$.type == \$kind" + +($tree with $t -> $replacement) compiles to (" + \($tree as lua expr):map(function(\($t as lua expr)) + \( + =lua (" + \$replacement.type == 'Block' and \($replacement as lua) or 'return '..\ + ..\($replacement as lua expr):text() + ") + ) + end) +") + +externally ($tree with vars $replacements) means + =lua (" + \$tree:map(function(\$t) + if \$t.type == "Var" then + return \$replacements[\$t:as_var()] + end + end) + ") + +(tree $tree with vars $replacements) compiles to (" + \(=lua "(\$tree):as_lua()"):map(function(t) + if t.type == "Var" then + return \($replacements as lua expr)[t:as_var()] + end + end) +") + +($tree has subtree $match_tree) compiles to (" + (function() + local match_tree = \($match_tree as lua expr) + for subtree in coroutine_wrap(function() \($tree as lua expr):map(yield) end) do + if subtree == match_tree then return true end + end + end)() +") + +externally (match $tree with $patt) means: + lua> (" + if \$patt.type == "Var" then return Dict{[\$patt:as_var()]=\$tree} end + if \$patt.type == "Action" and \$patt:get_stub() ~= \$tree:get_stub() then return nil end + if #\$patt ~= #\$tree then return nil end + local matches = Dict{} + for \($i)=1,#\$patt do + if SyntaxTree:is_instance(\$tree[\$i]) then + local submatch = \(match $tree.$i with $patt.$i) + if not submatch then return nil end + for k,v in pairs(submatch) do + if matches[k] and matches[k] ~= v then return nil end + matches[k] = v + end + end + end + return matches + ") + +test: + assume + ( + quote (" + one + "two" + ") + ) == "\"one\\n\\\"two\\\"\"" + +(quote $s) compiles to "tostring(\($s as lua expr)):as_lua()" +test: + assume (lua type of {}) == "table" + assume (type of {}) == "Dict" + assume ({} is a "Dict") + assume ("" is text) + assume ("" isn't a "Dict") +externally ($ is text) means (=lua "\(lua type of $) == 'string'") +externally [$ is not text, $ isn't text] all mean + =lua "\(lua type of $) ~= 'string'" + +externally (type of $) means: + lua> (" + local lua_type = \(lua type of $) + if lua_type == 'string' then return 'Text' + elseif lua_type == 'table' or lua_type == 'userdata' then + local mt = getmetatable(\$) + if mt and mt.__type then return mt.__type end + end + return lua_type + ") + +[$ is a $type, $ is an $type] all parse as ((type of $) == $type) +[$ isn't a $type, $ isn't an $type, $ is not a $type, $ is not an $type] +..all parse as ((type of $) != $type) + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +test: + (foo) means: + return 100 200 300 + assume (select 2 (foo)) == 200 + +# Return statement is wrapped in a do..end block because Lua is unhappy if you + put code after a return statement, unless you wrap it in a block. +(return (*extra arguments*)) compiles to: + lua> (" + local lua = \(Lua "do return ") + for i=1,select('#',...) do + if i > 1 then lua:add(", ") end + lua:add(\(nomsu environment):compile((select(i, ...)))) + end + lua:add(" end") + return lua + ") + +# Literals +(yes) compiles to "true" +(no) compiles to "false" +[nothing, nil, null] all compile to "nil" +(Nomsu syntax version) compiles to "NOMSU_SYNTAX_VERSION" +(Nomsu compiler version) compiles to "NOMSU_COMPILER_VERSION" +(core version) compiles to "NOMSU_CORE_VERSION" +(lib version) compiles to "NOMSU_LIB_VERSION" +(command line args) compiles to "COMMAND_LINE_ARGS" + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +# + (with local compile actions $body) compiles to (" + do + local OLD_RULES = COMPILE_RULES + local OLD_ENV = \(nomsu environment) + local \(nomsu environment) = setmetatable({ + COMPILE_RULES=setmetatable({}, {__index=OLD_RULES}) + }, {__index=OLD_ENV}) + \($body as lua) + end + ") + +externally (Nomsu version) means: + return (" + \(Nomsu syntax version).\(core version).\(Nomsu compiler version).\(lib version) + ") diff --git a/lib/core/operators.nom b/lib/core/operators.nom new file mode 100644 index 0000000..dee76b6 --- /dev/null +++ b/lib/core/operators.nom @@ -0,0 +1,263 @@ +#!/usr/bin/env nomsu -V6.14 +# + 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 is $b, $a == $b] all compile to "(\($a as lua expr) == \($b as lua expr))" +[$a isn't $b, $a is not $b, $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' 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' then + lua:add_free_vars({var_lua:text()}) + end + lua:add(' = ', \($value as lua expr)) + end + return lua + ") + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +test: + [$foozle, $y] = ["outer", "outer"] + externally (set global x local y) means: + external $foozle = "inner" + $y = "inner" + set global x local y + unless (($foozle == "inner") and ($y == "outer")): fail "external failed." +(external $var = $value) compiles to: + $lua = ((SyntaxTree {.type = "Action", .source = $var.source, .1 = $var, .2 = "=", .3 = $value}) as lua) + $lua, remove free vars + return $lua +test: + [$foozle, $y] = ["outer", "outer"] + externally (set global x local y) means: + with external [$foozle]: + $foozle = "inner" + $y = "inner" + set global x local y + unless (($foozle == "inner") and ($y == "outer")): + fail "'with external' failed." + +(with external $externs $body) compiles to: + $body_lua = ($body as lua) + lua> (" + \$body_lua:remove_free_vars(table.map(\$externs, function(v) return \(nomsu environment):compile(v):text() end)) + ") + return $body_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] 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 +# 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 version) == "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) +[#$list, size of $list] all compile to "(#\($list as lua expr))" +($list is empty) compiles to "(#\($list as lua expr) == 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 $)) diff --git a/lib/core/text.nom b/lib/core/text.nom new file mode 100644 index 0000000..1351af6 --- /dev/null +++ b/lib/core/text.nom @@ -0,0 +1,78 @@ +#!/usr/bin/env nomsu -V6.14 +# + This file contains some definitions of text escape sequences, including ANSI console + color codes. + +use "core/metaprogramming" +use "core/operators" +use "core/control_flow" + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +test: + assume "\[1, 2, 3]" == "[1, 2, 3]" + assume "foo = \(1 + 2)!" == "foo = 3!" + assume (" + one + two + ") == (" + one + two + ") + assume "nogap" == "nogap" + assume (["x", "y"], joined with ",") == "x,y" + assume (["x", "y"], joined) == "xy" + assume ("BAR", byte 2) == 65 + assume ("BAR", bytes 1 to 2) == [66, 65] + assume ("asdf", capitalized) == "Asdf" + assume ("asdf", uppercase) == "ASDF" + assume ("asdf", with "s" -> "X") == "aXdf" + assume + (" + one + two + + "), lines + ..== ["one", "two", ""] + + ($spec とは $body) parses as ($spec means $body) + +test: + $こんにちは = "こんにちは" + ($ と言う) とは "\($)世界" + assume ($こんにちは と言う) == "こんにちは世界" + +($expr for $match in $text matching $patt) compiles to: + define mangler + return + Lua (" + (function() + local \(mangle "comprehension") = List{} + for \($match as lua expr) in (\($text as lua expr)):gmatch(\($patt as lua expr)) do + \(mangle "comprehension")[#\(mangle "comprehension")+1] = \($expr as lua) + end + return \(mangle "comprehension") + end)() + ") + +test: + assume "\n" == (newline) + +test: + assume (0xDEADBEEF as hex) == "0xDEADBEEF" + +externally ($num as hex) means: + if ($num < 0): + return ("-0x%X", formatted with (- $num)) + ..else: + return ("0x%X", formatted with $num) + +# Text literals +$escapes = { + .nl = "\n", .newline = "\n", .tab = "\t", .bell = "\a", .cr = "\r", ."carriage return" = "\r" + .backspace = "\b", ."form feed" = "\f", .formfeed = "\f", ."vertical tab" = "\v" +} + +for $name = $str in $escapes: + with [$lua = (Lua (quote $str))]: + $(COMPILE RULES).$name = (-> $lua) -- cgit v1.2.3