Estrita
Paradigm(s) | imperative |
---|---|
Designed by | User:Aadenboy |
Appeared in | 2024 |
Computational class | Turing complete |
Major implementations | None |
Influenced by | Lua, TypeScript |
Influenced | None |
File extension(s) | .est |
Estrita (Portugese for Strict) is a satirical superset of Lua 5.4, and a direct parody of TypeScript. The language enforces extremely strict typing with no room for error, in direct contrast to Lua's otherwise lenient and flexible structure. The original plan for the language was to transpile directly into Lua, however it is planned to modify Lua bytecode to instead include the strict type checks in both runtime and compilation.
Added features try to follow the syntactic language of Lua.
Key Features
Strict Typing
All variables are required to be declared with a type annotation, following the syntax of local varName: type = value
. All valid types include those outputted by type()
[1], with the exception of table
and nil
. The usage of union types, custom types, and nil
are not allowed, however you may imply that the variable will be defined later with the use of a question mark.
Tables and functions have special syntax for type annotations, shown in the later sections.
local hello: string = "Hello, World!" local pi: number = 3.141 local setting: boolean = false local a = 5 -- Invalid, implicitly 'any' local b -- Invalid, implicitly 'any or nil' local c: number -- Invalid, type doesn't match value local c: number? -- Valid
Constant and to-be-closed variables work as normal.
local mode <const>: number = 5 local file <close>: userdata = io.open("./file.txt", "r")
Types are immutable and must be adhered to throughout the variable's lifecycle.
local foo: number = 5 foo = 7 foo = 9 foo = "10" -- Invalid local bar: number = 10 bar = 11 bar = nil -- Invalid local baz: number? = 12 baz = 15 baz = 21 baz = nil -- Valid baz = "21" -- Invalid, 'baz' is still defined within the scope
No Globals
Global variables are not allowed. If you require such variable, you must do so by directly referencing the _G
table, alongside following the schema. A later chapter will go over this in more detail, alongside changes to _G
itself.
Table Schemas
All tables are required to be declared with a schema. A schema is declared in a syntactically similar way to that of regular tables, with the left side used for a key, and the right side used for the type itself. The ...
operator may also be used here for variable length schemas. If you need more flexible definitions, you can use square brackets alongside a type in the middle for the key.
Note that while empty schemas are technically valid, there would be no way to use the table with the schema as all new entries to the table make the table invalid, thus empty schemas are considered to be illegal.
local account = {name = "John", balance = 100} -- Invalid, missing a schema local account: table = {name = "John", balance = 100} -- Invalid, 'table' isn't a valid type annotation local account: {name = string, balance = number} = {name = "John", balance = 100} -- Valid account.name = "Adam" account.balance = 200 account.id = 50 -- Invalid, table no longer follows schema account.name = nil -- Also invalid local array: {[number] = number} = {1, 2, 3} table.insert(array, #array, 4) table.insert(array, #array, 5) array[10] = 10 -- Note that gaps are allowed in this case local arrayStrict: {[1] = number, ...} = {1, 2, 3} -- '...' inherits the type annotation of whatever was previous of it arrayStrict[4] = 4 arrayStrict[6] = 6 -- No longer follows the schema since there is a 'nil' value where a 'number' should go
Schemas can be referenced by mentioning the variable with the schema, allowing reuse and containment. Schemas can also be extended with the and
keyword.
local account: {name = string, balance = number} = {name = "Account", balance = 0} local john: account = {name = "John", balance = 100} local admin: account and {permission = number} = {name = "Admin", balance = 100000, permission = 10} local list: {[string] = account?} = {John = john, Admin = admin}
Adopting the keyword from functions, you can use self
or this
within schemas and tables to refer to the topmost table.
local foo: {foo = self} = {self} print(tostring(foo)) --> table: 0x________ print(tostring(foo.foo)) --> table: 0x________ print(tostring(foo.foo.foo)) --> table: 0x________ -- same table addresses
Function Annotations
Functions are defined as usual, with the function
keyword. All inputs and outputs require type declarations. Unlike variables and tables, functions may allow for nil
to be used as an output. A semicolon is required after output type annotations. An additional never
type is provided for functions which will never halt (and this WILL be checked), but only if the Halting problem has been successfully solved and implemented.
function foo(a: number, b: number): number; return a + b end function bar(message: string): nil; io.write(message) end function baz(): never; while true do end end
Functions allow for method overloads. This is the same as defining the value holding the function repeatedly with new arguments and outputs.
function leftpad(str: string, width: number, padding: string?): string; padding = padding or " " return padding:rep(width - #str) .. str end function leftpad(num: number, width: number): string; return string.format("%0"..width.."d", num) end print(leftpad("hey", 5)) --> " hey" print(leftpad(102, 5)) --> "00102"
self
and this
will implicitly use the schema of the containing table, but only if they are defined as the first argument of the function. This structure will also enforce all calls to the function use the appropriate syntactic sugar.
_G Changes
- This section is still a work in progress. It may be changed in the future.
_G
now follows the schema of: {[string] = {[string] = function}, print = function, (and so on)
.
Metaprogramming
Metatables are separate from regular tables, and do not count towards schema checks. They do however have their own schemas. You would write metatables as you usually would.
local object: {[string]: string} = {} local Object: {mt: {[string] = function, __index = self}, new = function} = { mt = { __call = function(): nil; print("Hello, World!") end, __index = self }, new = function(self): object; local new: object = {} setmetatable(new, self.mt) return new end } local thing = Object.new() thing() --> Hello, World!
Notes
- ↑ The full list is:
boolean
,number
,string
,function
,userdata
andthread
.