Estrita

From Esolang
Jump to navigation Jump to search
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's logo.

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

  1. The full list is: boolean, number, string, function, userdata and thread.