Hacker News new | past | comments | ask | show | jobs | submit login
Luau: Augmenting Lua’s Syntax with Types (medium.com/roblox-tech-blog)
85 points by Apakovtac on Sept 12, 2020 | hide | past | favorite | 49 comments



Related HN discussion on Aug 5 of the Luau github page [1]: "Luau A fast small, safe gradually typed scripting language derived from Lua" [2]

[1] https://roblox.github.io/luau/

[2] https://news.ycombinator.com/item?id=24059432


Is it possible to run it outside of Roblox?

It seems silly to be concerned about parse time given that Luau uses a multi-pass optimizing compiler to generate bytecode.

Edit: see also teal[0] (newish) and typedlua[1] (dead?)

[0] https://github.com/teal-language/tl

[1] https://github.com/andremm/typedlua


It does not seem to be possible to run this outside the Roblox Engine:

From the site: https://roblox.github.io/luau/

"Luau is currently only runnable from the context of the Roblox engine, which exposes a large API surface documented on Roblox developer portal."


Also Ravi[0]: "Ravi is a dialect of Lua, featuring limited optional static typing and JIT compilers"

[0] https://github.com/dibyendumajumdar/ravi


Under 'Type arguments in function calls', they say C# and TypeScript can afford some cycles to disambiguate the following code:

    return someFunction<A, B>(x)
This is actually a case where TypeScript's promise:

> All valid JavaScript code is also TypeScript code. You might get type-checking errors, but that won't stop you from running the resulting JavaScript.

...is broken. The following JavaScript is perfectly valid:

    function f(something, other) {
        const A = 1;
        const B = 2;
        return something < A, B > (other + 1);
    }
    console.log(f(0, 0), f(2, 2));
If you run it in your browser you get 'true false' as output. Passing it through the TypeScript compiler produces the following:

    function f(something, other) {
        const A = 1;
        const B = 2;
        return something(other + 1);
    }
    console.log(f(0, 0), f(2, 2));
Trying to run that produces the error 'something is not a function'.

TypeScript can get away with things like this because such expressions are weird, rare, and mostly pointless.

The equivalent function in Lua would not be nearly as weird. It would simply return two booleans. There is no clear argument to be made here that the user actually meant a generic function call.


Strictly speaking, it doesn't "stop you from running the resulting JavaScript". The result isn't the same as if you'd run the JS directly, but it doesn't fail to output.


I don't understand the performance concerns of backtracking in case of the "as" operator. Backtracking should only occur when the parser was wrong (i.e. when it expected "x as y" but found "x as (" ), which is very rare and thus shouldn't impact the performance much.


