Luau Recap: August 2020

I saw the string.format analysis and that gave me an idea. What if Luau supported compile-time/runtime analysis things like Rust does?

Like, for example, defining a Luau function that actually gets executed during analysis that can access type information / tokens / etc. and can provide warnings/errors (squigglies) with a custom message.

But, of course, the Luau analyzer is probably not written in Luau which would make it very difficult and expensive to translate all of its internal state into a straightforward Luau data structure, so this will probably never be a thing… :(

Things like communicating what types the function takes based on a string you pass to it would be great to support generically rather than as a special feature for string.format.

Yeah that’s probably reasonable.

I believe the fact that %1 with no captures is valid is an accidental result of how the implementation works, not the specified behavior. %0 should be used instead.

More or less intentional to simplify the implementation, although might be worth checking.

The other issues look like bugs, thanks!

3 Likes

The 5.2 reference manual added this text to the description of string.gsub

In any case, if the pattern specifies no captures, then it behaves as if the whole pattern was inside a capture.

So it doesn’t seem like %1 working with no captures is an accident, considering the description was updated to include this case.

Luau’s type system is not sound. Function types’ parameters should be contravariant and generic parameters should be invariant.

--!strict
--- The parameters of function types should be contravariant, not covariant!

function apply(f: (BasePart) -> ())
	f(Instance.new("Part"));
end

-- valid, but should not be
apply(function(obj: SpawnLocation)
	print(obj.TeamColor)
end)

-- not valid, but should be
apply(function(obj: Instance)
	print(obj)
end)


--- Generic parameters should be invariant, not covariant!

type Array<T> = { [number]: T }

local x: Array<BasePart> = {}

-- valid, but should not be
local y: Array<Instance> = x
y[1] = Instance.new("Model")

-- not valid, and shouldn't be
local z: Array<SpawnLocation> = x;
z[1] = Instance.new("SpawnLocation")

Edit: generics should be invariant

2 Likes

In the generics example, this is where type assertions would definitely be useful, cause I can see a case where you get a function that returns a (newly constructed, rather than mutated) Array<BasePart>, and you want to generalize that to an Array<Instance>

Of course type assertions can go wrong if you lie to the type checker, but I think it’s better to have that be up to the user. On the other hand, as it works currently, things can definitely go wrong without type assertions since it lets you mutate x in that example in an invalid way.

Edit: that very last example is both not valid, nor should it be valid, since if you read an element from z, it’s not actually guaranteed to be a SpawnLocation

I have a few questions about this.


image
I think it should be possible to strong-type class properties like this.

could be a good solution for that I think. Would also reduce a lot of boilerplate code developers have to write for OOP.


Will this ever be available to the public?

1 Like

Woo, Type declaration.
Can’t wait to try out the new features.

Yeah you’re absolutely right.

I still believe it should be an error, not up to the user, and Roblox should introduce something akin to typescripts “v as X” that way you can only write potentially unsound code explicitly which will make it harder for the user to write unsafe code.

I have edited my post.

1 Like

I think that for me these luau updates have been the hype of the century. If anyone told me we we’d be able to do parralel processing in Roblox 3 years ago I’d not believe you in the slightest let alone most of the new stuff luau is adding already.

I have the feeling anyone who has seen me post on the topic of luau has probably noticed I have an unnatural obsession with squeezing every little nanosecond out of some of my Roblox code and every time I see lauu performance improvements mentioned I immediately get very excited.

(I’m too lazy to properly quote these right now because Android, and I’m probably missing some critical information on how discourse quoting works so uh)

This sounds like it would’ve been a huge disaster if I’m understanding correctly so it’s great that this isn’t a thing.

This is a piece of info I’m super interested on! Which pieces of info will Roblox’s form of debug.getinfo support?

Lastly, table.pack and table.unpack
[super]thankyouthankyouthankyouthankyouthankyouthankyou[/super]
When I originally looked at the docs for this, I scanned through them, but, I decided to take a more thorough look recently and I’ve realized how relevant this is to just about everything I do. This is absolutely INCREDIBLE for me and I’m sure others as well.

Things I am now considering being relatively feasible (and even performant!):

  1. rbxm in rbxlua :eyes: This would only take a port of lz4, and a port of an existing implementation (@CloneTrooper319 wrote a very nice one in C# and I think this wouldn’t be hugely difficult to port now!). Additionally, a great example of data that is stored in these files that’d be a very quick luau implementation would be the newer (holy cow how isn’t this released yet) attributes
  2. Bytecode interpreters for other languages (e.g. a custom x86 interpreter) There was nothing stopping anyone before, but, now it’s going to be a piece of cake (relatively speaking)
  3. Faster and smaller file format processing for all (File format plugins?)

If anyone wants to look into porting CloneTrooper’s rbxm implementation I’d be super interested! (rbxm in Lua has been one of my all time favorite ideas)

Lastly, (because I’m not sure most people are even still reading at the pretty pointless sheer size of this post) things that I am most definitely legally obliged to mention because I’ve only recently noticed that almost all of my luau posts contain something about my damn compression algorithm:

  1. It’s finally time for me to write the 10th revision (actually technically the 16th which would change any mechanisms if you consider smaller rewrites. I’m unreasonably satisfied that this will be the 16th AND 10th)
  2. I’m pretty sure I can turn about 60 lines give or take in my existing code into about just one. Not only does that imply a hefty performance boost given the expensivity of the code that’s a performance boost on top of the fact that iirc prior I was compressing a little over a quarter of a megabyte of test data in under a millisecond on my hardware (about 0.53 ms on average, which is nearly identical to lz4 speeds, and given what I was seeing with a couple tests with using lz4 data as input data this becomes an enormously useful fact)
  3. Because I’ve never gone into depth on the algorithm’s benefits and I think anyone still reading might be interested I’ll go ahead and talk about it in much much more detail rather than being kind of vague. I compare my algorithm to lz4 a lot but I also wouldn’t say it’s anywhere near as good as lz4 on its own (although to be completely fair I haven’t actually done much output size comparison), lz4 is still way better on its own. The generally best benefit I’ve noticed is that when using the algorithm on lz4 data it can achieve some hilariously small sizes on a majority of readable data. My algorithm is using a method I personally designed (which I have no idea if I’ll describe at any point, I’d like to get the project to a point where I can release it) and due to the mechanisms it uses to actually compress data it is locked to ratios in 8ths. As to why 8ths specifically, this is due to bytes having 8 bits. This number is actually pretty ideal for the algorithm itself I’d think because a number that’s much smaller would result in bad ratios a lot, and a larger number wouldn’t compress well. Additionally, this makes prediction and even guarantees for certain data pretty easy. The behaviour of actually compressing certain data is pretty predictable, for example, if your data is readable ASCII data, it will always compress it to ratios under 7/8ths. I will definitely take advantage of this fact and likely some math for prediction to automatically reencode data for even better ratios than what I have. As for why this currently improves lz4 output sizes? That comes down to the algorithm itself. The thing is, lz4 is a surprisingly simple algorithm at its core. The gist of it is that it mostly just indicates how many times to repeat certain data. So a majority of the old data is still intact. An estimate of the size of the “hybrid” output is lz4ratio * standardRatio. So, if lz4 produces data 2/3rds (~5.3/8ths) the size and my algorithm produces data 6/8ths the rough data size (which will likely be even smaller with any size optimizations of which I already implement a few simple and costless ones, but can still be a bit larger theoretically) the output will be about 4/8ths. That’s half the size vs both outputs being at least 2/3ds!
2 Likes

This bug was exploitable so I’m very happy we found it before exploiters did :smiley:

Still figuring that out. On one hand we could expose everything that Lua 5.x does. On another hand, maybe exposing a function object from the stack frame isn’t a great idea since then you can call it. On the third hand, setfenv() already allows you to mess with your callers so maybe it’s fine.

Our current implementation supports “source” (script path), “what” (C/Lua), “currentline”, “nupvals” (from Lua 5.x, not useful in absence of other debug methods that we don’t want to provide for sandboxing reasons so might go away), “nparams” (number of fixed arguments to the function), “isvararg” (is function variadic), “name” (function name), “namewhat” (historical artifact from Lua 5.x, will be removed), “func” (function object).

2 Likes

cough

I haven’t benchmarked it any and the implementation isn’t complete, but it exists. Should run in Luau just fine with some modifications – namely, because it’s meant for vanilla Lua it uses a module to fake all of the Roblox data types, so you would want to remove that bit and make it just use the real ones.

You’d also have to swap out the require statements but that’s easy.

4 Likes

If I have feature requests for typed Lua should I put them in these recaps or in separate feature request threads?

Invalid escape sequences aren’t treated as an error in Luau (they silently represent themselves instead). Is this intentional?

print("\LOL") --> LOL

So what actually happened to __meta, it kinda flew over my head when it was removed.

Other than that, good to hear that type-checking was fixed, having Players getting merged into Player was getting a bit confusing.

There’s a reason you should not be able to do that, because it can create invalid code:

local storage = {}
function foo(): Array<BasePart>
    return storage
end
local x: Array<Instance> = foo()
table.insert(x, Instance.new("IntValue"))
foo() --> This is now an array of BaseParts with an IntValue in it, yikes!

I know you specify “newly constructed”, but there’s not really any consistent way to handle things like that.

1 Like

Why would foo be able to return storage in the first place? Isn’t storage implicitly of type Array<any> there? Maybe I’m confusing Luau rules with TypeScript, but you would see an error on line 3 in TS (and a warning on line 1).

Edit: Actually I still see your point – if you had said storage: Array<BasePart>, then the code is invalid like you say.

Simpler invalid reasoning:

local storage: Array<BasePart> = {}
local x: Array<Instance> = storage
table.insert(x, Instance.new("IntValue")
-- storage now contains IntValue

Java has a different solution to that problem (though a similar solution would not work in an untyped language like Lua, even with type annotations), see discussion on ArrayStoreException for an interesting read: java - Dealing with an ArrayStoreException - Stack Overflow

1 Like

I have a couple questions.

  1. image
    Why is the Typechecker angry at me here? I have a Folder parented to ServerStorage named Folder.

  2. image
    Why is this a type mismatch? I have a Folder parented to ServerStorage named Folder.

  3. image
    2 questions here, I have a ModuleScript in ServerStorage called Bean. Why does this say Unknown require? Also, can I annotate a type exported in the ModuleScript I require on my Bean variable?

local Bean = {}
Bean.__index = Bean

function Bean.new()
return setmetatable({}, Bean)
end

export type alias = typeof(Bean.new())

return Bean

For 2 at least, FindFirstChild has no guaruntee that the object returned is a Folder type. It could be a Part called “Folder”.

Maybe FindFirstChildOfClass/WhichIsA or manually check the class and type cast it?