diff --git a/containers.lua b/containers.lua index 8063252..ed0825e 100644 --- a/containers.lua +++ b/containers.lua @@ -8,6 +8,8 @@ do local _obj_0 = require('utils') repr, stringify, equivalent, nth_to_last, size = _obj_0.repr, _obj_0.stringify, _obj_0.equivalent, _obj_0.nth_to_last, _obj_0.size end +local lpeg = require('lpeg') +local re = require('re') local List, Dict local _list_mt = { __eq = equivalent, @@ -90,6 +92,30 @@ local _list_mt = { _1_nd_to_last = nth_to_last, _1_rd_to_last = nth_to_last, _1_th_to_last = nth_to_last, + joined = function(self) + return table.concat((function() + local _accum_0 = { } + local _len_0 = 1 + for _index_0 = 1, #self do + local x = self[_index_0] + _accum_0[_len_0] = tostring(x) + _len_0 = _len_0 + 1 + end + return _accum_0 + end)()) + end, + joined_with_1 = function(self, glue) + return table.concat((function() + local _accum_0 = { } + local _len_0 = 1 + for _index_0 = 1, #self do + local x = self[_index_0] + _accum_0[_len_0] = tostring(x) + _len_0 = _len_0 + 1 + end + return _accum_0 + end)(), glue) + end, has_1 = function(self, item) for _index_0 = 1, #self do local x = self[_index_0] @@ -238,7 +264,146 @@ for i, entry in ipairs(Dict({ })) do assert(i == 1 and entry.key == "x" and entry.value == 99, "ipairs compatibility issue") end +local Text +do + local reverse, upper, lower, find, byte, match, gmatch, gsub, sub, format, rep + do + local _obj_0 = string + reverse, upper, lower, find, byte, match, gmatch, gsub, sub, format, rep = _obj_0.reverse, _obj_0.upper, _obj_0.lower, _obj_0.find, _obj_0.byte, _obj_0.match, _obj_0.gmatch, _obj_0.gsub, _obj_0.sub, _obj_0.format, _obj_0.rep + end + local as_lua_id + as_lua_id = function(str) + str = gsub(str, "^\3*$", "%1\3") + str = gsub(str, "x([0-9A-F][0-9A-F])", "x78%1") + str = gsub(str, "%W", function(c) + if c == ' ' then + return '_' + else + return format("x%02X", byte(c)) + end + end) + str = gsub(str, "^_*%d", "_%1") + return str + end + local line_matcher = re.compile([[ + lines <- {| line (%nl line)* |} + line <- {(!%nl .)*} + ]], { + nl = lpeg.P("\r") ^ -1 * lpeg.P("\n") + }) + local text_methods = { + reversed = function(self) + return reverse(tostring(self)) + end, + uppercase = function(self) + return upper(tostring(self)) + end, + lowercase = function(self) + return lower(tostring(self)) + end, + as_lua_id = function(self) + return as_lua_id(tostring(self)) + end, + formatted_with_1 = function(self, args) + return format(tostring(self), unpack(args)) + end, + byte_1 = function(self, i) + return byte(tostring(self), i) + end, + position_of_1 = function(self) + return find(tostring(self)) + end, + position_of_1_after_2 = function(self, i) + return find(tostring(self), i) + end, + bytes_1_to_2 = function(self, start, stop) + return List({ + byte(tostring(self), start, stop) + }) + end, + bytes = function(self) + return List({ + byte(tostring(self), 1, #self) + }) + end, + capitalized = function(self) + return gsub(tostring(self), '%l', upper, 1) + end, + lines = function(self) + return List(line_matcher:match(self)) + end, + matches_1 = function(self, patt) + return match(tostring(self), patt) and true or false + end, + [as_lua_id("* 1")] = function(self, n) + return rep(self, n) + end, + matching_1 = function(self, patt) + local result = { } + local stepper, x, i = gmatch(tostring(self), patt) + while true do + local tmp = List({ + stepper(x, i) + }) + if #tmp == 0 then + break + end + i = tmp[1] + result[#result + 1] = tmp + end + return List(result) + end, + [as_lua_id("with 1 -> 2")] = function(self, patt, sub) + return gsub(tostring(self), patt, sub) + end, + _coalesce = function(self) + if rawlen(self) > 1 then + local s = table.concat(self) + for i = rawlen(self), 2, -1 do + self[i] = nil + end + self[1] = s + end + return self + end + } + setmetatable(text_methods, { + __index = string + }) + getmetatable("").__index = function(self, i) + if type(i) == 'number' then + return sub(self, i, i) + elseif type(i) == 'table' then + return sub(self, i[1], i[2]) + else + return text_methods[i] + end + end + assert(("abc"):matches_1("ab")) + local _ = [==[ text_metatable = + __mul: (other)=> + assert(type(other) == 'number', "Invalid type for multiplication") + return rep(@, other) + __index: (i)=> + -- Use [] for accessing text characters, or s[{3,4}] for s:sub(3,4) + if type(i) == 'number' then return sub(@, i, i) + elseif type(i) == 'table' then return sub(@, i[1], i[2]) + else return text_methods[i] + __tostring: => @_coalesce![1] + __len: => #tostring(@) + __concat: (other)=> tostring(@), tostring(other) + __len: => #tostring(@) + __eq: (other)=> + type(@) == type(other) and getmetatable(@) == getmetatable(other) and tostring(@) == tostring(other) + __lt: (other)=> tostring(@) < tostring(other) + __le: (other)=> tostring(@) <= tostring(other) + __newindex: => error("Cannot modify Text") + + Text = (s)-> setmetatable(s, text_metatable) + ]==] +end return { List = List, - Dict = Dict + Dict = Dict, + Text = Text } diff --git a/containers.moon b/containers.moon index 68c618b..a9f6857 100644 --- a/containers.moon +++ b/containers.moon @@ -2,6 +2,8 @@ {:insert,:remove,:concat} = table {:repr, :stringify, :equivalent, :nth_to_last, :size} = require 'utils' +lpeg = require 'lpeg' +re = require 're' local List, Dict @@ -41,6 +43,9 @@ _list_mt = last: (=> @[#@]), first: (=> @[1]) _1_st_to_last:nth_to_last, _1_nd_to_last:nth_to_last _1_rd_to_last:nth_to_last, _1_th_to_last:nth_to_last + -- TODO: use stringify() to allow joining misc. objects? + joined: => table.concat([tostring(x) for x in *@]), + joined_with_1: (glue)=> table.concat([tostring(x) for x in *@], glue), has_1: (item)=> for x in *@ if x == item @@ -101,4 +106,98 @@ Dict = (t)-> setmetatable(t, _dict_mt) for i,entry in ipairs(Dict({x:99})) assert(i == 1 and entry.key == "x" and entry.value == 99, "ipairs compatibility issue") -return {:List, :Dict} +local Text +do + {:reverse, :upper, :lower, :find, :byte, :match, :gmatch, :gsub, :sub, :format, :rep} = string + + -- Convert an arbitrary text into a valid Lua identifier. This function is injective, + -- but not idempotent, i.e. if (x != y) then (as_lua_id(x) != as_lua_id(y)), + -- but as_lua_id(x) is not necessarily equal to as_lua_id(as_lua_id(x)) + as_lua_id = (str)-> + -- Empty strings are not valid lua identifiers, so treat them like "\3", + -- and treat "\3" as "\3\3", etc. to preserve injectivity. + str = gsub str, "^\3*$", "%1\3" + -- Escape 'x' when it precedes something that looks like an uppercase hex sequence. + -- This way, all Lua IDs can be unambiguously reverse-engineered, but normal usage + -- of 'x' won't produce ugly Lua IDs. + -- i.e. "x" -> "x", "oxen" -> "oxen", but "Hex2Dec" -> "Hex782Dec" and "He-ec" -> "Hex2Dec" + str = gsub str, "x([0-9A-F][0-9A-F])", "x78%1" + -- Map spaces to underscores, and everything else non-alphanumeric to hex escape sequences + str = gsub str, "%W", (c)-> + if c == ' ' then '_' + else format("x%02X", byte(c)) + -- Lua IDs can't start with numbers, so map "1" -> "_1", "_1" -> "__1", etc. + str = gsub str, "^_*%d", "_%1" + return str + + line_matcher = re.compile([[ + lines <- {| line (%nl line)* |} + line <- {(!%nl .)*} + ]], nl:lpeg.P("\r")^-1 * lpeg.P("\n")) + + text_methods = + reversed:=>reverse(tostring @) + uppercase:=>upper(tostring @) + lowercase:=>lower(tostring @) + as_lua_id:=>as_lua_id(tostring @) + formatted_with_1:(args)=>format(tostring(@), unpack(args)) + byte_1:(i)=>byte(tostring(@), i) + position_of_1:=>find(tostring @), + position_of_1_after_2:(i)=> find(tostring(@), i) + bytes_1_to_2: (start, stop)=> List{byte(tostring(@), start, stop)} + bytes: => List{byte(tostring(@), 1, #@)}, + capitalized: => gsub(tostring(@), '%l', upper, 1) + lines: => List(line_matcher\match(@)) + matches_1: (patt)=> match(tostring(@), patt) and true or false + [as_lua_id "* 1"]: (n)=> rep(@, n) + matching_1: (patt)=> + result = {} + stepper,x,i = gmatch(tostring(@), patt) + while true + tmp = List{stepper(x,i)} + break if #tmp == 0 + i = tmp[1] + result[#result+1] = tmp + return List(result) + [as_lua_id "with 1 -> 2"]: (patt, sub)=> gsub(tostring(@), patt, sub) + _coalesce: => + if rawlen(@) > 1 + s = table.concat(@) + for i=rawlen(@), 2, -1 do @[i] = nil + @[1] = s + return @ + + setmetatable(text_methods, {__index:string}) + + getmetatable("").__index = (i)=> + -- Use [] for accessing text characters, or s[{3,4}] for s:sub(3,4) + if type(i) == 'number' then return sub(@, i, i) + elseif type(i) == 'table' then return sub(@, i[1], i[2]) + else return text_methods[i] + + assert(("abc")\matches_1("ab")) + + [==[ + text_metatable = + __mul: (other)=> + assert(type(other) == 'number', "Invalid type for multiplication") + return rep(@, other) + __index: (i)=> + -- Use [] for accessing text characters, or s[{3,4}] for s:sub(3,4) + if type(i) == 'number' then return sub(@, i, i) + elseif type(i) == 'table' then return sub(@, i[1], i[2]) + else return text_methods[i] + __tostring: => @_coalesce![1] + __len: => #tostring(@) + __concat: (other)=> tostring(@), tostring(other) + __len: => #tostring(@) + __eq: (other)=> + type(@) == type(other) and getmetatable(@) == getmetatable(other) and tostring(@) == tostring(other) + __lt: (other)=> tostring(@) < tostring(other) + __le: (other)=> tostring(@) <= tostring(other) + __newindex: => error("Cannot modify Text") + + Text = (s)-> setmetatable(s, text_metatable) + ]==] + +return {:List, :Dict, :Text} diff --git a/core/errors.nom b/core/errors.nom index 874f2fe..638b487 100644 --- a/core/errors.nom +++ b/core/errors.nom @@ -18,6 +18,19 @@ compile [assume %condition] to: error(\(quote "\%assumption"), 0) end +compile [assume %a == %b] to: + lua> ".." + local \%assumption = 'Assumption failed: '..tostring(nomsu:tree_to_nomsu(\(\(%a == %b)))) + 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 + compile [assume %condition or barf %message] to (..) Lua ".." if not \(%condition as lua expr) then diff --git a/core/text.nom b/core/text.nom index d9d85b5..bd9335a 100644 --- a/core/text.nom +++ b/core/text.nom @@ -6,10 +6,8 @@ use "core/metaprogramming.nom" test: - assume (("x" + "y") == "xy") - %s = "list:\[1, 2, 3]" - assume (%s == "list:[1, 2, 3]") - assume ("foo = \(1 + 2)!" == "foo = 3!") + assume "\[1, 2, 3]" == "[1, 2, 3]" + assume "foo = \(1 + 2)!" == "foo = 3!" assume (..) ".." one @@ -20,73 +18,35 @@ test: no\ ..gap ..== "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\ntwo\n"::lines) == ["one", "two", ""] + parse [アクション %spec %body] as (action %spec %body) test: %こんにちは = "こんにちは" アクション [% と言う] "\(%)世界" - assume ((%こんにちは と言う) == "こんにちは世界") or barf ".." - Unicode doesn't work + assume (%こんにちは と言う) == "こんにちは世界" # Text functions -test: - assume ((["x", "y"] joined with ",") == "x,y") or barf "joined with failed" - assume ((["x", "y"] joined) == "xy") or barf "joined failed" -action [%texts joined with %glue] (..) - lua> ".." - local text_bits = {} - for i,bit in ipairs(\%texts) do text_bits[i] = stringify(bit) end - return table.concat(text_bits, \%glue) - -parse [joined %texts, %texts joined] as (%texts joined with "") - -test: - assume ((byte 2 of "BAR") == 65) - assume ((bytes 1 to 2 of "BAR") == [66, 65]) -compile [byte %i of %text] to (..) - Lua value "(\(%text as lua expr)):byte(\(%i as lua expr))" - -compile [bytes %start to %stop of %text] to (..) - Lua value ".." - _List{(\(%text as lua expr)):byte(\(%start as lua expr), \(%stop as lua expr))} - -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -action [bytes of %text] (bytes 1 to (=lua "#\%text") of %text) - -test: - assume (("asdf" capitalized) == "Asdf") -compile [capitalized %text, %text capitalized] to (..) - Lua value "(\(%text as lua expr)):gsub('%l', string.upper, 1)" - -test: - assume (("asdf" uppercase) == "ASDF") -compile [uppercase %text, %text uppercase] to (..) - Lua value "(\(%text as lua expr)):upper()" - -test: - assume (("asdf" with "X" instead of "s") == "aXdf") or barf ".." - substitution failed -compile [..] +parse [%texts joined with %glue] as (%texts::joined with %glue) +parse [%texts joined, joined %texts] as (%texts::joined) +parse [byte %i of %text] as (%text::byte %i) +parse [bytes %start to %stop of %text] as (%text::bytes %start to %stop) +parse [bytes of %text] as (%text::bytes) +parse [capitalized %text, %text capitalized] as (%text::capitalized) +parse [uppercase %text, %text uppercase] as (%text::uppercase) +parse [..] %text with %sub instead of %patt, %text with %patt replaced by %sub %text s/ %patt / %sub -..to (..) - Lua value ".." - ((\(%text as lua expr)):gsub(\(%patt as lua expr), \(%sub as lua expr))) - -test: - assume (..) - (..) - lines in ".." - one - two - ..== ["one", "two"] -action [lines in %text, lines of %text] (..) - lua> ".." - local result = _List{} - for line in (\%text):gmatch('[^\\n]+') do - result[#result+1] = line - end - return result +..as (%text::with %patt -> %sub) +parse [%text matches %pattern] as (%text::matches %pattern) +parse [%text matching %pattern] as ((%text::matching %pattern).1) compile [for %match in %text matching %patt %body] to (..) Lua ".." @@ -108,15 +68,8 @@ compile [%expr for %match in %text matching %patt] to (..) return ret end)() -compile [%text matches %pattern] to (..) - Lua value ".." - ((\(%text as lua expr)):match(\(%pattern as lua expr)) and true or false) - -compile [%text matching %pattern] to (..) - Lua value "(\(%text as lua expr)):match(\(%pattern as lua expr))" - test: - assume ("\n" == (newline)) or barf "Text literals failed." + assume "\n" == (newline) # Text literals lua> ".." diff --git a/nomsu_compiler.lua b/nomsu_compiler.lua index 02460b3..215617f 100644 --- a/nomsu_compiler.lua +++ b/nomsu_compiler.lua @@ -4,10 +4,10 @@ local utils = require('utils') local Files = require('files') local repr, stringify, equivalent repr, stringify, equivalent = utils.repr, utils.stringify, utils.equivalent -local List, Dict +local List, Dict, Text do local _obj_0 = require('containers') - List, Dict = _obj_0.List, _obj_0.Dict + List, Dict, Text = _obj_0.List, _obj_0.Dict, _obj_0.Text end colors = require('consolecolors') colored = setmetatable({ }, { @@ -74,23 +74,6 @@ table.copy = function(t) return _tbl_0 end)(), getmetatable(t)) end -do - local STRING_METATABLE = getmetatable("") - STRING_METATABLE.__add = function(self, other) - return self .. stringify(other) - end - STRING_METATABLE.__index = function(self, i) - local ret = string[i] - if ret ~= nil then - return ret - end - if type(i) == 'number' then - return sub(self, i, i) - elseif type(i) == 'table' then - return sub(self, i[1], i[2]) - end - end -end local MAX_LINE = 80 local NomsuCompiler = setmetatable({ name = "Nomsu" @@ -531,7 +514,12 @@ do end local lua = LuaCode.Value(tree.source) if tree.target then - lua:append(self:compile(tree.target), ":") + local target_lua = self:compile(tree.target) + if tostring(target_lua):match("^%(.*%)$") or tostring(target_lua):match("^[_a-zA-Z][_a-zA-Z0-9]*$") then + lua:append(target_lua, ":") + else + lua:append("(", target_lua, "):") + end end lua:append(string.as_lua_id(stub), "(") local args = { } diff --git a/nomsu_compiler.moon b/nomsu_compiler.moon index bae3f11..7a637f4 100644 --- a/nomsu_compiler.moon +++ b/nomsu_compiler.moon @@ -14,7 +14,7 @@ re = require 're' utils = require 'utils' Files = require 'files' {:repr, :stringify, :equivalent} = utils -{:List, :Dict} = require 'containers' +{:List, :Dict, :Text} = require 'containers' export colors, colored colors = require 'consolecolors' colored = setmetatable({}, {__index:(_,color)-> ((msg)-> colors[color]..tostring(msg or '')..colors.reset)}) @@ -57,18 +57,6 @@ table.copy = (t)-> setmetatable({k,v for k,v in pairs(t)}, getmetatable(t)) -- consider non-linear codegen, rather than doing thunks for things like comprehensions -- Re-implement nomsu-to-lua comment translation? --- Use + operator for string coercive concatenation (note: "asdf" + 3 == "asdf3") --- Use [] for accessing string characters, or s[{3,4}] for s:sub(3,4) --- Note: This globally affects all strings in this instance of Lua! -do - STRING_METATABLE = getmetatable("") - STRING_METATABLE.__add = (other)=> @ .. stringify(other) - STRING_METATABLE.__index = (i)=> - ret = string[i] - if ret != nil then return ret - if type(i) == 'number' then return sub(@, i, i) - elseif type(i) == 'table' then return sub(@, i[1], i[2]) - MAX_LINE = 80 -- For beautification purposes, try not to make lines much longer than this value NomsuCompiler = setmetatable {name:"Nomsu"}, __index: (k)=> if _self = rawget(@, "self") then _self[k] else nil @@ -339,8 +327,12 @@ with NomsuCompiler return ret lua = LuaCode.Value(tree.source) - if tree.target - lua\append @compile(tree.target), ":" + if tree.target -- Method call + target_lua = @compile tree.target + if tostring(target_lua)\match("^%(.*%)$") or tostring(target_lua)\match("^[_a-zA-Z][_a-zA-Z0-9]*$") + lua\append target_lua, ":" + else + lua\append "(", target_lua, "):" lua\append(string.as_lua_id(stub),"(") args = {} for i, tok in ipairs tree