I think they could just announce the deprecation and replace 'as' functions with 'as_' in user code (I mean, it wasn't many customers, maybe only one)


If you want to progressively add static types to Lua, decent IDE support, and not worry about transpilation; then you may be interested in my little project:

https://github.com/Benjamin-Dobell/IntelliJ-Luanalysis/

https://plugins.jetbrains.com/plugin/14698-luanalysis

It's essentially Flow for Lua. It's not at all trying to be its own language, but rather been built out of my own needs to write maintainable code in environments that only run Lua.


[flagged]


I've been programming games in Roblox for a while now and honestly once you get used to LUA the things that once appeared to be "horrors" are now just quirks that I've honestly gotten used to.


Looks like you're having a Lua Uppercase Accident


If 1 based indexing is the most you have to deal with, you aren't doing something very difficult.

Also you usually iterate through tables without using an explicit start index.


It’s a small language, so not many.


Just use LuaJIT which can handle zero based indexing too.


ipairs() will not iterate over 0th element though and # will return one less. If that is not an issue, Lua can "handle" it as well.

That is, if I didn't miss a flag in luaconf.h which solves that.


You can trivially write your own iterator that starts at 0, and still uses ipairs's (native) iterator internally:

  function ipairs0(arr)
      return ipairs(arr), arr, -1
  end


pairs() iterates over all elements. I never use ipairs() but plain numeric for loops. This works well with #. I only use pairs() in exceptional situations when I have to copy a table with unknown contents.


I agree with your first point wholeheartedly but your second is probably why you're being downvoted. Don't extrapolate like that.


> We want to be able to traverse the AST for deeper analysis, so we need a parser to produce that syntax tree. From there, we’re free to perform any operations we wish to do on that AST.

Sounds to me like they are trying to reinvent python3 in lua. Why not embed python anyway?


What had that quote got to do with Python3? In any case, Python is harder to embed and much slower.


Python has its own AST generator, type annotations and is embeddable easily via some helpers. Civilization 4 had it, so it isn't that uncommon. Not sure how fast it needs to perform once embedded, apparently you know.


> [Python] is embeddable easily via some helpers

You can embed Python, but making it safe to run untrusted code written by users will take enormous effort. Roblox is a platform for easily creating games and playing other people's games. They can't allow those games to pose a security risk to players' computers.

> Not sure how fast it needs to perform once embedded, apparently you know.

You're commenting this on a post that talks explicitly about wanting a faster interpreter than Lua 5.1. Nobody is switching from Lua to Python and getting better performance out of it.


Worth noting that Civilization 6 now embeds Lua instead.


> Why not embed python anyway?

Same reason TypeScript was created: to allow gradual typing in already-existing Lua codebases. Unless you mean adding it in addition to Lua, in which case, what advantages does that offer?


Why would they create their own obscure solution? I imagine LUA is very easy to run sandboxed, but I can’t shake the feeling roblox would be better off integrating scripting with JS. It would come with powerful typing via typescript, a great tooling (along with a type checker), and a lot of resources available online.


That's a popular opinion among people who have only used Javascript.

Few who have used both would give it a second thought.

Roblox is immensely popular, has a wide install base and thousands of developers with code in Lua already.

Why throw all of that away for an inferior language?


It’s also especially ironic to vouch for Javascript, and then in the next sentence switch to Typescript, which was precisely created to overcome some of JS defects.


> That's a popular opinion among people who have only used Javascript. Few who have used both would give it a second thought.

I have extensively used both languages for years and you haven't adequately answered my previous criticism [1].

> Why throw all of that away for an inferior language?

As if Lua (either the language, the implementation or the ecosystem) is not inferior.

> Roblox is immensely popular, has a wide install base and thousands of developers with code in Lua already.

This is probably the only reason to justify Luau, and shows yet another problem with the Lua ecosystem: they don't (or rather, can't) share their creations to each other. JavaScript is not a perfect language but the JS ecosystem has successfully converged into a single widespread set of solutions including TypeScript.

[1] https://news.ycombinator.com/item?id=24067362


Alright, if I must:

I still have no idea what your stubborn bug is.

Asking someone else to debug your code is obnoxious; insisting on it, doubly so.

I've never encountered it in the wild, and it sounds like a 'you' problem. Please don't bring it up again.

Or if you simply must, explain yourself. This isn't a tech interview; you're not paying me to solve puzzles, or even promising to.


string.gsub receives at most three arguments where the final optional argument is the maximum number of replacements, and returns two values where the second value is the number of replacements made. Therefore if the arguments do not have escape sequences the outer string.gsub receives 0 and `<arguments>` doesn't get replaced.

I intentionally asked you to find this bug out because you can acknowledge the particular class of bugs only after biten by that bug, and you didn't seem to even know what might be problematic. In the other words, by now you can't get away by saying "you should give all function calls a name" posthumously.

It's a frequent novice mistake to write `if (a = b)` in C/C++, but any good enough C/C++ programmer will point it out (and modern compilers will flag a warning). Eventually people get used with this problem and the cycle repeats, this time with less novices falling into the trap, so this class of bugs is---while problematic---not considered a huge deal. Given your reaction though, I doubt this is the case for Lua and that's yet, yet another reason to avoid Lua.


Edit: ah, I just realized what you're actually complaining about.

Being able to drop two arguments into a function call is great! It's one of the better things about Lua.

