1
0
Fork 0
mirror of https://github.com/Reuh/candran.git synced 2025-10-27 09:59:29 +00:00

Added safe operators, if/while with assignement, method stubs

This commit is contained in:
Étienne Fildadut 2019-08-27 17:07:33 +02:00
parent 6be81267d2
commit 851e9f89d6
11 changed files with 3688 additions and 2845 deletions

100
README.md
View file

@ -20,6 +20,8 @@ end
let a = {
hey = true,
child = nil,
method = :(foo, thing) -- short function declaration, with self
@hey = thing(foo) -- @ as an alias for self
end,
@ -33,6 +35,13 @@ a:method(42, (foo)
return "something " .. foo
end)
local fn = a:method -- bundles an object and method in a function
fn(42, (foo)
return "something" .. foo
end)
a.child?:method?() -- safe navigation operator
local odd = [ -- table comprehension
for i=1, 10 do
if i%2 == 0 then
@ -48,6 +57,12 @@ local a = if condition then "one" else "two" end -- statement as expressions
print("Hello %s":format("world")) -- methods calls on strings (and tables) litterals without enclosing parentheses
if f, err = io.open("data") then -- if condition with assignements
thing.process(f)
else
error("can't open data: "..err)
end
````
**Current status**: Candran is heavily used in several of my personal projects and works as expected.
@ -97,7 +112,7 @@ All theses operators can also be put right of the assigment operator, in which c
Right and left operator can be used at the same time.
**Please note** that the Lua code `a=-1` will be compiled into `a = 1 - a` and not `a = -1`! Write spaced code: `a = -1` works as expected.
**Please note** that the code `a=-1` will be compiled into `a = -1` and not `a = a - 1`, like in pure Lua. If you want the latter, spacing is required between the `=-` and the expression: `a=- 1`. Yes, this is also valid Lua code, but as far as I'm aware, nobody write code like this; people who really like spacing would write `a= - 1` or `a = - 1`, and Candran will read both of those as it is expected in pure Lua. This is the only incompatibility between Candran and pure Lua.
##### Default function parameters
```lua
@ -230,6 +245,68 @@ Values returned by the function will be inserted in the generated table in the o
The table generation function also have access to the `self` variable (and its alias `@`), which is the table which is being created, so you can set any of the table's field.
##### Safe navigation operators
```lua
a = nil
print(a?.b) -- nil
a = {b=true}
print(a?.b) -- true
-- So instead of typing
if object and object.child and object.child.isGreen then
-- stuff
end
-- you can type
if object?.child?.isGreen then
-- stuff
end
-- The ?. operator does not break the whole chain; make sure to use the operator on each index.
print(a?.undefined.field) -- a?.undefined returns nil, so this throws a "attempt to index a nil value"
-- Other safe navigator operators behave similarly:
print(a:method) -- nil if a is nil, other normal behaviour
print(a["key"]) -- nil if a is nil, other normal behaviour
print(a?()) -- nil if a is nil, other normal behaviour
```
Some operators can be prefixed by a `?` to turn into a safe version of the operator: if the base value if `nil`, the normal behaviour of the operator will be skipped and nil will be returned; otherwise, the operator run as usual. Is available safe dot index `?.`, safe array index `?[...]`, safe method stub `?:` and safe function call `?(...)`.
##### If and while with assignement in the condition
```lua
if f, err = io.open("somefile") then -- condition if verified if f is a truthy value (not nil or false)
-- do something with f
f:close()
elseif f2, err2 = io.open("anotherfile") then -- same behaviour on elseif
print("could not open somefile:", err) -- f and err stay in scope for the rest of the if-elseif-else block
-- do something with f2
f2:close()
else
print("could not open somefile:", err)
print("could not open anotherfile:", err2)
end
-- f, err, f2 and err2 are now out of scope
if (value = list[index = 2]) and yes = true then -- several assignements can be performed, anywhere in the expression; index is defined before value, yes is defined after these two. The condition is verified if both value and yes are thruthy.
print(index, value)
end
-- When used in a while, the expression is evaluated at each iteration.
while line = io.read() do
print(line)
end
-- The assignement have the same priority as regular assignements, i.e., the lowest.
if a = 1 and 2 then -- will be read as a = (1 and 2)
elseif (a = 1) and 2 then -- will be read as (a = 1) and 2
end
```
Assignements can be used in the condition of if, elseif and while statements. Several variables can be assigned; only the first will be tested in the condition, for each assignement. The assigned variables will be in scope the duration of the block; for if statements, they will also be in scope for the following elseif(s) and else.
For while statements, the assigned expression will be reevaluated at each iteration.
##### Suffixable string and table litterals
```lua
"some text":upper() -- "SOME TEXT". Same as ("some text"):upper() in Lua.
@ -246,6 +323,27 @@ String litterals, table litterals, and comprehensions can be suffixed with `:` m
**Please note**, that "normal" functions calls have priority over this syntax, in order to maintain Lua compatibility.
##### Method stubs
```lua
object = {
value = 25,
method = function(self, str)
print(str, self.value)
end
}
stub = object:method
object.method = error -- stub stores the method as it was when stub was defined
object = nil -- also stores the object
print(stub("hello")) -- hello 25
```
Create a closure function which bundles the variable and its method; when called it will call the method on the variable, without requiring to pass the variable as a first argument.
The closure stores the value of the variable and method when created.
##### Statement expressions
```lua
a = if false then

View file

@ -86,7 +86,7 @@ function candran.preprocess(input, options={})
-- @tparam modpath string module path
-- @tparam margs table preprocessor options to use when preprocessessing the module
env.import = function(modpath, margs={})
local filepath = assert(util.search(modpath), "No module named \""..modpath.."\"")
local filepath = assert(util.search(modpath, {"can", "lua"}), "No module named \""..modpath.."\"")
-- open module file
local f = io.open(filepath)

File diff suppressed because it is too large Load diff

View file

@ -1,3 +1,5 @@
targetName = "Lua 5.1"
states.continue = {} -- when in a loop that use continue
CONTINUE_START = ()
@ -19,12 +21,9 @@ tags.Break = ()
end
end
tags.Goto = ()
error("Lua 5.1 does not support the goto keyword")
end
tags.Label = ()
error("Lua 5.1 does not support labels")
end
-- Unsuported features
tags.Goto = nil
tags.Label = nil
#local patch = output
#output = ""

View file

@ -1,3 +1,5 @@
local targetName = "Lua 5.3"
return function(code, ast, options)
--- Line mapping
local lastInputPos = 1 -- last token position in the input code
@ -58,8 +60,9 @@ return function(code, ast, options)
end
--- AST traversal helpers
local loop = { "While", "Repeat", "Fornum", "Forin" } -- loops tags
local func = { "Function", "TableCompr", "DoExpr", "WhileExpr", "RepeatExpr", "IfExpr", "FornumExpr", "ForinExpr" } -- function scope tags
local loop = { "While", "Repeat", "Fornum", "Forin", "WhileExpr", "RepeatExpr", "FornumExpr", "ForinExpr" } -- loops tags (can contain continue)
local func = { "Function", "TableCompr", "DoExpr", "WhileExpr", "RepeatExpr", "IfExpr", "FornumExpr", "ForinExpr" } -- function scope tags (can contain push)
-- Returns the first node or subnode from the list "list" which tag is in the list "tags", or nil if there were none.
-- Won't recursively follow nodes which have a tag in "nofollow".
local function any(list, tags, nofollow={})
@ -85,6 +88,50 @@ return function(code, ast, options)
return nil
end
-- Like any, but returns a list of every node found.
-- Order: in the order of the list, from the deepest to the nearest
local function search(list, tags, nofollow={})
local tagsCheck = {}
for _, tag in ipairs(tags) do
tagsCheck[tag] = true
end
local nofollowCheck = {}
for _, tag in ipairs(nofollow) do
nofollowCheck[tag] = true
end
local found = {}
for _, node in ipairs(list) do
if type(node) == "table" then
if not nofollowCheck[node.tag] then
for _, n in ipairs(search(node, tags, nofollow)) do
table.insert(found, n)
end
end
if tagsCheck[node.tag] then
table.insert(found, node)
end
end
end
return found
end
-- Returns true if the all the nodes in list have their type in tags.
local function all(list, tags)
for _, node in ipairs(list) do
local ok = false
for _, tag in ipairs(tags) do
if node.tag == tag then
ok = true
break
end
end
if not ok then
return false
end
end
return true
end
--- State stacks
-- Used for context-sensitive syntax.
local states = {
@ -126,7 +173,7 @@ return function(code, ast, options)
return "do"..indent()
end
local CONTINUE_STOP = () -- at the start of loops using continue
return unindent() .. "end" .. newline() .. "::" .. var("continue") .. "::"
return unindent().."end"..newline().."::"..var"continue".."::"
end
--- Tag constructors
@ -140,7 +187,7 @@ return function(code, ast, options)
end
local r = ""
if hasPush then
r ..= push("push", var("push")) .. "local " .. var("push") .. " = {}" .. newline()
r ..= push("push", var"push").."local "..var"push".." = {}"..newline()
end
for i=1, #t-1, 1 do
r ..= lua(t[i])..newline()
@ -149,7 +196,7 @@ return function(code, ast, options)
r ..= lua(t[#t])
end
if hasPush and (t[#t] and t[#t].tag ~= "Return") then -- add return only if needed
r ..= newline() .. "return " .. UNPACK(var("push")) .. pop("push")
r ..= newline().."return "..UNPACK(var"push")..pop("push")
end
return r
end,
@ -190,8 +237,19 @@ return function(code, ast, options)
end,
-- While{ expr block }
While = (t)
local r = ""
local hasContinue = any(t[2], { "Continue" }, loop)
local r = "while " .. lua(t[1]) .. " do" .. indent()
local lets = search({ t[1] }, { "LetExpr" })
if #lets > 0 then
r ..= "do"..indent()
for _, l in ipairs(lets) do
r ..= lua(l, "Let")..newline()
end
end
r ..= "while "..lua(t[1]).." do"..indent()
if #lets > 0 then
r ..= "do"..indent()
end
if hasContinue then
r ..= CONTINUE_START()
end
@ -200,6 +258,12 @@ return function(code, ast, options)
r ..= CONTINUE_STOP()
end
r ..= unindent().."end"
if #lets > 0 then
for _, l in ipairs(lets) do
r ..= newline()..lua(l, "Set")
end
r ..= unindent().."end"..unindent().."end"
end
return r
end,
-- Repeat{ block expr }
@ -216,16 +280,40 @@ return function(code, ast, options)
r ..= unindent().."until "..lua(t[2])
return r
end,
-- If{ (expr block)+ block? }
-- If{ (lexpr block)+ block? }
If = (t)
local r = "if " .. lua(t[1]) .. " then" .. indent() .. lua(t[2]) .. unindent()
local r = ""
local toClose = 0 -- blocks that need to be closed at the end of the if
local lets = search({ t[1] }, { "LetExpr" })
if #lets > 0 then
r ..= "do"..indent()
toClose += 1
for _, l in ipairs(lets) do
r ..= lua(l, "Let")..newline()
end
end
r ..= "if "..lua(t[1]).." then"..indent()..lua(t[2])..unindent()
for i=3, #t-1, 2 do
r ..= "elseif " .. lua(t[i]) .. " then" .. indent() .. lua(t[i+1]) .. unindent()
lets = search({ t[i] }, { "LetExpr" })
if #lets > 0 then
r ..= "else"..indent()
toClose += 1
for _, l in ipairs(lets) do
r ..= lua(l, "Let")..newline()
end
else
r ..= "else"
end
r ..= "if "..lua(t[i]).." then"..indent()..lua(t[i+1])..unindent()
end
if #t % 2 == 1 then
r ..= "else"..indent()..lua(t[#t])..unindent()
end
return r .. "end"
r ..= "end"
for i=1, toClose do
r ..= unindent().."end"
end
return r
end,
-- Fornum{ ident expr expr expr? block }
Fornum = (t)
@ -280,10 +368,10 @@ return function(code, ast, options)
local nameList = lua(t[1], "_lhs")
local r = "local "..nameList
if t[2][1] then
if any(t[2], { "Function", "Table", "Paren" }) then -- predeclaration doesn't matter otherwise
r ..= newline() .. nameList .. " = " .. lua(t[2], "_lhs")
else
if all(t[2], { "Nil", "Dots", "Boolean", "Number", "String" }) then -- predeclaration doesn't matter here
r ..= " = "..lua(t[2], "_lhs")
else
r ..= newline()..nameList.." = "..lua(t[2], "_lhs")
end
end
return r
@ -321,7 +409,7 @@ return function(code, ast, options)
r ..= var.."[#"..var.."+1] = "..lua(t[i])..newline()
end
if t[#t] then
if t[#t].tag == "Call" or t[#t].tag == "Invoke" then
if t[#t].tag == "Call" then
r ..= APPEND(var, lua(t[#t]))
else
r ..= var.."[#"..var.."+1] = "..lua(t[#t])
@ -335,7 +423,7 @@ return function(code, ast, options)
end,
-- Continue
Continue = ()
return "goto " .. var("continue")
return "goto "..var"continue"
end,
-- apply (below)
@ -359,7 +447,7 @@ return function(code, ast, options)
end,
-- String{ <string> }
String = (t)
return ("%q"):format(t[1])
return "%q":format(t[1])
end,
-- Function{ { ( `ParPair{ Id expr } | `Id{ <string> } )* `Dots? } block }
_functionWithoutKeyword = (t)
@ -396,13 +484,13 @@ return function(code, ast, options)
end
local hasPush = any(t[2], { "Push" }, func)
if hasPush then
r ..= push("push", var("push")) .. "local " .. var("push") .. " = {}" .. newline()
r ..= push("push", var"push").."local "..var"push".." = {}"..newline()
else
push("push", false) -- no push here (make sure higher push don't affect us)
push("push", false) -- no push here (make sure higher push doesn't affect us)
end
r ..= lua(t[2])
if hasPush and (t[2][#t[2]] and t[2][#t[2]].tag ~= "Return") then -- add return only if needed
r ..= newline() .. "return " .. UNPACK(var("push"))
r ..= newline().."return "..UNPACK(var"push")
end
pop("push")
return r..unindent().."end"
@ -449,22 +537,46 @@ return function(code, ast, options)
Paren = (t)
return "("..lua(t[1])..")"
end,
-- MethodStub{ expr expr }
MethodStub = (t)
return "(function()"..indent() ..
"local "..var"object".." = "..lua(t[1])..newline()..
"local "..var"method".." = "..var"object".."."..lua(t[2], "Id")..newline() ..
"if "..var"method".." == nil then return nil end"..newline()..
"return function(...) return "..var"method".."("..var"object"..", ...) end"..unindent()..
"end)()"
end,
-- SafeMethodStub{ expr expr }
SafeMethodStub = (t)
return "(function()"..indent() ..
"local "..var"object".." = "..lua(t[1])..newline()..
"if "..var"object".." == nil then return nil end"..newline()..
"local "..var"method".." = "..var"object".."."..lua(t[2], "Id")..newline() ..
"if "..var"method".." == nil then return nil end"..newline()..
"return function(...) return "..var"method".."("..var"object"..", ...) end"..unindent()..
"end)()"
end,
-- statexpr (below)
-- apply (below)
-- lhs (below)
-- lexpr --
LetExpr = (t)
return lua(t[1][1])
end,
-- statexpr --
_statexpr = (t, stat)
local hasPush = any(t, { "Push" }, func)
local r = "(function()"..indent()
if hasPush then
r ..= push("push", var("push")) .. "local " .. var("push") .. " = {}" .. newline()
r ..= push("push", var"push").."local "..var"push".." = {}"..newline()
else
push("push", false) -- no push here (make sure higher push don't affect us)
end
r ..= lua(t, stat)
if hasPush then
r ..= newline() .. "return " .. UNPACK(var("push"))
r ..= newline().."return "..UNPACK(var"push")
end
pop("push")
r ..= unindent().."end)()"
@ -510,17 +622,22 @@ return function(code, ast, options)
Call = (t)
if t[1].tag == "String" or t[1].tag == "Table" then
return "("..lua(t[1])..")("..lua(t, "_lhs", 2)..")"
elseif t[1].tag == "MethodStub" then -- method call
if t[1][1].tag == "String" or t[1][1].tag == "Table" then
return "("..lua(t[1][1]).."):"..lua(t[1][2], "Id").."("..lua(t, "_lhs", 2)..")"
else
return lua(t[1][1])..":"..lua(t[1][2], "Id").."("..lua(t, "_lhs", 2)..")"
end
else
return lua(t[1]).."("..lua(t, "_lhs", 2)..")"
end
end,
-- Invoke{ expr `String{ <string> } expr* }
Invoke = (t)
if t[1].tag == "String" or t[1].tag == "Table" then
return "("..lua(t[1]).."):"..lua(t[2], "Id").."("..lua(t, "_lhs", 3)..")"
else
return lua(t[1])..":"..lua(t[2], "Id").."("..lua(t, "_lhs", 3)..")"
-- SafeCall{ expr expr* }
SafeCall = (t)
if t[1].tag ~= "Id" then -- side effect possible, only evaluate each expr once (or already in a safe context)
return lua(t, "SafeIndex")
else -- no side effects possible
return "("..lua(t[1]).." ~= nil and "..lua(t[1]).."("..lua(t, "_lhs", 2)..") or nil)"
end
end,
@ -549,6 +666,29 @@ return function(code, ast, options)
return lua(t[1]).."["..lua(t[2]).."]"
end
end,
-- SafeIndex{ expr expr }
SafeIndex = (t)
if t[1].tag ~= "Id" then -- side effect possible, only evaluate each expr once (or already in a safe context)
local l = {} -- list of immediately chained safeindex, from deepest to nearest (to simply generated code)
while t.tag == "SafeIndex" or t.tag == "SafeCall" do
table.insert(l, 1, t)
t = t[1]
end
local r = "(function()"..indent().."local "..var"safe".." = "..lua(l[1][1])..newline() -- base expr
for _, e in ipairs(l) do
r ..= "if "..var"safe".." == nil then return nil end"..newline()
if e.tag == "SafeIndex" then
r ..= var"safe".." = "..var"safe".."["..lua(e[2]).."]"..newline()
else
r ..= var"safe".." = "..var"safe".."("..lua(e, "_lhs", 2)..")"..newline()
end
end
r ..= "return "..var"safe"..unindent().."end)()"
return r
else -- no side effects possible
return "("..lua(t[1]).." ~= nil and "..lua(t[1]).."["..lua(t[2]).."] or nil)"
end
end,
-- opid --
_opid = {
@ -560,7 +700,7 @@ return function(code, ast, options)
}
}, {
__index = (self, key)
error("don't know how to compile a "..tostring(key).." to Lua 5.3")
error("don't know how to compile a "..tostring(key).." to "..targetName)
end
})

View file

@ -1,3 +1,5 @@
targetName = "luajit"
UNPACK = (list, i, j)
return "unpack(" .. list .. (i and (", " .. i .. (j and (", " .. j) or "")) or "") .. ")"
end

View file

@ -8,7 +8,7 @@ To be implemented, theese need to:
* be significantly useful compared to existing Candran/Lua code.
* be useful without having to rewrite APIs specifically for Candran. Candran intends to make Lua easier, not supersede it.
Example currently rejected ideas:
Example rejected ideas:
* Python-style function decorators (implemented in Candran 0.1.0):
Useless 95% of the time because most Lua APIs applying something to a function are used like applySomething(someArg,func) instead of func=applySomething(someArg)(func).
This could be adapted, but this will mean unecessary named functions in the environment and it will only works when the decorator returns the functions.
@ -48,15 +48,6 @@ finally
clean()
end
* Safe navigation operator
local name = articles?[0].author?.name (?[] and ?. index opeators)
or
local zip = lottery.drawWinner?().address?.zipcode (expr? existence test suffix)
if one is nil, returns nil
See http://coffeescript.org/#existential-operator
* static type checking
local a = externalFunc() -- unknown
if a == "hey" then
@ -103,13 +94,3 @@ meh
* Other potential inspiration
https://www.ruby-lang.org/fr/
* If with assignement
if f = io.open("file") then
f:close()
end
f is in the if scope
* Method self bundler
obj:method -- returns (...) return method(obj, ...) end

View file

@ -11,7 +11,7 @@ stat:
| `Set{ {lhs+} (opid? = opid?)? {expr+} } -- lhs1, lhs2... op=op e1, e2...
| `While{ expr block } -- while e do b end
| `Repeat{ block expr } -- repeat b until e
| `If{ (expr block)+ block? } -- if e1 then b1 [elseif e2 then b2] ... [else bn] end
| `If{ (lexpr block)+ block? } -- if e1 then b1 [elseif e2 then b2] ... [else bn] end
| `Fornum{ ident expr expr expr? block } -- for ident = e, e[, e] do b end
| `Forin{ {ident+} {expr+} block } -- for i1, i2... in e1, e2... do b end
| `Local{ {ident+} {expr+}? } -- local i1, i2... = e1, e2...
@ -36,10 +36,17 @@ expr:
| `Op{ opid expr expr? }
| `Paren{ expr } -- significant to cut multiple values returns
| `TableCompr{ block }
| `MethodStub{ expr expr }
| `SafeMethodStub{ expr expr }
| `SafeIndex{ expr expr }
| statexpr
| apply
| lhs
lexpr:
`LetExpr{ {ident+} {expr+}? }
| every node from expr
statexpr:
`DoExpr{ stat* }
| `WhileExpr{ expr block } -- while e do b end
@ -50,7 +57,7 @@ statexpr:
apply:
`Call{ expr expr* }
| `Invoke{ expr `String{ <string> } expr* }
| `SafeCall{ expr expr* }
lhs: `Id{ <string> } | `Index{ expr expr }
@ -262,15 +269,22 @@ local function markMethod(t, method)
return t
end
local function makeIndexOrCall (t1, t2)
if t2.tag == "Call" or t2.tag == "Invoke" then
local function makeSuffixedExpr (t1, t2)
if t2.tag == "Call" or t2.tag == "SafeCall" then
local t = { tag = t2.tag, pos = t1.pos, [1] = t1 }
for k, v in ipairs(t2) do
table.insert(t, v)
end
return t
end
elseif t2.tag == "MethodStub" or t2.tag == "SafeMethodStub" then
return { tag = t2.tag, pos = t1.pos, [1] = t1, [2] = t2[1] }
elseif t2.tag == "SafeDotIndex" or t2.tag == "SafeArrayIndex" then
return { tag = "SafeIndex", pos = t1.pos, [1] = t1, [2] = t2[1] }
elseif t2.tag == "DotIndex" or t2.tag == "ArrayIndex" then
return { tag = "Index", pos = t1.pos, [1] = t1, [2] = t2[1] }
else
error("unexpected tag in suffixed expression")
end
end
local function fixShortFunc (t)
@ -434,6 +448,30 @@ local function maybeBlockWithEnd() -- same as above but don't error if it doesn'
+ Cmt(V"BlockNoErr", searchEnd)
end
local stacks = {
lexpr = {}
}
local function push (f)
return Cmt(P"", function()
table.insert(stacks[f], true)
return true
end)
end
local function pop (f)
return Cmt(P"", function()
table.remove(stacks[f])
return true
end)
end
local function when (f)
return Cmt(P"", function()
return #stacks[f] > 0
end)
end
local function set (f, patt) -- patt *must* succeed (or throw an error) to preserve stack integrity
return push(f) * patt * pop(f)
end
-- grammar
local G = { V"Lua",
Lua = (V"Shebang"^-1 * V"Skip" * V"Block" * expect(P(-1), "Extra")) / fixStructure;
@ -451,12 +489,12 @@ local G = { V"Lua",
BlockNoErr = tagC("Block", V"Stat"^0 * ((V"RetStat" + V"ImplicitPushStat") * sym(";")^-1)^-1); -- used to check if something a valid block without throwing an error
IfStat = tagC("If", V"IfPart");
IfPart = kw("if") * expect(V"Expr", "ExprIf") * expectBlockOrSingleStatWithStartEnd(kw("then"), "ThenIf", "EndIf", V"ElseIfPart" + V"ElsePart");
ElseIfPart = kw("elseif") * expect(V"Expr", "ExprEIf") * expectBlockOrSingleStatWithStartEnd(kw("then"), "ThenEIf", "EndIf", V"ElseIfPart" + V"ElsePart");
IfPart = kw("if") * set("lexpr", expect(V"Expr", "ExprIf")) * expectBlockOrSingleStatWithStartEnd(kw("then"), "ThenIf", "EndIf", V"ElseIfPart" + V"ElsePart");
ElseIfPart = kw("elseif") * set("lexpr", expect(V"Expr", "ExprEIf")) * expectBlockOrSingleStatWithStartEnd(kw("then"), "ThenEIf", "EndIf", V"ElseIfPart" + V"ElsePart");
ElsePart = kw("else") * expectBlockWithEnd("EndIf");
DoStat = kw("do") * expectBlockWithEnd("EndDo") / tagDo;
WhileStat = tagC("While", kw("while") * expect(V"Expr", "ExprWhile") * V"WhileBody");
WhileStat = tagC("While", kw("while") * set("lexpr", expect(V"Expr", "ExprWhile")) * V"WhileBody");
WhileBody = expectBlockOrSingleStatWithStartEnd(kw("do"), "DoWhile", "EndWhile");
RepeatStat = tagC("Repeat", kw("repeat") * V"Block" * expect(kw("until"), "UntilRep") * expect(V"Expr", "ExprRep"));
@ -474,7 +512,7 @@ local G = { V"Lua",
LetStat = kw("let") * expect(V"LetAssign", "DefLet");
LetAssign = tagC("Let", V"NameList" * (sym("=") * expect(V"ExprList", "EListLAssign") + Ct(Cc())));
Assignment = tagC("Set", V"VarList" * V"BinOp"^-1 * (P"=" / "=") * V"BinOp"^-1 * V"Skip" * expect(V"ExprList", "EListAssign"));
Assignment = tagC("Set", V"VarList" * V"BinOp"^-1 * (P"=" / "=") * ((V"BinOp" - P"-") + #(P"-" * V"Space") * V"BinOp")^-1 * V"Skip" * expect(V"ExprList", "EListAssign"));
FuncStat = tagC("Set", kw("function") * expect(V"FuncName", "FuncName") * V"FuncBody") / fixFuncStat;
FuncName = Cf(V"Id" * (sym(".") * expect(V"StrId", "NameFunc1"))^0, insertIndex)
@ -520,35 +558,39 @@ local G = { V"Lua",
UnaryExpr = V"UnaryOp" * expect(V"UnaryExpr", "UnaryExpr") / unaryOp
+ V"PowExpr";
PowExpr = V"SimpleExpr" * (V"PowOp" * expect(V"UnaryExpr", "PowExpr"))^-1 / binaryOp;
SimpleExpr = tagC("Number", V"Number")
+ tagC("Nil", kw("nil"))
+ tagC("Boolean", kw("false") * Cc(false))
+ tagC("Boolean", kw("true") * Cc(true))
+ tagC("Dots", sym("..."))
+ V"FuncDef"
+ (when("lexpr") * tagC("LetExpr", V"NameList" * sym("=") * -sym("=") * expect(V"ExprList", "EListLAssign")))
+ V"ShortFuncDef"
+ V"SuffixedExpr"
+ V"StatExpr";
StatExpr = (V"IfStat" + V"DoStat" + V"WhileStat" + V"RepeatStat" + V"ForStat") / statToExpr;
FuncCall = Cmt(V"SuffixedExpr", function(s, i, exp) return exp.tag == "Call" or exp.tag == "Invoke", exp end);
FuncCall = Cmt(V"SuffixedExpr", function(s, i, exp) return exp.tag == "Call" or exp.tag == "SafeCall", exp end);
VarExpr = Cmt(V"SuffixedExpr", function(s, i, exp) return exp.tag == "Id" or exp.tag == "Index", exp end);
SuffixedExpr = Cf(V"PrimaryExpr" * (V"Index" + V"Invoke" + V"Call")^0
+ V"NoCallPrimaryExpr" * -V"Call" * (V"Index" + V"Invoke" + V"Call")^0
+ V"NoCallPrimaryExpr", makeIndexOrCall);
SuffixedExpr = Cf(V"PrimaryExpr" * (V"Index" + V"MethodStub" + V"Call")^0
+ V"NoCallPrimaryExpr" * -V"Call" * (V"Index" + V"MethodStub" + V"Call")^0
+ V"NoCallPrimaryExpr", makeSuffixedExpr);
PrimaryExpr = V"SelfId" * (V"SelfCall" + V"SelfIndex")
+ V"Id"
+ tagC("Paren", sym("(") * expect(V"Expr", "ExprParen") * expect(sym(")"), "CParenExpr"));
NoCallPrimaryExpr = tagC("String", V"String") + V"Table" + V"TableCompr";
Index = tagC("DotIndex", sym("." * -P".") * expect(V"StrId", "NameIndex"))
+ tagC("ArrayIndex", sym("[" * -P(S"=[")) * expect(V"Expr", "ExprIndex") * expect(sym("]"), "CBracketIndex"));
Call = tagC("Call", V"FuncArgs");
Invoke = tagC("Invoke", Cg(sym(":" * -P":") * expect(V"StrId", "NameMeth") * expect(V"FuncArgs", "MethArgs")));
+ tagC("ArrayIndex", sym("[" * -P(S"=[")) * expect(V"Expr", "ExprIndex") * expect(sym("]"), "CBracketIndex"))
+ tagC("SafeDotIndex", sym("?." * -P".") * expect(V"StrId", "NameIndex"))
+ tagC("SafeArrayIndex", sym("?[" * -P(S"=[")) * expect(V"Expr", "ExprIndex") * expect(sym("]"), "CBracketIndex"));
MethodStub = tagC("MethodStub", sym(":" * -P":") * expect(V"StrId", "NameMeth"))
+ tagC("SafeMethodStub", sym("?:" * -P":") * expect(V"StrId", "NameMeth"));
Call = tagC("Call", V"FuncArgs")
+ tagC("SafeCall", P"?" * V"FuncArgs");
SelfCall = tagC("MethodStub", V"StrId") * V"Call";
SelfIndex = tagC("DotIndex", V"StrId");
SelfCall = tagC("Invoke", Cg(V"StrId" * V"FuncArgs"));
FuncDef = (kw("function") * V"FuncBody");
FuncArgs = sym("(") * commaSep(V"Expr", "ArgList")^-1 * expect(sym(")"), "CParenArgs")

View file

@ -158,16 +158,6 @@ local function traverse_call (env, call)
return true
end
local function traverse_invoke (env, invoke)
local status, msg = traverse_exp(env, invoke[1])
if not status then return status, msg end
for i=3, #invoke do
status, msg = traverse_exp(env, invoke[i])
if not status then return status, msg end
end
return true
end
local function traverse_assignment (env, stm)
local status, msg = traverse_varlist(env, stm[1])
if not status then return status, msg end
@ -238,6 +228,18 @@ local function traverse_goto (env, stm)
return true
end
local function traverse_let (env, stm)
local status, msg = traverse_explist(env, stm[2])
if not status then return status, msg end
return true
end
local function traverse_letrec (env, stm)
local status, msg = traverse_exp(env, stm[2][1])
if not status then return status, msg end
return true
end
local function traverse_if (env, stm)
local len = #stm
if len % 2 == 0 then
@ -266,18 +268,6 @@ local function traverse_label (env, stm)
return true
end
local function traverse_let (env, stm)
local status, msg = traverse_explist(env, stm[2])
if not status then return status, msg end
return true
end
local function traverse_letrec (env, stm)
local status, msg = traverse_exp(env, stm[2][1])
if not status then return status, msg end
return true
end
local function traverse_repeat (env, stm)
begin_loop(env)
local status, msg = traverse_block(env, stm[1])
@ -327,6 +317,22 @@ function traverse_varlist (env, varlist)
return true
end
local function traverse_methodstub (env, var)
local status, msg = traverse_exp(env, var[1])
if not status then return status, msg end
status, msg = traverse_exp(env, var[2])
if not status then return status, msg end
return true
end
local function traverse_safeindex (env, var)
local status, msg = traverse_exp(env, var[1])
if not status then return status, msg end
status, msg = traverse_exp(env, var[2])
if not status then return status, msg end
return true
end
function traverse_exp (env, exp)
local tag = exp.tag
if tag == "Nil" or
@ -344,15 +350,17 @@ function traverse_exp (env, exp)
return traverse_op(env, exp)
elseif tag == "Paren" then -- `Paren{ expr }
return traverse_paren(env, exp)
elseif tag == "Call" then -- `Call{ expr expr* }
elseif tag == "Call" or tag == "SafeCall" then -- `(Safe)Call{ expr expr* }
return traverse_call(env, exp)
elseif tag == "Invoke" then -- `Invoke{ expr `String{ <string> } expr* }
return traverse_invoke(env, exp)
elseif tag == "Id" or -- `Id{ <string> }
tag == "Index" then -- `Index{ expr expr }
return traverse_var(env, exp)
elseif tag == "SafeIndex" then -- `SafeIndex{ expr expr }
return traverse_safeindex(env, exp)
elseif tag == "TableCompr" then -- `TableCompr{ block }
return traverse_tablecompr(env, exp)
elseif tag == "MethodStub" or tag == "SafeMethodStub" then -- `(Safe)MethodStub{ expr expr }
return traverse_methodstub(env, exp)
elseif tag:match("Expr$") then -- `StatExpr{ ... }
return traverse_statexpr(env, exp)
else
@ -399,8 +407,6 @@ function traverse_stm (env, stm)
return traverse_break(env, stm)
elseif tag == "Call" then -- `Call{ expr expr* }
return traverse_call(env, stm)
elseif tag == "Invoke" then -- `Invoke{ expr `String{ <string> } expr* }
return traverse_invoke(env, stm)
elseif tag == "Continue" then
return traverse_continue(env, stm)
elseif tag == "Push" then -- `Push{ <expr>* }

View file

@ -1,6 +1,6 @@
local util = {}
function util.search(modpath, exts={"can", "lua"})
function util.search(modpath, exts={})
for _, ext in ipairs(exts) do
for path in package.path:gmatch("[^;]+") do
local fpath = path:gsub("%.lua", "."..ext):gsub("%?", (modpath:gsub("%.", "/")))

View file

@ -1,4 +1,8 @@
local candran = dofile(arg[1] or "../candran.lua")
candran.default.indentation = "\t"
candran.default.mapLines = false
local load = require("lib.util").load
-- test helper
local results = {} -- tests result
@ -10,15 +14,17 @@ local function test(name, candranCode, expectedResult, options)
local success, code = pcall(candran.make, candranCode, options)
if not success then
self.result = "error"
self.message = "error while making code:\n"..code
self.message = "/!\\ error while making code:\n"..code
return
end
-- load code
local success, func = pcall(loadstring or load, code)
local env = {}
for k, v in pairs(_G) do env[k] = v end
local success, func = pcall(load, code, nil, env)
if not success then
self.result = "error"
self.message = "error while loading code:\n"..func
self.message = "/!\\ error while loading code:\n"..func.."\ngenerated code:\n"..code
return
end
@ -26,14 +32,14 @@ local function test(name, candranCode, expectedResult, options)
local success, output = pcall(func)
if not success then
self.result = "error"
self.message = "error while running code:\n"..output
self.message = "/!\\ error while running code:\n"..output.."\ngenerated code:\n"..code
return
end
-- check result
if output ~= expectedResult then
self.result = "fail"
self.message = "invalid result from the code; it returned "..tostring(output).." instead of "..tostring(expectedResult)
self.message = "/!\\ invalid result from the code; it returned "..tostring(output).." instead of "..tostring(expectedResult).."; generated code:\n"..code
return
else
self.result = "success"
@ -669,6 +675,154 @@ test("suffixable table comprehension array index", [[
return [@len=3]["len"]
]], 3)
-- let in condition expression
test("let in while condition, evaluation each iteration", [[
local s = ""
local i = 0
while (a = i+2) and i < 3 do
s = s .. tostring(a)
i = i + 1
a = 0
end
return s
]], "234")
test("let in while condition, scope", [[
local s = ""
local i = 0
while (a = i+2) and i < 3 do
s = s .. tostring(a)
i = i + 1
a = 0
end
return a
]], nil)
test("several let in while condition, evaluation order", [[
local s = ""
local i = 0
while (a = (b=i+1)+1) and i < 3 do
assert(b==i+1)
s = s .. tostring(a)
i = i + 1
a = 0
end
return s
]], "234")
test("several let in while condition, only test the first", [[
local s = ""
local i = 0
while (a,b = false,i) and i < 3 do
s = s .. tostring(a)
i = i + 1
end
return s
]], "")
test("let in if condition", [[
if a = false then
error("condition was false")
elseif b = nil then
error("condition was nil")
elseif c = true then
return "ok"
elseif d = true then
error("should not be reachable")
end
]], "ok")
test("let in if condition, scope", [[
local r
if a = false then
error("condition was false")
elseif b = nil then
error("condition was nil")
elseif c = true then
assert(a == false)
assert(d == nil)
r = "ok"
elseif d = true then
error("should not be reachable")
end
assert(c == nil)
return r
]], "ok")
test("several let in if condition, only test the first", [[
if a = false then
error("condition was false")
elseif b = nil then
error("condition was nil")
elseif c, d = false, "ok" then
error("should have tested against c")
else
return d
end
]], "ok")
test("several let in if condition, evaluation order", [[
local t = { k = "ok" }
if a = t[b,c = "k", "l"] then
assert(c == "l")
assert(b == "k")
return a
end
]], "ok")
-- Method stub
test("method stub, basic", [[
local t = { s = "ok", m = function(self) return self.s end }
local f = t:m
return f()
]], "ok")
test("method stub, store method", [[
local t = { s = "ok", m = function(self) return self.s end }
local f = t:m
t.m = function() return "not ok" end
return f()
]], "ok")
test("method stub, store object", [[
local t = { s = "ok", m = function(self) return self.s end }
local f = t:m
t = {}
return f()
]], "ok")
test("method stub, returns nil if method nil", [[
local t = { m = nil }
return t:m
]], nil)
-- Safe prefixes
test("safe method stub, when nil", [[
return t?:m
]], nil)
test("safe method stub, when non-nil", [[
local t = { s = "ok", m = function(self) return self.s end }
return t?:m()
]], "ok")
test("safe call, when nil", [[
return f?()
]], nil)
test("safe call, when non nil", [[
f = function() return "ok" end
return f?()
]], "ok")
test("safe index, when nil", [[
return f?.l
]], nil)
test("safe index, when non nil", [[
f = { l = "ok" }
return f?.l
]], "ok")
test("safe prefixes, random chaining", [[
f = { l = { m = function(s) return s or "ok" end } }
assert(f?.l?.m() == "ok")
assert(f?.l?.o == nil)
assert(f?.l?.o?() == nil)
assert(f?.lo?.o?() == nil)
assert(f?.l?:m?() == f.l)
assert(f?.l:mo == nil)
assert(f.l?:o?() == nil)
]])
-- results
local resultCounter = {}
local testCounter = 0