Allow optional enforcing of static typing in functions, methods, and variables

When working in a large code-base, or with many complicated functions, it would be incredibly useful to fully enforce typing.

This is already somewhat possible given the introduction of the --!native preprocessor command, however using the inappropriate type simply makes the system fall back to using an interpreted context which is not ideal when working with large code, and may result in some unnoticeable warnings that were normally thought to be faults of another developer’s typing.

Typing should have an option to be integrated into IR compilation, such that if a type mismatch occurs, it can be caught at IR compile time, like how the --!native command results in switching to interpreted context, but instead it will be an error and not a warning, resulting in a script that will not compile.

An example of how this might be toggled is with another command, e.g. --!static, which would apply --!strict and also fully enforce any manually defined or derived types throughout the script.

--!static
--!native

local function DoComplexMath(input1: number, input2: CFrame): number
    -- ....
    return result
end

DoComplexMath(2.0, CFrame.identity) -- ok

local x: string = "1234"
-- DoComplexMath(x, CFrame.identity) -- compile error: Type 'string' could not be converted into 'number'


local function strict_tonumber(val: any): number
    local result: number? = tonumber(val)
    if result == nil then error(`Type '{typeof(val}' could not be converted into 'number'`, 2) end
    return result :: number -- legal since it was checked against nil earlier
end

DoComplexMath(strict_tonumber(x), CFrame.identity) -- ok
4 Likes

This is something we are exploring, including displaying information about native functions that exit to interpreter for one reason or another. You are correct that --!native gives an easier path to implementation.

But something to note here is that Luau type system is much more complex than the checks we perform for --!native. We only have a limited number of ‘runtime-testable’ types.
For example, if a function accepts {number} we are not checking all array elements to be a number.
So there are some concerns about developers not being aware of which type annotations are enforced and which are not.
It will also change with time, native code doesn’t abort when seeing something else instead of CFrame (it will run along just fine), but we have plans to extract more optimizations for CFrame and other built-ins in the future.

2 Likes

I expect that in the case of {number}, if the array comes from a non-specific environment, then the code will execute in interpreted or unoptimized context, but if the code is in static context then the type will have to be derived implicitly or explicitly, and it would be checked at compile-time.
As for a complex type like this:

--!static 

type List = {Type="number", number} | {Type="string", string}

local numList: List  = {Type="number", 1, 2, 3}
local strList: List = {Type="string", "a", "b", "c"}

-- could optimize with inline, maybe add keyword for forced inlining?
local function printf<T...>(format: string, ...: T...)
	print(string.format(format, ...))
end

local inext = ipairs{} --not sure why this doesnt exist

local function doSomething(inputList: List)
    if inputList.Type == "string" then
        -- type of the list is cast natively, or items are cast when indexing
        for k, v in inext, inputList, 0 do printf("%q", v) end
    else
        for k, v in inext, inputList, 0 do print(v) end
    end
end

doSomething(numList) 
--> 1
--> 2
--> 3

doSomething(strList) 
--> "a"
--> "b"
--> "c"

Such a feature would make it easier also to compare types as opposed to comparing their names. This however, requires that types are attached to the objects at runtime, and identical types may cause issues if they are not simplified.

if hastype<someType>(luaObject) then 
    -- luaObject: someType
end

if hastype<{number}>(myList::{string}) then 
    -- myList: never 
end

type Array<T> = {T}
print(hastype<Array<number>>({1,2,3}))
-- true if comparing to Array<T>->{T}->{number}, false if comparing {number} to Array<number> explicitly

Too late, we already optimize and generate native code for {number} coming from any source :slight_smile:

We don’t have a goal for this kind of typechecking in Luau, so I can say upfront that this feature will not be implemented.

1 Like