But when you don't want it, wrap the call:

    f(a(b,c)) -- this will call f(r1,r2) with two returns
    -- vs
    f((a(b,c)) -- this will always call f(r1)
And now you know how to prevent this class of bug. Catching it in an intermediate variable is possible but by no means necessary.

Original reply follows:

Alright, yeah, that kinda sucks.

Here's another dumb one: table.insert, if you give it two values, it's table.insert(tab, val) and it inserts at #tab + 1.

If you give it three values, it's table.insert(tab, index, val), and that's bad enough: but if you pass it (tab, val, nil), it interprets val as index.

Which is surprising! you'd think f(a,b) and f(a,b,nil) would be the same thing, and usually, they are. But not in this one case.

But to be fair, here's an example of Lua done well:

    local L = require "lpeg"
    
    local end_str_P = L.P "]" * L.P "="^0 * L.P "]"
    
    local function _disp(first, last)
       return last - first - 2
    end
    
    -- capture an array containing the number of equals signs in each matching
    -- long-string close, e.g. "aa]]bbb]=]ccc]==]" returns {0, 1, 2}
    
    local find_str = L.Ct(((-end_str_P * 1)^0
                          * (L.Cp() * end_str_P * L.Cp()) / _disp)^1)
I'll let you reason about what it does, and why it would be such a pain in the butt with regular expressions.


> But when you don't want it, wrap the call: [...] And now you know how to prevent this class of bug. Catching it in an intermediate variable is possible but by no means necessary.

Well, I knew that; in fact what I've linked in the past discussion was the exact code to handle this case in my type checker. It is seriously confusing that a seemingly excess parenthesis affects the code anyway, and you can't exactly know whether the parenthesis is necessary or not without using a type checker.

There are some other languages that have multiple returns and can pass multiple arguments for nested calls, notably Go, but Go only does that when the inner call is the sole argument to the outer call. And Go does have a type system. Tons of other languages including JavaScript have an explicit "spread" operator which mostly retains the usability and avoids such problems.

> Which is surprising! you'd think f(a,b) and f(a,b,nil) would be the same thing, and usually, they are. But not in this one case.

Or any C function relying on lua_gettop. Seriously, nil should have eliminated in that stack.

> I'll let you reason about what it does, and why it would be such a pain in the butt with regular expressions.

I don't like a quirky EDSL alternative to the regular expression (or what it matters, parsing expression grammars) either. I prefer lpeg.re in that regard.

    local re = require "re"
    local long_str = re.compile(
        [[
            end_str <- ']' '='* ']'
            long_str <- {| ((!end_str .)+ ({} end_str {}) -> disp)+ |}
        ]],
        {
            disp = function (first, last)
                return last - first - 2
            end,
        })
Lpeg itself is a fine library, but its use of 1 as anything or -1 as the end of string is annoying (at the very least `lpeg.P(-1)` should have been `lpeg.Eos` instead). I would still use lpeg proper for constructing patterns programatically.

But you can't give lpeg as an answer to string.gsub and so on. First, the standard library still remains problematic and people will get tripped up (if you have no room for implementing `|`, one can require that `|` is escaped, m'kay?). Second, it's not a default, or at least not what you can easily `require`. The Lua ecosystem is fragmented primarily by not having a universally usable package system (I stress that LuaRocks is not) thus an external library is not always an option.

By the way I don't know why you would want to give that obscure example to brag about Lua. Your code is in fact slightly incorrect: `(L.Cp() ...) / _disp` looks like that the capture is applied only to that parenthesis but it's not, thanks to the operator precedence. The main problem of regular expressions is an inability to refactor and you haven't correctly demonstrated that.


> Well, I knew that

So a deficiency in your code, you chose to blame the language.

That's a you problem.

> I don't like a quirky EDSL

A you problem

> I prefer lpeg.re

So Lua offers two superior ways to process strings over one? and you're still mad? Sounds like a

> But you can't give lpeg as an answer

Ah but I did. You just don't like it, which is, you guessed it, a you problem.

Ierusalimchy wrote lpeg specifically to address known deficiencies in the pattern library.

Sure, you can't always use it. Just like you might be stuck on an obsolete version of JS in an embedded system.

