Luau compiler: Create compiler-generated functions to do runtime strict type checks

Currently, maintaining scripts that use arbitrary object structures, primarily related to instances as types (more on this in a moment), can be difficult when attempting to enforce type compliance. A simple typeof() and/or :IsA() check does not always work.

The idea I had to solve this would involve adding a new compiler-generated code option to add runtime type checking. For lack of a final name, I will say this new source option is named instanceof()

The instanceof() function would be placed in source like such:

type BaseCharacterModel = Model & {
    Humanoid: Humanoid;
    HumanoidRootPart: BasePart;
}

function DoThingToCharacter(character: BaseCharacterModel)
    if not instanceof(character, BaseCharacterModel) then
        error("Malformed object input.", 2)
    end
end

In this context, instanceof would be generated at compile time. To achieve this, all common uses of instanceof(obj, Type) would change into a reference to a function generated by the Luau compiler that ensures obj, at the least, has all of the mandated fields of the given type. obj is allowed to have more fields if it must, allowing supertypes to be used in this function.

This function will return true if the type of all values in the given object match that of the type declaration. For types like Instance, it uses IsA(), for dictionary types {[TKey]: TValue} and native types it simply checks if the type of the object’s corresponding value is table or the other Lua(u) native type via generating code that uses typeof(), but for explicitly defined structures, it will check individual indices and value types.

If the input type is a union between multiple types, then it will generate a function for each compliment of the set of types, that is,

type A = {
    V0: number;
}
type B = A & {
    V1: number;
}
type C = B & {
    V2: number;
}
  • instanceof(obj, A) would only check for the existence of V0
  • instanceof(obj, B) would begin by calling instanceof(obj, A), and iff that was successful, it continues by checking for the parts uniquely added by B (V1). This is because type unions are strictly additive, so some time can presumably be saved by checking the least common denominator of the types first.
  • instanceof(obj, C) would layer on this by checking A first, then checking B, and finally checking C in that order.

The implementation of this function would allow for the types to be dominant features during runtime without actually exposing types to the runtime environment.

16 Likes

This would be a great addition. I’m constantly sanity-checking with dynamic inputs from the client. I typecheck different things from remote events, for example:

RemoteEvent.OnServerEvent:Connect(function(Player, a: number, b: string)
    if type(a) ~= "number" and type(b) ~= "string" then
        return
    end
end)

But the problem with doing this is that the type checking system will warn you because a will always be a number and b will always be a string. Setting a and b to any wouldn’t solve the issue either. It does remove the warning but it doesn’t recognize it as number and string afterwards.

This would also be useful for datastore versions. Checking if a player has the correct version of data is quite a pain as sometimes you need to update how you store data for your convenience or some other reason. I would love a built-in method to do this.

5 Likes

I thought for sure strict mode would solve this but nope, strict type does not work in runtime which is something I majorly relied on in a new feature.

Your condition is only checking if both of the types are incorrect, if one of the types is incorrect then it will not immediately return. Because of this, it would be incorrect to infer the types number and string for a and b when you use the any type for them.

Would this instanceof actually be a function? The way you are providing the type is like a normal argument, which is currently impossible. Having types affect the code generated could be problematic with how type inference changes: a change in the type system could affect the behavior of existing programs.

1 Like

Would this instanceof actually be a function?

In source? No, it only looks like one. During runtime? No, it is discarded and replaced with compiler generated code that reflects upon the type, but does not actually carry that type information (in the scope of “being a type X = ... statement”) into runtime. This makes it more like a keyword that behaves like a function rather than a function in and of itself.

My example in `DoThingToCharacter` would generate the following code when compiled:
function SomeUniqueName_CompilerGenerated(character)
    -- This condition is generated by looking at the types of BaseCharacterModel
    -- First do Model check from left side of union. Technically speaking, under
    -- my original proposal, this first line would be its own function...
    if not (typeof(character) == "Instance" and character:IsA("Model")) then return false end
    -- Then do a check of the right side.
    -- As far as indexing instance children, there are a few ways to handle this
    -- but the implementation does not ultimately matter to the programmer, 
    -- so long as it verifies its existence (* unless it is nullable) and class.
    if not (typeof(character.Humanoid) == "Instance" and character.Humanoid:IsA("Humanoid")) then return false end
    if not (typeof(character.HumanoidRootPart) == "Instance" and character.HumanoidRootPart:IsA("BasePart")) then return false end
    return true
end

function DoThingToCharacter(character)
    if not SomeUniqueName_CompilerGenerated(character) then
        -- n.b. all uses of instanceof(x, BaseCharacterModel) would
        -- reference this one generated function.
        error("Malformed object input.", 2)
    end
end

As far as its usage, the righthand value must be an explicit type, much like the right side of a type X = ... statement. The same limit could be applied in fact. This should get rid of ambiguity and problems stemming from changes to the type system, save for something so drastic that it would render all source incompatible anyway.

Using something like any would generate a function that always returns true (but linter should probably warn for it), and something like typeof(nil) would basically be a glorified == nil check, another thing linter should probably warn for.

The only complexity comes up from how this code exists in the environment, and this is where it gets hairy, especially with things like tampering with the environment in mind.

1 Like

Any reason you don’t want to parse the type name as a string? It would make more sense considering how typeof and type return strings. The behavior you described also feels really out of ordinary for Lua.

The other problem is that instanceof variable names aren’t exactly a rare find (seen it twice in Love2D projects) and it might break reverse compatibility. I don’t see why Luau wouldn’t treat it the way it handles built-in libraries. Generating optimized bytecode, unless the functions have been changed in some way.

Not sure I follow the concern.

local tbl = {} -- some structure here
type Tbl = typeof(tbl) -- This will generate a type definition based on the literal value of tbl

Is this what you are confused about? It is a good point, considering that instanceof aims to be backwards compatible.

I believe this is a nonproblem considering the usage of instanceof here is invalid during runtime. It can be discerned if the right-hand parameter is a type or not. It could be implemented much like continue was, the difference being that here, it must look for the right-hand parameter being a luau type definition.

Still, if it is a problem, perhaps a pseudo-method added to the type itself could work.

type SomeType = ...
function SomeFunction(value: SomeType)
    if SomeType:IsAssignableFrom(value) then
        -- Code here for valid input.
        -- IsAssignableFrom is not a real method, its just syntax for the type.
    end
end```
1 Like