Luaty is an indent sensitive language that transpiles to readable Lua.
It appears like Lua to most syntax highlighting editors, and aims to be usable within minutes to one familiar with Lua.
The transpiler has a built-in static analyzer and limited but optional HM type inferencer.
Its name is a play on type homonym - requires less typing, but more typed than Lua.
The built-in static analyzer warns about:
- unused variables
- shadowed variables in the parent or the same scope
- assignment to undeclared (global) variables
- assignment having more expressions on the right side than the left
- unused labels and illegal gotos
- duplicate keys in table constructor
a = 1 -- undeclared identifier a
var c, d = 1, 2, 4 -- assigning 3 values to 2 variables
var p = print
var p = 'p' -- shadowing previous var p
var f = \z->
var z = 10 -- shadowing previous var z
goto g -- goto <g> jumps over variable 'gg' declared at line ...
var gg = 10 -- unused variable 'gg'
::g::
var tbl = {
x = 1
, x = 3 -- duplicate key 'x' in table
}
A command line switch can be enabled to check consistent usage of variables. Once enabled, the transpiler will try to statically infer variable types with a limited subset of Lua, but is probably wrong in non trivial cases for now.
Improving the type inferencer is a work in progress.
var j = \a -> return a
j(4, 5) -- function expects only 1 arguments but got 2
var k = \a -> return a + 0
k('s') -- function parameter 1 expects <num> instead of <str>
var p = {q = 5}
p.q.r = 7 -- assignment expects {} instead of <num> (q is a number)
var n
if n > 0 -- operator `>` expects <num> instead of <nil>
...
Lua code will be generated regardless of warning by the optional type checker.
Less syntax boilerplates due to indentation
- no more
end
- no more
do
afterfor
andwhile
- no more
then
afterif
Minor syntactical changes
repeat
becomesdo
local
becomesvar
elseif
becomeselse if
[[
and]]
become backquote(s) ` that can be repeated multiple times
var x = false -- `var` transpiles to `local`
if not x
print(`"nay"`) -- `then` and `end` not needed, `"nay"` transpiles to [["nay"]]
--`` this is a long
comment ``
Literal string or keyword as table key
var z = {
'a-str' = 'a-str' -- string as key
, var = 7 -- works as in Lua
, local = 6 -- keyword as key
, function = 5
, if = \...-> return ...
, goto = {true, false}
}
assert(z.var == 7) -- ok, z.var works as in Lua
assert(z.if(z.goto)[2] == false) -- works
Desugared functions
- function is defined using lambda expression with
->
or\param1, param2, ... ->
- a named function is always declared like a variable using
var
- function call always require parenthesis
- colon
:
is never used. Useself
or@
as the first paramenter or call argument instead
function f() -- error: use '->' instead of 'function'
-- note that this syntax creates f in global scope in Lua, unless local is specified
var f = -> -- ok, empty lambda assigned to f, \ optional if no parameter
-- consistent with variable declaration syntax making sure f is always locally scoped
\x -> print(x) -- error: lambda expression by itself not allowed
(\x -> print(x))(3) -- ok, immediately invoked lambda
print 'a' -- error: '=' expected instead of 'a'; but this is valid in Lua
print('a') -- ok obviously
var obj = {
value = 3
, foo = \@, k ->
return k * @.value -- `@` transpiles to `self`
, ['long-name'] = \@, n -> -- colon call syntax can't invoke function with special name
return n + @.value
}
print(obj:foo(2)) -- error: ')' expected instead of ':'
assert(obj.foo(@, 2) == 6) -- ok, transpiles to obj:foo(2)
var get = -> return obj
print(get()['long-name'](@, 10)) -- `@` *just works*, get() is only called once
The differences end here.
Some properly indented Lua code can even be hand converted to Luaty using just Find and Replace. In return, we get
- mostly shorter codes
- forced local variable declaration
- consistent function call and definition syntax
- static analyzer that may uncover hidden bugs in existing code
Due to backquote replacing [[
and ]]
, long comments need one extra hyphen if we want to use the uncomment trick
-- Uncommenting long comment trick
--`
print(10) -- commented out
---` -- ** use 3 hyphens at the end of comment **
-- Now, if we add a single hyphen to the first line, the code is in again:
---`
print(10) --> 10
---`
Luaty only requires LuaJIT to run.
With LuaJIT executable in your path, create a command alias for Luaty like below, replacing both path/to/luaty
with your extracted luaty folder location.
Linux/Unix shell
alias luaty="luajit -e \"package.path=package.path .. ';/path/to/luaty/?.lua'\" /path/to/luaty/lt.lua"
Windows command prompt
doskey luaty=luajit -e "package.path=package.path .. ';\\path\\to\\luaty\\?.lua'" \path\to\luaty\lt.lua $*
To begin a Read-Generate-Eval-Print Loop (RGEPL)
luaty
To run a Luaty source file
luaty /path/to/source
source is assumed to end with .lt
The transpiler processes its main input file and its dependencies, unless it's told otherwise. Given a main.lt file with its required .lt files under its subfolders, Luaty can transpile and generate a full mirror folder structure of .lua output files.
Suppose our source files are laid out like below, where main requires sub, which in turn requires foo and bar under lib folder:
/
├── src
│ ├── main.lt
├── sub.lt
└── lib/
├── foo.lt
├── bar.lt
├── orphan.lt
└── ...
To transpile src/main.lt file and its dependencies to /dst, specify /dst as the second argument.
cd src
luaty main /dst
If transpilation succeeds, the output should appear like below, with subfolders mirrored:
/
├── dst
│ ├── main.lua
├── sub.lua
└── lib/
├── foo.lua
├── bar.lua
└── ...
Since orphan.lt is not required, it will not be processed. Also, Lua package.path and dynamically constructed require() are not processed, because they are not statically resolvable.
Lua output files will not be overwritten if they exist.
To force overwriting, use -f
switch.
To transpile only main.lt file without its dependencies, provide a destination ending with .lua
luaty [-f] path/main /out/main.lua
Destination without .lua is considered a folder, which will be created if it does not exist. For eg:
luaty -f main main.lt
The output main.lua and its dependencies goes into main.lt/*.lua, so that output file can never overwrite input.
For all the commands above (including RGEPL), static type checker can be enabled by adding -t
switch. For eg:
luaty -t src
-
Either tabs or spaces can be used as indent, but not both in a single file.
-
Comments have no indent rule.
-
Blocks such as
if
,for
,while
,do
and lambda expression->
can have child statement(s).- A single child statement may choose to stay at the same line as its parent
- Multiple child statements must start at an indented newline
if true p(1) -- Ok, p(1) is the only child statement of `if`
p(2)
if true p(1) p(2) -- Error, two statements at the same line, `if` and p(2)
do -- Ok, multiple child statements are indented
p(1)
p(2)
print((-> return 'a', 1)()) -- Ok, immediately invoked one lined lambda expression
if x == nil for y = 1, 10 do until true else if x == 0 p(x) else if x p(x) else assert(not x)
-- Ok, `do` is the sole children of `for`, which in turn is the sole children of `if`
- A table constructor or function call can be indented, but the line having its closing brace/parenthesis must realign back to its starting indent level.
var y = { 1
,
2} -- Error: <dedent> expected
var z = { 1
,
2
} -- Ok, last line realign back with a dedent
print(
1,
2
, 3, -- commas can be anywhere
4, 5) -- Ok, last line realign back to `print(`
- For single-lined function, semicolon
;
can be used as function terminator if it causes ambiguity in a list of expressions. Note that any needed comma after the semicolon does not become optional.
print(pcall(\x-> return x, 10)) -- multiple return values. Prints true, nil, 10
print(pcall(\x -> return x;, 10)) -- ok, single lined function ended with `;`. Prints true, 10
print(pcall(\x ->
return x
, 10)) -- ok, same as above, function ended with dedent. Prints true, 10
var o = { fn = -> return 1, 2;, 3, 4 } -- use `;` to terminate single-lined function
assert(o[2] == 4)
var a, b = -> var d, e, f = 2, -> return -> return 9;;, 5;, 7
assert(b == 7) -- each `;` terminates one single-lined function
Luaty is written in itself and transpiled to Lua. To modify and overwrite Luaty itself, use
luaty -f lt.lt .
To run tests in the tests folder, use
luajit run-test.lua
See the tests folder for more code examples, and Luaty transpiler and Losty for real world usage.
Luaty is modified from the excellent LuaJIT Language Toolkit.
Some of the tests are gratefully taken and modified from official Lua test suite.