But this is neither Lua's problem, nor my problem. It might be your problem. I'm beginning to detect a pattern!

> Your code is in fact slightly incorrect

It is, in fact, not.

I recognize the trope here. Happily, I'm in a position to fire people who show it, and have.

In your case, I'll have to be content with never interacting with you again.


(I'm pretty sure that this won't get a reply, but I'll leave the final post for others.)

> So a deficiency in your code, you chose to blame the language.

The language is to blame if it encourages a deficiency. I've linked to the type checker code to show that this kind of bug is virtually invisible, not like the aforementioned `if (a = b)` case, even to who knew enough to write a type checker.

> So Lua offers two superior ways to process strings over one? and you're still mad?

Lua doesn't offer two superior ways, it's lpeg that offers. And what lpeg offers (first-class PEG) is irrelevant to my problem (Lua the language lets you to accidentally put unintended arguments) anyway.

> Ierusalimchy wrote lpeg specifically to address known deficiencies in the pattern library.

And yet he left the faulty version in the standard library.

> Sure, you can't always use it. Just like you might be stuck on an obsolete version of JS in an embedded system.

You don't need the embedded system to show that you can't always use the JS package system. Web browsers some years ago didn't support that. As a result webpack (among others) happened and we are now comfortable to compile JS down to the bundle. Does Lua have any equivalent?

When I raise the issue with Lua the language you point to Lua the ecosystem and sideskip the fact that Lua the ecosystem itself is fragmented thanks to Lua the implementation. My issue with Lua (either the language, the implementation and the ecosystem) is multitude and pretty much every issue is connected to each other.

> It is, in fact, not.

Your code is different to the following (which is what I consider more readable):

    -- [end_str_P and _disp omitted]

    -- returns 0 for [[, 1 for [=[, 2 for [==[, and so on
    local end_str_len_P = (L.Cp() * end_str_P * L.Cp()) / _disp

    local find_str = L.Ct(((-end_str_P * 1)^0 * end_str_len_P)^1)
Your code unintentionally groupped `(-end_str_P * 1)^0 * (L.Cp() * end_str_P * L.Cp())`, as `a * b / c` would be `(a * b) / c` instead of `a * (b / c)`. It is not "incorrect" as the former doesn't contain any capture, but has enough potential to become incorrect. That's what I want to avoid by not using a "quirky EDSL".


I'd love to see your list of problems with the language, if you can share sometime.


My standard argument against Lua is available from [1]. I have said a lot about Lua, so you can use the search [2] for the partial listing.

[1] https://news.ycombinator.com/item?id=18351788

[2] https://hn.algolia.com/?dateRange=all&page=0&prefix=false&qu...


I have a pretty decent experience in lua, js, python, perl and a few in other scripting languages, and personally wouldn't just throw js away in general. Yes, it is a legacy freak, but some latest parts of it are at least worth adopting in other languages. To name a few, constructing objects ({a, ...b, c:[...d, e]}), destructuring, arrow functions conciseness, proxies, default arguments. This helps much in scripting and libraries for scripting. Also I find separate objects and arrays being a better approach than just lua-tables. I like lua, really, but some parts of it... you have to struggle with constantly. Select and "..." sematics in general, meta restrictions, a bloaty syntax.

Imo, the next language with a little stricter typing than both js and lua, which takes best parts of these and gets rid of bad practices, and with a typed version in mind, will be a big win for everyone.


I upvoted this, although I disagree about tables vs. separate objects and arrays. If you were to say separate maps and a specialized array type, I would be more inclined to your point of view! Not sold completely, mind you.

In either case, it's a productive disagreement. I agree that Lua isn't a perfected language; merely a very, very good one.

From your list, I would takeaway default arguments as the first thing to bring over to this hypothetical successor language. Arrow functions could be avoided by the simple expedient of renaming the keyword `function` to simply `fn`.


What are proxies for?


> Roblox is immensely popular, has a wide install base and thousands of developers with code in Lua already.

100% agree till here:

> Why throw all of that away for an inferior language?

I'd say "not the right tool for the job, given their target audience".

Saying that JS is inferior to Lua (Lua's great IK) isn't much of a productive comment—if they built their audience with JS, they wouldn't be pivoting to Lua because it has achieved superiority in the eternal language wars.


It was a provocative comment, sure, but I wouldn't say unproductive: others asked why Lua is superior, and several people answered, myself included.

Heartily agree that the same basic argument would apply if Roblox had started with JS! Lua isn't so much better than JS that it would be worth scrapping collective centuries of labour over.

But given that it is the better language, in my not-so-humble opinion, why on Earth would they switch?

I think the only rational way to argue superiority of Javascript over Lua, is to point to aspects outside of the language itself: larger install base, more libraries, bigger, more vibrant community, better tooling, and so on.

But I prefer to call that the ecosystem, and can confidently say that Javascript, alas, has the superior ecosystem. A pity, because Lua is the superior language.


I agree that lua is a good fit here, being a smaller and easier language to embed, with a much simpler runtime to boot.

But could you elaborate on why you think JS is an inferior language?


Lua is smaller, more consistent, and much more flexible with integration since it just C.

Debugging is built into the language in a simple way.

_G gets a table of all global variables

debug.getlocal allows you get any and all variables at each stack frame at that point including name, value and type.

You can even use debug.sethook to have a callback after every function, line, or instruction.

On top of all that you have LuaJIT which not only runs ridiculously fast, but can deal with dynamically loaded files incredibly fast. It even has a direct foreign function interface and allows you to use shared libraries directly without writing any more C or recompiling them.


Your last paragraph is the essence of why I chose Lua for my own projects, especially the FFI, but unfortunately doesn't apply to all of Lua. The divergence between Lua and LuaJIT is certainly not a strength.

The 'stock' Lua interpreter is very fast, very small (good for cache retention) and all its C functions are reentrant, so no global interpreter lock: those apply to both flavors.

As the tiniest of nitpicks, `_G` is the default home of the global environment, but it's just a variable. The correct way to get the environment is `getfenv(1)` or just `_ENV`, depending on your flavor.


> Debugging is built into the language in a simple way.

Lua debugging interface notably lacks breakpoints. You can simulate breakpoints by per-line hooks but it's not performant. Many popular JS engines including V8 do support performant breakpoints by bytecode patching, and Lua do have multiple patches doing the same (well, a common theme) but none is official.


Everything CyberDildonics said, and also:

Tables are immensely better than JS objects, in every way. I could entertain a separate array type in the abstract, but given what actually exists, I will take tables over the JS collection of objects, arrays, and maps, any day.

Extensible syntax, which is used to implement "objects", but also to, well, extend the syntax. Indexing, assignment, calling a table like a function, operator overloading: it all works the same way, and it all works. Metatables are superior to prototype chains, simple as.

Coroutines. I don't have the energy to write up my personal take on why these are the best baseline abstraction for cooperative concurrency in a dynamic language; one of these days I'll publish something so I can simply link to it.

Multiple return values. A language without variadic parameters and multiple returns is less dynamic than one which has them, if you're going to go dynamic, go all in.

Environments! This is one of those things which you might not need very often, but when you do: oh man, great to have. Number one thing I would carry over into the browser, where we really don't want to leak context, and JS fights against this constantly.

As minor desiderata, Lua's type coercion, the fact that 0 is truthy, a separate `..` concatenation operator, and optional semicolons which basically cannot bite you: Lua has no need for a separate `==` and `===`, and I consider that a strength. I also think the separate `null` and `undefined` is clunky, but that opinion isn't strongly held; Lua has considered adding an `undef`, and it's an interesting debate which I'm just barely on the "no" side of.


It's not easy to tell all your players that their games don't work anymore or that they cannot use coroutines. Regarding the second point, more concretely: they no longer can attach a function to a game object that defines its behavior over any length of time, and they no longer can call the sleep() function to suspend execution for a length of time.


True, there’s some pain involved for the developers. The migration could happen over time, like unity3d did by deprecating scripting with boo.


Lua does exceptionally well as a nice, lightweight language embedded in another project.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: