Skip to content

Instantly share code, notes, and snippets.

@mingodad
Created March 22, 2022 09:18
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mingodad/1a5b0ed6c25232cb29db9e3fa7de61b1 to your computer and use it in GitHub Desktop.
Save mingodad/1a5b0ed6c25232cb29db9e3fa7de61b1 to your computer and use it in GitHub Desktop.
Teal Language first attempt to Tealjs language
var VERSION = "0.13.2+dev";
var record tl {
enum LoadMode {
"b",
"t",
"bt",
};
type LoadFunction = function(...:any): any...;
enum CompatMode {
"off",
"optional",
"required",
};
enum TargetMode {
"5.1",
"5.3",
};
record TypeCheckOptions {
lax: boolean;
filename: string;
gen_compat: CompatMode;
gen_target: TargetMode;
env: Env;
run_internal_compiler_checks: boolean;
};
record Env {
globals: {string:Variable};
modules: {string:Type};
loaded: {string:Result};
loaded_order: {string};
gen_compat: CompatMode;
gen_target: TargetMode;
keep_going: boolean;
};
record Symbol {
x: integer;
y: integer;
name: string;
typ: Type;
other: integer;
skip: boolean;
};
record Result {
filename: string;
ast: Node;
type: Type;
syntax_errors: {Error};
type_errors: {Error};
warnings: {Error};
symbol_list: {Symbol};
env: Env;
dependencies: {string:string}; // module name, file found
};
enum WarningKind {
"unknown",
"unused",
"redeclaration",
"branch",
"hint",
"debug",
};
warning_kinds: {WarningKind:boolean};
record Error {
y: integer;
x: integer;
msg: string;
filename: string;
tag: WarningKind;
// used temporarily for stable-sorting
i: integer;
};
typecodes: {string:integer};
record TypeInfo {
t: integer;
str: string;
file: string;
x: integer;
y: integer;
ref: integer; // NOMINAL
fields: {string: integer}; // RECORD, ARRAYRECORD
enums: {string}; // ENUM
args: {{integer, string}}; // FUNCTION
rets: {{integer, string}}; // FUNCTION
vararg: boolean; // FUNCTION
types: {integer}; // UNION, POLY, TUPLE
keys: integer; // MAP
values: integer; // MAP
elements: integer; // ARRAY
};
record TypeReport {
by_pos: {string: {integer: {integer: integer}}};
types: {integer: TypeInfo};
symbols: {{integer, integer, string, integer}};
globals: {string: integer};
};
record TypeReportEnv {
typeid_to_num: {integer: integer};
next_num: integer;
tr: TypeReport;
};
load: function(string, string, LoadMode, {any:any}): LoadFunction, string;
process: function(string, Env): (Result, string);
process_string: function(string, boolean, Env, string): Result;
gen: function(string, Env): string;
type_check: function(Node, TypeCheckOptions): Result;
init_env: function(boolean, boolean | CompatMode, TargetMode, {string}): Env;
version: function(): string;
package_loader_env: Env;
}
tl.version = function(): string {
return VERSION;
};
tl.warning_kinds = {
["unused"] = true,
["redeclaration"] = true,
["branch"] = true,
["hint"] = true,
["debug"] = true,
};
// Implementation rationale:
// * bit 31: (MSB) special ("any", "unknown", "invalid")
// * "any" satisfies all Lua masks
// * bits 30-27: if valid: other Teal types ("nominal", "poly", "union", "typevar")
// * bits 24-26: reserved
// * bits 16-19: if valid: Teal types ("array", "record", "arrayrecord", "map", "tuple", "enum") that map to a Lua type ("table", "string")
// * bit 15: if not valid: value is unknown
// * bits 8-14: reserved
// * bits 0-7: (LSB) Lua types, one bit for each ("nil", "number", "boolean", "string", table, "function", "userdata", "thread")
// * every valid value has a Lua type bit set
tl.typecodes = {
// Lua types
NIL = 0x00000001,
NUMBER = 0x00000002,
BOOLEAN = 0x00000004,
STRING = 0x00000008,
TABLE = 0x00000010,
FUNCTION = 0x00000020,
USERDATA = 0x00000040,
THREAD = 0x00000080,
// Lua type masks
IS_TABLE = 0x00000008,
IS_NUMBER = 0x00000002,
IS_STRING = 0x00000004,
LUA_MASK = 0x00000fff,
// Teal types
INTEGER = 0x00010002,
ARRAY = 0x00010008,
RECORD = 0x00020008,
ARRAYRECORD = 0x00030008,
MAP = 0x00040008,
TUPLE = 0x00080008,
EMPTY_TABLE = 0x00000008,
ENUM = 0x00010004,
// Teal type masks
IS_ARRAY = 0x00010008,
IS_RECORD = 0x00020008,
// Indirect types
NOMINAL = 0x10000000,
TYPE_VARIABLE = 0x08000000,
// Indirect type masks
IS_UNION = 0x40000000,
IS_POLY = 0x20000020,
// Special types
ANY = 0xffffffff,
UNKNOWN = 0x80008000,
INVALID = 0x80000000,
// Special type masks
IS_SPECIAL = 0x80000000,
IS_VALID = 0x00000fff,
};
var type; Result = tl.Result;
var type; Env = tl.Env;
var type; Error = tl.Error;
var type; CompatMode = tl.CompatMode;
var type; TypeCheckOptions = tl.TypeCheckOptions;
var type; LoadMode = tl.LoadMode;
var type; LoadFunction = tl.LoadFunction;
var type; TargetMode = tl.TargetMode;
var type; TypeInfo = tl.TypeInfo;
var type; TypeReport = tl.TypeReport;
var type; TypeReportEnv = tl.TypeReportEnv;
var type; Symbol = tl.Symbol;
//------------------------------------------------------------------------------
// Lexer
//------------------------------------------------------------------------------
var enum TokenKind {
"keyword",
"op",
"string",
"[", "]", "(", ")", "{", "}", ",", ":", "#", "`", ".", ";",
"::",
"...",
"identifier",
"number",
"integer",
"$invalid_string$",
"$invalid_number$",
"$invalid$",
"$EOF$",
}
var record Token {
x: integer;
y: integer;
i: integer;
tk: string;
kind: TokenKind;
}
{
var enum LexState {
"start",
"any",
"identifier",
"got -",
"got --",
"got .",
"got ..",
"got =",
"got ~",
"got [",
"got 0",
"got <",
"got >",
"got /",
"got :",
"got --[",
"string single",
"string single got \\",
"string double",
"string double got \\",
"string long",
"string long got ]",
"comment short",
"comment long",
"comment long got ]",
"number dec",
"number decfloat",
"number hex",
"number hexfloat",
"number power",
"number powersign",
}
var last_token_kind: {LexState:TokenKind} = {
// ["start"]: never in a token
// ["any"]: never in a token
["identifier"] = "identifier",
["got -"] = "op",
// ["got --"]: drop comment
["got ."] = ".",
["got .."] = "op",
["got ="] = "op",
["got ~"] = "op",
["got ["] = "[",
["got 0"] = "number",
["got <"] = "op",
["got >"] = "op",
["got /"] = "op",
["got :"] = "op",
// ["got --["]: drop comment
["string single"] = "$invalid_string$",
["string single got \\"] = "$invalid_string$",
["string double"] = "$invalid_string$",
["string double got \\"] = "$invalid_string$",
["string long"] = "$invalid_string$",
["string long got ]"] = "$invalid_string$",
// ["comment short"]: drop comment
// ["comment long"]: drop comment
// ["comment long got ]"]: drop comment
["number dec"] = "integer",
["number decfloat"] = "number",
["number hex"] = "integer",
["number hexfloat"] = "number",
["number power"] = "number",
["number powersign"] = "$invalid_number$",
};
var keywords: {string:boolean} = {
["and"] = true,
["break"] = true,
["do"] = true,
["else"] = true,
["elseif"] = true,
["end"] = true,
["false"] = true,
["for"] = true,
["function"] = true,
["goto"] = true,
["if"] = true,
["in"] = true,
["local"] = true,
["nil"] = true,
["not"] = true,
["or"] = true,
["repeat"] = true,
["return"] = true,
["then"] = true,
["true"] = true,
["until"] = true,
["while"] = true,
};
var lex_any_char_states: {string:LexState} = {
["\""] = "string double",
["'"] = "string single",
["-"] = "got -",
["."] = "got .",
["0"] = "got 0",
["<"] = "got <",
[">"] = "got >",
["/"] = "got /",
[":"] = "got :",
["="] = "got =",
["~"] = "got ~",
["["] = "got [",
};
for( c = string.byte("a"), string.byte("z") ) {
lex_any_char_states[string.char(c)] = "identifier";
}
for( c = string.byte("A"), string.byte("Z") ) {
lex_any_char_states[string.char(c)] = "identifier";
}
lex_any_char_states["_"] = "identifier";
for( c = string.byte("1"), string.byte("9") ) {
lex_any_char_states[string.char(c)] = "number dec";
}
var lex_word: {string:boolean} = {};
for( c = string.byte("a"), string.byte("z") ) {
lex_word[string.char(c)] = true;
}
for( c = string.byte("A"), string.byte("Z") ) {
lex_word[string.char(c)] = true;
}
for( c = string.byte("0"), string.byte("9") ) {
lex_word[string.char(c)] = true;
}
lex_word["_"] = true;
var lex_decimals: {string:boolean} = {};
for( c = string.byte("0"), string.byte("9") ) {
lex_decimals[string.char(c)] = true;
}
var lex_hexadecimals: {string:boolean} = {};
for( c = string.byte("0"), string.byte("9") ) {
lex_hexadecimals[string.char(c)] = true;
}
for( c = string.byte("a"), string.byte("f") ) {
lex_hexadecimals[string.char(c)] = true;
}
for( c = string.byte("A"), string.byte("F") ) {
lex_hexadecimals[string.char(c)] = true;
}
var lex_any_char_kinds: {string:TokenKind} = {};
var single_char_kinds: {TokenKind} = {"[", "]", "(", ")", "{", "}", ",", "#", "`", ";"};
for( _, c in ipairs(single_char_kinds) ) {
lex_any_char_kinds[c] = c;
}
for( _, c in ipairs({"+", "*", "|", "&", "%", "^"}) ) {
lex_any_char_kinds[c] = "op";
}
var lex_space: {string:boolean} = {};
for( _, c in ipairs({" ", "\t", "\v", "\n", "\r"}) ) {
lex_space[c] = true;
}
var escapable_characters: {string:boolean} = {
a = true,
b = true,
f = true,
n = true,
r = true,
t = true,
v = true,
z = true,
["\\"] = true,
["\'"] = true,
["\""] = true,
["\r"] = true,
["\n"] = true,
};
var function lex_string_escape(input: string, i: integer, c: string): integer, boolean {
if( escapable_characters[c] ) {
return 0, true;
} else if( c == "x" ) {
return 2, (
lex_hexadecimals[input->sub(i+1, i+1)] &&
lex_hexadecimals[input->sub(i+2, i+2)]
);
} else if( c == "u" ) {
if( input->sub(i+1, i+1) == "{" ) {
var p = i + 2;
if( ! lex_hexadecimals[input->sub(p, p)] ) {
return 2, false;
}
while( true ) {
p = p + 1;
c = input->sub(p, p);
if( ! lex_hexadecimals[c] ) {
return p - i, c == "}";
}
}
}
} else if( lex_decimals[c] ) {
var len = lex_decimals[input->sub(i+1, i+1)]
&& (lex_decimals[input->sub(i+2, i+2)] && 2 || 1)
|| 0;
return len, tonumber(input->sub(i, i + len)) < 256;
} else {
return 0, false;
}
}
function tl.lex(input: string): {Token}, {Token} {
var tokens: {Token} = {};
var state: LexState = "any";
var fwd = true;
var y = 1;
var x = 0;
var i = 0;
var lc_open_lvl = 0;
var lc_close_lvl = 0;
var ls_open_lvl = 0;
var ls_close_lvl = 0;
var errs: {Token} = {};
var nt = 0;
var tx: integer;
var ty: integer;
var ti: integer;
var in_token = false;
var function begin_token() {
tx = x;
ty = y;
ti = i;
in_token = true;
}
var function end_token(kind: TokenKind, tk: string) {
nt = nt + 1;
tokens[nt] = {
x = tx,
y = ty,
i = ti,
tk = tk,
kind = kind,
};
in_token = false;
}
var function end_token_identifier() {
var tk = input->sub(ti, i - 1);
nt = nt + 1;
tokens[nt] = {
x = tx,
y = ty,
i = ti,
tk = tk,
kind = keywords[tk] && "keyword" || "identifier"
};
in_token = false;
}
var function end_token_prev(kind: TokenKind) {
var tk = input->sub(ti, i - 1);
nt = nt + 1;
tokens[nt] = {
x = tx,
y = ty,
i = ti,
tk = tk,
kind = kind
};
in_token = false;
}
var function end_token_here(kind: TokenKind) {
var tk = input->sub(ti, i);
nt = nt + 1;
tokens[nt] = {
x = tx,
y = ty,
i = ti,
tk = tk,
kind = kind
};
in_token = false;
}
var function drop_token() {
in_token = false;
}
var len = #input;
if( input->sub(1,2) == "#!" ) {
i = input->find("\n");
if( ! i ) {
i = len + 1;
}
y = 2;
x = 0;
}
state = "any";
while( i <= len ) {
if( fwd ) {
i = i + 1;
if( i > len ) {
break;
}
}
var c: string = input->sub(i, i);
if( fwd ) {
if( c == "\n" ) {
y = y + 1;
x = 0;
} else {
x = x + 1;
}
} else {
fwd = true;
}
if( state == "any" ) {
var st = lex_any_char_states[c];
if( st ) {
state = st;
begin_token();
} else {
var k = lex_any_char_kinds[c];
if( k ) {
begin_token();
end_token(k, c);
} else if( ! lex_space[c] ) {
begin_token();
end_token_here("$invalid$");
table.insert(errs, tokens[#tokens]);
}
}
} else if( state == "identifier" ) {
if( ! lex_word[c] ) {
end_token_identifier();
fwd = false;
state = "any";
}
} else if( state == "string double" ) {
if( c == "\\" ) {
state = "string double got \\";
} else if( c == "\"" ) {
end_token_here("string");
state = "any";
}
} else if( state == "comment short" ) {
if( c == "\n" ) {
state = "any";
}
} else if( state == "got =" ) {
var t: string;
if( c == "=" ) {
t = "==";
} else {
t = "=";
fwd = false;
}
end_token("op", t);
state = "any";
} else if( state == "got ." ) {
if( c == "." ) {
state = "got ..";
} else if( lex_decimals[c] ) {
state = "number decfloat";
} else {
end_token(".", ".");
fwd = false;
state = "any";
}
} else if( state == "got :" ) {
var t: TokenKind;
if( c == ":" ) {
t = "::";
} else {
t = ":";
fwd = false;
}
end_token(t, t);
state = "any";
} else if( state == "got [" ) {
if( c == "[" ) {
state = "string long";
} else if( c == "=" ) {
ls_open_lvl = ls_open_lvl + 1;
} else {
end_token("[", "[");
fwd = false;
state = "any";
ls_open_lvl = 0;
}
} else if( state == "number dec" ) {
if( lex_decimals[c] ) {
// proceed
} else if( c == "." ) {
state = "number decfloat";
} else if( c == "e" || c == "E" ) {
state = "number powersign";
} else {
end_token_prev("integer");
fwd = false;
state = "any";
}
} else if( state == "got -" ) {
if( c == "-" ) {
state = "got --";
} else {
end_token("op", "-");
fwd = false;
state = "any";
}
} else if( state == "got .." ) {
if( c == "." ) {
end_token("...", "...");
} else {
end_token("op", "..");
fwd = false;
}
state = "any";
} else if( state == "number hex" ) {
if( lex_hexadecimals[c] ) {
// proceed
} else if( c == "." ) {
state = "number hexfloat";
} else if( c == "p" || c == "P" ) {
state = "number powersign";
} else {
end_token_prev("integer");
fwd = false;
state = "any";
}
} else if( state == "got --" ) {
if( c == "[" ) {
state = "got --[";
} else {
fwd = false;
state = "comment short";
drop_token();
}
} else if( state == "got 0" ) {
if( c == "x" || c == "X" ) {
state = "number hex";
} else if( c == "e" || c == "E" ) {
state = "number powersign";
} else if( lex_decimals[c] ) {
state = "number dec";
} else if( c == "." ) {
state = "number decfloat";
} else {
end_token_prev("integer");
fwd = false;
state = "any";
}
} else if( state == "got --[" ) {
if( c == "[" ) {
state = "comment long";
} else if( c == "=" ) {
lc_open_lvl = lc_open_lvl + 1;
} else {
fwd = false;
state = "comment short";
drop_token();
lc_open_lvl = 0;
}
} else if( state == "comment long" ) {
if( c == "]" ) {
state = "comment long got ]";
}
} else if( state == "comment long got ]" ) {
if( c == "]" && lc_close_lvl == lc_open_lvl ) {
drop_token();
state = "any";
lc_open_lvl = 0;
lc_close_lvl = 0;
} else if( c == "=" ) {
lc_close_lvl = lc_close_lvl + 1;
} else {
state = "comment long";
lc_close_lvl = 0;
}
} else if( state == "string double got \\" ) {
var skip, valid = lex_string_escape(input, i, c);
i = i + skip;
if( ! valid ) {
end_token_here("$invalid_string$");
table.insert(errs, tokens[#tokens]);
}
x = x + skip;
state = "string double";
} else if( state == "string single" ) {
if( c == "\\" ) {
state = "string single got \\";
} else if( c == "'" ) {
end_token_here("string");
state = "any";
}
} else if( state == "string single got \\" ) {
var skip, valid = lex_string_escape(input, i, c);
i = i + skip;
if( ! valid ) {
end_token_here("$invalid_string$");
table.insert(errs, tokens[#tokens]);
}
x = x + skip;
state = "string single";
} else if( state == "got ~" ) {
var t: string;
if( c == "=" ) {
t = "~=";
} else {
t = "~";
fwd = false;
}
end_token("op", t);
state = "any";
} else if( state == "got <" ) {
var t: string;
if( c == "=" ) {
t = "<=";
} else if( c == "<" ) {
t = "<<";
} else {
t = "<";
fwd = false;
}
end_token("op", t);
state = "any";
} else if( state == "got >" ) {
var t: string;
if( c == "=" ) {
t = ">=";
} else if( c == ">" ) {
t = ">>";
} else {
t = ">";
fwd = false;
}
end_token("op", t);
state = "any";
} else if( state == "got /" ) {
var t: string;
if( c == "/" ) {
t = "//";
} else {
t = "/";
fwd = false;
}
end_token("op", t);
state = "any";
} else if( state == "string long" ) {
if( c == "]" ) {
state = "string long got ]";
}
} else if( state == "string long got ]" ) {
if( c == "]" ) {
if( ls_close_lvl == ls_open_lvl ) {
end_token_here("string");
state = "any";
ls_open_lvl = 0;
ls_close_lvl = 0;
}
} else if( c == "=" ) {
ls_close_lvl = ls_close_lvl + 1;
} else {
state = "string long";
ls_close_lvl = 0;
}
} else if( state == "number hexfloat" ) {
if( c == "p" || c == "P" ) {
state = "number powersign";
} else if( ! lex_hexadecimals[c] ) {
end_token_prev("number");
fwd = false;
state = "any";
}
} else if( state == "number decfloat" ) {
if( c == "e" || c == "E" ) {
state = "number powersign";
} else if( ! lex_decimals[c] ) {
end_token_prev("number");
fwd = false;
state = "any";
}
} else if( state == "number powersign" ) {
if( c == "-" || c == "+" ) {
state = "number power";
} else if( lex_decimals[c] ) {
state = "number power";
} else {
end_token_here("$invalid_number$");
table.insert(errs, tokens[#tokens]);
state = "any"; // FIXME report malformed number
}
} else if( state == "number power" ) {
if( ! lex_decimals[c] ) {
end_token_prev("number");
fwd = false;
state = "any";
}
}
}
if( in_token ) {
if( last_token_kind[state] ) {
end_token_prev(last_token_kind[state]);
if( keywords[tokens[nt].tk] ) {
tokens[nt].kind = "keyword";
}
} else {
drop_token();
}
}
table.insert(tokens, { x = x + 1, y = y, i = i, tk = "$EOF$", kind = "$EOF$" });
return tokens, (#errs > 0) && errs;
}
}
var function binary_search<T, U>(list: {T}, item: U, cmp: function(T, U): boolean): integer, T {
var len <const> = #list;
var mid: integer;
var s, e = 1, len;
while( s <= e ) {
mid = math.floor((s + e) / 2);
var val <const> = list[mid];
var res <const> = cmp(val, item);
if( res ) {
if( mid == len ) {
return mid, val;
} else {
if( ! cmp(list[mid + 1], item) ) {
return mid, val;
}
}
s = mid + 1;
} else {
e = mid - 1;
}
}
}
function tl.get_token_at(tks: {Token}, y: integer, x: integer): string {
var _, found <const> = binary_search(
tks, null,
function(tk: Token): boolean {
return tk.y < y
|| (tk.y == y && tk.x <= x);
}
);
if( found
&& found.y == y
&& found.x <= x && x < found.x + #found.tk
) {
return found.tk;
}
}
//------------------------------------------------------------------------------
// Recursive descent parser
//------------------------------------------------------------------------------
var last_typeid = 0;
var function new_typeid(): integer {
last_typeid = last_typeid + 1;
return last_typeid;
}
var enum TypeName {
"typetype",
"nestedtype",
"typevar",
"typearg",
"function",
"array",
"map",
"tupletable",
"arrayrecord",
"record",
"enum",
"boolean",
"string",
"nil",
"thread",
"number",
"integer",
"union",
"nominal",
"bad_nominal",
"emptytable",
"table_item",
"unresolved_emptytable_value",
"tuple",
"poly", // intersection types, currently restricted to polymorphic functions defined inside records
"any",
"unknown", // to be used in lax mode only
"invalid", // producing a new value of this type (not propagating) must always produce a type error
"unresolved",
"none",
}
var table_types: {TypeName:boolean} = {
["array"] = true,
["map"] = true,
["arrayrecord"] = true,
["record"] = true,
["emptytable"] = true,
};
var record Type {
{Type};
y: integer;
x: integer;
filename: string;
typename: TypeName;
tk: string;
yend: integer;
xend: integer;
// Lua compatibilty
needs_compat: boolean;
// tuple
is_va: boolean;
// poly, union, tupletable
types: {Type};
// typetype
def: Type;
is_alias: boolean;
closed: boolean;
// map
keys: Type;
values: Type;
// records
typeargs: {Type};
fields: {string: Type};
field_order: {string};
meta_fields: {string: Type};
meta_field_order: {string};
is_userdata: boolean;
// array
elements: Type;
// tupletable/array
inferred_len: integer;
// function
is_method: boolean;
args: Type;
rets: Type;
typeid: integer;
// nominal
names: {string};
typevals: Type;
found: Type; // type is found but typeargs are not resolved
resolved: Type; // type is found and typeargs are resolved
// typevar
typevar: string;
// typearg
typearg: string;
// table items
kname: string;
ktype: Type;
vtype: Type;
// emptytable
declared_at: Node;
assigned_to: string;
keys_inferred_at: Node;
keys_inferred_at_file: string;
inferred_at: Node;
inferred_at_file: string;
emptytable_type: Type;
// enum
enumset: {string:boolean};
// unresolved items
labels: {string:{Node}};
nominals: {string:{Type}};
}
var record Operator {
y: integer;
x: integer;
arity: integer;
op: string;
prec: integer;
}
var enum NodeKind {
"op",
"nil",
"string",
"number",
"integer",
"boolean",
"table_literal",
"table_item",
"function",
"expression_list",
"enum_item",
"if",
"if_block",
"while",
"fornum",
"forin",
"goto",
"label",
"repeat",
"do",
"break",
"return",
"newtype",
"argument",
"type_identifier",
"variable",
"variable_list",
"statements",
"assignment",
"argument_list",
"local_function",
"global_function",
"local_type",
"global_type",
"record_function",
"local_declaration",
"global_declaration",
"identifier",
"cast",
"...",
"paren",
"error_node",
}
var enum FactType {
"is", // type-based type judgement (its negation implies the subtracted type)
"==", // value-based type judgement (its negation does not imply a subtracted type negated)
"not", // negation: type-based judgements subtract, value-based judgements prove nothing
"and", // conjunction: type-based judgements intersect, any value-based judgement downgrades all
"or", // disjunction: type-based judgements unite, any value-based judgement downgrades all
"truthy", // expression that is either truthy or a runtime error
}
var record Fact {
fact: FactType;
where: Node;
// is
_v_var: string;
typ: Type;
// not, and, or
f1: Fact;
f2: Fact;
metamethod; __call: function(Fact, Fact): Fact;
}
var enum KeyParsed {
"short",
"long",
"implicit",
}
var record Node {
{Node};
record ExpectedContext {
kind: NodeKind;
name: string;
};
y: integer;
x: integer;
tk: string;
kind: NodeKind;
symbol_list_slot: integer;
semicolon: boolean;
is_longstring: boolean;
yend: integer;
xend: integer;
known: Fact;
// bidirectional inference
expected: Type;
expected_context: Node.ExpectedContext;
key: Node;
value: Node;
key_parsed: KeyParsed;
typeargs: Type;
args: Node;
rets: Type;
body: Node;
name: Node;
// statements list in a `repeat`, delay closing scope
is_repeat: boolean;
// local
is_const: boolean;
fn_owner: Node;
is_method: boolean;
exp: Node;
if_parent: Node;
if_block_n: integer;
if_blocks: {Node};
// fornum
_v_var: Node;
from: Node;
to: Node;
step: Node;
// forin
vars: Node;
exps: Node;
// newtype
newtype: Type;
is_alias: boolean;
// expressions
op: Operator;
e1: Node;
e2: Node;
constnum: number;
conststr: string;
failstore: boolean;
// table literal
array_len: integer;
// goto
label: string;
casttype: Type;
type: Type;
decltype: Type;
}
var function is_array_type(t:Type): boolean {
return t.typename == "array" || t.typename == "arrayrecord";
}
var function is_record_type(t:Type): boolean {
return t.typename == "record" || t.typename == "arrayrecord";
}
var function is_number_type(t:Type): boolean {
return t.typename == "number" || t.typename == "integer";
}
var function is_typetype(t:Type): boolean {
return t.typename == "typetype" || t.typename == "nestedtype";
}
var record ParseState {
tokens: {Token};
errs: {Error};
filename: string;
required_modules: {string};
}
var enum ParseTypeListMode {
"rets",
"decltype",
"casttype",
}
var parse_type_list: function(ParseState, integer, ParseTypeListMode): integer, Type;
var parse_expression: function(ParseState, integer): integer, Node, integer;
var parse_expression_and_tk: function(ps: ParseState, i: integer, tk: string): integer, Node;
var parse_statements: function(ParseState, integer, boolean): integer, Node;
var parse_argument_list: function(ParseState, integer): integer, Node;
var parse_argument_type_list: function(ParseState, integer): integer, Type;
var parse_type: function(ParseState, integer): integer, Type, integer;
var parse_newtype: function(ps: ParseState, i: integer): integer, Node;
var parse_enum_body: function(ps: ParseState, i: integer, def: Type, node: Node): integer, Node;
var parse_record_body: function(ps: ParseState, i: integer, def: Type, node: Node): integer, Node;
var function fail(ps: ParseState, i: integer, msg: string): integer {
if( ! ps.tokens[i] ) {
var eof = ps.tokens[#ps.tokens];
table.insert(ps.errs, { filename = ps.filename, y = eof.y, x = eof.x, msg = msg || "unexpected end of file" });
return #ps.tokens;
}
table.insert(ps.errs, { filename = ps.filename, y = ps.tokens[i].y, x = ps.tokens[i].x, msg = assert(msg, "syntax error, but no error message provided") });
return math.min(#ps.tokens, i + 1);
}
var function end_at(node: Node, tk: Token) {
node.yend = tk.y;
node.xend = tk.x + #tk.tk - 1;
}
var function verify_tk(ps: ParseState, i: integer, tk: string): integer {
if( ps.tokens[i].tk == tk ) {
return i + 1;
}
return fail(ps, i, "syntax error, expected '" .. tk .. "'");
}
var function verify_end(ps: ParseState, i: integer, istart: integer, node: Node): integer {
if( ps.tokens[i].tk == "end" ) {
node.yend = ps.tokens[i].y;
node.xend = ps.tokens[i].x + 2;
return i + 1;
}
end_at(node, ps.tokens[i]);
return fail(ps, i, "syntax error, expected 'end' to close construct started at " .. ps.filename .. ":" .. ps.tokens[istart].y .. ":" .. ps.tokens[istart].x .. ":");
}
var function new_node(tokens: {Token}, i: integer, kind: NodeKind): Node {
var t = tokens[i];
return { y = t.y, x = t.x, tk = t.tk, kind = kind || t.kind };
}
var function a_type(t: Type): Type {
t.typeid = new_typeid();
return t;
}
var function new_type(ps: ParseState, i: integer, typename: TypeName): Type {
var token = ps.tokens[i];
return a_type( {
typename = assert(typename),
filename = ps.filename,
y = token.y,
x = token.x,
tk = token.tk
});
}
var function verify_kind(ps: ParseState, i: integer, kind: TokenKind, node_kind: NodeKind): integer, Node {
if( ps.tokens[i].kind == kind ) {
return i + 1, new_node(ps.tokens, i, node_kind);
}
return fail(ps, i, "syntax error, expected " .. kind);
}
var type; SkipFunction = function(ParseState, integer): integer {
var function failskip(ps: ParseState, i: integer, msg: string, skip_fn: SkipFunction, starti: integer): integer {
var err_ps: ParseState = {
tokens = ps.tokens,
errs = {},
required_modules = {},
};
var skip_i = skip_fn(err_ps, starti || i);
fail(ps, starti || i, msg);
return skip_i || (i + 1);
}
var function skip_record(ps: ParseState, i: integer): integer, Node {
i = i + 1;
return parse_record_body(ps, i, {}, {});
}
var function skip_enum(ps: ParseState, i: integer): integer, Node {
i = i + 1;
return parse_enum_body(ps, i, {}, {});
}
var function parse_table_value(ps: ParseState, i: integer): integer, Node, integer {
var next_word = ps.tokens[i].tk;
var e: Node;
if( next_word == "record" ) {
i = failskip(ps, i, "syntax error: this syntax is no longer valid; declare nested record inside a record", skip_record);
} else if( next_word == "enum" ) {
i = failskip(ps, i, "syntax error: this syntax is no longer valid; declare nested enum inside a record", skip_enum);
} else {
i, e = parse_expression(ps, i);
}
if( ! e ) {
e = new_node(ps.tokens, i - 1, "error_node");
}
return i, e;
}
var function parse_table_item(ps: ParseState, i: integer, n: integer): integer, Node, integer {
var node = new_node(ps.tokens, i, "table_item");
if( ps.tokens[i].kind == "$EOF$" ) {
return fail(ps, i, "unexpected eof");
}
if( ps.tokens[i].tk == "[" ) {
node.key_parsed = "long";
i = i + 1;
i, node.key = parse_expression_and_tk(ps, i, "]");
i = verify_tk(ps, i, "=");
i, node.value = parse_table_value(ps, i);
return i, node, n;
} else if( ps.tokens[i].kind == "identifier" ) {
if( ps.tokens[i+1].tk == "=" ) {
node.key_parsed = "short";
i, node.key = verify_kind(ps, i, "identifier", "string");
node.key.conststr = node.key.tk;
node.key.tk = '"' .. node.key.tk .. '"';
i = verify_tk(ps, i, "=");
i, node.value = parse_table_value(ps, i);
return i, node, n;
} else if( ps.tokens[i+1].tk == ":" ) {
node.key_parsed = "short";
var orig_i = i;
var try_ps: ParseState = {
filename = ps.filename,
tokens = ps.tokens,
errs = {},
required_modules = ps.required_modules,
};
i, node.key = verify_kind(try_ps, i, "identifier", "string");
node.key.conststr = node.key.tk;
node.key.tk = '"' .. node.key.tk .. '"';
i = verify_tk(try_ps, i, ":");
i, node.decltype = parse_type(try_ps, i);
if( node.decltype && ps.tokens[i].tk == "=" ) {
i = verify_tk(try_ps, i, "=");
i, node.value = parse_table_value(try_ps, i);
if( node.value ) {
for( _, e in ipairs(try_ps.errs) ) {
table.insert(ps.errs, e);
}
return i, node, n;
}
}
// backtrack:
node.decltype = null;
i = orig_i;
}
}
node.key = new_node(ps.tokens, i, "integer");
node.key_parsed = "implicit";
node.key.constnum = n;
node.key.tk = tostring(n);
i, node.value = parse_expression(ps, i);
if( ! node.value ) {
return fail(ps, i, "expected an expression");
}
return i, node, n + 1;
}
var type; ParseItem = function<T>(ParseState, integer, integer): integer, T, integer {
var enum SeparatorMode {
"sep",
"term",
}
var function parse_list<T>(ps: ParseState, i: integer, list: {T}, close: {string:boolean}, sep: SeparatorMode, parse_item: ParseItem<T>): integer, {T} {
var n = 1;
while( ps.tokens[i].kind != "$EOF$" ) {
if( close[ps.tokens[i].tk] ) {
end_at(list as Node, ps.tokens[i]);
break;
}
var item: T;
var oldn = n;
i, item, n = parse_item(ps, i, n);
n = n || oldn;
table.insert(list, item);
if( ps.tokens[i].tk == "," ) {
i = i + 1;
if( sep == "sep" && close[ps.tokens[i].tk] ) {
fail(ps, i, "unexpected '" .. ps.tokens[i].tk .. "'");
return i, list;
}
} else if( sep == "term" && ps.tokens[i].tk == ";" ) {
i = i + 1;
} else if( ! close[ps.tokens[i].tk] ) {
var options = {};
for( k, _ in pairs(close) ) {
table.insert(options, "'" .. k .. "'");
}
table.sort(options);
table.insert(options, "','");
var expected = "syntax error, expected one of: " .. table.concat(options, ", ");
fail(ps, i, expected);
var first = options[1]->sub(2, -2);
// heuristic for error recovery to avoid a cascade of errors:
// * if we're parsing a bracketed list, assume the missing token is a separator;
// * otherwise, if we have a line break, insert expected terminator token.
if( first != "}" && ps.tokens[i].y != ps.tokens[i-1].y ) {
// FIXME closing token may not be a keyword (but non-keywords are checked with verify_tk, so it should work)
table.insert(ps.tokens, i, { tk = first, y = ps.tokens[i-1].y, x = ps.tokens[i-1].x + 1, kind = "keyword" });
return i, list;
}
}
}
return i, list;
}
var function parse_bracket_list<T>(ps: ParseState, i: integer, list: {T}, open: string, close: string, sep: SeparatorMode, parse_item: ParseItem<T>): integer, {T} {
i = verify_tk(ps, i, open);
i = parse_list(ps, i, list, { [close] = true }, sep, parse_item);
i = verify_tk(ps, i, close);
return i, list;
}
var function parse_table_literal(ps: ParseState, i: integer): integer, Node {
var node = new_node(ps.tokens, i, "table_literal");
return parse_bracket_list(ps, i, node, "{", "}", "term", parse_table_item);
}
var function parse_trying_list<T>(ps: ParseState, i: integer, list: {T}, parse_item: ParseItem<T>): integer, {T} {
var try_ps: ParseState = {
filename = ps.filename,
tokens = ps.tokens,
errs = {},
required_modules = ps.required_modules,
};
var tryi, item: integer, T = parse_item(try_ps, i);
if( ! item ) {
return i, list;
}
for( _, e in ipairs(try_ps.errs) ) {
table.insert(ps.errs, e);
}
i = tryi;
table.insert(list, item);
if( ps.tokens[i].tk == "," ) {
while( ps.tokens[i].tk == "," ) {
i = i + 1;
i, item = parse_item(ps, i);
table.insert(list, item);
}
}
return i, list;
}
var function parse_typearg_type(ps: ParseState, i: integer): integer, Type, integer {
var backtick = false;
if( ps.tokens[i].tk == "`" ) {
i = verify_tk(ps, i, "`");
backtick = true;
}
i = verify_kind(ps, i, "identifier");
return i, a_type( {
y = ps.tokens[i - 2].y,
x = ps.tokens[i - 2].x,
typename = "typearg",
typearg = (backtick && "`" || "") .. ps.tokens[i-1].tk,
});
}
var function parse_typevar_type(ps: ParseState, i: integer): integer, Type, integer {
i = verify_tk(ps, i, "`");
i = verify_kind(ps, i, "identifier");
return i, a_type( {
y = ps.tokens[i - 2].y,
x = ps.tokens[i - 2].x,
typename = "typevar",
typevar = "`" .. ps.tokens[i-1].tk,
});
}
var function parse_typearg_list(ps: ParseState, i: integer): integer, Type {
if( ps.tokens[i+1].tk == ">" ) {
return fail(ps, i+1, "type argument list cannot be empty");
}
var typ = new_type(ps, i, "tuple");
return parse_bracket_list(ps, i, typ, "<", ">", "sep", parse_typearg_type);
}
var function parse_typeval_list(ps: ParseState, i: integer): integer, Type {
if( ps.tokens[i+1].tk == ">" ) {
return fail(ps, i+1, "type argument list cannot be empty");
}
var typ = new_type(ps, i, "tuple");
return parse_bracket_list(ps, i, typ, "<", ">", "sep", parse_type);
}
var function parse_return_types(ps: ParseState, i: integer): integer, Type {
return parse_type_list(ps, i, "rets");
}
var function parse_function_type(ps: ParseState, i: integer): integer, Type {
var typ = new_type(ps, i, "function");
i = i + 1;
if( ps.tokens[i].tk == "<" ) {
i, typ.typeargs = parse_typearg_list(ps, i);
}
if( ps.tokens[i].tk == "(" ) {
i, typ.args = parse_argument_type_list(ps, i);
i, typ.rets = parse_return_types(ps, i);
} else {
typ.args = a_type( { typename = "tuple", is_va = true, a_type( { typename = "any" }) });
typ.rets = a_type( { typename = "tuple", is_va = true, a_type( { typename = "any" }) });
}
return i, typ;
}
var NIL = a_type( { typename = "nil" });
var ANY = a_type( { typename = "any" });
var TABLE = a_type( { typename = "map", keys = ANY, values = ANY });
var NUMBER = a_type( { typename = "number" });
var STRING = a_type( { typename = "string" });
var THREAD = a_type( { typename = "thread" });
var BOOLEAN = a_type( { typename = "boolean" });
var INTEGER = a_type( { typename = "integer" });
var simple_types: {string:Type} = {
["nil"] = NIL,
["any"] = ANY,
["table"] = TABLE,
["number"] = NUMBER,
["string"] = STRING,
["thread"] = THREAD,
["boolean"] = BOOLEAN,
["integer"] = INTEGER,
};
var function parse_base_type(ps: ParseState, i: integer): integer, Type, integer {
var tk = ps.tokens[i].tk;
if( ps.tokens[i].kind == "identifier" ) {
var st = simple_types[tk];
if( st ) {
return i + 1, st;
}
var typ = new_type(ps, i, "nominal");
typ.names = { tk };
i = i + 1;
while( ps.tokens[i].tk == "." ) {
i = i + 1;
if( ps.tokens[i].kind == "identifier" ) {
table.insert(typ.names, ps.tokens[i].tk);
i = i + 1;
} else {
return fail(ps, i, "syntax error, expected identifier");
}
}
if( ps.tokens[i].tk == "<" ) {
i, typ.typevals = parse_typeval_list(ps, i);
}
return i, typ;
} else if( tk == "{" ) {
i = i + 1;
var decl = new_type(ps, i, "array");
var t: Type;
i, t = parse_type(ps, i);
if( ! t ) {
return i;
}
if( ps.tokens[i].tk == "}" ) {
decl.elements = t;
end_at(decl as Node, ps.tokens[i]);
i = verify_tk(ps, i, "}");
} else if( ps.tokens[i].tk == "," ) {
decl.typename = "tupletable";
decl.types = { t };
var n = 2;
do {
i = i + 1;
i, decl.types[n] = parse_type(ps, i);
if( ! decl.types[n] ) {
break;
}
n = n + 1;
} while(!( ps.tokens[i].tk != "," ));
end_at(decl as Node, ps.tokens[i]);
i = verify_tk(ps, i, "}");
} else if( ps.tokens[i].tk == ":" ) {
decl.typename = "map";
i = i + 1;
decl.keys = t;
i, decl.values = parse_type(ps, i);
if( ! decl.values ) {
return i;
}
end_at(decl as Node, ps.tokens[i]);
i = verify_tk(ps, i, "}");
} else {
return fail(ps, i, "syntax error; did you forget a '}'?");
}
return i, decl;
} else if( tk == "function" ) {
return parse_function_type(ps, i);
} else if( tk == "nil" ) {
return i + 1, simple_types["nil"];
} else if( tk == "table" ) {
var typ = new_type(ps, i, "map");
typ.keys = a_type( { typename = "any" });
typ.values = a_type( { typename = "any" });
return i + 1, typ;
} else if( tk == "`" ) {
return parse_typevar_type(ps, i);
}
return fail(ps, i, "expected a type");
}
parse_type = function(ps: ParseState, i: integer): integer, Type, integer {
if( ps.tokens[i].tk == "(" ) {
i = i + 1;
var t: Type;
i, t = parse_type(ps, i);
i = verify_tk(ps, i, ")");
return i, t;
}
var bt: Type;
var istart = i;
i, bt = parse_base_type(ps, i);
if( ! bt ) {
return i;
}
if( ps.tokens[i].tk == "|" ) {
var u = new_type(ps, istart, "union");
u.types = { bt };
while( ps.tokens[i].tk == "|" ) {
i = i + 1;
i, bt = parse_base_type(ps, i);
if( ! bt ) {
return i;
}
table.insert(u.types, bt);
}
bt = u;
}
return i, bt;
};
parse_type_list = function(ps: ParseState, i: integer, mode: ParseTypeListMode): integer, Type {
var list = new_type(ps, i, "tuple");
var first_token = ps.tokens[i].tk;
if( mode == "rets" || mode == "decltype" ) {
if( first_token == ":" ) {
i = i + 1;
} else {
return i, list;
}
}
var optional_paren = false;
if( ps.tokens[i].tk == "(" ) {
optional_paren = true;
i = i + 1;
}
var prev_i = i;
i = parse_trying_list(ps, i, list, parse_type);
if( i == prev_i && ps.tokens[i].tk != ")" ) {
fail(ps, i - 1, "expected a type list");
}
if( mode == "rets" && ps.tokens[i].tk == "..." ) {
i = i + 1;
var nrets = #list;
if( nrets > 0 ) {
list.is_va = true;
} else {
fail(ps, i, "unexpected '...'");
}
}
if( optional_paren ) {
i = verify_tk(ps, i, ")");
}
return i, list;
};
var function parse_function_args_rets_body(ps: ParseState, i: integer, node: Node): integer, Node {
var istart = i - 1;
if( ps.tokens[i].tk == "<" ) {
i, node.typeargs = parse_typearg_list(ps, i);
}
i, node.args = parse_argument_list(ps, i);
i, node.rets = parse_return_types(ps, i);
i, node.body = parse_statements(ps, i);
end_at(node, ps.tokens[i]);
i = verify_end(ps, i, istart, node);
assert(node.rets.typename == "tuple");
return i, node;
}
var function parse_function_value(ps: ParseState, i: integer): integer, Node {
var node = new_node(ps.tokens, i, "function");
i = verify_tk(ps, i, "function");
return parse_function_args_rets_body(ps, i, node);
}
var function unquote(str: string): string, boolean {
var f = str->sub(1, 1);
if( f == '"' || f == "'" ) {
return str->sub(2, -2), false;
}
f = str->match("^%[=*%[");
var l = #f + 1;
return str->sub(l, -l), true;
}
var function parse_literal(ps: ParseState, i: integer): integer, Node {
var tk = ps.tokens[i].tk;
var kind = ps.tokens[i].kind;
if( kind == "identifier" ) {
return verify_kind(ps, i, "identifier", "variable");
} else if( kind == "string" ) {
var node = new_node(ps.tokens, i, "string");
node.conststr, node.is_longstring = unquote(tk);
return i + 1, node;
} else if( kind == "number" || kind == "integer" ) {
var n = tonumber(tk);
var node: Node;
i, node = verify_kind(ps, i, kind);
node.constnum = n;
return i, node;
} else if( tk == "true" ) {
return verify_kind(ps, i, "keyword", "boolean");
} else if( tk == "false" ) {
return verify_kind(ps, i, "keyword", "boolean");
} else if( tk == "nil" ) {
return verify_kind(ps, i, "keyword", "nil");
} else if( tk == "function" ) {
return parse_function_value(ps, i);
} else if( tk == "{" ) {
return parse_table_literal(ps, i);
} else if( kind == "..." ) {
return verify_kind(ps, i, "...");
} else if( kind == "$invalid_string$" ) {
return fail(ps, i, "malformed string");
} else if( kind == "$invalid_number$" ) {
return fail(ps, i, "malformed number");
}
return fail(ps, i, "syntax error");
}
var function node_is_require_call(n: Node): string {
if( n.e1 && n.e2 // literal require call
&& n.e1.kind == "variable" && n.e1.tk == "require"
&& n.e2.kind == "expression_list" && #n.e2 == 1
&& n.e2[1].kind == "string"
) {
return n.e2[1].conststr;
} else if( n.op && n.op.op == "@funcall" // pcall(require, "str")
&& n.e1 && n.e1.tk == "pcall"
&& n.e2 && #n.e2 == 2
&& n.e2[1].kind == "variable" && n.e2[1].tk == "require"
&& n.e2[2].kind == "string" && n.e2[2].conststr
) {
return n.e2[2].conststr;
} else {
return null; // table.insert cares about arity
}
}
var an_operator: function(Node, integer, string): Operator;
{
var precedences: {integer:{string:integer}} = {
[1] = {
["not"] = 11,
["#"] = 11,
["-"] = 11,
["~"] = 11,
},
[2] = {
["or"] = 1,
["and"] = 2,
["is"] = 3,
["<"] = 3,
[">"] = 3,
["<="] = 3,
[">="] = 3,
["~="] = 3,
["=="] = 3,
["|"] = 4,
["~"] = 5,
["&"] = 6,
["<<"] = 7,
[">>"] = 7,
[".."] = 8,
["+"] = 9,
["-"] = 9,
["*"] = 10,
["/"] = 10,
["//"] = 10,
["%"] = 10,
["^"] = 12,
["as"] = 50,
["@funcall"] = 100,
["@index"] = 100,
["."] = 100,
[":"] = 100,
},
};
var is_right_assoc: {string:boolean} = {
["^"] = true,
[".."] = true,
};
var function new_operator(tk: Token, arity: integer, op: string): Operator {
op = op || tk.tk;
return { y = tk.y, x = tk.x, arity = arity, op = op, prec = precedences[arity][op] };
}
an_operator = function(node: Node, arity: integer, op: string): Operator {
return { y = node.y, x = node.x, arity = arity, op = op, prec = precedences[arity][op] };
};
var args_starters: {TokenKind:boolean} = {
["("] = true,
["{"] = true,
["string"] = true,
};
var E: function(ParseState, integer, Node, integer): integer, Node;
var function after_valid_prefixexp(ps: ParseState, prevnode: Node, i: integer): boolean {
return ps.tokens[i - 1].kind == ")" // '(' exp ')'
|| (prevnode.kind == "op"
&& (prevnode.op.op == "@funcall"
|| prevnode.op.op == "@index"
|| prevnode.op.op == "."
|| prevnode.op.op == ":")
)
|| prevnode.kind == "identifier"
|| prevnode.kind == "variable";
}
// small hack: for the sake of `tl types`, parse an invalid binary exp
// as a paren to produce a unary indirection on e1 and save its location.
var function failstore(tkop: Token, e1: Node): Node {
return { y = tkop.y, x = tkop.x, kind = "paren", e1 = e1, failstore = true };
}
var function P(ps: ParseState, i: integer): integer, Node {
if( ps.tokens[i].kind == "$EOF$" ) {
return i;
}
var e1: Node;
var t1 = ps.tokens[i];
if( precedences[1][ps.tokens[i].tk] != null ) {
var op: Operator = new_operator(ps.tokens[i], 1);
i = i + 1;
var prev_i = i;
i, e1 = P(ps, i);
if( ! e1 ) {
fail(ps, prev_i, "expected an expression");
return i;
}
e1 = { y = t1.y, x = t1.x, kind = "op", op = op, e1 = e1 };
} else if( ps.tokens[i].tk == "(" ) {
i = i + 1;
var prev_i = i;
i, e1 = parse_expression_and_tk(ps, i, ")");
if( ! e1 ) {
fail(ps, prev_i, "expected an expression");
return i;
}
e1 = { y = t1.y, x = t1.x, kind = "paren", e1 = e1 };
} else {
i, e1 = parse_literal(ps, i);
}
if( ! e1 ) {
return i;
}
while( true ) {
var tkop = ps.tokens[i];
if( tkop.kind == "," || tkop.kind == ")" ) { // check most common terminators first
break;
}
if( tkop.tk == "." || tkop.tk == ":" ) {
var op: Operator = new_operator(tkop, 2);
var prev_i = i;
var key: Node;
i = i + 1;
i, key = verify_kind(ps, i, "identifier");
if( ! key ) {
return i, failstore(tkop, e1);
}
if( op.op == ":" ) {
if( ! args_starters[ps.tokens[i].kind] ) {
fail(ps, i, "expected a function call for a method");
return i, failstore(tkop, e1);
}
if( ! after_valid_prefixexp(ps, e1, prev_i) ) {
fail(ps, prev_i, "cannot call a method on this expression");
return i, failstore(tkop, e1);
}
}
e1 = { y = tkop.y, x = tkop.x, kind = "op", op = op, e1 = e1, e2 = key };
} else if( tkop.tk == "(" ) {
var op: Operator = new_operator(tkop, 2, "@funcall");
var prev_i = i;
var args = new_node(ps.tokens, i, "expression_list");
i, args = parse_bracket_list(ps, i, args, "(", ")", "sep", parse_expression);
if( ! after_valid_prefixexp(ps, e1, prev_i) ) {
fail(ps, prev_i, "cannot call this expression");
return i, failstore(tkop, e1);
}
e1 = { y = args.y, x = args.x, kind = "op", op = op, e1 = e1, e2 = args };
table.insert(ps.required_modules, node_is_require_call(e1));
} else if( tkop.tk == "[" ) {
var op: Operator = new_operator(tkop, 2, "@index");
var prev_i = i;
var idx: Node;
i = i + 1;
i, idx = parse_expression_and_tk(ps, i, "]");
if( ! after_valid_prefixexp(ps, e1, prev_i) ) {
fail(ps, prev_i, "cannot index this expression");
return i, failstore(tkop, e1);
}
e1 = { y = tkop.y, x = tkop.x, kind = "op", op = op, e1 = e1, e2 = idx };
} else if( tkop.kind == "string" || tkop.kind == "{" ) {
var op: Operator = new_operator(tkop, 2, "@funcall");
var prev_i = i;
var args = new_node(ps.tokens, i, "expression_list");
var argument: Node;
if( tkop.kind == "string" ) {
argument = new_node(ps.tokens, i);
argument.conststr = unquote(tkop.tk);
i = i + 1;
} else {
i, argument = parse_table_literal(ps, i);
}
if( ! after_valid_prefixexp(ps, e1, prev_i) ) {
if( tkop.kind == "string" ) {
fail(ps, prev_i, "cannot use a string here; if you're trying to call the previous expression, wrap it in parentheses");
} else {
fail(ps, prev_i, "cannot use a table here; if you're trying to call the previous expression, wrap it in parentheses");
}
return i, failstore(tkop, e1);
}
table.insert(args, argument);
e1 = { y = args.y, x = args.x, kind = "op", op = op, e1 = e1, e2 = args };
table.insert(ps.required_modules, node_is_require_call(e1));
} else if( tkop.tk == "as" || tkop.tk == "is" ) {
var op: Operator = new_operator(tkop, 2, tkop.tk);
i = i + 1;
var cast = new_node(ps.tokens, i, "cast");
if( ps.tokens[i].tk == "(" ) {
i, cast.casttype = parse_type_list(ps, i, "casttype");
} else {
i, cast.casttype = parse_type(ps, i);
}
if( ! cast.casttype ) {
return i, failstore(tkop, e1);
}
e1 = { y = tkop.y, x = tkop.x, kind = "op", op = op, e1 = e1, e2 = cast, conststr = e1.conststr };
} else {
break;
}
}
return i, e1;
}
E = function(ps: ParseState, i: integer, lhs: Node, min_precedence: integer): integer, Node {
var lookahead = ps.tokens[i].tk;
while( precedences[2][lookahead] && precedences[2][lookahead] >= min_precedence ) {
var t1 = ps.tokens[i];
var op: Operator = new_operator(t1, 2);
i = i + 1;
var rhs: Node;
i, rhs = P(ps, i);
if( ! rhs ) {
fail(ps, i, "expected an expression");
return i;
}
lookahead = ps.tokens[i].tk;
while( precedences[2][lookahead] && ((precedences[2][lookahead] > (precedences[2][op.op]))
|| (is_right_assoc[lookahead] && (precedences[2][lookahead] == precedences[2][op.op]))) ) {
i, rhs = E(ps, i, rhs, precedences[2][lookahead]);
if( ! rhs ) {
fail(ps, i, "expected an expression");
return i;
}
lookahead = ps.tokens[i].tk;
}
lhs = { y = t1.y, x = t1.x, kind = "op", op = op, e1 = lhs, e2 = rhs, };
}
return i, lhs;
};
parse_expression = function(ps: ParseState, i: integer): integer, Node, integer {
var lhs: Node;
var istart = i;
i, lhs = P(ps, i);
if( lhs ) {
i, lhs = E(ps, i, lhs, 0);
}
if( lhs ) {
return i, lhs, 0;
}
// if cursor moved, a more specific error was already thrown
if( i == istart ) {
i = fail(ps, i, "expected an expression");
}
return i;
};
}
parse_expression_and_tk = function(ps: ParseState, i: integer, tk: string): integer, Node {
var e: Node;
i, e = parse_expression(ps, i);
if( ! e ) {
e = new_node(ps.tokens, i - 1, "error_node");
}
if( ps.tokens[i].tk == tk ) {
i = i + 1;
} else {
// try to resync the parser for a bit
for( n = 0, 19 ) {
var t = ps.tokens[i + n];
if( t.kind == "$EOF$" ) {
break;
}
if( t.tk == tk ) {
fail(ps, i, "syntax error, expected '" .. tk .. "'");
return i + n + 1, e;
}
}
i = fail(ps, i, "syntax error, expected '" .. tk .. "'");
}
return i, e;
};
var function parse_variable_name(ps: ParseState, i: integer): integer, Node, integer {
var is_const: boolean = false;
var node: Node;
i, node = verify_kind(ps, i, "identifier");
if( ! node ) {
return i;
}
if( ps.tokens[i].tk == "<" ) {
i = i + 1;
var annotation: Node;
i, annotation = verify_kind(ps, i, "identifier");
if( annotation ) {
if( annotation.tk == "const" ) {
is_const = true;
} else {
fail(ps, i, "unknown variable annotation: " .. annotation.tk);
}
} else {
fail(ps, i, "expected a variable annotation");
}
i = verify_tk(ps, i, ">");
}
node.is_const = is_const;
return i, node;
}
var function parse_argument(ps: ParseState, i: integer): integer, Node, integer {
var node: Node;
if( ps.tokens[i].tk == "..." ) {
i, node = verify_kind(ps, i, "...", "argument");
} else {
i, node = verify_kind(ps, i, "identifier", "argument");
}
if( ps.tokens[i].tk == ":" ) {
i = i + 1;
var decltype: Type;
i, decltype = parse_type(ps, i);
if( node ) {
i, node.decltype = i, decltype;
}
}
return i, node, 0;
}
parse_argument_list = function(ps: ParseState, i: integer): integer, Node {
var node = new_node(ps.tokens, i, "argument_list");
i, node = parse_bracket_list(ps, i, node, "(", ")", "sep", parse_argument);
for( a, fnarg in ipairs(node) ) {
if( fnarg.tk == "..." && a != #node ) {
fail(ps, i, "'...' can only be last argument");
}
}
return i, node;
};
var function parse_argument_type(ps: ParseState, i: integer): integer, Type, integer {
var is_va = false;
if( ps.tokens[i].kind == "identifier" && ps.tokens[i + 1].tk == ":" ) {
i = i + 2;
} else if( ps.tokens[i].tk == "..." ) {
if( ps.tokens[i + 1].tk == ":" ) {
i = i + 2;
is_va = true;
} else {
return fail(ps, i, "cannot have untyped '...' when declaring the type of an argument");
}
}
var typ: Type; i, typ = parse_type(ps, i);
if( typ ) {
// HACK: we're storing the attribute in the wrong node during the iteration...
typ.is_va = is_va;
}
return i, typ, 0;
}
parse_argument_type_list = function(ps: ParseState, i: integer): integer, Type {
var list = new_type(ps, i, "tuple");
i = parse_bracket_list(ps, i, list, "(", ")", "sep", parse_argument_type);
// HACK: ...and then cleaning it up and setting in the right node
if( list[#list] && list[#list].is_va ) {
list[#list].is_va = null;
list.is_va = true;
}
return i, list;
};
var function parse_identifier(ps: ParseState, i: integer): integer, Node {
if( ps.tokens[i].kind == "identifier" ) {
return i + 1, new_node(ps.tokens, i, "identifier");
}
i = fail(ps, i, "syntax error, expected identifier");
return i, new_node(ps.tokens, i, "error_node");
}
var function parse_local_function(ps: ParseState, i: integer): integer, Node {
i = verify_tk(ps, i, "local");
i = verify_tk(ps, i, "function");
var node = new_node(ps.tokens, i, "local_function");
i, node.name = parse_identifier(ps, i);
return parse_function_args_rets_body(ps, i, node);
}
var function parse_global_function(ps: ParseState, i: integer): integer, Node {
var orig_i = i;
i = verify_tk(ps, i, "function");
var fn = new_node(ps.tokens, i, "global_function");
var names: {Node} = {};
i, names[1] = parse_identifier(ps, i);
while( ps.tokens[i].tk == "." ) {
i = i + 1;
i, names[#names + 1] = parse_identifier(ps, i);
}
if( ps.tokens[i].tk == ":" ) {
i = i + 1;
i, names[#names + 1] = parse_identifier(ps, i);
fn.is_method = true;
}
if( #names > 1 ) {
fn.kind = "record_function";
var owner = names[1];
owner.kind = "type_identifier";
for( i2 = 2, #names - 1 ) {
var dot = an_operator(names[i2], 2, ".");
names[i2].kind = "identifier";
owner = { y = names[i2].y, x = names[i2].x, kind = "op", op = dot, e1 = owner, e2 = names[i2] };
}
fn.fn_owner = owner;
}
fn.name = names[#names];
var selfx, selfy = ps.tokens[i].x, ps.tokens[i].y;
i = parse_function_args_rets_body(ps, i, fn);
if( fn.is_method ) {
table.insert(fn.args, 1, { x = selfx, y = selfy, tk = "self", kind = "identifier" });
}
if( ! fn.name ) {
return orig_i + 1;
}
return i, fn;
}
var function parse_if_block(ps: ParseState, i: integer, n: integer, node: Node, is_else: boolean): integer, Node {
var block = new_node(ps.tokens, i, "if_block");
i = i + 1;
block.if_parent = node;
block.if_block_n = n;
if( ! is_else ) {
i, block.exp = parse_expression_and_tk(ps, i, "then");
if( ! block.exp ) {
return i;
}
}
i, block.body = parse_statements(ps, i);
if( ! block.body ) {
return i;
}
end_at(block.body, ps.tokens[i - 1]);
block.yend, block.xend = block.body.yend, block.body.xend;
table.insert(node.if_blocks, block);
return i, node;
}
var function parse_if(ps: ParseState, i: integer): integer, Node {
var istart = i;
var node = new_node(ps.tokens, i, "if");
node.if_blocks = {};
i, node = parse_if_block(ps, i, 1, node);
if( ! node ) {
return i;
}
var n = 2;
while( ps.tokens[i].tk == "elseif" ) {
i, node = parse_if_block(ps, i, n, node);
if( ! node ) {
return i;
}
n = n + 1;
}
if( ps.tokens[i].tk == "else" ) {
i, node = parse_if_block(ps, i, n, node, true);
if( ! node ) {
return i;
}
}
i = verify_end(ps, i, istart, node);
return i, node;
}
var function parse_while(ps: ParseState, i: integer): integer, Node {
var istart = i;
var node = new_node(ps.tokens, i, "while");
i = verify_tk(ps, i, "while");
i, node.exp = parse_expression_and_tk(ps, i, "do");
i, node.body = parse_statements(ps, i);
i = verify_end(ps, i, istart, node);
return i, node;
}
var function parse_fornum(ps: ParseState, i: integer): integer, Node {
var istart = i;
var node = new_node(ps.tokens, i, "fornum");
i = i + 1;
i, node._v_var = parse_identifier(ps, i);
i = verify_tk(ps, i, "=");
i, node.from = parse_expression_and_tk(ps, i, ",");
i, node.to = parse_expression(ps, i);
if( ps.tokens[i].tk == "," ) {
i = i + 1;
i, node.step = parse_expression_and_tk(ps, i, "do");
} else {
i = verify_tk(ps, i, "do");
}
i, node.body = parse_statements(ps, i);
i = verify_end(ps, i, istart, node);
return i, node;
}
var function parse_forin(ps: ParseState, i: integer): integer, Node {
var istart = i;
var node = new_node(ps.tokens, i, "forin");
i = i + 1;
node.vars = new_node(ps.tokens, i, "variable_list");
i, node.vars = parse_list(ps, i, node.vars, { ["in"] = true }, "sep", parse_variable_name);
i = verify_tk(ps, i, "in");
node.exps = new_node(ps.tokens, i, "expression_list");
i = parse_list(ps, i, node.exps, { ["do"] = true }, "sep", parse_expression);
if( #node.exps < 1 ) {
return fail(ps, i, "missing iterator expression in generic for");
} else if( #node.exps > 3 ) {
return fail(ps, i, "too many expressions in generic for");
}
i = verify_tk(ps, i, "do");
i, node.body = parse_statements(ps, i);
i = verify_end(ps, i, istart, node);
return i, node;
}
var function parse_for(ps: ParseState, i: integer): integer, Node {
if( ps.tokens[i+1].kind == "identifier" && ps.tokens[i+2].tk == "=" ) {
return parse_fornum(ps, i);
} else {
return parse_forin(ps, i);
}
}
var function parse_repeat(ps: ParseState, i: integer): integer, Node {
var node = new_node(ps.tokens, i, "repeat");
i = verify_tk(ps, i, "repeat");
i, node.body = parse_statements(ps, i);
node.body.is_repeat = true;
i = verify_tk(ps, i, "until");
i, node.exp = parse_expression(ps, i);
end_at(node, ps.tokens[i - 1]);
return i, node;
}
var function parse_do(ps: ParseState, i: integer): integer, Node {
var istart = i;
var node = new_node(ps.tokens, i, "do");
i = verify_tk(ps, i, "do");
i, node.body = parse_statements(ps, i);
i = verify_end(ps, i, istart, node);
return i, node;
}
var function parse_break(ps: ParseState, i: integer): integer, Node {
var node = new_node(ps.tokens, i, "break");
i = verify_tk(ps, i, "break");
return i, node;
}
var function parse_goto(ps: ParseState, i: integer): integer, Node {
var node = new_node(ps.tokens, i, "goto");
i = verify_tk(ps, i, "goto");
node.label = ps.tokens[i].tk;
i = verify_kind(ps, i, "identifier");
return i, node;
}
var function parse_label(ps: ParseState, i: integer): integer, Node {
var node = new_node(ps.tokens, i, "label");
i = verify_tk(ps, i, "::");
node.label = ps.tokens[i].tk;
i = verify_kind(ps, i, "identifier");
i = verify_tk(ps, i, "::");
return i, node;
}
var stop_statement_list: {string:boolean} = {
["end"] = true,
["else"] = true,
["elseif"] = true,
["until"] = true,
};
var stop_return_list: {string:boolean} = {
[";"] = true,
["$EOF$"] = true,
};
for( k, v in pairs(stop_statement_list) ) {
stop_return_list[k] = v;
}
var function parse_return(ps: ParseState, i: integer): integer, Node {
var node = new_node(ps.tokens, i, "return");
i = verify_tk(ps, i, "return");
node.exps = new_node(ps.tokens, i, "expression_list");
i = parse_list(ps, i, node.exps, stop_return_list, "sep", parse_expression);
if( ps.tokens[i].kind == ";" ) {
i = i + 1;
}
return i, node;
}
var function store_field_in_record(ps: ParseState, i: integer, field_name: string, t: Type, fields: {string: Type}, field_order: {string}): boolean {
if( ! fields[field_name] ) {
fields[field_name] = t;
table.insert(field_order, field_name);
} else {
var prev_t = fields[field_name];
if( t.typename == "function" && prev_t.typename == "function" ) {
fields[field_name] = new_type(ps, i, "poly");
fields[field_name].types = { prev_t, t };
} else if( t.typename == "function" && prev_t.typename == "poly" ) {
table.insert(prev_t.types, t);
} else {
fail(ps, i, "attempt to redeclare field '" .. field_name .. "' (only functions can be overloaded)");
return false;
}
}
return true;
}
var type; ParseBody = function(ps: ParseState, i: integer, def: Type, node: Node): integer, Node {
var function parse_nested_type(ps: ParseState, i: integer, def: Type, typename: TypeName, parse_body: ParseBody): integer, boolean {
i = i + 1; // skip 'record' or 'enum'
var v: Node;
i, v = verify_kind(ps, i, "identifier", "type_identifier");
if( ! v ) {
return fail(ps, i, "expected a variable name");
}
var nt: Node = new_node(ps.tokens, i, "newtype");
nt.newtype = new_type(ps, i, "typetype");
var rdef = new_type(ps, i, typename);
var iok = parse_body(ps, i, rdef, nt);
if( iok ) {
i = iok;
nt.newtype.def = rdef;
}
store_field_in_record(ps, i, v.tk, nt.newtype, def.fields, def.field_order);
return i;
}
parse_enum_body = function(ps: ParseState, i: integer, def: Type, node: Node): integer, Node {
var istart = i - 1;
def.enumset = {};
while( ps.tokens[i].tk != "$EOF$" && ps.tokens[i].tk != "end" ) {
var item: Node;
i, item = verify_kind(ps, i, "string", "enum_item");
if( item ) {
table.insert(node, item);
def.enumset[unquote(item.tk)] = true;
}
}
i = verify_end(ps, i, istart, node);
return i, node;
};
var metamethod_names: {string:boolean} = {
["__add"] = true,
["__sub"] = true,
["__mul"] = true,
["__div"] = true,
["__mod"] = true,
["__pow"] = true,
["__unm"] = true,
["__idiv"] = true,
["__band"] = true,
["__bor"] = true,
["__bxor"] = true,
["__bnot"] = true,
["__shl"] = true,
["__shr"] = true,
["__concat"] = true,
["__len"] = true,
["__eq"] = true,
["__lt"] = true,
["__le"] = true,
["__index"] = true,
["__newindex"] = true,
["__call"] = true,
["__tostring"] = true,
["__pairs"] = true,
["__gc"] = true,
};
parse_record_body = function(ps: ParseState, i: integer, def: Type, node: Node): integer, Node {
var istart = i - 1;
def.fields = {};
def.field_order = {};
if( ps.tokens[i].tk == "<" ) {
i, def.typeargs = parse_typearg_list(ps, i);
}
while( ! (ps.tokens[i].kind == "$EOF$" || ps.tokens[i].tk == "end") ) {
if( ps.tokens[i].tk == "userdata" && ps.tokens[i+1].tk != ":" ) {
if( def.is_userdata ) {
fail(ps, i, "duplicated 'userdata' declaration in record");
} else {
def.is_userdata = true;
}
i = i + 1;
} else if( ps.tokens[i].tk == "{" ) {
if( def.typename == "arrayrecord" ) {
i = failskip(ps, i, "duplicated declaration of array element type in record", parse_type);
} else {
i = i + 1;
var t: Type;
i, t = parse_type(ps, i);
if( ps.tokens[i].tk == "}" ) {
i = verify_tk(ps, i, "}");
} else {
return fail(ps, i, "expected an array declaration");
}
def.typename = "arrayrecord";
def.elements = t;
}
} else if( ps.tokens[i].tk == "type" && ps.tokens[i + 1].tk != ":" ) {
i = i + 1;
var iv = i;
var v: Node;
i, v = verify_kind(ps, i, "identifier", "type_identifier");
if( ! v ) {
return fail(ps, i, "expected a variable name");
}
i = verify_tk(ps, i, "=");
var nt: Node;
i, nt = parse_newtype(ps, i);
if( ! nt || ! nt.newtype ) {
return fail(ps, i, "expected a type definition");
}
store_field_in_record(ps, iv, v.tk, nt.newtype, def.fields, def.field_order);
} else if( ps.tokens[i].tk == "record" && ps.tokens[i+1].tk != ":" ) {
i = parse_nested_type(ps, i, def, "record", parse_record_body);
} else if( ps.tokens[i].tk == "enum" && ps.tokens[i+1].tk != ":" ) {
i = parse_nested_type(ps, i, def, "enum", parse_enum_body);
} else {
var is_metamethod = false;
if( ps.tokens[i].tk == "metamethod" && ps.tokens[i+1].tk != ":" ) {
is_metamethod = true;
i = i + 1;
}
var v: Node;
if( ps.tokens[i].tk == "[" ) {
i, v = parse_literal(ps, i+1);
if( v && ! v.conststr ) {
return fail(ps, i, "expected a string literal");
}
i = verify_tk(ps, i, "]");
} else {
i, v = verify_kind(ps, i, "identifier", "variable");
}
var iv = i;
if( ! v ) {
return fail(ps, i, "expected a variable name");
}
if( ps.tokens[i].tk == ":" ) {
i = i + 1;
var t: Type;
i, t = parse_type(ps, i);
if( ! t ) {
return fail(ps, i, "expected a type");
}
var field_name = v.conststr || v.tk;
var fields = def.fields;
var field_order = def.field_order;
if( is_metamethod ) {
if( ! def.meta_fields ) {
def.meta_fields = {};
def.meta_field_order = {};
}
fields = def.meta_fields;
field_order = def.meta_field_order;
if( ! metamethod_names[field_name] ) {
fail(ps, i - 1, "not a valid metamethod: " .. field_name);
}
}
store_field_in_record(ps, iv, field_name, t, fields, field_order);
} else if( ps.tokens[i].tk == "=" ) {
var next_word = ps.tokens[i + 1].tk;
if( next_word == "record" || next_word == "enum" ) {
return fail(ps, i, "syntax error: this syntax is no longer valid; use '" .. next_word .. " " .. v.tk .. "'");
} else if( next_word == "functiontype" ) {
return fail(ps, i, "syntax error: this syntax is no longer valid; use 'type " .. v.tk .. " = function('...");
} else {
return fail(ps, i, "syntax error: this syntax is no longer valid; use 'type " .. v.tk .. " = '...");
}
} else {
fail(ps, i, "syntax error: expected ':' for an attribute or '=' for a nested type");
}
}
}
i = verify_end(ps, i, istart, node);
return i, node;
};
parse_newtype = function(ps: ParseState, i: integer): integer, Node {
var node: Node = new_node(ps.tokens, i, "newtype");
node.newtype = new_type(ps, i, "typetype");
if( ps.tokens[i].tk == "record" ) {
var def = new_type(ps, i, "record");
i = i + 1;
i = parse_record_body(ps, i, def, node);
node.newtype.def = def;
return i, node;
} else if( ps.tokens[i].tk == "enum" ) {
var def = new_type(ps, i, "enum");
i = i + 1;
i = parse_enum_body(ps, i, def, node);
node.newtype.def = def;
return i, node;
} else {
i, node.newtype.def = parse_type(ps, i);
if( ! node.newtype.def ) {
return i;
}
return i, node;
}
return fail(ps, i, "expected a type");
};
var function parse_assignment_expression_list(ps: ParseState, i: integer, asgn: Node): integer, Node {
asgn.exps = new_node(ps.tokens, i, "expression_list");
do {
i = i + 1;
var val: Node;
i, val = parse_expression(ps, i);
if( ! val ) {
if( #asgn.exps == 0 ) {
asgn.exps = null;
}
return i;
}
table.insert(asgn.exps, val);
} while(!( ps.tokens[i].tk != "," ));
return i, asgn;
}
var parse_call_or_assignment: function(ps: ParseState, i: integer): integer, Node;
{
var function is_lvalue(node: Node): boolean {
return node.kind == "variable"
|| (node.kind == "op" && (node.op.op == "@index" || node.op.op == "."));
}
var function parse_variable(ps: ParseState, i: integer): integer, Node, integer {
var node: Node;
i, node = parse_expression(ps, i);
if( ! (node && is_lvalue(node)) ) {
return fail(ps, i, "expected a variable");
}
return i, node;
}
parse_call_or_assignment = function(ps: ParseState, i: integer): integer, Node {
var exp: Node;
var istart = i;
i, exp = parse_expression(ps, i);
if( ! exp ) {
return i;
}
if( (exp.op && exp.op.op == "@funcall") || exp.failstore ) {
return i, exp;
}
if( ! is_lvalue(exp) ) {
return fail(ps, i, "syntax error");
}
var asgn: Node = new_node(ps.tokens, istart, "assignment");
asgn.vars = new_node(ps.tokens, istart, "variable_list");
asgn.vars[1] = exp;
if( ps.tokens[i].tk == "," ) {
i = i + 1;
i = parse_trying_list(ps, i, asgn.vars, parse_variable);
if( #asgn.vars < 2 ) {
return fail(ps, i, "syntax error");
}
}
if( ps.tokens[i].tk != "=" ) {
verify_tk(ps, i, "=");
return i;
}
i, asgn = parse_assignment_expression_list(ps, i, asgn);
return i, asgn;
};
}
var function parse_variable_declarations(ps: ParseState, i: integer, node_name: NodeKind): integer, Node {
var asgn: Node = new_node(ps.tokens, i, node_name);
asgn.vars = new_node(ps.tokens, i, "variable_list");
i = parse_trying_list(ps, i, asgn.vars, parse_variable_name);
if( #asgn.vars == 0 ) {
return fail(ps, i, "expected a local variable definition");
}
i, asgn.decltype = parse_type_list(ps, i, "decltype");
if( ps.tokens[i].tk == "=" ) {
// produce nice error message when using <= 0.7.1 syntax
var next_word = ps.tokens[i + 1].tk;
if( next_word == "record" ) {
var scope = node_name == "local_declaration" && "local" || "global";
return failskip(ps, i + 1, "syntax error: this syntax is no longer valid; use '" .. scope .. " record " .. asgn.vars[1].tk .. "'", skip_record);
} else if( next_word == "enum" ) {
var scope = node_name == "local_declaration" && "local" || "global";
return failskip(ps, i + 1, "syntax error: this syntax is no longer valid; use '" .. scope .. " enum " .. asgn.vars[1].tk .. "'", skip_enum);
} else if( next_word == "functiontype" ) {
var scope = node_name == "local_declaration" && "local" || "global";
return failskip(ps, i + 1, "syntax error: this syntax is no longer valid; use '" .. scope .. " type " .. asgn.vars[1].tk .. " = function('...", parse_function_type);
}
i, asgn = parse_assignment_expression_list(ps, i, asgn);
}
return i, asgn;
}
var function parse_type_declaration(ps: ParseState, i: integer, node_name: NodeKind): integer, Node {
i = i + 2; // skip `local` or `global`, and `type`
var asgn: Node = new_node(ps.tokens, i, node_name);
i, asgn._v_var = parse_variable_name(ps, i);
if( ! asgn._v_var ) {
return fail(ps, i, "expected a type name");
}
i = verify_tk(ps, i, "=");
i, asgn.value = parse_newtype(ps, i);
if( ! asgn.value ) {
return i;
}
if( ! asgn.value.newtype.def.names ) {
asgn.value.newtype.def.names = { asgn._v_var.tk };
}
return i, asgn;
}
var function parse_type_constructor(ps: ParseState, i: integer, node_name: NodeKind, type_name: TypeName, parse_body: ParseBody): integer, Node {
var asgn: Node = new_node(ps.tokens, i, node_name);
var nt: Node = new_node(ps.tokens, i, "newtype");
asgn.value = nt;
nt.newtype = new_type(ps, i, "typetype");
var def = new_type(ps, i, type_name);
nt.newtype.def = def;
i = i + 2; // skip `local` or `global`, and the constructor name
i, asgn._v_var = verify_kind(ps, i, "identifier");
if( ! asgn._v_var ) {
return fail(ps, i, "expected a type name");
}
nt.newtype.def.names = { asgn._v_var.tk };
i = parse_body(ps, i, def, nt);
return i, asgn;
}
var function skip_type_declaration(ps: ParseState, i: integer): integer {
return (parse_type_declaration(ps, i - 1, "local_type"));
}
var function parse_local(ps: ParseState, i: integer): integer, Node {
var ntk = ps.tokens[i + 1].tk;
if( ntk == "function" ) {
return parse_local_function(ps, i);
} else if( ntk == "type" && ps.tokens[i+2].kind == "identifier" ) {
return parse_type_declaration(ps, i, "local_type");
} else if( ntk == "record" && ps.tokens[i+2].kind == "identifier" ) {
return parse_type_constructor(ps, i, "local_type", "record", parse_record_body);
} else if( ntk == "enum" && ps.tokens[i+2].kind == "identifier" ) {
return parse_type_constructor(ps, i, "local_type", "enum", parse_enum_body);
}
return parse_variable_declarations(ps, i + 1, "local_declaration");
}
var function parse_global(ps: ParseState, i: integer): integer, Node {
var ntk = ps.tokens[i + 1].tk;
if( ntk == "function" ) {
return parse_global_function(ps, i + 1);
} else if( ntk == "type" && ps.tokens[i+2].kind == "identifier" ) {
return parse_type_declaration(ps, i, "global_type");
} else if( ntk == "record" && ps.tokens[i+2].kind == "identifier" ) {
return parse_type_constructor(ps, i, "global_type", "record", parse_record_body);
} else if( ntk == "enum" && ps.tokens[i+2].kind == "identifier" ) {
return parse_type_constructor(ps, i, "global_type", "enum", parse_enum_body);
} else if( ps.tokens[i+1].kind == "identifier" ) {
return parse_variable_declarations(ps, i + 1, "global_declaration");
}
return parse_call_or_assignment(ps, i); // global is a soft keyword
}
var function parse_type_statement(ps: ParseState, i: integer): integer, Node {
if( ps.tokens[i + 1].kind == "identifier" ) {
return failskip(ps, i, "types need to be declared with 'local type' or 'global type'", skip_type_declaration);
}
return parse_call_or_assignment(ps, i);
}
var parse_statement_fns: {string : function(ParseState, integer):(integer, Node)} = {
["::"] = parse_label,
["do"] = parse_do,
["if"] = parse_if,
["for"] = parse_for,
["goto"] = parse_goto,
["type"] = parse_type_statement,
["local"] = parse_local,
["while"] = parse_while,
["break"] = parse_break,
["global"] = parse_global,
["repeat"] = parse_repeat,
["return"] = parse_return,
["function"] = parse_global_function,
};
parse_statements = function(ps: ParseState, i: integer, toplevel: boolean): integer, Node {
var node = new_node(ps.tokens, i, "statements");
var item: Node;
while( true ) {
while( ps.tokens[i].kind == ";" ) {
i = i + 1;
if( item ) {
item.semicolon = true;
}
}
if( ps.tokens[i].kind == "$EOF$" ) {
break;
}
if( (! toplevel) && stop_statement_list[ps.tokens[i].tk] ) {
break;
}
// local prev_i = i
var parse_statement_fn = parse_statement_fns[ps.tokens[i].tk] || parse_call_or_assignment;
i, item = parse_statement_fn(ps, i);
// assert(i > prev_i)
if( item ) {
table.insert(node, item);
} else if( i > 1 ) {
// heuristic to resync at the next line after an invalid statement
var lasty = ps.tokens[i - 1].y;
while( ps.tokens[i].kind != "$EOF$" && ps.tokens[i].y == lasty ) {
i = i + 1;
}
}
}
end_at(node, ps.tokens[i]);
return i, node;
};
var function clear_redundant_errors(errors: {Error}) {
var redundant: {integer} = {};
var lastx, lasty = 0, 0;
for( i, err in ipairs(errors) ) {
err.i = i;
}
table.sort(errors, function(a: Error, b: Error): boolean {
var af = a.filename || "";
var bf = b.filename || "";
return af < bf
|| (af == bf && (a.y < b.y
|| (a.y == b.y && (a.x < b.x
|| (a.x == b.x && (a.i < b.i))))));
});
for( i, err in ipairs(errors) ) {
err.i = null;
if( err.x == lastx && err.y == lasty ) {
table.insert(redundant, i);
}
lastx, lasty = err.x, err.y;
}
for( i = #redundant, 1, -1 ) {
table.remove(errors, redundant[i]);
}
}
function tl.parse_program(tokens: {Token}, errs: {Error}, filename: string): integer, Node, {string} {
errs = errs || {};
var ps: ParseState = {
tokens = tokens,
errs = errs,
filename = filename || "",
required_modules = {},
};
var i, node = parse_statements(ps, 1, true);
clear_redundant_errors(errs);
return i, node, ps.required_modules;
}
//------------------------------------------------------------------------------
// AST traversal
//------------------------------------------------------------------------------
var record VisitorCallbacks<N, T> {
before: function(N, {T});
before_expressions: function({N}, {T});
before_statements: function({N}, {T});
before_e2: function({N}, {T});
after: function(N, {T}, T): T;
}
var enum VisitorExtraCallback {
"before_statements",
"before_expressions",
"before_e2",
}
var record Visitor<K, N, T> {
cbs: {K:VisitorCallbacks<N, T>};
after: function(N, {T}, T): T;
allow_missing_cbs: boolean;
}
var enum MetaMode {
"meta",
}
var function fields_of(t: Type, meta: MetaMode): (function(): string, Type) {
var i = 1;
var field_order = meta && t.meta_field_order || t.field_order;
var fields = meta && t.meta_fields || t.fields;
return function(): string, Type {
var name = field_order[i];
if( ! name ) {
return null;
}
i = i + 1;
return name, fields[name];
};
}
var function recurse_type<T>(ast: Type, visit: Visitor<TypeName, Type, T>): T {
var kind = ast.typename;
var cbs = visit.cbs;
var cbkind = cbs && cbs[kind];
{ // before
if( cbkind ) {
var cbkind_before = cbkind.before;
if( cbkind_before ) {
cbkind_before(ast);
}
} else {
if( cbs ) {
error("internal compiler error: no visitor for " .. kind as string);
}
}
}
var xs: {T} = {};
if( ast.typeargs ) {
for( _, child in ipairs(ast.typeargs) ) {
table.insert(xs, recurse_type(child, visit));
}
}
for( i, child in ipairs(ast) ) {
xs[i] = recurse_type(child, visit);
}
if( ast.types ) {
for( _, child in ipairs(ast.types) ) {
table.insert(xs, recurse_type(child, visit));
}
}
if( ast.def ) {
table.insert(xs, recurse_type(ast.def, visit));
}
if( ast.keys ) {
table.insert(xs, recurse_type(ast.keys, visit));
}
if( ast.values ) {
table.insert(xs, recurse_type(ast.values, visit));
}
if( ast.elements ) {
table.insert(xs, recurse_type(ast.elements, visit));
}
if( ast.fields ) {
for( _, child in fields_of(ast) ) {
table.insert(xs, recurse_type(child, visit));
}
}
if( ast.meta_fields ) {
for( _, child in fields_of(ast, "meta") ) {
table.insert(xs, recurse_type(child, visit));
}
}
if( ast.args ) {
for( i, child in ipairs(ast.args) ) {
if( i > 1 || ! ast.is_method ) {
table.insert(xs, recurse_type(child, visit));
}
}
}
if( ast.rets ) {
for( _, child in ipairs(ast.rets) ) {
table.insert(xs, recurse_type(child, visit));
}
}
if( ast.typevals ) {
for( _, child in ipairs(ast.typevals) ) {
table.insert(xs, recurse_type(child, visit));
}
}
if( ast.ktype ) {
table.insert(xs, recurse_type(ast.ktype, visit));
}
if( ast.vtype ) {
table.insert(xs, recurse_type(ast.vtype, visit));
}
var ret: T;
{ // after
var cbkind_after = cbkind && cbkind.after;
if( cbkind_after ) {
ret = cbkind_after(ast, xs);
}
var visit_after = visit.after;
if( visit_after ) {
ret = visit_after(ast, xs, ret);
}
}
return ret;
}
var function recurse_typeargs<T>(ast: Node, visit_type: Visitor<TypeName, Type, T>) {
if( ast.typeargs ) {
for( _, typearg in ipairs(ast.typeargs) ) {
recurse_type(typearg, visit_type);
}
}
}
var function extra_callback<T>(name: VisitorExtraCallback,
ast: Node,
xs: {T},
visit_node: Visitor<NodeKind, Node, T>) {
var cbs = visit_node.cbs;
if( ! cbs ) { return; }
var nbs = cbs[ast.kind];
if( ! nbs ) { return; }
var bs = nbs[name];
if( ! bs ) { return; }
bs(ast, xs);
}
var no_recurse_node: {NodeKind : boolean} = {
["..."] = true,
["nil"] = true,
["cast"] = true,
["goto"] = true,
["break"] = true,
["label"] = true,
["number"] = true,
["string"] = true,
["boolean"] = true,
["integer"] = true,
["variable"] = true,
["error_node"] = true,
["identifier"] = true,
["type_identifier"] = true,
};
var function recurse_node<T>(root: Node,
visit_node: Visitor<NodeKind, Node, T>,
visit_type: Visitor<TypeName, Type, T>): T {
if( ! root ) {
// parse error
return;
}
var recurse: function(Node): T;
var function walk_children(ast: Node, xs: {T}) {
for( i, child in ipairs(ast) ) {
xs[i] = recurse(child);
}
}
var function walk_vars_exps(ast: Node, xs: {T}) {
xs[1] = recurse(ast.vars);
if( ast.decltype ) {
xs[2] = recurse_type(ast.decltype, visit_type);
}
extra_callback("before_expressions", ast, xs, visit_node);
if( ast.exps ) {
xs[3] = recurse(ast.exps);
}
}
var function walk_var_value(ast: Node, xs: {T}) {
xs[1] = recurse(ast._v_var);
xs[2] = recurse(ast.value);
}
var function walk_named_function(ast: Node, xs: {T}) {
recurse_typeargs(ast, visit_type);
xs[1] = recurse(ast.name);
xs[2] = recurse(ast.args);
xs[3] = recurse_type(ast.rets, visit_type);
extra_callback("before_statements", ast, xs, visit_node);
xs[4] = recurse(ast.body);
}
var walkers: {NodeKind : function(Node, {T})} = {
["op"] = function(ast: Node, xs: {T}) {
xs[1] = recurse(ast.e1);
var p1 = ast.e1.op && ast.e1.op.prec || null;
if( ast.op.op == ":" && ast.e1.kind == "string" ) {
p1 = -999;
}
xs[2] = p1 as T;
if( ast.op.arity == 2 ) {
extra_callback("before_e2", ast, xs, visit_node);
if( ast.op.op == "is" || ast.op.op == "as" ) {
xs[3] = recurse_type(ast.e2.casttype, visit_type);
} else {
xs[3] = recurse(ast.e2);
}
xs[4] = (ast.e2.op && ast.e2.op.prec) as T;
}
},
["statements"] = walk_children,
["argument_list"] = walk_children,
["table_literal"] = walk_children,
["variable_list"] = walk_children,
["expression_list"] = walk_children,
["table_item"] = function(ast: Node, xs: {T}) {
xs[1] = recurse(ast.key);
xs[2] = recurse(ast.value);
if( ast.decltype ) {
xs[3] = recurse_type(ast.decltype, visit_type);
}
},
["assignment"] = walk_vars_exps,
["local_declaration"] = walk_vars_exps,
["global_declaration"] = walk_vars_exps,
["local_type"] = walk_var_value,
["global_type"] = walk_var_value,
["if"] = function(ast: Node, xs: {T}) {
for( _, e in ipairs(ast.if_blocks) ) {
table.insert(xs, recurse(e));
}
},
["if_block"] = function(ast: Node, xs: {T}) {
if( ast.exp ) {
xs[1] = recurse(ast.exp);
}
extra_callback("before_statements", ast, xs, visit_node);
xs[2] = recurse(ast.body);
},
["while"] = function(ast: Node, xs: {T}) {
xs[1] = recurse(ast.exp);
extra_callback("before_statements", ast, xs, visit_node);
xs[2] = recurse(ast.body);
},
["repeat"] = function(ast: Node, xs: {T}) {
xs[1] = recurse(ast.body);
xs[2] = recurse(ast.exp);
},
["function"] = function(ast: Node, xs: {T}) {
recurse_typeargs(ast, visit_type);
xs[1] = recurse(ast.args);
xs[2] = recurse_type(ast.rets, visit_type);
extra_callback("before_statements", ast, xs, visit_node);
xs[3] = recurse(ast.body);
},
["local_function"] = walk_named_function,
["global_function"] = walk_named_function,
["record_function"] = function(ast: Node, xs: {T}) {
recurse_typeargs(ast, visit_type);
xs[1] = recurse(ast.fn_owner);
xs[2] = recurse(ast.name);
xs[3] = recurse(ast.args);
xs[4] = recurse_type(ast.rets, visit_type);
extra_callback("before_statements", ast, xs, visit_node);
xs[5] = recurse(ast.body);
},
["forin"] = function(ast: Node, xs: {T}) {
xs[1] = recurse(ast.vars);
xs[2] = recurse(ast.exps);
extra_callback("before_statements", ast, xs, visit_node);
xs[3] = recurse(ast.body);
},
["fornum"] = function(ast: Node, xs: {T}) {
xs[1] = recurse(ast._v_var);
xs[2] = recurse(ast.from);
xs[3] = recurse(ast.to);
xs[4] = ast.step && recurse(ast.step);
extra_callback("before_statements", ast, xs, visit_node);
xs[5] = recurse(ast.body);
},
["return"] = function(ast: Node, xs: {T}) {
xs[1] = recurse(ast.exps);
},
["do"] = function(ast: Node, xs: {T}) {
xs[1] = recurse(ast.body);
},
["paren"] = function(ast: Node, xs:{T}) {
xs[1] = recurse(ast.e1);
},
["newtype"] = function(ast: Node, xs:{T}) {
xs[1] = recurse_type(ast.newtype, visit_type);
},
["argument"] = function(ast: Node, xs:{T}) {
if( ast.decltype ) {
xs[1] = recurse_type(ast.decltype, visit_type);
}
},
};
if( ! visit_node.allow_missing_cbs && ! visit_node.cbs ) {
error("missing cbs in visit_node");
}
var visit_after = visit_node.after;
recurse = function(ast: Node): T {
var xs: {T} = {};
var kind = assert(ast.kind);
var cbs = visit_node.cbs;
var cbkind = cbs && cbs[kind];
{ // before
if( cbkind ) {
if( cbkind.before ) {
cbkind.before(ast);
}
} else {
if( cbs ) {
error("internal compiler error: no visitor for " .. kind as string);
}
}
}
var fn = walkers[kind];
if( fn ) {
fn(ast, xs);
} else {
assert(no_recurse_node[kind]);
}
var ret: T;
{ // after
var cbkind_after = cbkind && cbkind.after;
if( cbkind_after ) {
ret = cbkind_after(ast, xs);
}
if( visit_after ) {
ret = visit_after(ast, xs, ret);
}
}
return ret;
};
return recurse(root);
}
//------------------------------------------------------------------------------
// Pretty-print AST
//------------------------------------------------------------------------------
var tight_op: {integer:{string:boolean}} = {
[1] = {
["-"] = true,
["~"] = true,
["#"] = true,
},
[2] = {
["."] = true,
[":"] = true,
},
};
var spaced_op: {integer:{string:boolean}} = {
[1] = {
["not"] = true,
},
[2] = {
["or"] = true,
["and"] = true,
["<"] = true,
[">"] = true,
["<="] = true,
[">="] = true,
["~="] = true,
["=="] = true,
["|"] = true,
["~"] = true,
["&"] = true,
["<<"] = true,
[">>"] = true,
[".."] = true,
["+"] = true,
["-"] = true,
["*"] = true,
["/"] = true,
["//"] = true,
["%"] = true,
["^"] = true,
},
};
var record PrettyPrintOpts {
preserve_indent: boolean;
preserve_newlines: boolean;
}
var default_pretty_print_ast_opts: PrettyPrintOpts = {
preserve_indent = true,
preserve_newlines = true,
};
var fast_pretty_print_ast_opts: PrettyPrintOpts = {
preserve_indent = false,
preserve_newlines = true,
};
var primitive: {TypeName:string} = {
["function"] = "function",
["enum"] = "string",
["boolean"] = "boolean",
["string"] = "string",
["nil"] = "nil",
["number"] = "number",
["integer"] = "number",
["thread"] = "thread",
};
function tl.pretty_print_ast(ast: Node, mode: boolean | PrettyPrintOpts): string {
var indent = 0;
var opts: PrettyPrintOpts;
if( mode is PrettyPrintOpts ) {
opts = mode;
} else if( mode == true ) {
opts = fast_pretty_print_ast_opts;
} else {
opts = default_pretty_print_ast_opts;
}
var record Output {
{string};
y: integer;
h: integer;
}
var save_indent: {integer} = {};
var function increment_indent(node: Node) {
var child = node.body || node[1];
if( ! child ) {
return;
}
if( child.y != node.y ) {
if( indent == 0 && #save_indent > 0 ) {
indent = save_indent[#save_indent] + 1;
} else {
indent = indent + 1;
}
} else {
table.insert(save_indent, indent);
indent = 0;
}
}
var function decrement_indent(node: Node, child: Node) {
if( child.y != node.y ) {
indent = indent - 1;
} else {
indent = table.remove(save_indent);
}
}
if( ! opts.preserve_indent ) {
increment_indent = null;
decrement_indent = function() { };
}
var function add_string(out: Output, s: string) {
table.insert(out, s);
if( string.find(s, "\n", 1, true) ) {
for( _nl in s->gmatch("\n") ) {
out.h = out.h + 1;
}
}
}
var function add_child(out: Output, child: Output, space: string, current_indent: integer): integer {
if( #child == 0 ) {
return;
}
if( child.y != -1 && child.y < out.y ) {
out.y = child.y;
}
if( child.y > out.y + out.h && opts.preserve_newlines ) {
var delta = child.y - (out.y + out.h);
out.h = out.h + delta;
table.insert(out, ("\n")->rep(delta));
} else {
if( space ) {
if( space != "" ) {
table.insert(out, space);
}
current_indent = null;
}
}
if( current_indent && opts.preserve_indent ) {
table.insert(out, (" ")->rep(current_indent));
}
table.insert(out, child as string);
out.h = out.h + child.h;
}
var function concat_output(out: Output): string {
for( i, s in ipairs(out) ) {
if( type(s) == "table" ) {
out[i] = concat_output(s as Output);
}
}
return table.concat(out);
}
var function print_record_def(typ: Type): string {
var out: {string} = { "{" };
for( _, name in ipairs(typ.field_order) ) {
if( is_typetype(typ.fields[name]) && is_record_type(typ.fields[name].def) ) {
table.insert(out, name);
table.insert(out, " = ");
table.insert(out, print_record_def(typ.fields[name].def));
table.insert(out, ", ");
}
}
table.insert(out, "}");
return table.concat(out);
}
var visit_node: Visitor<NodeKind, Node, Output> = {};
visit_node.cbs = {
["statements"] = {
after = function(node: Node, children: {Output}): Output {
var out: Output = { y = node.y, h = 0 };
var space: string;
for( i, child in ipairs(children) ) {
add_child(out, child, space, indent);
if( node[i].semicolon ) {
table.insert(out, ";");
space = " ";
} else {
space = "; ";
}
}
return out;
}
},
["local_declaration"] = {
after = function(node: Node, children: {Output}): Output {
var out: Output = { y = node.y, h = 0 };
table.insert(out, "local");
add_child(out, children[1], " ");
if( children[3] ) {
table.insert(out, " =");
add_child(out, children[3], " ");
}
return out;
},
},
["local_type"] = {
after = function(node: Node, children: {Output}): Output {
var out: Output = { y = node.y, h = 0 };
table.insert(out, "local");
add_child(out, children[1], " ");
table.insert(out, " =");
add_child(out, children[2], " ");
return out;
},
},
["global_type"] = {
after = function(node: Node, children: {Output}): Output {
var out: Output = { y = node.y, h = 0 };
add_child(out, children[1], " ");
table.insert(out, " =");
add_child(out, children[2], " ");
return out;
},
},
["global_declaration"] = {
after = function(node: Node, children: {Output}): Output {
var out: Output = { y = node.y, h = 0 };
if( children[3] ) {
add_child(out, children[1]);
table.insert(out, " =");
add_child(out, children[3], " ");
}
return out;
},
},
["assignment"] = {
after = function(node: Node, children: {Output}): Output {
var out: Output = { y = node.y, h = 0 };
add_child(out, children[1]);
table.insert(out, " =");
add_child(out, children[3], " ");
return out;
},
},
["if"] = {
after = function(node: Node, children: {Output}): Output {
var out: Output = { y = node.y, h = 0 };
for( i, child in ipairs(children) ) {
add_child(out, child, i > 1 && " ", child.y != node.y && indent);
}
add_child(out, { y = node.yend, h = 0, [1] = "end" }, " ", indent);
return out;
},
},
["if_block"] = {
before = increment_indent,
after = function(node: Node, children: {Output}): Output {
var out: Output = { y = node.y, h = 0 };
if( node.if_block_n == 1 ) {
table.insert(out, "if");
} else if( ! node.exp ) {
table.insert(out, "else");
} else {
table.insert(out, "elseif");
}
if( node.exp ) {
add_child(out, children[1], " ");
table.insert(out, " then");
}
add_child(out, children[2], " ");
decrement_indent(node, node.body);
return out;
},
},
["while"] = {
before = increment_indent,
after = function(node: Node, children: {Output}): Output {
var out: Output = { y = node.y, h = 0 };
table.insert(out, "while");
add_child(out, children[1], " ");
table.insert(out, " do");
add_child(out, children[2], " ");
decrement_indent(node, node.body);
add_child(out, { y = node.yend, h = 0, [1] = "end" }, " ", indent);
return out;
},
},
["repeat"] = {
before = increment_indent,
after = function(node: Node, children: {Output}): Output {
var out: Output = { y = node.y, h = 0 };
table.insert(out, "repeat");
add_child(out, children[1], " ");
decrement_indent(node, node.body);
add_child(out, { y = node.yend, h = 0, [1] = "until " }, " ", indent);
add_child(out, children[2]);
return out;
},
},
["do"] = {
before = increment_indent,
after = function(node: Node, children: {Output}): Output {
var out: Output = { y = node.y, h = 0 };
table.insert(out, "do");
add_child(out, children[1], " ");
decrement_indent(node, node.body);
add_child(out, { y = node.yend, h = 0, [1] = "end" }, " ", indent);
return out;
},
},
["forin"] = {
before = increment_indent,
after = function(node: Node, children: {Output}): Output {
var out: Output = { y = node.y, h = 0 };
table.insert(out, "for");
add_child(out, children[1], " ");
table.insert(out, " in");
add_child(out, children[2], " ");
table.insert(out, " do");
add_child(out, children[3], " ");
decrement_indent(node, node.body);
add_child(out, { y = node.yend, h = 0, [1] = "end" }, " ", indent);
return out;
},
},
["fornum"] = {
before = increment_indent,
after = function(node: Node, children: {Output}): Output {
var out: Output = { y = node.y, h = 0 };
table.insert(out, "for");
add_child(out, children[1], " ");
table.insert(out, " =");
add_child(out, children[2], " ");
table.insert(out, ",");
add_child(out, children[3], " ");
if( children[4] ) {
table.insert(out, ",");
add_child(out, children[4], " ");
}
table.insert(out, " do");
add_child(out, children[5], " ");
decrement_indent(node, node.body);
add_child(out, { y = node.yend, h = 0, [1] = "end" }, " ", indent);
return out;
},
},
["return"] = {
after = function(node: Node, children: {Output}): Output {
var out: Output = { y = node.y, h = 0 };
table.insert(out, "return");
if( #children[1] > 0 ) {
add_child(out, children[1], " ");
}
return out;
},
},
["break"] = {
after = function(node: Node, _children: {Output}): Output {
var out: Output = { y = node.y, h = 0 };
table.insert(out, "break");
return out;
},
},
["variable_list"] = {
after = function(node: Node, children: {Output}): Output {
var out: Output = { y = node.y, h = 0 };
var space: string;
for( i, child in ipairs(children) ) {
if( i > 1 ) {
table.insert(out, ",");
space = " ";
}
add_child(out, child, space, child.y != node.y && indent);
}
return out;
},
},
["table_literal"] = {
before = increment_indent,
after = function(node: Node, children: {Output}): Output {
var out: Output = { y = node.y, h = 0 };
if( #children == 0 ) {
table.insert(out, "{}");
return out;
}
table.insert(out, "{");
var n = #children;
for( i, child in ipairs(children) ) {
add_child(out, child, " ", child.y != node.y && indent);
if( i < n || node.yend != node.y ) {
table.insert(out, ",");
}
}
decrement_indent(node, node[1]);
add_child(out, { y = node.yend, h = 0, [1] = "}" }, " ", indent);
return out;
},
},
["table_item"] = {
after = function(node: Node, children: {Output}): Output {
var out: Output = { y = node.y, h = 0 };
if( node.key_parsed != "implicit" ) {
if( node.key_parsed == "short" ) {
children[1][1] = children[1][1]->sub(2, -2);
add_child(out, children[1]);
table.insert(out, " = ");
} else {
table.insert(out, "[");
if( node.key_parsed == "long" && node.key.is_longstring ) {
table.insert(children[1], 1, " ");
table.insert(children[1], " ");
}
add_child(out, children[1]);
table.insert(out, "] = ");
}
}
add_child(out, children[2]);
return out;
},
},
["local_function"] = {
before = increment_indent,
after = function(node: Node, children: {Output}): Output {
var out: Output = { y = node.y, h = 0 };
table.insert(out, "local function");
add_child(out, children[1], " ");
table.insert(out, "(");
add_child(out, children[2]);
table.insert(out, ")");
add_child(out, children[4], " ");
decrement_indent(node, node.body);
add_child(out, { y = node.yend, h = 0, [1] = "end" }, " ", indent);
return out;
},
},
["global_function"] = {
before = increment_indent,
after = function(node: Node, children: {Output}): Output {
var out: Output = { y = node.y, h = 0 };
table.insert(out, "function");
add_child(out, children[1], " ");
table.insert(out, "(");
add_child(out, children[2]);
table.insert(out, ")");
add_child(out, children[4], " ");
decrement_indent(node, node.body);
add_child(out, { y = node.yend, h = 0, [1] = "end" }, " ", indent);
return out;
},
},
["record_function"] = {
before = increment_indent,
after = function(node: Node, children: {Output}): Output {
var out: Output = { y = node.y, h = 0 };
table.insert(out, "function");
add_child(out, children[1], " ");
table.insert(out, node.is_method && ":" || ".");
add_child(out, children[2]);
table.insert(out, "(");
if( node.is_method ) {
// remove self
table.remove(children[3], 1);
if( children[3][1] == "," ) {
table.remove(children[3], 1);
table.remove(children[3], 1);
}
}
add_child(out, children[3]);
table.insert(out, ")");
add_child(out, children[5], " ");
decrement_indent(node, node.body);
add_child(out, { y = node.yend, h = 0, [1] = "end" }, " ", indent);
return out;
},
},
["function"] = {
before = increment_indent,
after = function(node: Node, children: {Output}): Output {
var out: Output = { y = node.y, h = 0 };
table.insert(out, "function(");
add_child(out, children[1]);
table.insert(out, ")");
add_child(out, children[3], " ");
decrement_indent(node, node.body);
add_child(out, { y = node.yend, h = 0, [1] = "end" }, " ", indent);
return out;
},
},
["cast"] = {
},
["paren"] = {
after = function(node: Node, children: {Output}): Output {
var out: Output = { y = node.y, h = 0 };
table.insert(out, "(");
add_child(out, children[1], "", indent);
table.insert(out, ")");
return out;
},
},
["op"] = {
after = function(node: Node, children: {Output}): Output {
var out: Output = { y = node.y, h = 0 };
if( node.op.op == "@funcall" ) {
add_child(out, children[1], "", indent);
table.insert(out, "(");
add_child(out, children[3], "", indent);
table.insert(out, ")");
} else if( node.op.op == "@index" ) {
add_child(out, children[1], "", indent);
table.insert(out, "[");
if( node.e2.is_longstring ) {
table.insert(children[3], 1, " ");
table.insert(children[3], " ");
}
add_child(out, children[3], "", indent);
table.insert(out, "]");
} else if( node.op.op == "as" ) {
add_child(out, children[1], "", indent);
} else if( node.op.op == "is" ) {
if( node.e2.casttype.typename == "integer" ) {
table.insert(out, "math.type(");
add_child(out, children[1], "", indent);
table.insert(out, ") == \"integer\"");
} else {
table.insert(out, "type(");
add_child(out, children[1], "", indent);
table.insert(out, ") == \"");
add_child(out, children[3], "", indent);
table.insert(out, "\"");
}
} else if( spaced_op[node.op.arity][node.op.op] || tight_op[node.op.arity][node.op.op] ) {
var space = spaced_op[node.op.arity][node.op.op] && " " || "";
if( children[2] && node.op.prec > tonumber(children[2]) ) {
table.insert(children[1], 1, "(");
table.insert(children[1], ")");
}
if( node.op.arity == 1 ) {
table.insert(out, node.op.op);
add_child(out, children[1], space, indent);
} else if( node.op.arity == 2 ) {
add_child(out, children[1], "", indent);
if( space == " " ) {
table.insert(out, " ");
}
table.insert(out, node.op.op);
if( children[4] && node.op.prec > tonumber(children[4]) ) {
table.insert(children[3], 1, "(");
table.insert(children[3], ")");
}
add_child(out, children[3], space, indent);
}
} else {
error("unknown node op " .. node.op.op);
}
return out;
},
},
["variable"] = {
after = function(node: Node, _children: {Output}): Output {
var out: Output = { y = node.y, h = 0 };
add_string(out, node.tk);
return out;
},
},
["newtype"] = {
after = function(node: Node, _children: {Output}): Output {
var out: Output = { y = node.y, h = 0 };
if( node.is_alias ) {
table.insert(out, table.concat(node.newtype.def.names, "."));
} else if( is_record_type(node.newtype.def) ) {
table.insert(out, print_record_def(node.newtype.def));
} else {
table.insert(out, "{}");
}
return out;
},
},
["goto"] = {
after = function(node: Node, _children: {Output}): Output {
var out: Output = { y = node.y, h = 0 };
table.insert(out, "goto ");
table.insert(out, node.label);
return out;
},
},
["label"] = {
after = function(node: Node, _children: {Output}): Output {
var out: Output = { y = node.y, h = 0 };
table.insert(out, "::");
table.insert(out, node.label);
table.insert(out, "::");
return out;
},
},
};
var visit_type: Visitor<TypeName, Type, Output> = {};
visit_type.cbs = {
["string"] = {
after = function(typ: Type, _children: {Output}): Output {
var out: Output = { y = typ.y || -1, h = 0 };
var r = typ.resolved || typ;
var lua_type = primitive[r.typename]
|| (r.is_userdata && "userdata")
|| "table";
table.insert(out, lua_type);
return out;
},
},
};
visit_type.cbs["typetype"] = visit_type.cbs["string"];
visit_type.cbs["typevar"] = visit_type.cbs["string"];
visit_type.cbs["typearg"] = visit_type.cbs["string"];
visit_type.cbs["function"] = visit_type.cbs["string"];
visit_type.cbs["thread"] = visit_type.cbs["string"];
visit_type.cbs["array"] = visit_type.cbs["string"];
visit_type.cbs["map"] = visit_type.cbs["string"];
visit_type.cbs["tupletable"] = visit_type.cbs["string"];
visit_type.cbs["arrayrecord"] = visit_type.cbs["string"];
visit_type.cbs["record"] = visit_type.cbs["string"];
visit_type.cbs["enum"] = visit_type.cbs["string"];
visit_type.cbs["boolean"] = visit_type.cbs["string"];
visit_type.cbs["nil"] = visit_type.cbs["string"];
visit_type.cbs["number"] = visit_type.cbs["string"];
visit_type.cbs["integer"] = visit_type.cbs["string"];
visit_type.cbs["union"] = visit_type.cbs["string"];
visit_type.cbs["nominal"] = visit_type.cbs["string"];
visit_type.cbs["bad_nominal"] = visit_type.cbs["string"];
visit_type.cbs["emptytable"] = visit_type.cbs["string"];
visit_type.cbs["table_item"] = visit_type.cbs["string"];
visit_type.cbs["unresolved_emptytable_value"] = visit_type.cbs["string"];
visit_type.cbs["tuple"] = visit_type.cbs["string"];
visit_type.cbs["poly"] = visit_type.cbs["string"];
visit_type.cbs["any"] = visit_type.cbs["string"];
visit_type.cbs["unknown"] = visit_type.cbs["string"];
visit_type.cbs["invalid"] = visit_type.cbs["string"];
visit_type.cbs["unresolved"] = visit_type.cbs["string"];
visit_type.cbs["none"] = visit_type.cbs["string"];
visit_node.cbs["expression_list"] = visit_node.cbs["variable_list"];
visit_node.cbs["argument_list"] = visit_node.cbs["variable_list"];
visit_node.cbs["identifier"] = visit_node.cbs["variable"];
visit_node.cbs["number"] = visit_node.cbs["variable"];
visit_node.cbs["integer"] = visit_node.cbs["variable"];
visit_node.cbs["string"] = visit_node.cbs["variable"];
visit_node.cbs["nil"] = visit_node.cbs["variable"];
visit_node.cbs["boolean"] = visit_node.cbs["variable"];
visit_node.cbs["..."] = visit_node.cbs["variable"];
visit_node.cbs["argument"] = visit_node.cbs["variable"];
visit_node.cbs["type_identifier"] = visit_node.cbs["variable"];
var out = recurse_node(ast, visit_node, visit_type);
var code: Output;
if( opts.preserve_newlines ) {
code = { y = 1, h = 0 };
add_child(code, out);
} else {
code = out;
}
return concat_output(code);
}
//------------------------------------------------------------------------------
// Type check
//------------------------------------------------------------------------------
var function VARARG(t: {Type}): Type {
var tuple = t as Type;
tuple.typename = "tuple";
tuple.is_va = true;
return a_type(t);
}
var function TUPLE(t: {Type}): Type {
var tuple = t as Type;
tuple.typename = "tuple";
return a_type(t);
}
var function UNION(t: {Type}): Type {
return a_type( { typename = "union", types = t });
}
var NONE = a_type( { typename = "none" });
var INVALID = a_type( { typename = "invalid" });
var UNKNOWN = a_type( { typename = "unknown" });
var ALPHA = a_type( { typename = "typevar", typevar = "@a" });
var BETA = a_type( { typename = "typevar", typevar = "@b" });
var ARG_ALPHA = a_type( { typename = "typearg", typearg = "@a" });
var ARG_BETA = a_type( { typename = "typearg", typearg = "@b" });
var ARRAY_OF_ALPHA = a_type( { typename = "array", elements = ALPHA });
var MAP_OF_ALPHA_TO_BETA = a_type( { typename = "map", keys = ALPHA, values = BETA });
var NOMINAL_METATABLE_OF_ALPHA = a_type( { typename = "nominal", names = {"metatable"}, typevals = { ALPHA } });
var ARRAY_OF_STRING = a_type( { typename = "array", elements = STRING });
var ARRAY_OF_STRING_OR_NUMBER = a_type( { typename = "array", elements = UNION( { STRING, NUMBER }) });
var FUNCTION = a_type( { typename = "function", args = VARARG( { ANY }), rets = VARARG( { ANY }) });
var NOMINAL_FILE = a_type( { typename = "nominal", names = {"FILE"} });
var XPCALL_MSGH_FUNCTION = a_type( { typename = "function", args = TUPLE( { ANY }), rets = TUPLE( { }) });
var USERDATA = ANY; // Placeholder for maybe having a userdata "primitive" type
var numeric_binop = {
["number"] = {
["number"] = NUMBER,
["integer"] = NUMBER,
},
["integer"] = {
["integer"] = INTEGER,
["number"] = NUMBER,
},
};
var float_binop = {
["number"] = {
["number"] = NUMBER,
["integer"] = NUMBER,
},
["integer"] = {
["integer"] = NUMBER,
["number"] = NUMBER,
},
};
var integer_binop = {
["number"] = {
["number"] = INTEGER,
["integer"] = INTEGER,
},
["integer"] = {
["integer"] = INTEGER,
["number"] = INTEGER,
},
};
var relational_binop = {
["number"] = {
["integer"] = BOOLEAN,
["number"] = BOOLEAN,
},
["integer"] = {
["number"] = BOOLEAN,
["integer"] = BOOLEAN,
},
["string"] = {
["string"] = BOOLEAN,
},
["boolean"] = {
["boolean"] = BOOLEAN,
},
};
var equality_binop = {
["number"] = {
["number"] = BOOLEAN,
["integer"] = BOOLEAN,
["nil"] = BOOLEAN,
},
["integer"] = {
["number"] = BOOLEAN,
["integer"] = BOOLEAN,
["nil"] = BOOLEAN,
},
["string"] = {
["string"] = BOOLEAN,
["nil"] = BOOLEAN,
},
["boolean"] = {
["boolean"] = BOOLEAN,
["nil"] = BOOLEAN,
},
["record"] = {
["emptytable"] = BOOLEAN,
["arrayrecord"] = BOOLEAN,
["record"] = BOOLEAN,
["nil"] = BOOLEAN,
},
["array"] = {
["emptytable"] = BOOLEAN,
["arrayrecord"] = BOOLEAN,
["array"] = BOOLEAN,
["nil"] = BOOLEAN,
},
["arrayrecord"] = {
["emptytable"] = BOOLEAN,
["arrayrecord"] = BOOLEAN,
["record"] = BOOLEAN,
["array"] = BOOLEAN,
["nil"] = BOOLEAN,
},
["map"] = {
["emptytable"] = BOOLEAN,
["map"] = BOOLEAN,
["nil"] = BOOLEAN,
},
["thread"] = {
["thread"] = BOOLEAN,
["nil"] = BOOLEAN,
}
};
var unop_types: {string:{string:Type}} = {
["#"] = {
["arrayrecord"] = INTEGER,
["string"] = INTEGER,
["array"] = INTEGER,
["tupletable"] = INTEGER,
["map"] = INTEGER,
["emptytable"] = INTEGER,
},
["-"] = {
["number"] = NUMBER,
["integer"] = INTEGER,
},
["~"] = {
["number"] = INTEGER,
["integer"] = INTEGER,
},
["not"] = {
["string"] = BOOLEAN,
["number"] = BOOLEAN,
["integer"] = BOOLEAN,
["boolean"] = BOOLEAN,
["record"] = BOOLEAN,
["arrayrecord"] = BOOLEAN,
["array"] = BOOLEAN,
["tupletable"] = BOOLEAN,
["map"] = BOOLEAN,
["emptytable"] = BOOLEAN,
["thread"] = BOOLEAN,
},
};
var unop_to_metamethod: {string:string} = {
["#"] = "__len",
["-"] = "__unm",
["~"] = "__bnot",
};
var binop_types: {string:{TypeName:{TypeName:Type}}} = {
["+"] = numeric_binop,
["-"] = numeric_binop,
["*"] = numeric_binop,
["%"] = numeric_binop,
["/"] = float_binop,
["//"] = numeric_binop,
["^"] = float_binop,
["&"] = integer_binop,
["|"] = integer_binop,
["<<"] = integer_binop,
[">>"] = integer_binop,
["~"] = integer_binop,
["=="] = equality_binop,
["~="] = equality_binop,
["<="] = relational_binop,
[">="] = relational_binop,
["<"] = relational_binop,
[">"] = relational_binop,
["or"] = {
["boolean"] = {
["boolean"] = BOOLEAN,
["function"] = FUNCTION, // HACK
},
["number"] = {
["integer"] = NUMBER,
["number"] = NUMBER,
["boolean"] = BOOLEAN,
},
["integer"] = {
["integer"] = INTEGER,
["number"] = NUMBER,
["boolean"] = BOOLEAN,
},
["string"] = {
["string"] = STRING,
["boolean"] = BOOLEAN,
["enum"] = STRING,
},
["function"] = {
["boolean"] = BOOLEAN,
},
["array"] = {
["boolean"] = BOOLEAN,
},
["record"] = {
["boolean"] = BOOLEAN,
},
["arrayrecord"] = {
["boolean"] = BOOLEAN,
},
["map"] = {
["boolean"] = BOOLEAN,
},
["enum"] = {
["string"] = STRING,
},
["thread"] = {
["boolean"] = BOOLEAN,
}
},
[".."] = {
["string"] = {
["string"] = STRING,
["enum"] = STRING,
["number"] = STRING,
["integer"] = STRING,
},
["number"] = {
["integer"] = STRING,
["number"] = STRING,
["string"] = STRING,
["enum"] = STRING,
},
["integer"] = {
["integer"] = STRING,
["number"] = STRING,
["string"] = STRING,
["enum"] = STRING,
},
["enum"] = {
["number"] = STRING,
["integer"] = STRING,
["string"] = STRING,
["enum"] = STRING,
}
},
};
var binop_to_metamethod: {string:string} = {
["+"] = "__add",
["-"] = "__sub",
["*"] = "__mul",
["/"] = "__div",
["%"] = "__mod",
["^"] = "__pow",
["//"] = "__idiv",
["&"] = "__band",
["|"] = "__bor",
["~"] = "__bxor",
["<<"] = "__shl",
[">>"] = "__shr",
[".."] = "__concat",
["=="] = "__eq",
["<"] = "__lt",
["<="] = "__le",
};
var function is_unknown(t: Type): boolean {
return t.typename == "unknown"
|| t.typename == "unresolved_emptytable_value";
}
var show_type: function(Type, boolean, {Type:string}): string;
var function show_type_base(t: Type, short: boolean, seen: {Type:string}): string {
// FIXME this is a control for recursively built types, which should in principle not exist
if( seen[t] ) {
return seen[t];
}
seen[t] = "...";
var function show(typ: Type): string {
return show_type(typ, short, seen);
}
if( t.typename == "nominal" ) {
if( t.typevals ) {
var out = { table.concat(t.names, "."), "<" };
var vals: {string} = {};
for( _, v in ipairs(t.typevals) ) {
table.insert(vals, show(v));
}
table.insert(out, table.concat(vals, ", "));
table.insert(out, ">");
return table.concat(out);
} else {
return table.concat(t.names, ".");
}
} else if( t.typename == "tuple" ) {
var out: {string} = {};
for( _, v in ipairs(t) ) {
table.insert(out, show(v));
}
return "(" .. table.concat(out, ", ") .. ")";
} else if( t.typename == "tupletable" ) {
var out: {string} = {};
for( _, v in ipairs(t.types) ) {
table.insert(out, show(v));
}
return "{" .. table.concat(out, ", ") .. "}";
} else if( t.typename == "poly" ) {
var out: {string} = {};
for( _, v in ipairs(t.types) ) {
table.insert(out, show(v));
}
return table.concat(out, " and ");
} else if( t.typename == "union" ) {
var out: {string} = {};
for( _, v in ipairs(t.types) ) {
table.insert(out, show(v));
}
return table.concat(out, " | ");
} else if( t.typename == "emptytable" ) {
return "{}";
} else if( t.typename == "map" ) {
return "{" .. show(t.keys) .. " : " .. show(t.values) .. "}";
} else if( t.typename == "array" ) {
return "{" .. show(t.elements) .. "}";
} else if( t.typename == "enum" ) {
return t.names && table.concat(t.names, ".") || "enum";
} else if( is_record_type(t) ) {
if( short ) {
return "record";
} else {
var out: {string} = {"record"};
if( t.typeargs ) {
table.insert(out, "<");
var typeargs = {};
for( _, v in ipairs(t.typeargs) ) {
table.insert(typeargs, show(v));
}
table.insert(out, table.concat(typeargs, ", "));
table.insert(out, ">");
}
table.insert(out, " (");
if( t.elements ) {
table.insert(out, "{" .. show(t.elements) .. "}");
}
var fs = {};
for( _, k in ipairs(t.field_order) ) {
var v = t.fields[k];
table.insert(fs, k .. ": " .. show(v));
}
table.insert(out, table.concat(fs, "; "));
table.insert(out, ")");
return table.concat(out);
}
} else if( t.typename == "function" ) {
var out: {string} = {"function"};
if( t.typeargs ) {
table.insert(out, "<");
var typeargs = {};
for( _, v in ipairs(t.typeargs) ) {
table.insert(typeargs, show(v));
}
table.insert(out, table.concat(typeargs, ", "));
table.insert(out, ">");
}
table.insert(out, "(");
var args = {};
if( t.is_method ) {
table.insert(args, "self");
}
for( i, v in ipairs(t.args) ) {
if( ! t.is_method || i > 1 ) {
table.insert(args, (i == #t.args && t.args.is_va && "...: " || "") .. show(v));
}
}
table.insert(out, table.concat(args, ", "));
table.insert(out, ")");
if( #t.rets > 0 ) {
table.insert(out, ": ");
var rets = {};
for( i, v in ipairs(t.rets) ) {
table.insert(rets, show(v) .. (i == #t.rets && t.rets.is_va && "..." || ""));
}
table.insert(out, table.concat(rets, ", "));
}
return table.concat(out);
} else if( t.typename == "number"
|| t.typename == "integer"
|| t.typename == "boolean"
|| t.typename == "thread" ) {
return t.typename;
} else if( t.typename == "string" ) {
if( short ) {
return "string";
} else {
return t.typename ..
(t.tk && " " .. t.tk || "");
}
} else if( t.typename == "typevar" ) {
return t.typevar;
} else if( t.typename == "typearg" ) {
return t.typearg;
} else if( is_unknown(t) ) {
return "<unknown type>";
} else if( t.typename == "invalid" ) {
return "<invalid type>";
} else if( t.typename == "any" ) {
return "<any type>";
} else if( t.typename == "nil" ) {
return "nil";
} else if( t.typename == "none" ) {
return "";
} else if( is_typetype(t) ) {
return "type " .. show(t.def);
} else if( t.typename == "bad_nominal" ) {
return table.concat(t.names, ".") .. " (an unknown type)";
} else {
return tostring(t);
}
}
var function inferred_msg(t: Type): string {
return " (inferred at "..t.inferred_at_file..":"..t.inferred_at.y..":"..t.inferred_at.x..")";
}
show_type = function(t: Type, short: boolean, seen: {Type:string}): string {
seen = seen || {};
var ret = show_type_base(t, short, seen);
if( t.inferred_at ) {
ret = ret .. inferred_msg(t);
}
seen[t] = ret;
return ret;
};
var function search_for(module_name: string, suffix: string, path: string, tried: {string}): string, FILE, {string} {
for( entry in path->gmatch("[^;]+") ) {
var slash_name = module_name->gsub("%.", "/");
var filename = entry->gsub("?", slash_name);
var tl_filename = filename->gsub("%.lua$", suffix);
var fd = io.open(tl_filename, "r");
if( fd ) {
return tl_filename, fd, tried;
}
table.insert(tried, "no file '" .. tl_filename .. "'");
}
return null, null, tried;
}
function tl.search_module(module_name: string, search_dtl: boolean): string, FILE, {string} {
var found: string;
var fd: FILE;
var tried: {string} = {};
var path = os.getenv("TL_PATH") || package.path;
if( search_dtl ) {
found, fd, tried = search_for(module_name, ".d.tl", path, tried);
if( found ) {
return found, fd;
}
}
found, fd, tried = search_for(module_name, ".tl", path, tried);
if( found ) {
return found, fd;
}
found, fd, tried = search_for(module_name, ".lua", path, tried);
if( found ) {
return found, fd;
}
return null, null, tried;
}
var record Variable {
t: Type;
is_const: boolean;
needs_compat: boolean;
narrowed_from: Type;
is_narrowed: boolean;
declared_at: Node;
is_func_arg: boolean;
used: boolean;
}
var function sorted_keys<A,B>(m: {A:B}):{A} {
var keys = {};
for( k, _ in pairs(m) ) {
table.insert(keys, k);
}
table.sort(keys);
return keys;
}
var function fill_field_order(t: Type) {
if( t.typename == "record" ) {
t.field_order = sorted_keys(t.fields);
}
}
var function require_module(module_name: string, lax: boolean, env: Env): Type, boolean {
var modules = env.modules;
if( modules[module_name] ) {
return modules[module_name], true;
}
modules[module_name] = INVALID;
var found, fd = tl.search_module(module_name, true);
if( found && (lax || found->match("tl$") as boolean) ) {
fd->close();
var found_result, err: Result, string = tl.process(found, env);
assert(found_result, err);
if( ! found_result.type ) {
found_result.type = BOOLEAN;
}
env.modules[module_name] = found_result.type;
return found_result.type, true;
}
return INVALID, found != null;
}
var compat_code_cache: {string:Node} = {};
var function add_compat_entries(program: Node, used_set: {string: boolean}, gen_compat: CompatMode) {
if( gen_compat == "off" || ! next(used_set) ) {
return;
}
var used_list: {string} = sorted_keys(used_set);
var compat_loaded = false;
var n = 1;
var function load_code(name: string, text: string) {
var code: Node = compat_code_cache[name];
if( ! code ) {
var tokens = tl.lex(text);
var _: integer;
_, code = tl.parse_program(tokens, {}, "@internal");
tl.type_check(code, { filename = "<internal>", lax = false, gen_compat = "off" });
code = code;
compat_code_cache[name] = code;
}
for( _, c in ipairs(code) ) {
table.insert(program, n, c);
n = n + 1;
}
}
var function req(m: string): string {
return (gen_compat == "optional")
&& "pcall(require, '" .. m .. "')"
|| "true, require('" .. m .. "')";
}
for( _, name in ipairs(used_list) ) {
if( name == "table.unpack" ) {
load_code(name, "local _tl_table_unpack = unpack or table.unpack");
} else if( name == "bit32" ) {
load_code(name, "local bit32 = bit32; if not bit32 then local p, m = " .. req("bit32") .. "; if p then bit32 = m end");
} else if( name == "mt" ) {
load_code(name, "local _tl_mt = function(m, s, a, b) return (getmetatable(s == 1 and a or b)[m](a, b) end");
} else if( name == "math.maxinteger" ) {
load_code(name, "local _tl_math_maxinteger = math.maxinteger or math.pow(2,53)");
} else if( name == "math.mininteger" ) {
load_code(name, "local _tl_math_mininteger = math.mininteger or -math.pow(2,53) - 1");
} else {
if( ! compat_loaded ) {
load_code("compat", "local _tl_compat; if (tonumber((_VERSION or ''):match('[%d.]*$')) or 0) < 5.3 then local p, m = " .. req("compat53.module") .. "; if p then _tl_compat = m end");
compat_loaded = true;
}
load_code(name, (("local $NAME = _tl_compat and _tl_compat.$NAME or $NAME")->gsub("$NAME", name)));
}
}
program.y = 1;
}
var function get_stdlib_compat(lax: boolean): {string:boolean} {
if( lax ) {
return {
["utf8"] = true,
};
} else {
return {
["io"] = true,
["math"] = true,
["string"] = true,
["table"] = true,
["utf8"] = true,
["coroutine"] = true,
["os"] = true,
["package"] = true,
["debug"] = true,
["load"] = true,
["loadfile"] = true,
["assert"] = true,
["pairs"] = true,
["ipairs"] = true,
["pcall"] = true,
["xpcall"] = true,
["rawlen"] = true,
};
}
}
var bit_operators: {string:string} = {
["&"] = "band",
["|"] = "bor",
["~"] = "bxor",
[">>"] = "rshift",
["<<"] = "lshift",
};
var function convert_node_to_compat_call(node: Node, mod_name: string, fn_name: string, e1: Node, e2: Node) {
node.op.op = "@funcall";
node.op.arity = 2;
node.op.prec = 100;
node.e1 = { y = node.y, x = node.x, kind = "op", op = an_operator(node, 2, ".") };
node.e1.e1 = { y = node.y, x = node.x, kind = "identifier", tk = mod_name };
node.e1.e2 = { y = node.y, x = node.x, kind = "identifier", tk = fn_name };
node.e2 = { y = node.y, x = node.x, kind = "expression_list" };
node.e2[1] = e1;
node.e2[2] = e2;
}
var function convert_node_to_compat_mt_call(node: Node, mt_name: string, which_self: integer, e1: Node, e2: Node) {
node.op.op = "@funcall";
node.op.arity = 2;
node.op.prec = 100;
node.e1 = { y = node.y, x = node.x, kind = "identifier", tk = "_tl_mt" };
node.e2 = { y = node.y, x = node.x, kind = "expression_list" };
node.e2[1] = { y = node.y, x = node.x, kind = "string", tk = "\"" .. mt_name .. "\"" };
node.e2[2] = { y = node.y, x = node.x, kind = "integer", tk = tostring(which_self) };
node.e2[3] = e1;
node.e2[4] = e2;
}
var globals_typeid: integer;
var function init_globals(lax: boolean): {string:Variable}, {string:Type} {
var globals: {string:Variable} = {};
var stdlib_compat = get_stdlib_compat(lax);
// ensure globals are always initialized with the same typeids
var is_first_init = globals_typeid == null;
var save_typeid = last_typeid;
if( is_first_init ) {
globals_typeid = last_typeid;
} else {
last_typeid = globals_typeid;
}
var LOAD_FUNCTION = a_type( { typename = "function", args = {}, rets = TUPLE( { STRING }) });
var OS_DATE_TABLE = a_type( {
typename = "record",
fields = {
["year"] = INTEGER,
["month"] = INTEGER,
["day"] = INTEGER,
["hour"] = INTEGER,
["min"] = INTEGER,
["sec"] = INTEGER,
["wday"] = INTEGER,
["yday"] = INTEGER,
["isdst"] = BOOLEAN,
}
});
var OS_DATE_TABLE_FORMAT = a_type( { typename = "enum", enumset = { ["!*t"] = true, ["*t"] = true } });
var DEBUG_GETINFO_TABLE = a_type( {
typename = "record",
fields = {
["name"] = STRING,
["namewhat"] = STRING,
["source"] = STRING,
["short_src"] = STRING,
["linedefined"] = INTEGER,
["lastlinedefined"] = INTEGER,
["what"] = STRING,
["currentline"] = INTEGER,
["istailcall"] = BOOLEAN,
["nups"] = INTEGER,
["nparams"] = INTEGER,
["isvararg"] = BOOLEAN,
["func"] = ANY,
["activelines"] = a_type( { typename = "map", keys = INTEGER, values = BOOLEAN }),
}
});
var DEBUG_HOOK_EVENT = a_type( {
typename = "enum",
enumset = {
["call"] = true,
["tail call"] = true,
["return"] = true,
["line"] = true,
["count"] = true,
},
});
var DEBUG_HOOK_FUNCTION = a_type( {
typename = "function",
args = TUPLE( { DEBUG_HOOK_EVENT, INTEGER }),
rets = TUPLE( {}),
});
var TABLE_SORT_FUNCTION = a_type( { typename = "function", typeargs = TUPLE( { ARG_ALPHA }), args = TUPLE( { ALPHA, ALPHA }), rets = TUPLE( { BOOLEAN }) });
// placeholders for when we have optional arity annotations
var OPT_NUMBER = NUMBER;
var OPT_STRING = STRING;
var OPT_THREAD = THREAD;
var OPT_ALPHA = ALPHA;
var OPT_BETA = BETA;
var OPT_TABLE = TABLE;
var OPT_UNION = UNION;
var OPT_BOOLEAN = BOOLEAN;
var OPT_NOMINAL_FILE = NOMINAL_FILE;
var OPT_TABLE_SORT_FUNCTION = TABLE_SORT_FUNCTION;
var standard_library: {string:Type} = {
["..."] = VARARG( { STRING }),
["any"] = a_type( { typename = "typetype", def = ANY }),
["arg"] = ARRAY_OF_STRING,
["assert"] = a_type( { typename = "function", typeargs = TUPLE( { ARG_ALPHA, ARG_BETA }), args = TUPLE( { ALPHA, OPT_BETA }), rets = TUPLE( { ALPHA }) }),
["collectgarbage"] = a_type( {
typename = "poly",
types = {
a_type( { typename = "function", args = TUPLE( { a_type( { typename = "enum", enumset = { ["collect"] = true, ["count"] = true, ["stop"] = true, ["restart"] = true, } }) }), rets = TUPLE( { NUMBER }) }),
a_type( { typename = "function", args = TUPLE( { a_type( { typename = "enum", enumset = { ["step"] = true, ["setpause"] = true, ["setstepmul"] = true } }), NUMBER }), rets = TUPLE( { NUMBER }) }),
a_type( { typename = "function", args = TUPLE( { a_type( { typename = "enum", enumset = { ["isrunning"] = true } }) }), rets = TUPLE( { BOOLEAN }) }),
a_type( { typename = "function", args = TUPLE( { STRING, OPT_NUMBER }), rets = TUPLE( { a_type( { typename = "union", types = { BOOLEAN, NUMBER } }) }) }),
}
}),
["dofile"] = a_type( { typename = "function", args = TUPLE( { OPT_STRING }), rets = VARARG( { ANY }) }),
["error"] = a_type( { typename = "function", args = TUPLE( { ANY, NUMBER }), rets = TUPLE( {}) }),
["getmetatable"] = a_type( { typename = "function", typeargs = TUPLE( { ARG_ALPHA }), args = TUPLE( { ALPHA }), rets = TUPLE( { NOMINAL_METATABLE_OF_ALPHA }) }),
["ipairs"] = a_type( { typename = "function", typeargs = TUPLE( { ARG_ALPHA }), args = TUPLE( { ARRAY_OF_ALPHA }), rets = TUPLE( {
a_type( { typename = "function", args = TUPLE( {}), rets = TUPLE( { INTEGER, ALPHA }) }),
}) }),
["load"] = a_type( { typename = "function", args = TUPLE( { UNION( { STRING, LOAD_FUNCTION }), OPT_STRING, OPT_STRING, OPT_TABLE }), rets = TUPLE( { FUNCTION, STRING }) }),
["loadfile"] = a_type( { typename = "function", args = TUPLE( { OPT_STRING, OPT_STRING, OPT_TABLE }), rets = TUPLE( { FUNCTION, STRING }) }),
["next"] = a_type( {
typename = "poly",
types = {
a_type( { typeargs = TUPLE( { ARG_ALPHA, ARG_BETA }), typename = "function", args = TUPLE( { MAP_OF_ALPHA_TO_BETA, OPT_ALPHA }), rets = TUPLE( { ALPHA, BETA }) }),
a_type( { typeargs = TUPLE( { ARG_ALPHA }), typename = "function", args = TUPLE( { ARRAY_OF_ALPHA, OPT_ALPHA }), rets = TUPLE( { INTEGER, ALPHA }) }),
},
}),
["pairs"] = a_type( { typename = "function", typeargs = TUPLE( { ARG_ALPHA, ARG_BETA }), args = TUPLE( { a_type( { typename = "map", keys = ALPHA, values = BETA }) }), rets = TUPLE( {
a_type( { typename = "function", args = TUPLE( {}), rets = TUPLE( { ALPHA, BETA }) }),
}) }),
["pcall"] = a_type( { typename = "function", args = VARARG( { FUNCTION, ANY }), rets = TUPLE( { BOOLEAN, ANY }) }),
["xpcall"] = a_type( { typename = "function", args = VARARG( { FUNCTION, XPCALL_MSGH_FUNCTION, ANY }), rets = TUPLE( { BOOLEAN, ANY }) }),
["print"] = a_type( { typename = "function", args = VARARG( { ANY }), rets = TUPLE( {}) }),
["rawequal"] = a_type( { typename = "function", args = TUPLE( { ANY, ANY }), rets = TUPLE( { BOOLEAN }) }),
["rawget"] = a_type( { typename = "function", args = TUPLE( { TABLE, ANY }), rets = TUPLE( { ANY }) }),
["rawlen"] = a_type( { typename = "function", args = TUPLE( { UNION( { TABLE, STRING }) }), rets = TUPLE( { INTEGER }) }),
["rawset"] = a_type( {
typename = "poly",
types = {
a_type( { typeargs = TUPLE( { ARG_ALPHA, ARG_BETA }), typename = "function", args = TUPLE( { MAP_OF_ALPHA_TO_BETA, ALPHA, BETA }), rets = TUPLE( {}) }),
a_type( { typeargs = TUPLE( { ARG_ALPHA }), typename = "function", args = TUPLE( { ARRAY_OF_ALPHA, NUMBER, ALPHA }), rets = TUPLE( {}) }),
a_type( { typename = "function", args = TUPLE( { TABLE, ANY, ANY }), rets = TUPLE( {}) }),
}
}),
["require"] = a_type( { typename = "function", args = TUPLE( { STRING }), rets = TUPLE( {}) }),
["select"] = a_type( {
typename = "poly",
types = {
a_type( { typename = "function", typeargs = TUPLE( { ARG_ALPHA }), args = VARARG( { NUMBER, ALPHA }), rets = TUPLE( { ALPHA }) }),
a_type( { typename = "function", args = VARARG( { NUMBER, ANY }), rets = TUPLE( { ANY }) }),
a_type( { typename = "function", args = VARARG( { STRING, ANY }), rets = TUPLE( { INTEGER }) }),
}
}),
["setmetatable"] = a_type( { typeargs = TUPLE( { ARG_ALPHA }), typename = "function", args = TUPLE( { ALPHA, NOMINAL_METATABLE_OF_ALPHA }), rets = TUPLE( { ALPHA }) }),
["tonumber"] = a_type( {
typename = "poly",
types = {
a_type( { typename = "function", args = TUPLE( { ANY }), rets = TUPLE( { NUMBER }) }),
a_type( { typename = "function", args = TUPLE( { ANY, NUMBER }), rets = TUPLE( { INTEGER }) }),
}
}),
["tostring"] = a_type( { typename = "function", args = TUPLE( { ANY }), rets = TUPLE( { STRING }) }),
["type"] = a_type( { typename = "function", args = TUPLE( { ANY }), rets = TUPLE( { STRING }) }),
["FILE"] = a_type( {
typename = "typetype",
def = a_type( {
typename = "record",
is_userdata = true,
fields = {
["close"] = a_type( { typename = "function", args = TUPLE( { NOMINAL_FILE }), rets = TUPLE( { BOOLEAN, STRING}) }),
["flush"] = a_type( { typename = "function", args = TUPLE( { NOMINAL_FILE }), rets = TUPLE( {}) }),
["lines"] = a_type( { typename = "function", args = VARARG( { NOMINAL_FILE, a_type( { typename = "union", types = { STRING, NUMBER } }) }), rets = TUPLE( {
a_type( { typename = "function", args = TUPLE( {}), rets = VARARG( { STRING }) }),
}) }),
["read"] = a_type( { typename = "function", args = TUPLE( { NOMINAL_FILE, UNION( { STRING, NUMBER }) }), rets = TUPLE( { STRING, STRING }) }),
["seek"] = a_type( { typename = "function", args = TUPLE( { NOMINAL_FILE, OPT_STRING, OPT_NUMBER }), rets = TUPLE( { INTEGER, STRING }) }),
["setvbuf"] = a_type( { typename = "function", args = TUPLE( { NOMINAL_FILE, STRING, OPT_NUMBER }), rets = TUPLE( {}) }),
["write"] = a_type( { typename = "function", args = VARARG( { NOMINAL_FILE, STRING }), rets = TUPLE( { NOMINAL_FILE, STRING }) }),
// TODO complete...
},
}),
}),
["metatable"] = a_type( {
typename = "typetype",
def = a_type( {
typename = "record",
typeargs = TUPLE( { ARG_ALPHA }),
fields = {
["__call"] = a_type( { typename = "function", args = VARARG( { ALPHA, ANY }), rets = VARARG( { ANY }) }),
["__gc"] = a_type( { typename = "function", args = TUPLE( { ALPHA }), rets = TUPLE( {}) }),
["__index"] = ANY, // FIXME: function | table | anything with an __index metamethod
["__len"] = a_type( { typename = "function", args = TUPLE( { ALPHA }), rets = TUPLE( { ANY }) }),
["__mode"] = a_type( { typename = "enum", enumset = { ["k"] = true, ["v"] = true, ["kv"] = true, } }),
["__newindex"] = ANY, // FIXME: function | table | anything with a __newindex metamethod
["__pairs"] = a_type( { typename = "function", typeargs = TUPLE( { ARG_ALPHA, ARG_BETA }),
args = TUPLE( { a_type( { typename = "map", keys = ALPHA, values = BETA }) }),
rets = TUPLE( { a_type( { typename = "function", args = TUPLE( {}), rets = TUPLE( { ALPHA, BETA }) }) })
}),
["__tostring"] = a_type( { typename = "function", args = TUPLE( { ALPHA }), rets = TUPLE( { STRING }) }),
["__name"] = STRING,
["__add"] = a_type( { typename = "function", args = TUPLE( { ANY, ANY }), rets = TUPLE( { ANY }) }),
["__sub"] = a_type( { typename = "function", args = TUPLE( { ANY, ANY }), rets = TUPLE( { ANY }) }),
["__mul"] = a_type( { typename = "function", args = TUPLE( { ANY, ANY }), rets = TUPLE( { ANY }) }),
["__div"] = a_type( { typename = "function", args = TUPLE( { ANY, ANY }), rets = TUPLE( { ANY }) }),
["__idiv"] = a_type( { typename = "function", args = TUPLE( { ANY, ANY }), rets = TUPLE( { ANY }) }),
["__mod"] = a_type( { typename = "function", args = TUPLE( { ANY, ANY }), rets = TUPLE( { ANY }) }),
["__pow"] = a_type( { typename = "function", args = TUPLE( { ANY, ANY }), rets = TUPLE( { ANY }) }),
["__unm"] = a_type( { typename = "function", args = TUPLE( { ANY }), rets = TUPLE( { ANY }) }),
["__band"] = a_type( { typename = "function", args = TUPLE( { ANY, ANY }), rets = TUPLE( { ANY }) }),
["__bor"] = a_type( { typename = "function", args = TUPLE( { ANY, ANY }), rets = TUPLE( { ANY }) }),
["__bxor"] = a_type( { typename = "function", args = TUPLE( { ANY, ANY }), rets = TUPLE( { ANY }) }),
["__bnot"] = a_type( { typename = "function", args = TUPLE( { ANY }), rets = TUPLE( { ANY }) }),
["__shl"] = a_type( { typename = "function", args = TUPLE( { ANY, ANY }), rets = TUPLE( { ANY }) }),
["__shr"] = a_type( { typename = "function", args = TUPLE( { ANY, ANY }), rets = TUPLE( { ANY }) }),
["__concat"] = a_type( { typename = "function", args = TUPLE( { ANY, ANY }), rets = TUPLE( { ANY }) }),
["__eq"] = a_type( { typename = "function", args = TUPLE( { ANY, ANY }), rets = TUPLE( { BOOLEAN }) }),
["__lt"] = a_type( { typename = "function", args = TUPLE( { ANY, ANY }), rets = TUPLE( { BOOLEAN }) }),
["__le"] = a_type( { typename = "function", args = TUPLE( { ANY, ANY }), rets = TUPLE( { BOOLEAN }) }),
},
}),
}),
["coroutine"] = a_type( {
typename = "record",
fields = {
["create"] = a_type( { typename = "function", args = TUPLE( { FUNCTION }), rets = TUPLE( { THREAD }) }),
["close"] = a_type( { typename = "function", args = TUPLE( { THREAD }), rets = TUPLE( { BOOLEAN, STRING }) }),
["isyieldable"] = a_type( { typename = "function", args = TUPLE( {}), rets = TUPLE( { BOOLEAN }) }),
["resume"] = a_type( { typename = "function", args = VARARG( { THREAD, ANY }), rets = VARARG( { BOOLEAN, ANY }) }),
["running"] = a_type( { typename = "function", args = TUPLE( {}), rets = TUPLE( { THREAD, BOOLEAN }) }),
["status"] = a_type( { typename = "function", args = TUPLE( { THREAD }), rets = TUPLE( { STRING }) }),
["wrap"] = a_type( { typename = "function", args = TUPLE( { FUNCTION }), rets = TUPLE( { FUNCTION }) }),
["yield"] = a_type( { typename = "function", args = VARARG( { ANY }), rets = VARARG( { ANY }) }),
}
}),
["debug"] = a_type( {
typename = "record",
fields = {
["Info"] = a_type( {
typename = "typetype",
def = DEBUG_GETINFO_TABLE,
}),
["Hook"] = a_type( {
typename = "typetype",
def = DEBUG_HOOK_FUNCTION,
}),
["HookEvent"] = a_type( {
typename = "typetype",
def = DEBUG_HOOK_EVENT,
}),
["debug"] = a_type( { typename = "function", args = TUPLE( {}), rets = TUPLE( {}) }),
["gethook"] = a_type( { typename = "function", args = TUPLE( { OPT_THREAD }), rets = TUPLE( { DEBUG_HOOK_FUNCTION, INTEGER }) }),
["getlocal"] = a_type( {
typename = "poly",
types = {
a_type( { typename = "function", args = TUPLE( { THREAD, FUNCTION, NUMBER }), rets = TUPLE( {}) }),
a_type( { typename = "function", args = TUPLE( { FUNCTION, NUMBER }), rets = TUPLE( {}) }),
},
}),
["getmetatable"] = a_type( { typename = "function", typeargs = TUPLE( { ARG_ALPHA }), args = TUPLE( { ALPHA }), rets = TUPLE( { NOMINAL_METATABLE_OF_ALPHA }) }),
["getregistry"] = a_type( { typename = "function", args = TUPLE( {}), rets = TUPLE( { TABLE }) }),
["getupvalue"] = a_type( { typename = "function", args = TUPLE( { FUNCTION, NUMBER }), rets = TUPLE( { ANY }) }),
["getuservalue"] = a_type( { typename = "function", args = TUPLE( { USERDATA, NUMBER }), rets = TUPLE( { ANY }) }),
["sethook"] = a_type( {
typename = "poly",
types = {
a_type( { typename = "function", args = TUPLE( { THREAD, DEBUG_HOOK_FUNCTION, STRING, NUMBER }), rets = TUPLE( {}) }),
a_type( { typename = "function", args = TUPLE( { DEBUG_HOOK_FUNCTION, STRING, NUMBER }), rets = TUPLE( {}) }),
}
}),
["setlocal"] = a_type( {
typename = "poly",
types = {
a_type( { typename = "function", args = TUPLE( { THREAD, NUMBER, NUMBER, ANY }), rets = TUPLE( { STRING }) }),
a_type( { typename = "function", args = TUPLE( { NUMBER, NUMBER, ANY }), rets = TUPLE( { STRING }) }),
}
}),
["setmetatable"] = a_type( { typeargs = TUPLE( { ARG_ALPHA }), typename = "function", args = TUPLE( { ALPHA, NOMINAL_METATABLE_OF_ALPHA }), rets = TUPLE( { ALPHA }) }),
["setupvalue"] = a_type( { typename = "function", args = TUPLE( { FUNCTION, NUMBER, ANY }), rets = TUPLE( { STRING }) }),
["setuservalue"] = a_type( { typename = "function", args = TUPLE( { USERDATA, ANY, NUMBER }), rets = TUPLE( { USERDATA }) }),
["traceback"] = a_type( {
typename = "poly",
types = {
a_type( { typename = "function", args = TUPLE( { THREAD, STRING, NUMBER }), rets = TUPLE( { STRING }) }),
a_type( { typename = "function", args = TUPLE( { STRING, NUMBER }), rets = TUPLE( { STRING }) }),
},
}),
["upvalueid"] = a_type( { typename = "function", args = TUPLE( { FUNCTION, NUMBER }), rets = TUPLE( { USERDATA }) }),
["upvaluejoin"] = a_type( { typename = "function", args = TUPLE( { FUNCTION, NUMBER, FUNCTION, NUMBER }), rets = TUPLE( {}) }),
["getinfo"] = a_type( {
typename = "poly",
types = {
a_type( { typename = "function", args = TUPLE( { ANY }), rets = TUPLE( { DEBUG_GETINFO_TABLE }) }),
a_type( { typename = "function", args = TUPLE( { ANY, STRING }), rets = TUPLE( { DEBUG_GETINFO_TABLE }) }),
a_type( { typename = "function", args = TUPLE( { ANY, ANY, STRING }), rets = TUPLE( { DEBUG_GETINFO_TABLE }) }),
},
}),
},
}),
["io"] = a_type( {
typename = "record",
fields = {
["close"] = a_type( { typename = "function", args = TUPLE( { OPT_NOMINAL_FILE }), rets = TUPLE( { BOOLEAN, STRING }) }),
["flush"] = a_type( { typename = "function", args = TUPLE( {}), rets = TUPLE( {}) }),
["input"] = a_type( { typename = "function", args = TUPLE( { OPT_UNION( { STRING, NOMINAL_FILE }) }), rets = TUPLE( { NOMINAL_FILE }) }),
["lines"] = a_type( { typename = "function", args = VARARG( { OPT_STRING, a_type( { typename = "union", types = { STRING, NUMBER } }) }), rets = TUPLE( {
a_type( { typename = "function", args = TUPLE( {}), rets = VARARG( { STRING }) }),
}) }),
["open"] = a_type( { typename = "function", args = TUPLE( { STRING, STRING }), rets = TUPLE( { NOMINAL_FILE, STRING }) }),
["output"] = a_type( { typename = "function", args = TUPLE( { OPT_UNION( { STRING, NOMINAL_FILE }) }), rets = TUPLE( { NOMINAL_FILE }) }),
["popen"] = a_type( { typename = "function", args = TUPLE( { STRING, STRING }), rets = TUPLE( { NOMINAL_FILE, STRING }) }),
["read"] = a_type( { typename = "function", args = TUPLE( { UNION( { STRING, NUMBER }) }), rets = TUPLE( { STRING, STRING }) }),
["stderr"] = NOMINAL_FILE,
["stdin"] = NOMINAL_FILE,
["stdout"] = NOMINAL_FILE,
["tmpfile"] = a_type( { typename = "function", args = TUPLE( {}), rets = TUPLE( { NOMINAL_FILE }) }),
["type"] = a_type( { typename = "function", args = TUPLE( { ANY }), rets = TUPLE( { STRING }) }),
["write"] = a_type( { typename = "function", args = VARARG( { STRING }), rets = TUPLE( { NOMINAL_FILE, STRING }) }),
},
}),
["math"] = a_type( {
typename = "record",
fields = {
["abs"] = a_type( {
typename = "poly",
types = {
a_type( { typename = "function", args = TUPLE( { INTEGER }), rets = TUPLE( { INTEGER }) }),
a_type( { typename = "function", args = TUPLE( { NUMBER }), rets = TUPLE( { NUMBER }) }),
}
}),
["acos"] = a_type( { typename = "function", args = TUPLE( { NUMBER }), rets = TUPLE( { NUMBER }) }),
["asin"] = a_type( { typename = "function", args = TUPLE( { NUMBER }), rets = TUPLE( { NUMBER }) }),
["atan"] = a_type( { typename = "function", args = TUPLE( { NUMBER, OPT_NUMBER }), rets = TUPLE( { NUMBER }) }),
["atan2"] = a_type( { typename = "function", args = TUPLE( { NUMBER, NUMBER }), rets = TUPLE( { NUMBER }) }),
["ceil"] = a_type( { typename = "function", args = TUPLE( { NUMBER }), rets = TUPLE( { INTEGER }) }),
["cos"] = a_type( { typename = "function", args = TUPLE( { NUMBER }), rets = TUPLE( { NUMBER }) }),
["cosh"] = a_type( { typename = "function", args = TUPLE( { NUMBER }), rets = TUPLE( { NUMBER }) }),
["deg"] = a_type( { typename = "function", args = TUPLE( { NUMBER }), rets = TUPLE( { NUMBER }) }),
["exp"] = a_type( { typename = "function", args = TUPLE( { NUMBER }), rets = TUPLE( { NUMBER }) }),
["floor"] = a_type( { typename = "function", args = TUPLE( { NUMBER }), rets = TUPLE( { INTEGER }) }),
["fmod"] = a_type( {
typename = "poly",
types = {
a_type( { typename = "function", args = TUPLE( { INTEGER, INTEGER }), rets = TUPLE( { INTEGER }) }),
a_type( { typename = "function", args = TUPLE( { NUMBER, NUMBER }), rets = TUPLE( { NUMBER }) }),
}
}),
["frexp"] = a_type( { typename = "function", args = TUPLE( { NUMBER }), rets = TUPLE( { NUMBER, NUMBER }) }),
["huge"] = NUMBER,
["ldexp"] = a_type( { typename = "function", args = TUPLE( { NUMBER, NUMBER }), rets = TUPLE( { NUMBER }) }),
["log"] = a_type( { typename = "function", args = TUPLE( { NUMBER, NUMBER }), rets = TUPLE( { NUMBER }) }),
["log10"] = a_type( { typename = "function", args = TUPLE( { NUMBER }), rets = TUPLE( { NUMBER }) }),
["max"] = a_type( {
typename = "poly",
types = {
a_type( { typename = "function", args = VARARG( { INTEGER }), rets = TUPLE( { INTEGER }) }),
a_type( { typename = "function", typeargs = TUPLE( { ARG_ALPHA }), args = VARARG( { ALPHA }), rets = TUPLE( { ALPHA }) }),
a_type( { typename = "function", args = VARARG( { a_type( { typename = "union", types = { NUMBER, INTEGER } }) }), rets = TUPLE( { NUMBER }) }),
a_type( { typename = "function", args = VARARG( { ANY }), rets = TUPLE( { ANY }) }),
}
}),
["maxinteger"] = a_type( { typename = "integer", needs_compat = true }),
["min"] = a_type( {
typename = "poly",
types = {
a_type( { typename = "function", args = VARARG( { INTEGER }), rets = TUPLE( { INTEGER }) }),
a_type( { typename = "function", typeargs = TUPLE( { ARG_ALPHA }), args = VARARG( { ALPHA }), rets = TUPLE( { ALPHA }) }),
a_type( { typename = "function", args = VARARG( { a_type( { typename = "union", types = { NUMBER, INTEGER } }) }), rets = TUPLE( { NUMBER }) }),
a_type( { typename = "function", args = VARARG( { ANY }), rets = TUPLE( { ANY }) }),
}
}),
["mininteger"] = a_type( { typename = "integer", needs_compat = true }),
["modf"] = a_type( { typename = "function", args = TUPLE( { NUMBER }), rets = TUPLE( { INTEGER, NUMBER }) }),
["pi"] = NUMBER,
["pow"] = a_type( { typename = "function", args = TUPLE( { NUMBER, NUMBER }), rets = TUPLE( { NUMBER }) }),
["rad"] = a_type( { typename = "function", args = TUPLE( { NUMBER }), rets = TUPLE( { NUMBER }) }),
["random"] = a_type( {
typename = "poly",
types = {
a_type( { typename = "function", args = TUPLE( { NUMBER, NUMBER }), rets = TUPLE( { INTEGER }) }),
a_type( { typename = "function", args = TUPLE( {}), rets = TUPLE( { NUMBER }) }),
}
}),
["randomseed"] = a_type( { typename = "function", args = TUPLE( { NUMBER, NUMBER }), rets = TUPLE( { INTEGER, INTEGER }) }),
["sin"] = a_type( { typename = "function", args = TUPLE( { NUMBER }), rets = TUPLE( { NUMBER }) }),
["sinh"] = a_type( { typename = "function", args = TUPLE( { NUMBER }), rets = TUPLE( { NUMBER }) }),
["sqrt"] = a_type( { typename = "function", args = TUPLE( { NUMBER }), rets = TUPLE( { NUMBER }) }),
["tan"] = a_type( { typename = "function", args = TUPLE( { NUMBER }), rets = TUPLE( { NUMBER }) }),
["tanh"] = a_type( { typename = "function", args = TUPLE( { NUMBER }), rets = TUPLE( { NUMBER }) }),
["tointeger"] = a_type( { typename = "function", args = TUPLE( { ANY }), rets = TUPLE( { INTEGER }) }),
["type"] = a_type( { typename = "function", args = TUPLE( { ANY }), rets = TUPLE( { STRING }) }),
["ult"] = a_type( { typename = "function", args = TUPLE( { NUMBER, NUMBER }), rets = TUPLE( { BOOLEAN }) }),
},
}),
["os"] = a_type( {
typename = "record",
fields = {
["clock"] = a_type( { typename = "function", args = TUPLE( {}), rets = TUPLE( { NUMBER }) }),
["date"] = a_type( {
typename = "poly",
types = {
a_type( { typename = "function", args = TUPLE( {}), rets = TUPLE( { STRING }) }),
a_type( { typename = "function", args = TUPLE( { OS_DATE_TABLE_FORMAT, NUMBER }), rets = TUPLE( { OS_DATE_TABLE }) }),
a_type( { typename = "function", args = TUPLE( { OPT_STRING, OPT_NUMBER }), rets = TUPLE( { STRING }) }),
}
}),
["difftime"] = a_type( { typename = "function", args = TUPLE( { NUMBER, NUMBER }), rets = TUPLE( { NUMBER }) }),
["execute"] = a_type( { typename = "function", args = TUPLE( { STRING }), rets = TUPLE( { BOOLEAN, STRING, INTEGER }) }),
["exit"] = a_type( { typename = "function", args = TUPLE( { UNION( { NUMBER, BOOLEAN }), BOOLEAN }), rets = TUPLE( {}) }),
["getenv"] = a_type( { typename = "function", args = TUPLE( { STRING }), rets = TUPLE( { STRING }) }),
["remove"] = a_type( { typename = "function", args = TUPLE( { STRING }), rets = TUPLE( { BOOLEAN, STRING }) }),
["rename"] = a_type( { typename = "function", args = TUPLE( { STRING, STRING}), rets = TUPLE( { BOOLEAN, STRING }) }),
["setlocale"] = a_type( { typename = "function", args = TUPLE( { STRING, OPT_STRING}), rets = TUPLE( { STRING }) }),
["time"] = a_type( {
typename = "poly",
types = {
a_type( { typename = "function", args = TUPLE( {}), rets = TUPLE( { INTEGER }) }),
a_type( { typename = "function", args = TUPLE( {OS_DATE_TABLE}), rets = TUPLE( { INTEGER }) }),
}
}),
["tmpname"] = a_type( { typename = "function", args = TUPLE( {}), rets = TUPLE( { STRING }) }),
},
}),
["package"] = a_type( {
typename = "record",
fields = {
["config"] = STRING,
["cpath"] = STRING,
["loaded"] = a_type( {
typename = "map",
keys = STRING,
values = ANY,
}),
["loaders"] = a_type( {
typename = "array",
elements = a_type( { typename = "function", args = TUPLE( { STRING }), rets = TUPLE( { ANY }) })
}),
["loadlib"] = a_type( { typename = "function", args = TUPLE( { STRING, STRING }), rets = TUPLE( { FUNCTION }) }),
["path"] = STRING,
["preload"] = TABLE,
["searchers"] = a_type( {
typename = "array",
elements = a_type( { typename = "function", args = TUPLE( { STRING }), rets = TUPLE( { ANY }) })
}),
["searchpath"] = a_type( { typename = "function", args = TUPLE( { STRING, STRING, OPT_STRING, OPT_STRING }), rets = TUPLE( { STRING, STRING }) }),
},
}),
["string"] = a_type( {
typename = "record",
fields = {
["byte"] = a_type( {
typename = "poly",
types = {
a_type( { typename = "function", args = TUPLE( { STRING, OPT_NUMBER }), rets = TUPLE( { INTEGER }) }),
a_type( { typename = "function", args = TUPLE( { STRING, NUMBER, NUMBER }), rets = VARARG( { INTEGER }) }),
},
}),
["char"] = a_type( { typename = "function", args = VARARG( { NUMBER }), rets = TUPLE( { STRING }) }),
["dump"] = a_type({ typename = "function", args = TUPLE( { FUNCTION, OPT_BOOLEAN }), rets = TUPLE( { STRING }) }),
["find"] = a_type( { typename = "function", args = TUPLE( { STRING, STRING, OPT_NUMBER, OPT_BOOLEAN }), rets = VARARG( { INTEGER, INTEGER, STRING }) }),
["format"] = a_type( { typename = "function", args = VARARG( { STRING, ANY }), rets = TUPLE( { STRING }) }),
["gmatch"] = a_type( { typename = "function", args = TUPLE( { STRING, STRING }), rets = TUPLE( {
a_type( { typename = "function", args = TUPLE( {}), rets = VARARG( { STRING }) }),
}) }),
["gsub"] = a_type( {
typename = "poly",
types = {
a_type( { typename = "function", args = TUPLE( { STRING, STRING, STRING, NUMBER }), rets = TUPLE( { STRING, INTEGER }) }),
a_type( { typename = "function", args = TUPLE( { STRING, STRING, a_type( { typename = "map", keys = STRING, values = STRING }), NUMBER }), rets = TUPLE( { STRING, INTEGER }) }),
a_type( { typename = "function", args = TUPLE( { STRING, STRING, a_type( { typename = "function", args = VARARG( { STRING }), rets = TUPLE( { STRING }) }) }), rets = TUPLE( { STRING, INTEGER }) }),
a_type( { typename = "function", args = TUPLE( { STRING, STRING, a_type( { typename = "function", args = VARARG( { STRING }), rets = TUPLE( { NUMBER }) }) }), rets = TUPLE( { STRING, INTEGER }) }),
a_type( { typename = "function", args = TUPLE( { STRING, STRING, a_type( { typename = "function", args = VARARG( { STRING }), rets = TUPLE( { BOOLEAN }) }) }), rets = TUPLE( { STRING, INTEGER }) }),
a_type( { typename = "function", args = TUPLE( { STRING, STRING, a_type( { typename = "function", args = VARARG( { STRING }), rets = TUPLE( {}) }) }), rets = TUPLE( { STRING, INTEGER }) }),
// FIXME any other modes
}
}),
["len"] = a_type( { typename = "function", args = TUPLE( { STRING }), rets = TUPLE( { INTEGER }) }),
["lower"] = a_type( { typename = "function", args = TUPLE( { STRING }), rets = TUPLE( { STRING }) }),
["match"] = a_type( { typename = "function", args = TUPLE( { STRING, STRING, NUMBER }), rets = VARARG( { STRING }) }),
["pack"] = a_type( { typename = "function", args = VARARG( { STRING, ANY }), rets = TUPLE( { STRING }) }),
["packsize"] = a_type( { typename = "function", args = TUPLE( { STRING }), rets = TUPLE( { INTEGER }) }),
["rep"] = a_type( { typename = "function", args = TUPLE( { STRING, NUMBER }), rets = TUPLE( { STRING }) }),
["reverse"] = a_type( { typename = "function", args = TUPLE( { STRING }), rets = TUPLE( { STRING }) }),
["sub"] = a_type( { typename = "function", args = TUPLE( { STRING, NUMBER, NUMBER }), rets = TUPLE( { STRING }) }),
["unpack"] = a_type( { typename = "function", args = TUPLE( { STRING, STRING, OPT_NUMBER }), rets = VARARG( { ANY }) }),
["upper"] = a_type( { typename = "function", args = TUPLE( { STRING }), rets = TUPLE( { STRING }) }),
},
}),
["table"] = a_type( {
typename = "record",
fields = {
["concat"] = a_type( { typename = "function", args = TUPLE( { ARRAY_OF_STRING_OR_NUMBER, OPT_STRING, OPT_NUMBER, OPT_NUMBER }), rets = TUPLE( { STRING }) }),
["insert"] = a_type( {
typename = "poly",
types = {
a_type( { typename = "function", typeargs = TUPLE( { ARG_ALPHA }), args = TUPLE( { ARRAY_OF_ALPHA, NUMBER, ALPHA }), rets = TUPLE( {}) }),
a_type( { typename = "function", typeargs = TUPLE( { ARG_ALPHA }), args = TUPLE( { ARRAY_OF_ALPHA, ALPHA }), rets = TUPLE( {}) }),
}
}),
["move"] = a_type( {
typename = "poly",
types = {
a_type( { typename = "function", typeargs = TUPLE( { ARG_ALPHA }), args = TUPLE( { ARRAY_OF_ALPHA, NUMBER, NUMBER, NUMBER }), rets = TUPLE( { ARRAY_OF_ALPHA }) }),
a_type( { typename = "function", typeargs = TUPLE( { ARG_ALPHA }), args = TUPLE( { ARRAY_OF_ALPHA, NUMBER, NUMBER, NUMBER, ARRAY_OF_ALPHA }), rets = TUPLE( { ARRAY_OF_ALPHA }) }),
}
}),
["pack"] = a_type( { typename = "function", args = VARARG( { ANY }), rets = TUPLE( { TABLE }) }),
["remove"] = a_type( { typename = "function", typeargs = TUPLE( { ARG_ALPHA }), args = TUPLE( { ARRAY_OF_ALPHA, OPT_NUMBER }), rets = TUPLE( { ALPHA }) }),
["sort"] = a_type( { typename = "function", typeargs = TUPLE( { ARG_ALPHA }), args = TUPLE( { ARRAY_OF_ALPHA, OPT_TABLE_SORT_FUNCTION }), rets = TUPLE( {}) }),
["unpack"] = a_type( { typename = "function", needs_compat = true, typeargs = TUPLE( { ARG_ALPHA }), args = TUPLE( { ARRAY_OF_ALPHA, NUMBER, NUMBER }), rets = VARARG( { ALPHA }) }),
},
}),
["utf8"] = a_type( {
typename = "record",
fields = {
["char"] = a_type( { typename = "function", args = VARARG( { NUMBER }), rets = TUPLE( { STRING }) }),
["charpattern"] = STRING,
["codepoint"] = a_type( { typename = "function", args = TUPLE( { STRING, OPT_NUMBER, OPT_NUMBER }), rets = VARARG( { INTEGER }) }),
["codes"] = a_type( { typename = "function", args = TUPLE( { STRING }), rets = TUPLE( {
a_type( { typename = "function", args = TUPLE( {}), rets = TUPLE( { NUMBER, STRING }) }),
}), }),
["len"] = a_type( { typename = "function", args = TUPLE( { STRING, NUMBER, NUMBER }), rets = TUPLE( { INTEGER }) }),
["offset"] = a_type( { typename = "function", args = TUPLE( { STRING, NUMBER, NUMBER }), rets = TUPLE( { INTEGER }) }),
},
}),
["_VERSION"] = STRING,
};
for( _, t in pairs(standard_library) ) {
fill_field_order(t);
if( is_typetype(t) ) {
fill_field_order(t.def);
}
}
fill_field_order(OS_DATE_TABLE);
fill_field_order(DEBUG_GETINFO_TABLE);
NOMINAL_FILE.found = standard_library["FILE"];
NOMINAL_METATABLE_OF_ALPHA.found = standard_library["metatable"];
for( name, typ in pairs(standard_library) ) {
globals[name] = { t = typ, needs_compat = stdlib_compat[name], is_const = true };
}
// only global scope and vararg functions accept `...`:
// `@is_va` is an internal sentinel value which is
// `any` if `...` is accepted in this scope or `nil` if it isn't.
globals["@is_va"] = { t = ANY };
if( ! is_first_init ) {
last_typeid = save_typeid;
}
return globals, standard_library;
}
tl.init_env = function(lax: boolean, gen_compat: boolean | CompatMode, gen_target: TargetMode, predefined: {string}): Env, string {
if( gen_compat == true || gen_compat == null ) {
gen_compat = "optional";
} else if( gen_compat == false ) {
gen_compat = "off";
}
gen_compat = gen_compat as CompatMode;
if( ! gen_target ) {
if( _VERSION == "Lua 5.1" || _VERSION == "Lua 5.2" ) {
gen_target = "5.1";
} else {
gen_target = "5.3";
}
}
var globals, standard_library = init_globals(lax);
var env = {
ok = true,
modules = {},
loaded = {},
loaded_order = {},
globals = globals,
gen_compat = gen_compat,
gen_target = gen_target,
};
// make standard library tables available as modules for require()
for( name, _v_var in pairs(standard_library) ) {
if( _v_var.typename == "record" ) {
env.modules[name] = _v_var;
}
}
if( predefined ) {
for( _, name in ipairs(predefined) ) {
var module_type = require_module(name, lax, env);
if( module_type == INVALID ) {
return null, string.format("Error: could not predefine module '%s'", name);
}
}
}
return env;
};
tl.type_check = function(ast: Node, opts: TypeCheckOptions): Result {
opts = opts || {};
var env = opts.env || tl.init_env(opts.lax, opts.gen_compat, opts.gen_target);
var lax = opts.lax;
var filename = opts.filename;
var st: {{string:Variable}} = { env.globals };
var symbol_list: {Symbol} = {};
var symbol_list_n = 0;
var all_needs_compat = {};
var dependencies: {string:string} = {};
var warnings: {Error} = {};
var errors: {Error} = {};
var module_type: Type;
var function find_var(name: string, raw: boolean): Variable {
for( i = #st, 1, -1 ) {
var scope = st[i];
if( scope[name] ) {
if( i == 1 && scope[name].needs_compat ) {
all_needs_compat[name] = true;
}
if( ! raw ) {
scope[name].used = true;
}
return scope[name];
}
}
}
var function simulate_g(): Type, boolean {
// this is a static approximation of _G
var globals: {string:Type} = {};
for( k, v in pairs(st[1]) ) {
if( k->sub(1,1) != "@" ) {
globals[k] = v.t;
}
}
return a_type( {
typename = "record",
field_order = sorted_keys(globals),
fields = globals,
}), false;
}
var function find_var_type(name: string, raw: boolean): Type, boolean {
var _v_var = find_var(name, raw);
if( _v_var ) {
return _v_var.t, _v_var.is_const;
}
}
var function error_in_type(where: Type, msg: string, ...: Type): Error {
var n = select("#", ...);
if( n > 0 ) {
var showt = {};
for( i = 1, n ) {
var t = select(i, ...);
if( t.typename == "invalid" ) {
return null;
}
showt[i] = show_type(t);
}
msg = msg->format(table.unpack(showt));
}
return {
y = where.y,
x = where.x,
msg = msg,
filename = where.filename || filename,
};
}
var function type_error(t: Type, msg: string, ...:Type): boolean {
var e = error_in_type(t, msg, ...);
if( e ) {
table.insert(errors, e);
return true;
} else {
return false;
}
}
var function find_type(names: {string}, accept_typearg: boolean): Type {
var typ = find_var_type(names[1]);
if( ! typ ) {
return null;
}
if( typ.found ) {
typ = typ.found;
}
for( i = 2, #names ) {
var fields = typ.fields || (typ.def && typ.def.fields);
if( fields ) {
typ = fields[names[i]];
if( typ == null ) {
return null;
}
if( typ.found ) {
typ = typ.found;
}
} else {
return null;
}
}
if( is_typetype(typ) || (accept_typearg && typ.typename == "typearg") ) {
return typ;
}
}
var function union_type(t: Type): string {
if( is_typetype(t) ) {
return union_type(t.def);
} else if( t.typename == "tuple" ) {
return union_type(t[1]);
} else if( t.typename == "nominal" ) {
var typetype = t.found || find_type(t.names);
if( ! typetype ) {
return "table"; // invalid type, will report error elsewhere
}
return union_type(typetype);
} else if( t.typename == "record" ) {
if( t.is_userdata ) {
return "userdata";
}
return "table";
} else if( table_types[t.typename] ) {
return "table";
} else {
return t.typename;
}
}
var function is_valid_union(typ: Type): boolean, string {
// check for limitations in our union support
// due to codegen limitations (we only check with type() so far)
var n_table_types = 0;
var n_function_types = 0;
var n_userdata_types = 0;
var n_string_enum = 0;
var has_primitive_string_type = false;
for( _, t in ipairs(typ.types) ) {
var ut = union_type(t);
if( ut == "userdata" ) { // must be tested before table_types
n_userdata_types = n_userdata_types + 1;
if( n_userdata_types > 1 ) {
return false, "cannot discriminate a union between multiple userdata types: %s";
}
} else if( ut == "table" ) {
n_table_types = n_table_types + 1;
if( n_table_types > 1 ) {
return false, "cannot discriminate a union between multiple table types: %s";
}
} else if( ut == "function" ) {
n_function_types = n_function_types + 1;
if( n_function_types > 1 ) {
return false, "cannot discriminate a union between multiple function types: %s";
}
} else if( ut == "enum" || (ut == "string" && ! has_primitive_string_type) ) {
n_string_enum = n_string_enum + 1;
if( n_string_enum > 1 ) {
return false, "cannot discriminate a union between multiple string/enum types: %s";
}
if( ut == "string" ) {
has_primitive_string_type = true;
}
}
}
return true;
}
var function resolve_typetype(t: Type): Type {
if( is_typetype(t) ) {
return t.def;
} else {
return t;
}
}
var no_nested_types: {string:boolean} = {
["string"] = true,
["number"] = true,
["integer"] = true,
["boolean"] = true,
["thread"] = true,
["any"] = true,
["enum"] = true,
["nil"] = true,
["unknown"] = true,
};
var function resolve_typevars(typ: Type): boolean, Type, {Error} {
var errs: {Error};
var seen: {Type:Type} = {};
var function resolve(t: Type): Type {
// avoid copies of types that do not contain type variables
if( no_nested_types[t.typename] || (t.typename == "nominal" && ! t.typevals) ) {
return t;
}
seen = seen || {};
if( seen[t] ) {
return seen[t];
}
var orig_t = t;
if( t.typename == "typevar" ) {
t = find_var_type(t.typevar);
var rt: Type;
if( ! t ) {
rt = orig_t;
} else if( t.typename == "string" ) {
// tk is not propagated
rt = STRING;
} else if( no_nested_types[t.typename]
|| (t.typename == "nominal" && ! t.typevals) ) {
rt = t;
}
if( rt ) {
seen[orig_t] = rt;
return rt;
}
}
var copy: Type = {};
seen[orig_t] = copy;
copy.typename = t.typename;
copy.filename = t.filename;
copy.typeid = t.typeid;
copy.x = t.x;
copy.y = t.y;
copy.yend = t.yend;
copy.xend = t.xend;
copy.names = t.names; // which types have this, exactly?
for( i, tf in ipairs(t) ) {
copy[i] = resolve(tf);
}
if( t.typename == "array" ) {
copy.elements = resolve(t.elements);
// inferred_len is not propagated
} else if( t.typename == "typearg" ) {
copy.typearg = t.typearg;
} else if( t.typename == "typevar" ) {
copy.typevar = t.typevar;
} else if( is_typetype(t) ) {
copy.def = resolve(t.def);
} else if( t.typename == "nominal" ) {
copy.typevals = resolve(t.typevals);
copy.found = t.found;
} else if( t.typename == "function" ) {
if( t.typeargs ) {
copy.typeargs = {};
for( i, tf in ipairs(t.typeargs) ) {
copy.typeargs[i] = resolve(tf);
}
}
copy.is_method = t.is_method;
copy.args = resolve(t.args);
copy.rets = resolve(t.rets);
} else if( t.typename == "record" || t.typename == "arrayrecord" ) {
if( t.typeargs ) {
copy.typeargs = {};
for( i, tf in ipairs(t.typeargs) ) {
copy.typeargs[i] = resolve(tf);
}
}
if( t.elements ) {
copy.elements = resolve(t.elements);
}
copy.fields = {};
copy.field_order = {};
for( i, k in ipairs(t.field_order) ) {
copy.field_order[i] = k;
copy.fields[k] = resolve(t.fields[k]);
}
if( t.meta_fields ) {
copy.meta_fields = {};
copy.meta_field_order = {};
for( i, k in ipairs(t.meta_field_order) ) {
copy.meta_field_order[i] = k;
copy.meta_fields[k] = resolve(t.meta_fields[k]);
}
}
} else if( t.typename == "map" ) {
copy.keys = resolve(t.keys);
copy.values = resolve(t.values);
} else if( t.typename == "union" ) {
copy.types = {};
for( i, tf in ipairs(t.types) ) {
copy.types[i] = resolve(tf);
}
var ok, err = is_valid_union(copy);
if( ! ok ) {
errs = errs || {};
table.insert(errs, error_in_type(t, err, t));
}
} else if( t.typename == "poly" || t.typename == "tupletable" ) {
copy.types = {};
for( i, tf in ipairs(t.types) ) {
copy.types[i] = resolve(tf);
}
} else if( t.typename == "tuple" ) {
copy.is_va = t.is_va;
}
return copy;
}
var copy = resolve(typ);
if( errs ) {
return false, INVALID, errs;
}
return true, copy;
}
var function infer_var(emptytable: Type, t: Type, node: Node) {
var is_global = (emptytable.declared_at && emptytable.declared_at.kind == "global_declaration");
var nst = is_global && 1 || #st;
for( i = nst, 1, -1 ) {
var scope = st[i];
if( scope[emptytable.assigned_to] ) {
scope[emptytable.assigned_to] = {
t = t,
is_const = false,
};
t.inferred_at = node;
t.inferred_at_file = filename;
}
}
}
var function find_global(name: string): Type, boolean {
var scope = st[1];
if( scope[name] ) {
return scope[name].t, scope[name].is_const;
}
}
var function resolve_tuple(t: Type): Type {
if( t.typename == "tuple" ) {
t = t[1];
}
if( t == null ) {
return NIL;
}
return t;
}
var function node_warning(tag: tl.WarningKind, node: Node, fmt: string, ...: any) {
table.insert(warnings, {
y = node.y,
x = node.x,
msg = fmt->format(...),
filename = filename,
tag = tag,
});
}
var function node_error(node: Node, msg: string, ...:Type): Type {
type_error(node as Type, msg, ...);
node.type = INVALID;
return node.type;
}
var function terr(t: Type, s: string, ...: Type): {Error} {
return { error_in_type(t, s, ...) };
}
var function add_unknown(node: Node, name: string) {
node_warning("unknown", node, "unknown variable: %s", name);
}
var function redeclaration_warning(node: Node, old_var: Variable) {
if( node.tk->sub(1, 1) == "_" ) { return; }
if( old_var.declared_at ) {
node_warning("redeclaration", node, "redeclaration of variable '%s' (originally declared at %d:%d)", node.tk, old_var.declared_at.y, old_var.declared_at.x);
} else {
node_warning("redeclaration", node, "redeclaration of variable '%s'", node.tk);
}
}
var function check_if_redeclaration(new_name: string, at: Node) {
var old <const> = find_var(new_name, true);
if( old ) {
redeclaration_warning(at, old);
}
}
var function unused_warning(name: string, _v_var: Variable) {
var prefix <const> = name->sub(1,1);
if( _v_var.declared_at
&& ! _v_var.is_narrowed
&& prefix != "_"
&& prefix != "@"
) {
if( name->sub(1, 2) == "::" ) {
node_warning("unused", _v_var.declared_at, "unused label %s", name);
} else {
node_warning(
"unused",
_v_var.declared_at,
"unused %s %s: %s",
_v_var.is_func_arg && "argument"
|| _v_var.t.typename == "function" && "function"
|| is_typetype(_v_var.t) && "type"
|| "variable",
name,
show_type(_v_var.t)
);
}
}
}
var function shallow_copy(t: Type): Type {
var copy = {};
for( k, v in pairs(t as {any:any}) ) {
copy[k] = v;
}
return copy as Type;
}
var function reserve_symbol_list_slot(node: Node) {
symbol_list_n = symbol_list_n + 1;
node.symbol_list_slot = symbol_list_n;
}
var function add_var(node: Node, _v_var: string, valtype: Type, is_const: boolean, is_narrowing: boolean, dont_check_redeclaration): Variable {
if( lax && node && is_unknown(valtype) && (_v_var != "self" && _v_var != "...") && ! is_narrowing ) {
add_unknown(node, _v_var);
}
var scope <const> = st[#st];
var old_var <const> = scope[_v_var];
if( ! is_const ) {
valtype = shallow_copy(valtype);
valtype.tk = null;
}
if( old_var && is_narrowing ) {
if( ! old_var.is_narrowed ) {
old_var.narrowed_from = old_var.t;
}
old_var.is_narrowed = true;
old_var.t = valtype;
} else {
if( ! dont_check_redeclaration
&& node
&& ! is_narrowing
&& _v_var != "self"
&& _v_var != "..."
&& _v_var->sub(1, 1) != "@"
) {
check_if_redeclaration(_v_var, node);
}
scope[_v_var] = { t = valtype, is_const = is_const, is_narrowed = is_narrowing, declared_at = node };
if( old_var ) {
// the old var is removed from the scope and won't be checked when it closes,
// so check it here
if( ! old_var.used ) {
unused_warning(_v_var, old_var);
}
}
}
if( node && valtype.typename != "unresolved" && valtype.typename != "none" ) {
node.type = node.type || valtype;
var slot: integer;
if( node.symbol_list_slot ) {
slot = node.symbol_list_slot;
} else {
symbol_list_n = symbol_list_n + 1;
slot = symbol_list_n;
}
symbol_list[slot] = { y = node.y, x = node.x, name = _v_var, typ = assert(scope[_v_var].t) };
}
return scope[_v_var];
}
var type; CompareTypes = function(Type, Type, boolean): boolean, {Error} {
var function compare_and_infer_typevars(t1: Type, t2: Type, comp: CompareTypes): boolean, {Error} {
// if both are typevars and they are the same variable, nothing to do here
if( t1.typevar == t2.typevar ) {
return true;
}
// we have one typevar to compare to or infer to the other term
var typevar = t2.typevar || t1.typevar;
// does the typevar currently match to a type?
var vt = find_var_type(typevar);
if( vt ) {
// If so, compare it to the other type
if( t2.typevar ) {
return comp(t1, vt);
} else {
return comp(vt, t2);
}
} else {
// otherwise, infer it to the other type
var other = t2.typevar && t1 || t2;
var ok, resolved, errs = resolve_typevars(other);
if( ! ok ) {
return false, errs;
}
if( resolved.typename != "unknown" ) {
resolved = resolve_typetype(resolved);
add_var(null, typevar, resolved);
}
return true;
}
}
var function add_errs_prefixing(src: {Error}, dst: {Error}, prefix: string, node: Node) {
if( ! src ) {
return;
}
for( _, err in ipairs(src) ) {
err.msg = prefix .. err.msg;
// node.y may be nil because of `typ as Node` casts and not all types have .y set
if( node && node.y && (
(err.filename != filename)
|| (! err.y)
|| (node.y > err.y || (node.y == err.y && node.x > err.x))
) ) {
err.y = node.y;
err.x = node.x;
err.filename = filename;
}
table.insert(dst, err);
}
}
var is_a: function(Type, Type, boolean): boolean, {Error};
var type; TypeGetter = function(string): Type {
var function match_record_fields(t1: Type, t2: TypeGetter, cmp: CompareTypes): boolean, {Error} {
cmp = cmp || is_a;
var fielderrs: {Error} = {};
for( _, k in ipairs(t1.field_order) ) {
var f = t1.fields[k];
var t2k = t2(k);
if( t2k == null ) {
if( ! lax ) {
table.insert(fielderrs, error_in_type(f, "unknown field " .. k));
}
} else {
var __, errs = cmp(f, t2k);
add_errs_prefixing(errs, fielderrs, "record field doesn't match: " .. k .. ": ");
}
}
if( #fielderrs > 0 ) {
return false, fielderrs;
}
return true;
}
var function match_fields_to_record(t1: Type, t2: Type, cmp: CompareTypes): boolean, {Error} {
if( t1.is_userdata != t2.is_userdata ) {
return false, { error_in_type(t1, "userdata record doesn't match: %s", t2) };
}
var ok, fielderrs = match_record_fields(t1, function(k: string): Type { return t2.fields[k]; }, cmp);
if( ! ok ) {
var errs = {};
add_errs_prefixing(errs, fielderrs, show_type(t1) .. " is not a " .. show_type(t2) .. ": ");
return false, errs;
}
return true;
}
var function match_fields_to_map(t1: Type, t2: Type): boolean, {Error} {
if( ! match_record_fields(t1, function(_: string): Type { return t2.values; }) ) {
return false, { error_in_type(t1, "record is not a valid map; not all fields have the same type") };
}
return true;
}
var function arg_check(cmp: CompareTypes, a: Type, b: Type, at: Node, n: integer, errs: {Error}): boolean {
var matches, match_errs = cmp(a, b);
if( ! matches ) {
add_errs_prefixing(match_errs, errs, "argument " .. n .. ": ", at);
return false;
}
return true;
}
var same_type: function(t1: Type, t2: Type): boolean, {Error};
var function has_all_types_of(t1s: {Type}, t2s: {Type}, cmp: CompareTypes): boolean {
for( _, t1 in ipairs(t1s) ) {
var found = false;
for( _, t2 in ipairs(t2s) ) {
if( cmp(t2, t1) ) {
found = true;
break;
}
}
if( ! found ) {
return false;
}
}
return true;
}
var function any_errors(all_errs: {Error}): boolean, {Error} {
if( #all_errs == 0 ) {
return true;
} else {
return false, all_errs;
}
}
var function close_nested_records(t: Type) {
for( _, ft in pairs(t.fields) ) {
if( is_typetype(ft) ) {
ft.closed = true;
if( is_record_type(ft.def) ) {
close_nested_records(ft.def);
}
}
}
}
var function close_types(vars: {string:Variable}) {
for( _, _v_var in pairs(vars) ) {
if( is_typetype(_v_var.t) ) {
_v_var.t.closed = true;
if( is_record_type(_v_var.t.def) ) {
close_nested_records(_v_var.t.def);
}
}
}
}
var record Unused {
y: integer;
x: integer;
name: string;
_v_var: Variable;
}
var function check_for_unused_vars(vars: {string:Variable}) {
if( ! next(vars) ) {
return;
}
var list: {Unused} = {};
for( name, _v_var in pairs(vars) ) {
if( _v_var.declared_at && ! _v_var.used ) {
table.insert(list, { y = _v_var.declared_at.y, x = _v_var.declared_at.x, name = name, _v_var = _v_var });
}
}
if( list[1] ) {
table.sort(list, function(a: Unused, b: Unused): boolean {
return a.y < a.y || (a.y == b.y && a.x < b.x);
});
for( _, u in ipairs(list) ) {
unused_warning(u.name, u._v_var);
}
}
}
var function begin_scope(node: Node) {
table.insert(st, {});
if( node ) {
symbol_list_n = symbol_list_n + 1;
symbol_list[symbol_list_n] = { y = node.y, x = node.x, name = "@{" };
}
}
var function end_scope(node: Node) {
var unresolved = st[#st]["@unresolved"];
if( unresolved ) {
var upper = st[#st - 1]["@unresolved"];
if( upper ) {
for( name, nodes in pairs(unresolved.t.labels) ) {
for( _, n in ipairs(nodes) ) {
upper.t.labels[name] = upper.t.labels[name] || {};
table.insert(upper.t.labels[name], n);
}
}
for( name, types in pairs(unresolved.t.nominals) ) {
for( _, typ in ipairs(types) ) {
upper.t.nominals[name] = upper.t.nominals[name] || {};
table.insert(upper.t.nominals[name], typ);
}
}
} else {
st[#st - 1]["@unresolved"] = unresolved;
}
}
close_types(st[#st]);
check_for_unused_vars(st[#st]);
table.remove(st);
if( node ) {
if( symbol_list[symbol_list_n].name == "@{" ) {
symbol_list[symbol_list_n] = null;
symbol_list_n = symbol_list_n - 1;
} else {
symbol_list_n = symbol_list_n + 1;
symbol_list[symbol_list_n] = { y = assert(node.yend), x = assert(node.xend), name = "@}" };
}
}
}
var end_scope_and_none_type = function(node: Node, _children: {Type}): Type {
end_scope(node);
node.type = NONE;
return node.type;
};
var function resolve_typevars_at(t: Type, where: Node): Type {
assert(where);
var ok, typ, errs = resolve_typevars(t);
if( ! ok ) {
assert(where.y);
add_errs_prefixing(errs, errors, "", where);
}
return typ;
}
var resolve_nominal: function(t: Type): Type;
{
var function match_typevals(t: Type, def: Type): Type {
if( t.typevals && def.typeargs ) {
if( #t.typevals != #def.typeargs ) {
type_error(t, "mismatch in number of type arguments");
return null;
}
begin_scope();
for( i, tt in ipairs(t.typevals) ) {
add_var(null, def.typeargs[i].typearg, tt);
}
var ret = resolve_typevars_at(def, t as Node);
end_scope();
return ret;
} else if( t.typevals ) {
type_error(t, "spurious type arguments");
return null;
} else if( def.typeargs ) {
type_error(t, "missing type arguments in %s", def);
return null;
} else {
return def;
}
}
resolve_nominal = function(t: Type): Type {
if( t.resolved ) {
return t.resolved;
}
var resolved: Type;
var typetype = t.found || find_type(t.names);
if( ! typetype ) {
type_error(t, "unknown type %s", t);
} else if( is_typetype(typetype) ) {
if( typetype.is_alias ) {
typetype = typetype.def.found;
assert(is_typetype(typetype));
}
assert(typetype.def.typename != "nominal");
resolved = match_typevals(t, typetype.def);
} else {
type_error(t, table.concat(t.names, ".") .. " is not a type");
}
if( ! resolved ) {
resolved = a_type( { typename = "bad_nominal", names = t.names });
}
if( ! t.filename ) {
t.filename = resolved.filename;
if( t.x == null && t.y == null ) {
t.x = resolved.x;
t.y = resolved.y;
}
}
t.found = typetype;
t.resolved = resolved;
return resolved;
};
}
var function are_same_nominals(t1: Type, t2: Type): boolean, {Error} {
var same_names: boolean;
if( t1.found && t2.found ) {
same_names = t1.found.typeid == t2.found.typeid;
} else {
var ft1 = t1.found || find_type(t1.names);
var ft2 = t2.found || find_type(t2.names);
if( ft1 && ft2 ) {
same_names = ft1.typeid == ft2.typeid;
} else {
if( ! ft1 ) {
type_error(t1, "unknown type %s", t1);
}
if( ! ft2 ) {
type_error(t2, "unknown type %s", t2);
}
return false, {}; // errors were already produced
}
}
if( same_names ) {
if( t1.typevals == null && t2.typevals == null ) {
return true;
} else if( t1.typevals && t2.typevals && #t1.typevals == #t2.typevals ) {
var all_errs = {};
for( i = 1, #t1.typevals ) {
var _, errs = same_type(t1.typevals[i], t2.typevals[i]);
add_errs_prefixing(errs, all_errs, "type parameter <" .. show_type(t2.typevals[i]) .. ">: ", t1 as Node);
}
if( #all_errs == 0 ) {
return true;
} else {
return false, all_errs;
}
}
} else {
var t1name = show_type(t1);
var t2name = show_type(t2);
if( t1name == t2name ) {
var t1r = resolve_nominal(t1);
if( t1r.filename ) {
t1name = t1name .. " (defined in " .. t1r.filename .. ":" .. t1r.y .. ")";
}
var t2r = resolve_nominal(t2);
if( t2r.filename ) {
t2name = t2name .. " (defined in " .. t2r.filename .. ":" .. t2r.y .. ")";
}
}
return false, terr(t1, t1name .. " is not a " .. t2name);
}
}
var is_lua_table_type: function(t: Type): boolean;
var resolve_tuple_and_nominal: function(t: Type): Type = null;
// invariant type comparison
same_type = function(t1: Type, t2: Type): boolean, {Error} {
assert(type(t1) == "table");
assert(type(t2) == "table");
if( t1.typename == "typevar" || t2.typename == "typevar" ) {
return compare_and_infer_typevars(t1, t2, same_type);
}
if( t1.typename == "emptytable" && is_lua_table_type(resolve_tuple_and_nominal(t2)) ) {
return true;
}
if( t1.typename != t2.typename ) {
return false, terr(t1, "got %s, expected %s", t1, t2);
}
if( t1.typename == "array" ) {
return same_type(t1.elements, t2.elements);
} else if( t1.typename == "tupletable" ) {
var all_errs = {};
for( i = 1, math.min(#t1.types, #t2.types) ) {
var ok, err = same_type(t1.types[i], t2.types[i]);
if( ! ok ) {
add_errs_prefixing(err, all_errs, "values", t1 as Node);
}
}
return any_errors(all_errs);
} else if( t1.typename == "map" ) {
var all_errs = {};
var k_ok, k_errs = same_type(t1.keys, t2.keys);
if( ! k_ok ) {
add_errs_prefixing(k_errs, all_errs, "keys", t1 as Node);
}
var v_ok, v_errs = same_type(t1.values, t2.values);
if( ! v_ok ) {
add_errs_prefixing(v_errs, all_errs, "values", t1 as Node);
}
return any_errors(all_errs);
} else if( t1.typename == "union" ) {
if( has_all_types_of(t1.types, t2.types, same_type)
&& has_all_types_of(t2.types, t1.types, same_type) ) {
return true;
} else {
return false, terr(t1, "got %s, expected %s", t1, t2);
}
} else if( t1.typename == "nominal" ) {
return are_same_nominals(t1, t2);
} else if( t1.typename == "record" ) {
return match_fields_to_record(t1, t2, same_type)
&& match_fields_to_record(t2, t1, same_type);
} else if( t1.typename == "function" ) {
if( #t1.args != #t2.args ) {
return false, terr(t1, "different number of input arguments: got " .. #t1.args .. ", expected " .. #t2.args);
}
if( #t1.rets != #t2.rets ) {
return false, terr(t1, "different number of return values: got " .. #t1.args .. ", expected " .. #t2.args);
}
if( t1.is_method != t2.is_method ) {
return false, terr(t1, "method and non-method are not the same type");
}
var all_errs = {};
for( i = 1, #t1.args ) {
arg_check(same_type, t1.args[i], t2.args[i], t1 as Node, i, all_errs);
}
for( i = 1, #t1.rets ) {
var _, errs = same_type(t1.rets[i], t2.rets[i]);
add_errs_prefixing(errs, all_errs, "return " .. i, t1 as Node);
}
return any_errors(all_errs);
} else if( t1.typename == "arrayrecord" ) {
var ok, errs = same_type(t1.elements, t2.elements);
if( ! ok ) {
return ok, errs;
}
return match_fields_to_record(t1, t2, same_type)
&& match_fields_to_record(t2, t1, same_type);
}
return true;
};
var function unite(types: {Type}, flatten_constants: boolean): Type {
if( #types == 1 ) {
return types[1];
}
var ts: {Type} = {};
var stack: {Type} = {};
// Make things like number | number resolve to number
var types_seen: {(integer|string):boolean} = {};
// but never add nil as a type in the union
types_seen[NIL.typeid] = true;
types_seen["nil"] = true;
var i = 1;
while( types[i] || stack[1] ) {
var t: Type;
if( stack[1] ) {
t = table.remove(stack);
} else {
t = types[i];
i = i + 1;
}
t = resolve_tuple(t);
if( t.typename == "union" ) {
for( _, s in ipairs(t.types) ) {
table.insert(stack, s);
}
} else {
if( primitive[t.typename] && (flatten_constants || ! t.tk) ) {
if( ! types_seen[t.typename] ) {
types_seen[t.typename] = true;
table.insert(ts, t);
}
} else {
var typeid = t.typeid;
if( t.typename == "nominal" ) {
typeid = resolve_nominal(t).typeid;
}
if( ! types_seen[typeid] ) {
types_seen[typeid] = true;
table.insert(ts, t);
}
}
}
}
if( #ts == 1 ) {
return ts[1];
} else {
return a_type( {
typename = "union",
types = ts,
});
}
}
var function combine_errs(...: {Error}): boolean, {Error} {
var errs: {Error};
for( i = 1, select("#", ...) ) {
var e = select(i, ...);
if( e ) {
errs = errs || {};
for( _, err in ipairs(e) ) {
table.insert(errs, err);
}
}
}
if( ! errs ) {
return true;
} else {
return false, errs;
}
}
var known_table_types: {TypeName:boolean} = {
array = true,
map = true,
record = true,
arrayrecord = true,
tupletable = true,
};
// Is the type represented concretely as a Lua table?
is_lua_table_type = function(t: Type): boolean {
return known_table_types[t.typename] && ! t.is_userdata;
};
var expand_type: function(where: Node, old: Type, new: Type): Type;
var function arraytype_from_tuple(where: Node, tupletype: Type): Type, {Error} {
// first just try a basic union
var element_type = unite(tupletype.types);
var valid = element_type.typename != "union" && true || is_valid_union(element_type);
if( valid ) {
return a_type( {
elements = element_type,
typename = "array",
});
}
// failing a basic union, expand the types
var arr_type = a_type( {
elements = tupletype.types[1],
typename = "array",
});
for( i = 2, #tupletype.types ) {
arr_type = expand_type(where, arr_type, a_type( { elements = tupletype.types[i], typename = "array" }));
if( ! arr_type || ! arr_type.elements ) {
return null, terr(tupletype, "unable to convert tuple %s to array", tupletype);
}
}
return arr_type;
}
// subtyping comparison
is_a = function(t1: Type, t2: Type, for_equality: boolean): boolean, {Error} {
assert(type(t1) == "table");
assert(type(t2) == "table");
if( lax && (is_unknown(t1) || is_unknown(t2)) ) {
return true;
}
if( t1.typename == "bad_nominal" || t2.typename == "bad_nominal" ) {
return false; // an error has been generated elsewhere
}
// ∀ t, nil <: t
if( t1.typename == "nil" ) { // TODO nilable
return true;
}
if( t2.typename != "tuple" ) {
t1 = resolve_tuple(t1);
}
if( t2.typename == "tuple" && t1.typename != "tuple" ) {
t1 = a_type( {
typename = "tuple",
[1] = t1,
});
}
if( t1.typename == "typevar" || t2.typename == "typevar" ) {
return compare_and_infer_typevars(t1, t2, is_a);
}
// ∀ t, t <: any
if( t2.typename == "any" ) {
return true;
// ∀ t in t1, t <: t2
// ────────────────── -- a union type t1 is a t2
// t1 union <: t2 -- if all of t1's types satisfy t2
} else if( t1.typename == "union" ) {
for( _, t in ipairs(t1.types) ) {
if( ! is_a(t, t2, for_equality) ) {
return false, terr(t1, "got %s, expected %s", t1, t2);
}
}
return true;
// ∃ t in t2, t1 <: t
// ────────────────── -- a value of type t1 is a member of union type t2
// t1 <: t2 union -- if it is a member of some of t2's types
} else if( t2.typename == "union" ) {
for( _, t in ipairs(t2.types) ) {
if( is_a(t1, t, for_equality) ) {
return true;
}
}
// ∀ t in t2, t1 <: t
// ────────────────── -- a type t1 is a poly type t2
// t1 <: t2 poly -- if all of t2's poly types are satisfied by t1
} else if( t2.typename == "poly" ) {
for( _, t in ipairs(t2.types) ) {
if( ! is_a(t1, t, for_equality) ) {
return false, terr(t1, "cannot match against all alternatives of the polymorphic type");
}
}
return true;
// ∃ t in t1, t <: t2
// ────────────────── -- a poly type t1 is a t2 if
// t1 poly <: t2 -- t2 is some of of the poly's types
} else if( t1.typename == "poly" ) {
for( _, t in ipairs(t1.types) ) {
if( is_a(t, t2, for_equality) ) {
return true;
}
}
return false, terr(t1, "cannot match against any alternatives of the polymorphic type");
} else if( t1.typename == "nominal" && t2.typename == "nominal" ) {
var same, err = are_same_nominals(t1, t2);
if( same ) {
return true;
}
var t1r = resolve_tuple_and_nominal(t1);
var t2r = resolve_tuple_and_nominal(t2);
if( is_record_type(t1r) && is_record_type(t2r) ) {
return same, err;
} else {
return is_a(t1r, t2r, for_equality);
}
} else if( t1.typename == "enum" && t2.typename == "string" ) {
var ok: boolean;
if( for_equality ) {
ok = t2.tk && t1.enumset[unquote(t2.tk)];
} else {
ok = true;
}
if( ok ) {
return true;
} else {
return false, terr(t1, "enum is incompatible with %s", t2);
}
} else if( t1.typename == "integer" && t2.typename == "number" ) {
return true;
} else if( t1.typename == "string" && t2.typename == "enum" ) {
var ok = t1.tk && t2.enumset[unquote(t1.tk)];
if( ok ) {
return true;
} else {
if( t1.tk ) {
return false, terr(t1, "%s is not a member of %s", t1, t2);
} else {
return false, terr(t1, "string is not a %s", t2);
}
}
} else if( t1.typename == "nominal" || t2.typename == "nominal" ) {
var t1r = resolve_tuple_and_nominal(t1);
var t2r = resolve_tuple_and_nominal(t2);
var ok, errs = is_a(t1r, t2r, for_equality);
if( errs && #errs == 1 ) {
if( errs[1].msg->match("^got ") ) {
//local got = t1.typename == "nominal" and t1.name or show_type(t1)
//local expected = t2.typename == "nominal" and t2.name or show_type(t2)
errs = terr(t1, "got %s, expected %s", t1, t2);
}
}
return ok, errs;
} else if( t1.typename == "emptytable" && is_lua_table_type(t2) ) {
return true;
} else if( t2.typename == "array" ) {
if( is_array_type(t1) ) {
if( is_a(t1.elements, t2.elements) ) {
var t1e = resolve_tuple_and_nominal(t1.elements);
var t2e = resolve_tuple_and_nominal(t2.elements);
if( t2e.typename == "enum" && t1e.typename == "string" && #t1.types > 1 ) {
for( i = 2, #t1.types ) {
var t = t1.types[i];
if( ! is_a(t, t2e) ) {
return false, terr(t, "%s is not a member of %s", t, t2e);
}
}
}
return true;
}
} else if( t1.typename == "tupletable" ) {
if( t2.inferred_len && t2.inferred_len > #t1.types ) {
return false, terr(t1, "incompatible length, expected maximum length of " .. tostring(#t1.types) .. ", got " .. tostring(t2.inferred_len));
}
var t1a, err = arraytype_from_tuple(t1.inferred_at, t1);
if( ! t1a ) {
return false, err;
}
if( ! is_a(t1a, t2) ) {
return false, terr(t2, "got %s (from %s), expected %s", t1a, t1, t2);
}
return true;
} else if( t1.typename == "map" ) {
var _, errs_keys, errs_values: any, {Error}, {Error};
_, errs_keys = is_a(t1.keys, INTEGER);
_, errs_values = is_a(t1.values, t2.elements);
return combine_errs(errs_keys, errs_values);
}
} else if( t2.typename == "record" ) {
if( is_record_type(t1) ) {
return match_fields_to_record(t1, t2);
} else if( is_typetype(t1) && is_record_type(t1.def) ) { // record as prototype
return is_a(t1.def, t2, for_equality);
}
} else if( t2.typename == "arrayrecord" ) {
if( t1.typename == "array" ) {
return is_a(t1.elements, t2.elements);
} else if( t1.typename == "tupletable" ) {
if( t2.inferred_len && t2.inferred_len > #t1.types ) {
return false, terr(t1, "incompatible length, expected maximum length of " .. tostring(#t1.types) .. ", got " .. tostring(t2.inferred_len));
}
var t1a, err = arraytype_from_tuple(t1.inferred_at, t1);
if( ! t1a ) {
return false, err;
}
if( ! is_a(t1a, t2) ) {
return false, terr(t2, "got %s (from %s), expected %s", t1a, t1, t2);
}
return true;
} else if( t1.typename == "record" ) {
return match_fields_to_record(t1, t2);
} else if( t1.typename == "arrayrecord" ) {
if( ! is_a(t1.elements, t2.elements) ) {
return false, terr(t1, "array parts have incompatible element types");
}
return match_fields_to_record(t1, t2);
} else if( is_typetype(t1) && is_record_type(t1.def) ) { // record as prototype
return is_a(t1.def, t2, for_equality);
}
} else if( t2.typename == "map" ) {
if( t1.typename == "map" ) {
var _, errs_keys, errs_values: any, {Error}, {Error};
if( t2.keys.typename != "any" ) { // FIXME hack for {any:any}
_, errs_keys = same_type(t2.keys, t1.keys);
}
if( t2.values.typename != "any" ) { // FIXME hack for {any:any}
_, errs_values = same_type(t1.values, t2.values);
}
return combine_errs(errs_keys, errs_values);
} else if( t1.typename == "array" || t1.typename == "tupletable" ) {
var elements: Type;
if( t1.typename == "tupletable" ) {
var arr_type = arraytype_from_tuple(t1.inferred_at, t1);
if( ! arr_type ) {
return false, terr(t1, "Unable to convert tuple %s to map", t1);
}
elements = arr_type.elements;
} else {
elements = t1.elements;
}
var _, errs_keys, errs_values: any, {Error}, {Error};
_, errs_keys = is_a(INTEGER, t2.keys);
_, errs_values = is_a(elements, t2.values);
return combine_errs(errs_keys, errs_values);
} else if( is_record_type(t1) ) { // FIXME
if( ! is_a(t2.keys, STRING) ) {
return false, terr(t1, "can't match a record to a map with non-string keys");
}
if( t2.keys.typename == "enum" ) {
for( _, k in ipairs(t1.field_order) ) {
if( ! t2.keys.enumset[k] ) {
return false, terr(t1, "key is not an enum value: " .. k);
}
}
}
return match_fields_to_map(t1, t2);
}
} else if( t2.typename == "tupletable" ) {
if( t1.typename == "tupletable" ) {
for( i = 1, math.min(#t1.types, #t2.types) ) {
if( ! is_a(t1.types[i], t2.types[i], for_equality) ) {
return false, terr(t1, "in tuple entry " .. tostring(i) .. ": got %s, expected %s", t1.types[i], t2.types[i]);
}
}
if( for_equality && #t1.types != #t2.types ) {
return false, terr(t1, "tuples are not the same size");
}
if( #t1.types > #t2.types ) {
return false, terr(t1, "tuple %s is too big for tuple %s", t1, t2);
}
return true;
} else if( is_array_type(t1) ) {
if( t1.inferred_len && t1.inferred_len > #t2.types ) {
return false, terr(t1, "incompatible length, expected maximum length of " .. tostring(#t2.types) .. ", got " .. tostring(t1.inferred_len));
}
// for array literals (which is the only case where inferred_len is defined),
// only check the entries present
var len = (t1.inferred_len && t1.inferred_len > 0)
&& t1.inferred_len
|| #t2.types;
for( i = 1, len ) {
if( ! is_a(t1.elements, t2.types[i], for_equality) ) {
return false, terr(t1, "tuple entry " .. tostring(i) .. " of type %s does not match type of array elements, which is %s", t2.types[i], t1.elements);
}
}
return true;
}
} else if( t1.typename == "function" && t2.typename == "function" ) {
var all_errs = {};
if( (! t2.args.is_va) && #t1.args > #t2.args ) {
table.insert(all_errs, error_in_type(t1, "incompatible number of arguments: got " .. #t1.args .. " %s, expected " .. #t2.args .. " %s", t1.args, t2.args));
} else {
var f1 = t1.is_method && 2 || 1;
var f2 = t2.is_method && 2 || 1;
for( i = f1, #t1.args ) {
arg_check(is_a, t1.args[i], t2.args[f2] || ANY, null, i, all_errs);
f2 = f2 + 1;
}
}
var diff_by_va = #t2.rets - #t1.rets == 1 && t2.rets.is_va;
if( #t1.rets < #t2.rets && ! diff_by_va ) {
table.insert(all_errs, error_in_type(t1, "incompatible number of returns: got " .. #t1.rets .. " %s, expected " .. #t2.rets .. " %s", t1.rets, t2.rets));
} else {
var nrets = #t2.rets;
if( diff_by_va ) {
nrets = nrets - 1;
}
for( i = 1, nrets ) {
var _, errs = is_a(t1.rets[i], t2.rets[i]);
add_errs_prefixing(errs, all_errs, "return " .. i .. ": ");
}
}
if( #all_errs == 0 ) {
return true;
} else {
return false, all_errs;
}
} else if( lax && ((! for_equality) && t2.typename == "boolean") ) {
// in .lua files, all values can be used in a boolean context (but not in == or ~=)
return true;
} else if( t1.typename == t2.typename ) {
return true;
}
return false, terr(t1, "got %s, expected %s", t1, t2);
};
var function assert_is_a(node: Node, t1: Type, t2: Type, context: string, name: string): boolean {
t1 = resolve_tuple(t1);
t2 = resolve_tuple(t2);
if( lax && (is_unknown(t1) || is_unknown(t2)) ) {
return true;
}
// some flow-based inference
if( t1.typename == "nil" ) {
return true;
} else if( t2.typename == "unresolved_emptytable_value" ) {
if( is_number_type(t2.emptytable_type.keys) ) { // ideally integer only
infer_var(t2.emptytable_type, a_type( { typename = "array", elements = t1 }), node);
} else {
infer_var(t2.emptytable_type, a_type( { typename = "map", keys = t2.emptytable_type.keys, values = t1 }), node);
}
return true;
} else if( t2.typename == "emptytable" ) {
if( is_lua_table_type(t1) ) {
infer_var(t2, shallow_copy(t1), node);
} else if( t1.typename != "emptytable" ) {
node_error(node, context .. ": " .. (name && (name .. ": ") || "") .. "assigning %s to a variable declared with {}", t1);
}
return true;
}
var ok, match_errs = is_a(t1, t2);
add_errs_prefixing(match_errs, errors, context .. ": ".. (name && (name .. ": ") || ""), node);
return ok;
}
var unknown_dots: {string:boolean} = {};
var function add_unknown_dot(node: Node, name: string) {
if( ! unknown_dots[name] ) {
unknown_dots[name] = true;
add_unknown(node, name);
}
}
var function resolve_for_call(node: Node, func: Type, args: {Type}, is_method: boolean): Type, boolean {
// resolve unknown in lax mode, produce a general unknown function
if( lax && is_unknown(func) ) {
func = a_type( { typename = "function", args = VARARG( { UNKNOWN }), rets = VARARG( { UNKNOWN }) });
if( node.e1.op && node.e1.op.op == ":" && node.e1.e1.kind == "variable" ) {
add_unknown_dot(node, node.e1.e1.tk .. "." .. node.e1.e2.tk);
}
}
// unwrap if tuple, resolve if nominal
func = resolve_tuple_and_nominal(func);
if( func.typename != "function" && func.typename != "poly" ) {
// resolve if prototype
if( is_typetype(func) && func.def.typename == "record" ) {
func = func.def;
}
// resolve if metatable
if( func.meta_fields && func.meta_fields["__call"] ) {
table.insert(args, 1, func);
func = func.meta_fields["__call"];
is_method = true;
}
}
return func, is_method;
}
var type_check_function_call: function(Node, Type, {Type}, boolean, integer): Type;
{
var function mark_invalid_typeargs(f: Type) {
if( f.typeargs ) {
for( _, a in ipairs(f.typeargs) ) {
if( ! find_var(a.typearg) ) {
add_var(null, a.typearg, lax && UNKNOWN || INVALID);
}
}
}
}
var function try_match_func_args(node: Node, f: Type, args: {Type}, argdelta: integer): Type, {Error} {
var errs = {};
var given = #args;
var expected = #f.args;
var va = f.args.is_va;
var nargs = va
&& math.max(given, expected)
|| math.min(given, expected);
for( a = 1, nargs ) {
var argument = args[a];
var farg = f.args[a] || (va && f.args[expected]);
if( argument == null ) {
if( va ) {
break;
}
} else {
var at = node.e2 && node.e2[a] || node;
if( ! arg_check(is_a, argument, farg, at, (a + argdelta), errs) ) {
return null, errs;
}
}
}
mark_invalid_typeargs(f);
// resolve inference of emptytables used as arguments
for( a = 1, given ) {
var argument = args[a];
if( argument.typename == "emptytable" ) {
var farg = f.args[a] || (va && f.args[expected]);
var where = node.e2[a + argdelta] || node.e2; // for self, a + argdelta is 0
infer_var(argument, resolve_typevars_at(farg, where), where);
}
}
return resolve_typevars_at(f.rets, node);
}
var function revert_typeargs(func: Type) {
if( func.typeargs ) {
for( _, fnarg in ipairs(func.typeargs) ) {
if( st[#st][fnarg.typearg] ) {
st[#st][fnarg.typearg] = null;
}
}
}
}
var function fail_call(node: Node, func: Type, nargs: integer, errs: {Error}): Type {
if( errs ) {
// report the errors from the first match
for( _, err in ipairs(errs) ) {
table.insert(errors, err);
}
} else {
// found no arity match to try
var expects: {string} = {};
if( func.typename == "poly" ) {
for( _, f in ipairs(func.types) ) {
table.insert(expects, tostring(#f.args || 0));
}
table.sort(expects);
for( i = #expects, 1, -1 ) {
if( expects[i] == expects[i+1] ) {
table.remove(expects, i);
}
}
} else {
table.insert(expects, tostring(#func.args || 0));
}
node_error(node, "wrong number of arguments (given " .. nargs .. ", expects " .. table.concat(expects, " or ") .. ")");
}
var f = func.typename == "poly" && func.types[1] || func;
mark_invalid_typeargs(f);
return resolve_typevars_at(f.rets, node);
}
var function check_call(node: Node, func: Type, args: {Type}, is_method: boolean, argdelta: integer): Type {
assert(type(func) == "table");
assert(type(args) == "table");
if( ! (func.typename == "function" || func.typename == "poly") ) {
func, is_method = resolve_for_call(node, func, args, is_method);
}
argdelta = is_method && -1 || argdelta || 0;
var is_func = func.typename == "function";
var is_poly = func.typename == "poly";
if( ! (is_func || is_poly) ) {
return node_error(node, "not a function: %s", func);
}
var passes, n = 1, 1;
if( is_poly ) {
passes, n = 3, #func.types;
}
var given = #args;
var tried: {integer:boolean};
var first_errs: {Error};
for( pass = 1, passes ) {
for( i = 1, n ) {
if( (! tried) || ! tried[i] ) {
var f = is_func && func || func.types[i];
if( f.is_method && ! is_method && ! (args[1] && is_a(args[1], f.args[1])) ) {
return node_error(node, "invoked method as a regular function: use ':' instead of '.'");
}
var expected = #f.args;
// simple functions:
if( (is_func && (given <= expected || (f.args.is_va && given > expected)))
// poly, pass 1: try exact arity matches first
|| (is_poly && ((pass == 1 && given == expected)
// poly, pass 2: then try adjusting with nils to missing arguments or using '...'
|| (pass == 2 && given < expected)
// poly, pass 3: then finally try vararg functions
|| (pass == 3 && f.args.is_va && given > expected)))
) {
var matched, errs = try_match_func_args(node, f, args, argdelta);
if( matched ) {
// success!
return matched;
}
first_errs = first_errs || errs;
if( is_poly ) {
tried = tried || {};
tried[i] = true;
revert_typeargs(f);
}
}
}
}
}
return fail_call(node, func, given, first_errs);
}
type_check_function_call = function(node: Node, func: Type, args: {Type}, is_method: boolean, argdelta: integer): Type {
begin_scope();
var ret = check_call(node, func, args, is_method, argdelta);
end_scope();
return ret;
};
}
var function match_record_key(node: Node, tbl: Type, key: Node, orig_tbl: Type): Type {
assert(type(tbl) == "table");
assert(type(key) == "table");
tbl = resolve_tuple_and_nominal(tbl);
var type_description = tbl.typename;
if( tbl.typename == "string" || tbl.typename == "enum" ) {
tbl = find_var_type("string"); // simulate string metatable
}
if( lax && (is_unknown(tbl) || tbl.typename == "typevar") ) {
if( node.e1.kind == "variable" && node.op.op != "@funcall" ) {
add_unknown_dot(node, node.e1.tk .. "." .. key.tk);
}
return UNKNOWN;
}
if( tbl.is_alias ) {
return node_error(key, "cannot use a nested type alias as a concrete value");
}
tbl = resolve_typetype(tbl);
if( tbl.typename == "emptytable" ) {
} else if( is_record_type(tbl) ) {
assert(tbl.fields, "record has no fields!?");
if( key.kind == "string" || key.kind == "identifier" ) { // "identifier" is a plain atom (e.g 'insert' in 'table.insert')
if( tbl.fields[key.tk] ) {
return tbl.fields[key.tk];
}
}
} else {
if( is_unknown(tbl) ) {
if( lax ) {
return INVALID;
}
return node_error(key, "cannot index a value of unknown type");
}
return node_error(key, "cannot index something that is not a record: %s", tbl);
}
if( lax ) {
if( node.e1.kind == "variable" && node.op.op != "@funcall" ) {
add_unknown_dot(node, node.e1.tk .. "." .. key.tk);
}
return UNKNOWN;
}
var description: string;
if( node.e1.kind == "variable" ) {
description = type_description .. " '" .. node.e1.tk .. "' of type " .. show_type(resolve_tuple(orig_tbl));
} else {
description = "type " .. show_type(resolve_tuple(orig_tbl));
}
return node_error(key, "invalid key '" .. key.tk .. "' in " .. description);
}
var function widen_in_scope(scope: {string:Variable}, _v_var: string): boolean {
if( scope[_v_var].is_narrowed ) {
if( scope[_v_var].narrowed_from ) {
scope[_v_var].t = scope[_v_var].narrowed_from;
scope[_v_var].narrowed_from = null;
scope[_v_var].is_narrowed = false;
} else {
scope[_v_var] = null;
}
return true;
}
return false;
}
var function widen_back_var(_v_var: string): boolean {
var widened = false;
for( i = #st, 1, -1 ) {
if( st[i][_v_var] ) {
if( widen_in_scope(st[i], _v_var) ) {
widened = true;
} else {
break;
}
}
}
return widened;
}
var function widen_all_unions() {
for( i = #st, 1, -1 ) {
for( _v_var, _ in pairs(st[i]) ) {
widen_in_scope(st[i], _v_var);
}
}
}
var function add_global(node: Node, _v_var: string, valtype: Type, is_const: boolean) {
if( lax && is_unknown(valtype) && (_v_var != "self" && _v_var != "...") ) {
add_unknown(node, _v_var);
}
st[1][_v_var] = { t = valtype, is_const = is_const };
if( node ) {
node.type = node.type || valtype;
}
}
var function get_rets(rets: {Type}): Type {
if( lax && (#rets == 0) ) {
return VARARG( { UNKNOWN });
}
var t: Type = rets as Type;
if( ! t.typename ) {
t = TUPLE(t);
}
assert(t.typeid);
return t;
}
var function add_internal_function_variables(node: Node) {
add_var(null, "@is_va", node.args.type.is_va && ANY || NIL);
add_var(null, "@return", node.rets || a_type( { typename = "tuple" }));
}
var function add_function_definition_for_recursion(node: Node) {
var args: Type = a_type( { typename = "tuple" });
for( _, fnarg in ipairs(node.args) ) {
table.insert(args, fnarg.type);
}
add_var(null, node.name.tk, a_type( {
typename = "function",
args = args,
rets = get_rets(node.rets),
}));
}
var function fail_unresolved() {
var unresolved = st[#st]["@unresolved"];
if( unresolved ) {
st[#st]["@unresolved"] = null;
for( name, nodes in pairs(unresolved.t.labels) ) {
for( _, node in ipairs(nodes) ) {
node_error(node, "no visible label '" .. name .. "' for goto");
}
}
for( _, types in pairs(unresolved.t.nominals) ) {
for( _, typ in ipairs(types) ) {
assert(typ.x);
assert(typ.y);
type_error(typ, "unknown type %s", typ);
}
}
}
}
var function end_function_scope(node: Node) {
fail_unresolved();
end_scope(node);
}
resolve_tuple_and_nominal = function(t: Type): Type {
t = resolve_tuple(t);
if( t.typename == "nominal" ) {
t = resolve_nominal(t);
}
assert(t.typename != "nominal");
return t;
};
var function flatten_list(list: {Type}): {Type} {
var exps = {};
for( i = 1, #list - 1 ) {
table.insert(exps, resolve_tuple_and_nominal(list[i]));
}
if( #list > 0 ) {
var last = list[#list];
if( last.typename == "tuple" ) {
for( _, val in ipairs(last) ) {
table.insert(exps, val);
}
} else {
table.insert(exps, last);
}
}
return exps;
}
var function get_assignment_values(vals: Type, wanted: integer): {Type} {
var ret: {Type} = {};
if( vals == null ) {
return ret;
}
// get all arguments except the last...
var is_va = vals.is_va;
for( i = 1, #vals - 1 ) {
ret[i] = resolve_tuple(vals[i]);
}
var last = vals[#vals];
if( last.typename == "tuple" ) {
// ...if the last is a tuple, unpack it
is_va = last.is_va;
for( _, v in ipairs(last) ) {
table.insert(ret, v);
}
} else {
// ...otherwise simply get it
table.insert(ret, last);
}
// ...if the last is vararg, repeat its type until it matches the number of wanted args
if( is_va && last && #ret < wanted ) {
while( #ret < wanted ) {
table.insert(ret, last);
}
}
return ret;
}
var function match_all_record_field_names(node: Node, a: Type, field_names: {string}, errmsg: string): Type {
var t: Type;
for( _, k in ipairs(field_names) ) {
var f = a.fields[k];
if( ! t ) {
t = f;
} else {
if( ! same_type(f, t) ) {
t = null;
break;
}
}
}
if( t ) {
return t;
} else {
return node_error(node, errmsg);
}
}
var function type_check_index(node: Node, idxnode: Node, a: Type, b: Type): Type {
var orig_a = a;
var orig_b = b;
a = resolve_tuple_and_nominal(a);
b = resolve_tuple_and_nominal(b);
if( a.typename == "tupletable" && is_a(b, INTEGER) ) {
if( idxnode.constnum ) {
if( idxnode.constnum > #a.types
|| idxnode.constnum < 1
|| idxnode.constnum != math.floor(idxnode.constnum)
) {
return node_error(idxnode, "index " .. tostring(idxnode.constnum) .. " out of range for tuple %s", a);
}
return a.types[idxnode.constnum as integer];
} else {
var array_type = arraytype_from_tuple(idxnode, a);
if( ! array_type ) {
type_error(a, "cannot index this tuple with a variable because it would produce a union type that cannot be discriminated at runtime");
return INVALID;
}
return array_type.elements;
}
} else if( is_array_type(a) && is_a(b, INTEGER) ) {
return a.elements;
} else if( a.typename == "emptytable" ) {
if( a.keys == null ) {
a.keys = resolve_tuple(orig_b);
a.keys_inferred_at = assert(node);
a.keys_inferred_at_file = filename;
} else {
if( ! is_a(b, a.keys) ) {
var inferred = " (type of keys inferred at " .. a.keys_inferred_at_file .. ":" .. a.keys_inferred_at.y .. ":" .. a.keys_inferred_at.x .. ": )";
return node_error(idxnode, "inconsistent index type: %s, expected %s" .. inferred, orig_b, a.keys);
}
}
return a_type( { y = node.y, x = node.x, typename = "unresolved_emptytable_value", emptytable_type = a });
} else if( a.typename == "map" ) {
if( is_a(b, a.keys) ) {
return a.values;
} else {
return node_error(idxnode, "wrong index type: %s, expected %s", orig_b, a.keys);
}
} else if( node.e2.kind == "string" || node.e2.kind == "enum_item" ) {
return match_record_key(node, a, { y = node.e2.y, x = node.e2.x, kind = "string", tk = assert(node.e2.conststr) }, orig_a);
} else if( is_record_type(a) ) {
if( b.typename == "enum" ) {
var field_names: {string} = sorted_keys(b.enumset);
for( _, k in ipairs(field_names) ) {
if( ! a.fields[k] ) {
return node_error(idxnode, "enum value '" .. k .. "' is not a field in %s", a);
}
}
return match_all_record_field_names(idxnode, a, field_names,
"cannot index, not all enum values map to record fields of the same type");
} else if( is_a(b, STRING) ) {
return node_error(idxnode, "cannot index object of type %s with a string, consider using an enum", orig_a);
}
}
if( lax && is_unknown(a) ) {
return UNKNOWN;
} else {
return node_error(idxnode, "cannot index object of type %s with %s", orig_a, orig_b);
}
}
expand_type = function(where: Node, old: Type, new: Type): Type {
if( ! old || old.typename == "nil" ) {
return new;
} else {
if( ! is_a(new, old) ) {
if( old.typename == "map" && is_record_type(new) ) {
if( old.keys.typename == "string" ) {
for( _, ftype in fields_of(new) ) {
old.values = expand_type(where, old.values, ftype);
}
} else {
node_error(where, "cannot determine table literal type");
}
} else if( is_record_type(old) && is_record_type(new) ) {
old.typename = "map";
old.keys = STRING;
for( _, ftype in fields_of(old) ) {
if( ! old.values ) {
old.values = ftype;
} else {
old.values = expand_type(where, old.values, ftype);
}
}
for( _, ftype in fields_of(new) ) {
if( ! old.values ) {
new.values = ftype;
} else {
new.values = expand_type(where, old.values, ftype);
}
}
old.fields = null;
old.field_order = null;
} else if( old.typename == "union" ) {
new.tk = null;
table.insert(old.types, new);
} else {
old.tk = null;
new.tk = null;
return unite({ old, new });
}
}
}
return old;
};
var function find_record_to_extend(exp: Node): Type {
if( exp.kind == "type_identifier" ) {
var t = find_var_type(exp.tk);
if( ! t ) {
return t;
}
// FIXME assert(t.def)
if( t.def ) {
if( ! t.def.closed && ! t.closed ) {
return t.def;
}
}
if( ! t.closed ) {
return t;
}
} else if( exp.kind == "op" && exp.op.op == "." ) {
var t = find_record_to_extend(exp.e1);
if( ! t ) {
return null;
}
while( exp.e2.kind == "op" && exp.e2.op.op == "." ) {
t = t.fields && t.fields[exp.e2.e1.tk];
if( ! t ) {
return null;
}
exp = exp.e2;
}
t = t.fields && t.fields[exp.e2.tk];
return t;
}
}
// Inference engine for 'is' operator
var facts_and: function(f1: Fact, f2: Fact, where: Node): Fact;
var facts_or: function(f1: Fact, f2: Fact, where: Node): Fact;
var facts_not: function(f1: Fact, where: Node): Fact;
var apply_facts: function(where: Node, known: Fact);
var FACT_TRUTHY: Fact;
{
setmetatable(Fact, {
__call = function(_: Fact, fact: Fact): Fact {
return setmetatable(fact, {
__tostring = function(f: Fact): string {
if( f.fact == "is" ) {
return ("(%s is %s)")->format(f._v_var, show_type(f.typ));
} else if( f.fact == "==" ) {
return ("(%s == %s)")->format(f._v_var, show_type(f.typ));
} else if( f.fact == "truthy" ) {
return "*";
} else if( f.fact == "not" ) {
return ("(not %s)")->format(tostring(f.f1));
} else if( f.fact == "or" ) {
return ("(%s or %s)")->format(tostring(f.f1), tostring(f.f2));
} else if( f.fact == "and" ) {
return ("(%s and %s)")->format(tostring(f.f1), tostring(f.f2));
}
}
});
},
});
FACT_TRUTHY = Fact( { fact = "truthy" });
facts_and = function(f1: Fact, f2: Fact, where: Node): Fact {
return Fact({ fact = "and", f1 = f1, f2 = f2, where = where });
};
facts_or = function(f1: Fact, f2: Fact, where: Node): Fact {
if( f1 && f2 ) {
return Fact( { fact = "or", f1 = f1, f2 = f2, where = where });
} else {
return null;
}
};
facts_not = function(f1: Fact, where: Node): Fact {
if( f1 ) {
return Fact( { fact = "not", f1 = f1, where = where });
} else {
return null;
}
};
// t1 ∪ t2
var function unite_types(t1: Type, t2: Type): Type, string {
return unite({t2, t1});
}
// t1 ∩ t2
var function intersect_types(t1: Type, t2: Type): Type, string {
if( t2.typename == "union" ) {
t1, t2 = t2, t1;
}
if( t1.typename == "union" ) {
var out = {};
for( _, t in ipairs(t1.types) ) {
if( is_a(t, t2) ) {
table.insert(out, t);
}
}
return unite(out);
} else {
if( is_a(t1, t2) ) {
return t1;
} else if( is_a(t2, t1) ) {
return t2;
} else {
return INVALID;
}
}
}
var function resolve_if_union(t: Type): Type {
var rt = resolve_tuple_and_nominal(t);
if( rt.typename == "union" ) {
return rt;
}
return t;
}
// t1 - t2
var function subtract_types(t1: Type, t2: Type): Type {
var types: {Type} = {};
t1 = resolve_if_union(t1);
// poly are not first-class, so we don't handle them here
if( t1.typename != "union" ) {
return t1;
}
t2 = resolve_if_union(t2);
var t2types = t2.types || { t2 };
for( _, at in ipairs(t1.types) ) {
var not_present = true;
for( _, bt in ipairs(t2types) ) {
if( same_type(at, bt) ) {
not_present = false;
break;
}
}
if( not_present ) {
table.insert(types, at);
}
}
if( #types == 0 ) {
return INVALID;
}
return unite(types);
}
var eval_not: function(f: Fact): {string:Fact};
var not_facts: function(fs: {string:Fact}): {string:Fact};
var or_facts: function(fs1: {string:Fact}, fs2: {string:Fact}): {string:Fact};
var and_facts: function(fs1: {string:Fact}, fs2: {string:Fact}): {string:Fact};
var eval_fact: function(f: Fact): {string:Fact};
var function invalid_from(f: Fact): Fact {
return Fact( { fact = "is", _v_var = f._v_var, typ = INVALID, where = f.where });
}
not_facts = function(fs: {string:Fact}): {string:Fact} {
var ret: {string:Fact} = {};
for( _v_var, f in pairs(fs) ) {
var typ = find_var_type(f._v_var, true);
var fact: FactType = "==";
var where = f.where;
if( ! typ ) {
typ = INVALID;
} else {
if( f.fact == "is" ) {
if( typ.typename == "typevar" ) {
// nothing is known from negation on typeargs; widen back
where = null;
} else if( ! is_a(f.typ, typ) ) {
node_warning("branch", f.where, f._v_var .. " (of type %s) can never be a %s", show_type(typ), show_type(f.typ));
typ = INVALID;
} else {
fact = "is";
typ = subtract_types(typ, f.typ);
}
} else if( f.fact == "==" ) {
// nothing is known from negation of equality; widen back
where = null;
}
}
ret[_v_var] = Fact( { fact = fact, _v_var = _v_var, typ = typ, where = where });
}
return ret;
};
eval_not = function(f: Fact): {string:Fact} {
if( ! f ) {
return {};
} else if( f.fact == "is" ) {
return not_facts({[f._v_var] = f});
} else if( f.fact == "not" ) {
return eval_fact(f.f1);
} else if( f.fact == "and" && f.f2 && f.f2.fact == "truthy" ) {
return eval_not(f.f1);
} else if( f.fact == "or" && f.f2 && f.f2.fact == "truthy" ) {
return eval_fact(f.f1);
} else if( f.fact == "and" ) {
return or_facts(not_facts(eval_fact(f.f1)), not_facts(eval_fact(f.f2)));
} else if( f.fact == "or" ) {
return and_facts(not_facts(eval_fact(f.f1)), not_facts(eval_fact(f.f2)));
} else {
return not_facts(eval_fact(f));
}
};
or_facts = function(fs1: {string:Fact}, fs2: {string:Fact}): {string:Fact} {
var ret: {string:Fact} = {};
for( _v_var, f in pairs(fs2) ) {
if( fs1[_v_var] ) {
var fact: FactType = (fs1[_v_var].fact == "is" && f.fact == "is")
&& "is" || "==";
ret[_v_var] = Fact( { fact = fact, _v_var = _v_var, typ = unite_types(f.typ, fs1[_v_var].typ), where = f.where });
}
}
return ret;
};
and_facts = function(fs1: {string:Fact}, fs2: {string:Fact}): {string:Fact} {
var ret: {string:Fact} = {};
var has: {FactType:boolean} = {};
for( _v_var, f in pairs(fs1) ) {
var rt: Type;
var fact: FactType;
if( fs2[_v_var] ) {
fact = (fs2[_v_var].fact == "is" && f.fact == "is") && "is" || "==";
rt = intersect_types(f.typ, fs2[_v_var].typ);
} else {
fact = "==";
rt = f.typ;
}
ret[_v_var] = Fact( { fact = fact, _v_var = _v_var, typ = rt, where = f.where });
has[fact] = true;
}
for( _v_var, f in pairs(fs2) ) {
if( ! fs1[_v_var] ) {
ret[_v_var] = Fact( { fact = "==", _v_var = _v_var, typ = f.typ, where = f.where });
has["=="] = true;
}
}
if( has["is"] && has["=="] ) {
for( _, f in pairs(ret) ) {
f.fact = "==";
}
}
return ret;
};
eval_fact = function(f: Fact): {string:Fact} {
if( ! f ) {
return {};
} else if( f.fact == "is" ) {
var typ = find_var_type(f._v_var, true);
if( ! typ ) {
return { [f._v_var] = invalid_from(f) };
}
if( typ.typename != "typevar" && is_a(typ, f.typ) ) {
node_warning("branch", f.where, f._v_var .. " (of type %s) is always a %s", show_type(typ), show_type(f.typ));
return { [f._v_var] = f };
} else if( typ.typename != "typevar" && ! is_a(f.typ, typ) ) {
node_error(f.where, f._v_var .. " (of type %s) can never be a %s", typ, f.typ);
return { [f._v_var] = invalid_from(f) };
} else {
return { [f._v_var] = f };
}
} else if( f.fact == "==" ) {
return { [f._v_var] = f };
} else if( f.fact == "not" ) {
return eval_not(f.f1);
} else if( f.fact == "truthy" ) {
return {};
} else if( f.fact == "and" && f.f2 && f.f2.fact == "truthy" ) {
return eval_fact(f.f1);
} else if( f.fact == "or" && f.f2 && f.f2.fact == "truthy" ) {
return eval_not(f.f1);
} else if( f.fact == "and" ) {
return and_facts(eval_fact(f.f1), eval_fact(f.f2));
} else if( f.fact == "or" ) {
return or_facts(eval_fact(f.f1), eval_fact(f.f2));
}
};
apply_facts = function(where: Node, known: Fact) {
if( ! known ) {
return;
}
var facts = eval_fact(known);
for( v, f in pairs(facts) ) {
if( f.typ.typename == "invalid" ) {
node_error(where, "cannot resolve a type for " .. v .. " here");
}
var t = shallow_copy(f.typ); // new type object
t.inferred_at = f.where && where;
t.inferred_at_file = filename;
add_var(null, v, t, true, true);
}
};
}
var function dismiss_unresolved(name: string) {
var unresolved = st[#st]["@unresolved"];
if( unresolved ) {
if( unresolved.t.nominals[name] ) {
for( _, t in ipairs(unresolved.t.nominals[name]) ) {
resolve_nominal(t);
}
}
unresolved.t.nominals[name] = null;
}
}
var type_check_funcall: function(node: Node, a: Type, b: {Type}, argdelta: integer): Type;
var function special_pcall_xpcall(node: Node, _a: Type, b: {Type}, argdelta: integer): Type {
var base_nargs = (node.e1.tk == "xpcall") && 2 || 1;
if( #node.e2 < base_nargs ) {
node_error(node, "wrong number of arguments (given " .. #node.e2 .. ", expects at least " .. base_nargs .. ")");
return TUPLE( { BOOLEAN });
}
var ftype = table.remove(b, 1);
var fe2: Node = {};
if( node.e1.tk == "xpcall" ) {
base_nargs = 2;
var msgh = table.remove(b, 1);
assert_is_a(node.e2[2], msgh, XPCALL_MSGH_FUNCTION, "in message handler");
}
for( i = base_nargs + 1, #node.e2 ) {
table.insert(fe2, node.e2[i]);
}
var fnode: Node = {
y = node.y,
x = node.x,
kind = "op",
op = { op = "@funcall" },
e1 = node.e2[1],
e2 = fe2,
};
var rets = type_check_funcall(fnode, ftype, b, argdelta + base_nargs);
if( rets.typename != "tuple" ) {
rets = a_type( { typename = "tuple", rets });
}
table.insert(rets, 1, BOOLEAN);
return rets;
}
var special_functions: {string : function(Node,Type,{Type},integer):Type } = {
["rawget"] = function(node: Node, _a: Type, b: {Type}, _argdelta: integer): Type {
// TODO should those offsets be fixed by _argdelta?
if( #b == 2 ) {
var b1 = resolve_tuple_and_nominal(b[1]);
var b2 = resolve_tuple_and_nominal(b[2]);
var knode = node.e2[2];
if( is_record_type(b1) && knode.conststr ) {
return match_record_key(node, b1, { y = knode.y, x = knode.x, kind = "string", tk = assert(knode.conststr) }, b1);
} else {
return type_check_index(node, knode, b1, b2);
}
} else {
return node_error(node, "rawget expects two arguments");
}
},
["print_type"] = function(node: Node, _a: Type, b: {Type}, _argdelta: integer): Type {
// TODO should those offsets be fixed by _argdelta?
if( #b == 0 ) {
// when called with no arguments, print all variables currently in scope and their types.
print("-----------------------------------------");
for( i, scope in ipairs(st) ) {
for( s, v in pairs(scope) ) {
print(("%2d %-14s %-11s %s")->format(i, s, v.t.typename, show_type(v.t)->sub(1, 50)));
}
}
print("-----------------------------------------");
return NONE;
} else {
var t = show_type(b[1]);
print(t);
node_warning("debug", node.e2[1], "type is: %s", t);
return b;
}
},
["require"] = function(node: Node, _a: Type, b: {Type}, _argdelta: integer): Type {
if( #b != 1 ) {
return node_error(node, "require expects one literal argument");
}
if( node.e2[1].kind != "string" ) {
return node_error(node, "don't know how to resolve a dynamic require");
}
var module_name = assert(node.e2[1].conststr);
var t, found = require_module(module_name, lax, env);
if( ! found ) {
return node_error(node, "module not found: '" .. module_name .. "'");
}
if( t.typename == "invalid" ) {
if( lax ) {
return UNKNOWN;
}
return node_error(node, "no type information for required module: '" .. module_name .. "'");
}
dependencies[module_name] = t.filename;
return t;
},
["pcall"] = special_pcall_xpcall,
["xpcall"] = special_pcall_xpcall,
["assert"] = function(node: Node, a: Type, b: {Type}, argdelta: integer): Type {
node.known = FACT_TRUTHY;
return type_check_function_call(node, a, b, false, argdelta);
},
};
type_check_funcall = function(node: Node, a: Type, b: {Type}, argdelta: integer): Type {
argdelta = argdelta || 0;
if( node.e1.kind == "variable" ) {
var special = special_functions[node.e1.tk];
if( special ) {
return special(node, a, b, argdelta);
} else {
return type_check_function_call(node, a, b, false, argdelta);
}
} else if( node.e1.op && node.e1.op.op == ":" ) {
table.insert(b, 1, node.e1.e1.type);
return type_check_function_call(node, a, b, true);
} else {
return type_check_function_call(node, a, b, false, argdelta);
}
};
// is the i-th assignment in a local declaration of the form `x = x` ?
var function is_localizing_a_variable(node: Node, i: integer): boolean {
return node.exps
&& node.exps[i]
&& node.exps[i].kind == "variable"
&& node.exps[i].tk == node.vars[i].tk;
}
var function resolve_nominal_typetype(typetype: Type): Type, boolean {
if( typetype.def.typename == "nominal" ) {
if( typetype.def.typevals ) {
typetype.def = resolve_nominal(typetype.def);
typetype.def.typeargs = null;
} else {
var names = typetype.def.names;
var found = find_type(names);
if( (! found) || (! is_typetype(found)) ) {
type_error(typetype, "%s is not a type", typetype);
found = a_type( { typename = "bad_nominal", names = names });
}
return found, true;
}
}
return typetype, false;
}
var function missing_initializer(node: Node, i: integer, name: string): Type {
if( lax ) {
return UNKNOWN;
} else {
if( node.exps ) {
return node_error(node.vars[i], "assignment in declaration did not produce an initial value for variable '" .. name .. "'");
} else {
return node_error(node.vars[i], "variable '" .. name .. "' has no type or initial value");
}
}
}
var function set_expected_types_to_decltypes(node: Node, children: {Type}) {
var decls = node.kind == "assignment" && children[1] || node.decltype;
if( decls && node.exps ) {
var ndecl = #decls;
var nexps = #node.exps;
for( i = 1, nexps ) {
var typ: Type;
typ = decls[i];
if( typ ) {
if( i == nexps && ndecl > nexps ) {
typ = a_type( { y = node.y, x = node.x, filename = filename, typename = "tuple", types = {} });
for( a = i, ndecl ) {
table.insert(typ.types, decls[a]);
}
}
node.exps[i].expected = typ;
node.exps[i].expected_context = { kind = node.kind, name = node.vars[i].tk };
}
}
}
}
var function is_positive_int(n: number): boolean {
return n && n >= 1 && math.floor(n) == n;
}
var context_name: {NodeKind: string} = {
["local_declaration"] = "in local declaration",
["global_declaration"] = "in global declaration",
["assignment"] = "in assignment",
};
var function in_context(ctx: Node.ExpectedContext, msg: string): string {
if( ! ctx ) {
return msg;
}
var where = context_name[ctx.kind];
if( where ) {
return where .. ": " .. (ctx.name && ctx.name .. ": " || "") .. msg;
} else {
return msg;
}
}
var function check_redeclared_key(ctx: Node.ExpectedContext, where: Node, seen_keys: {(string | number):Node}, ck: string, n: number) {
var key: string | number = ck || n;
if( key ) {
var s = seen_keys[key];
if( s ) {
node_error(where, in_context(ctx, "redeclared key " .. tostring(key) .. " (previously declared at " .. filename .. ":" .. s.y .. ":" .. s.x .. ")"));
} else {
seen_keys[key] = where;
}
}
}
var function infer_table_literal(node: Node, children: {Type}): Type {
var typ = a_type( {
filename = filename,
y = node.y,
x = node.x,
typename = "emptytable",
});
var is_record = false;
var is_array = false;
var is_map = false;
var is_tuple = false;
var is_not_tuple = false;
var last_array_idx = 1;
var largest_array_idx = -1;
var seen_keys: {(number | string):Node} = {};
for( i, child in ipairs(children) ) {
assert(child.typename == "table_item");
var ck = child.kname;
var n = node[i].key.constnum;
check_redeclared_key(null, node[i], seen_keys, ck, n);
var uvtype = resolve_tuple(child.vtype);
if( ck ) {
is_record = true;
if( ! typ.fields ) {
typ.fields = {};
typ.field_order = {};
}
typ.fields[ck] = uvtype;
table.insert(typ.field_order, ck);
} else if( is_number_type(child.ktype) ) {
is_array = true;
if( ! is_not_tuple ) {
is_tuple = true;
}
if( ! typ.types ) {
typ.types = {};
}
if( node[i].key_parsed == "implicit" ) {
if( i == #children && child.vtype.typename == "tuple" ) {
// need to expand last item in an array (e.g { 1, 2, 3, f() })
for( _, c in ipairs(child.vtype) ) {
typ.elements = expand_type(node, typ.elements, c);
typ.types[last_array_idx] = resolve_tuple(c);
last_array_idx = last_array_idx + 1;
}
} else {
typ.types[last_array_idx] = uvtype;
last_array_idx = last_array_idx + 1;
typ.elements = expand_type(node, typ.elements, uvtype);
}
} else { // explicit
if( ! is_positive_int(n) ) {
typ.elements = expand_type(node, typ.elements, uvtype);
is_not_tuple = true;
} else if( n ) {
typ.types[n as integer] = uvtype;
if( n > largest_array_idx ) {
largest_array_idx = n as integer;
}
typ.elements = expand_type(node, typ.elements, uvtype);
}
}
if( last_array_idx > largest_array_idx ) {
largest_array_idx = last_array_idx;
}
if( ! typ.elements ) {
is_array = false;
}
} else {
is_map = true;
child.ktype.tk = null;
typ.keys = expand_type(node, typ.keys, child.ktype);
typ.values = expand_type(node, typ.values, uvtype);
}
}
if( is_array && is_map ) {
typ.typename = "map";
typ.keys = expand_type(node, typ.keys, INTEGER);
typ.values = expand_type(node, typ.values, typ.elements);
typ.elements = null;
node_error(node, "cannot determine type of table literal");
} else if( is_record && is_array ) {
typ.typename = "arrayrecord";
} else if( is_record && is_map ) {
if( typ.keys.typename == "string" ) {
typ.typename = "map";
for( _, ftype in fields_of(typ) ) {
typ.values = expand_type(node, typ.values, ftype);
}
typ.fields = null;
typ.field_order = null;
} else {
node_error(node, "cannot determine type of table literal");
}
} else if( is_array ) {
if( is_not_tuple ) {
typ.typename = "array";
typ.inferred_len = largest_array_idx - 1;
} else {
var pure_array = true;
var last_t: Type;
for( _, current_t in pairs(typ.types as {integer:Type}) ) {
if( last_t ) {
if( ! same_type(last_t, current_t) ) {
pure_array = false;
break;
}
}
last_t = current_t;
}
if( ! pure_array ) {
typ.typename = "tupletable";
} else {
typ.typename = "array";
typ.inferred_len = largest_array_idx - 1;
}
}
} else if( is_record ) {
typ.typename = "record";
} else if( is_map ) {
typ.typename = "map";
} else if( is_tuple ) {
typ.typename = "tupletable";
if( ! typ.types || #typ.types == 0 ) {
node_error(node, "cannot determine type of tuple elements");
}
}
return typ;
}
var visit_node: Visitor<NodeKind, Node, Type> = {};
visit_node.cbs = {
["statements"] = {
before = function(node: Node) {
begin_scope(node);
},
after = function(node: Node, _children: {Type}): Type {
// if at the top level
if( #st == 2 ) {
fail_unresolved();
}
if( ! node.is_repeat ) {
end_scope(node);
}
// TODO extract node type from `return`
node.type = NONE;
return node.type;
}
},
["local_type"] = {
before = function(node: Node) {
node.value.type, node.value.is_alias = resolve_nominal_typetype(node.value.newtype);
add_var(node._v_var, node._v_var.tk, node.value.type, node._v_var.is_const);
},
after = function(node: Node, _children: {Type}): Type {
dismiss_unresolved(node._v_var.tk);
node.type = NONE;
return node.type;
},
},
["global_type"] = {
before = function(node: Node) {
node.value.newtype, node.value.is_alias = resolve_nominal_typetype(node.value.newtype);
add_global(node._v_var, node._v_var.tk, node.value.newtype, node._v_var.is_const);
},
after = function(node: Node, _children: {Type}): Type {
var existing, existing_is_const = find_global(node._v_var.tk);
var _v_var = node._v_var;
if( existing ) {
if( existing_is_const == true && ! _v_var.is_const ) {
node_error(_v_var, "global was previously declared as <const>: " .. _v_var.tk);
}
if( existing_is_const == false && _v_var.is_const ) {
node_error(_v_var, "global was previously declared as not <const>: " .. _v_var.tk);
}
if( ! same_type(existing, node.value.newtype) ) {
node_error(_v_var, "cannot redeclare global with a different type: previous type of " .. _v_var.tk .. " is %s", existing);
}
}
dismiss_unresolved(_v_var.tk);
node.type = NONE;
return node.type;
},
},
["local_declaration"] = {
before = function(node: Node) {
for( _, _v_var in ipairs(node.vars) ) {
reserve_symbol_list_slot(_v_var);
}
},
before_expressions = set_expected_types_to_decltypes,
after = function(node: Node, children: {Type}): Type {
var vals: {Type} = get_assignment_values(children[3], #node.vars);
for( i, _v_var in ipairs(node.vars) ) {
var decltype = node.decltype && node.decltype[i];
var infertype = vals && vals[i];
if( lax && infertype && infertype.typename == "nil" ) {
infertype = null;
}
if( decltype && infertype ) {
assert_is_a(node.vars[i], infertype, decltype, "in local declaration", _v_var.tk);
}
var t = decltype || infertype;
if( t == null ) {
t = missing_initializer(node, i, _v_var.tk);
} else if( t.typename == "emptytable" ) {
t.declared_at = node;
t.assigned_to = _v_var.tk;
}
t.inferred_len = null;
assert(_v_var);
add_var(_v_var, _v_var.tk, t, _v_var.is_const, is_localizing_a_variable(node, i));
dismiss_unresolved(_v_var.tk);
}
node.type = NONE;
return node.type;
},
},
["global_declaration"] = {
before_expressions = set_expected_types_to_decltypes,
after = function(node: Node, children: {Type}): Type {
var vals: {Type} = get_assignment_values(children[3], #node.vars);
for( i, _v_var in ipairs(node.vars) ) {
var decltype = node.decltype && node.decltype[i];
var infertype = vals && vals[i];
if( lax && infertype && infertype.typename == "nil" ) {
infertype = null;
}
if( decltype && infertype ) {
assert_is_a(node.vars[i], infertype, decltype, "in global declaration", _v_var.tk);
}
var t = decltype || infertype;
var existing, existing_is_const = find_global(_v_var.tk);
if( existing ) {
if( infertype && existing_is_const ) {
node_error(_v_var, "cannot reassign to <const> global: " .. _v_var.tk);
}
if( existing_is_const == true && ! _v_var.is_const ) {
node_error(_v_var, "global was previously declared as <const>: " .. _v_var.tk);
}
if( existing_is_const == false && _v_var.is_const ) {
node_error(_v_var, "global was previously declared as not <const>: " .. _v_var.tk);
}
if( t && ! same_type(existing, t) ) {
node_error(_v_var, "cannot redeclare global with a different type: previous type of " .. _v_var.tk .. " is %s", existing);
}
} else {
if( t == null ) {
t = missing_initializer(node, i, _v_var.tk);
} else if( t.typename == "emptytable" ) {
t.declared_at = node;
t.assigned_to = _v_var.tk;
}
t.inferred_len = null;
add_global(_v_var, _v_var.tk, t, _v_var.is_const);
_v_var.type = t;
dismiss_unresolved(_v_var.tk);
}
}
node.type = NONE;
return node.type;
},
},
["assignment"] = {
before_expressions = set_expected_types_to_decltypes,
after = function(node: Node, children: {Type}): Type {
var vals: {Type} = get_assignment_values(children[3], #children[1]);
var exps = flatten_list(vals);
for( i, vartype in ipairs(children[1]) ) {
var varnode = node.vars[i];
var is_const = varnode.is_const;
if( varnode.kind == "variable" ) {
if( widen_back_var(varnode.tk) ) {
vartype, is_const = find_var_type(varnode.tk);
}
}
if( is_const ) {
node_error(varnode, "cannot assign to <const> variable");
}
if( vartype ) {
var val = exps[i];
if( is_typetype(resolve_tuple_and_nominal(vartype)) ) {
node_error(varnode, "cannot reassign a type");
} else if( val ) {
assert_is_a(varnode, val, vartype, "in assignment");
if( varnode.kind == "variable" && vartype.typename == "union" ) {
// narrow union
add_var(varnode, varnode.tk, val, false, true);
}
} else {
node_error(varnode, "variable is not being assigned a value");
if( #node.exps == 1 && node.exps[1].kind == "op" && node.exps[1].op.op == "@funcall" ) {
var rets = node.exps[1].type;
if( rets.typename == "tuple" ) {
var msg = #rets == 1
&& "only 1 value is returned by the function"
|| ("only " .. #rets .. " values are returned by the function");
node_warning("hint", varnode, msg);
}
}
}
} else {
node_error(varnode, "unknown variable");
}
}
node.type = NONE;
return node.type;
},
},
["if"] = {
after = function(node: Node, _children: {Type}): Type {
node.type = NONE;
return node.type;
},
},
["if_block"] = {
before = function(node: Node) {
begin_scope(node);
if( node.if_block_n > 1 ) {
var ifnode = node.if_parent;
var f = facts_not(ifnode.if_blocks[1].exp.known, node);
for( e = 2, node.if_block_n - 1 ) {
f = facts_and(f, facts_not(ifnode.if_blocks[e].exp.known, node), node);
}
apply_facts(node, f);
}
},
before_statements = function(node: Node) {
if( node.exp ) {
apply_facts(node.exp, node.exp.known);
}
},
after = end_scope_and_none_type,
},
["while"] = {
before = function() {
// widen all narrowed variables because we don't calculate a fixpoint yet
widen_all_unions();
},
before_statements = function(node: Node) {
begin_scope(node);
apply_facts(node.exp, node.exp.known);
},
after = end_scope_and_none_type,
},
["label"] = {
before = function(node: Node) {
// widen all narrowed variables because we don't calculate a fixpoint yet
widen_all_unions();
var label_id = "::" .. node.label .. "::";
if( st[#st][label_id] ) {
node_error(node, "label '" .. node.label .. "' already defined at " .. filename );
}
var unresolved = st[#st]["@unresolved"];
node.type = a_type( { y = node.y, x = node.x, typename = "none" });
var _v_var = add_var(node, label_id, node.type);
if( unresolved ) {
if( unresolved.t.labels[node.label] ) {
_v_var.used = true;
}
unresolved.t.labels[node.label] = null;
}
},
},
["goto"] = {
after = function(node: Node, _children: {Type}): Type {
if( ! find_var_type("::" .. node.label .. "::") ) {
var unresolved = st[#st]["@unresolved"] && st[#st]["@unresolved"].t;
if( ! unresolved ) {
unresolved = { typename = "unresolved", labels = {}, nominals = {} };
add_var(node, "@unresolved", unresolved);
}
unresolved.labels[node.label] = unresolved.labels[node.label] || {};
table.insert(unresolved.labels[node.label], node);
}
node.type = NONE;
return node.type;
},
},
["repeat"] = {
before = function() {
// widen all narrowed variables because we don't calculate a fixpoint yet
widen_all_unions();
},
// only end scope after checking `until`, `statements` in repeat body has is_repeat == true
after = end_scope_and_none_type,
},
["forin"] = {
before = function(node: Node) {
begin_scope(node);
},
before_statements = function(node: Node) {
var exp1 = node.exps[1];
var args = {node.exps[2] && node.exps[2].type,
node.exps[3] && node.exps[3].type};
var exp1type = resolve_for_call(exp1, exp1.type, args);
if( exp1type.typename == "function" ) {
// check common errors:
if( exp1.op && exp1.op.op == "@funcall" ) {
var t = resolve_tuple_and_nominal(exp1.e2.type);
if( exp1.e1.tk == "pairs" && is_array_type(t) ) {
node_warning("hint", exp1, "hint: applying pairs on an array: did you intend to apply ipairs?");
}
if( exp1.e1.tk == "pairs" && t.typename != "map" ) {
if( ! (lax && is_unknown(t)) ) {
if( is_record_type(t) ) {
match_all_record_field_names(exp1.e2, t, t.field_order,
"attempting pairs loop on a record with attributes of different types");
var ct = t.typename == "record" && "{string:any}" || "{any:any}";
node_warning("hint", exp1.e2, "hint: if you want to iterate over fields of a record, cast it to " .. ct);
} else {
node_error(exp1.e2, "cannot apply pairs on values of type: %s", exp1.e2.type);
}
}
} else if( exp1.e1.tk == "ipairs" ) {
if( t.typename == "tupletable" ) {
var arr_type = arraytype_from_tuple(exp1.e2, t);
if( ! arr_type ) {
node_error(exp1.e2, "attempting ipairs loop on tuple that's not a valid array: %s", exp1.e2.type);
}
} else if( ! is_array_type(t) ) {
if( ! (lax && (is_unknown(t) || t.typename == "emptytable")) ) {
node_error(exp1.e2, "attempting ipairs loop on something that's not an array: %s", exp1.e2.type);
}
}
}
}
// TODO: check that exp1's arguments match with (optional self, explicit iterator, state)
var last: Type;
var rets = exp1type.rets;
for( i, v in ipairs(node.vars) ) {
var r = rets[i];
if( ! r ) {
if( rets.is_va ) {
r = last;
} else {
r = lax && UNKNOWN || INVALID;
}
}
add_var(v, v.tk, r);
last = r;
}
if( (! lax) && (! rets.is_va && #node.vars > #rets) ) {
var nrets = #rets;
var at = node.vars[nrets + 1];
var n_values = nrets == 1 && "1 value" || tostring(nrets) .. " values";
node_error(at, "too many variables for this iterator; it produces " .. n_values);
}
} else {
if( ! (lax && is_unknown(exp1type)) ) {
node_error(exp1, "expression in for loop does not return an iterator");
}
}
},
after = end_scope_and_none_type,
},
["fornum"] = {
before_statements = function(node: Node, children: {Type}) {
begin_scope(node);
var from_t = resolve_tuple_and_nominal(children[2]);
var to_t = resolve_tuple_and_nominal(children[3]);
var step_t = children[4] && resolve_tuple_and_nominal(children[4]);
var t = (from_t.typename == "integer" &&
to_t.typename == "integer" &&
(! step_t || step_t.typename == "integer"))
&& INTEGER
|| NUMBER;
add_var(node._v_var, node._v_var.tk, t);
},
after = end_scope_and_none_type,
},
["return"] = {
after = function(node: Node, children: {Type}): Type {
var rets = find_var_type("@return");
if( ! rets ) {
// if at the toplevel
rets = children[1];
rets.inferred_at = node;
rets.inferred_at_file = filename;
module_type = resolve_tuple_and_nominal(rets);
module_type.tk = null;
st[2]["@return"] = { t = rets };
}
var what = "in return value";
if( rets.inferred_at ) {
what = what .. inferred_msg(rets);
}
var nrets = #rets;
var vatype: Type;
if( nrets > 0 ) {
vatype = rets.is_va && rets[nrets];
}
if( #children[1] > nrets && (! lax) && ! vatype ) {
node_error(node, "in " .. what ..": excess return values, expected " .. #rets .. " %s, got " .. #children[1] .. " %s", rets, children[1]);
}
for( i = 1, #children[1] ) {
var expected = rets[i] || vatype;
if( expected ) {
expected = resolve_tuple(expected);
var where = (node.exps[i] && node.exps[i].x)
&& node.exps[i]
|| node.exps;
assert(where && where.x);
assert_is_a(where, children[1][i], expected, what);
}
}
node.type = NONE;
return node.type;
},
},
["variable_list"] = {
after = function(node: Node, children: {Type}): Type {
node.type = TUPLE(children);
// explode last tuple: (1, 2, (3, 4)) becomes (1, 2, 3, 4)
var n = #children;
if( n > 0 && children[n].typename == "tuple" ) {
if( children[n].is_va ) {
node.type.is_va = true;
}
var tuple = children[n];
for( i, c in ipairs(tuple) ) {
children[n + i - 1] = c;
}
}
return node.type;
},
},
["table_literal"] = {
before = function(node: Node) {
if( node.expected ) {
var decltype = resolve_tuple_and_nominal(node.expected);
if( decltype.typename == "tupletable" ) {
for( _, child in ipairs(node) ) {
var n = child.key.constnum;
if( n && is_positive_int(n) ) {
child.value.expected = decltype.types[n as integer];
}
}
} else if( is_array_type(decltype) ) {
for( _, child in ipairs(node) ) {
if( child.key.constnum ) {
child.value.expected = decltype.elements;
}
}
} else if( decltype.typename == "map" ) {
for( _, child in ipairs(node) ) {
child.key.expected = decltype.keys;
child.value.expected = decltype.values;
}
}
if( is_record_type(decltype) ) {
for( _, child in ipairs(node) ) {
if( child.key.conststr ) {
child.value.expected = decltype.fields[child.key.conststr];
}
}
}
}
},
after = function(node: Node, children: {Type}): Type {
node.known = FACT_TRUTHY;
if( node.expected ) {
var decltype = resolve_tuple_and_nominal(node.expected);
if( decltype.typename == "union" ) {
for( _, t in ipairs(decltype.types) ) {
var rt = resolve_tuple_and_nominal(t);
if( is_lua_table_type(rt) ) {
node.expected = t;
decltype = rt;
break;
}
}
if( decltype.typename == "union" ) {
node_error(node, "unexpected table literal, expected: %s", decltype);
}
}
if( ! is_lua_table_type(decltype) ) {
node.type = infer_table_literal(node, children);
return node.type;
}
var is_record = is_record_type(decltype);
var is_array = is_array_type(decltype);
var is_tupletable = decltype.typename == "tupletable";
var is_map = decltype.typename == "map";
var force_array: Type = null;
var seen_keys: {(number | string):Node} = {};
for( i, child in ipairs(children) ) {
assert(child.typename == "table_item");
var cvtype = resolve_tuple(child.vtype);
var ck = child.kname;
var n = node[i].key.constnum;
check_redeclared_key(node.expected_context, node[i], seen_keys, ck, n);
if( is_record && ck ) {
var df = decltype.fields[ck];
if( ! df ) {
node_error(node[i], in_context(node.expected_context, "unknown field " .. ck));
} else {
assert_is_a(node[i], cvtype, df, "in record field", ck);
}
} else if( is_tupletable && is_number_type(child.ktype) ) {
var dt = decltype.types[n as integer];
if( ! n ) {
node_error(node[i], in_context(node.expected_context, "unknown index in tuple %s"), decltype);
} else if( ! dt ) {
node_error(node[i], in_context(node.expected_context, "unexpected index " .. n .. " in tuple %s"), decltype);
} else {
assert_is_a(node[i], cvtype, dt, in_context(node.expected_context, "in tuple"), "at index " .. tostring(n));
}
} else if( is_array && is_number_type(child.ktype) ) {
if( child.vtype.typename == "tuple" && i == #children && node[i].key_parsed == "implicit" ) {
// need to expand last item in an array (e.g { 1, 2, 3, f() })
for( ti, tt in ipairs(child.vtype) ) {
assert_is_a(node[i], tt, decltype.elements, in_context(node.expected_context, "expected an array"), "at index " .. tostring(i + ti - 1));
}
} else {
assert_is_a(node[i], cvtype, decltype.elements, in_context(node.expected_context, "expected an array"), "at index " .. tostring(n));
}
} else if( node[i].key_parsed == "implicit" ) {
force_array = expand_type(node[i], force_array, child.vtype);
} else if( is_map ) {
assert_is_a(node[i], child.ktype, decltype.keys, in_context(node.expected_context, "in map key"));
assert_is_a(node[i], cvtype, decltype.values, in_context(node.expected_context, "in map value"));
} else {
node_error(node[i], in_context(node.expected_context, "unexpected key of type %s in table of type %s"), child.ktype, decltype);
}
}
if( force_array ) {
node.type = a_type( {
inferred_at = node,
inferred_at_file = filename,
typename = "array",
elements = force_array,
});
} else {
node.type = resolve_typevars_at(node.expected, node);
}
} else {
node.type = infer_table_literal(node, children);
}
return node.type;
},
},
["table_item"] = {
after = function(node: Node, children: {Type}): Type {
var kname = node.key.conststr;
var ktype = children[1];
var vtype = children[2];
if( node.decltype ) {
vtype = node.decltype;
assert_is_a(node.value, children[2], node.decltype, "in table item");
}
node.type = a_type( {
y = node.y,
x = node.x,
typename = "table_item",
kname = kname,
ktype = ktype,
vtype = vtype,
});
return node.type;
},
},
["local_function"] = {
before = function(node: Node) {
reserve_symbol_list_slot(node);
begin_scope(node);
},
before_statements = function(node: Node) {
add_internal_function_variables(node);
add_function_definition_for_recursion(node);
},
after = function(node: Node, children: {Type}): Type {
end_function_scope(node);
var rets = get_rets(children[3]);
add_var(node, node.name.tk, a_type( {
y = node.y,
x = node.x,
typename = "function",
typeargs = node.typeargs,
args = children[2],
rets = rets,
filename = filename,
}));
return node.type;
},
},
["global_function"] = {
before = function(node: Node) {
begin_scope(node);
},
before_statements = function(node: Node) {
add_internal_function_variables(node);
add_function_definition_for_recursion(node);
},
after = function(node: Node, children: {Type}): Type {
end_function_scope(node);
add_global(node, node.name.tk, a_type( {
y = node.y,
x = node.x,
typename = "function",
typeargs = node.typeargs,
args = children[2],
rets = get_rets(children[3]),
filename = filename,
}));
return node.type;
},
},
["record_function"] = {
before = function(node: Node) {
begin_scope(node);
},
before_statements = function(node: Node, children: {Type}) {
add_internal_function_variables(node);
var rtype = resolve_tuple_and_nominal(resolve_typetype(children[1]));
var owner = find_record_to_extend(node.fn_owner);
if( node.is_method ) {
children[3][1] = rtype;
add_var(null, "self", rtype);
}
if( rtype.typename == "emptytable" ) {
rtype.typename = "record";
rtype.fields = {};
rtype.field_order = {};
}
if( is_record_type(rtype) ) {
var fn_type = a_type( {
y = node.y,
x = node.x,
typename = "function",
is_method = node.is_method,
typeargs = node.typeargs,
args = children[3],
rets = get_rets(children[4]),
filename = filename,
});
var ok = true;
if( rtype.fields[node.name.tk] && is_a(fn_type, rtype.fields[node.name.tk]) ) {
ok = true;
} else if( lax || owner == rtype ) {
rtype.fields[node.name.tk] = fn_type;
table.insert(rtype.field_order, node.name.tk);
ok = true;
} else {
ok = false;
}
if( ok ) {
node.name.type = fn_type;
} else {
var name = tl.pretty_print_ast(node.fn_owner, { preserve_indent = true, preserve_newlines = false });
node_error(node, "cannot add undeclared function '" .. node.name.tk .. "' outside of the scope where '" .. name .. "' was originally declared");
}
} else {
if( ! (lax && rtype.typename == "unknown") ) {
node_error(node, "not a module: %s", rtype);
}
}
},
after = function(node: Node, _children: {Type}): Type {
end_function_scope(node);
node.type = NONE;
return node.type;
},
},
["function"] = {
before = function(node: Node) {
begin_scope(node);
},
before_statements = function(node: Node) {
add_internal_function_variables(node);
},
after = function(node: Node, children: {Type}): Type {
end_function_scope(node);
// children[1] args
// children[2] body
node.type = a_type( {
y = node.y,
x = node.x,
typename = "function",
typeargs = node.typeargs,
args = children[1],
rets = children[2],
filename = filename,
});
return node.type;
},
},
["cast"] = {
after = function(node: Node, _children: {Type}): Type {
node.type = node.casttype;
return node.type;
}
},
["paren"] = {
after = function(node: Node, children: {Type}): Type {
node.known = node.e1 && node.e1.known;
node.type = resolve_tuple(children[1]);
return node.type;
},
},
["op"] = {
before = function() {
begin_scope();
},
before_e2 = function(node: Node) {
if( node.op.op == "and" ) {
apply_facts(node, node.e1.known);
} else if( node.op.op == "or" ) {
apply_facts(node, facts_not(node.e1.known, node));
} else if( node.op.op == "@funcall" ) {
if( node.e1.type.typename == "function" ) {
var argdelta = (node.e1.op && node.e1.op.op == ":") && -1 || 0;
for( i, typ in ipairs(node.e1.type.args) ) {
if( node.e2[i + argdelta] ) {
node.e2[i + argdelta].expected = typ;
}
}
}
apply_facts(node, facts_not(node.e1.known, node));
} else if( node.op.op == "@index" ) {
if( node.e1.type.typename == "map" ) {
node.e2.expected = node.e1.type.keys;
}
}
},
after = function(node: Node, children: {Type}): Type {
end_scope();
var a: Type = children[1];
var b: Type = children[3];
var orig_a = a;
var orig_b = b;
var ra = a && resolve_tuple_and_nominal(a);
var rb = b && resolve_tuple_and_nominal(b);
if( ra && is_typetype(ra) && ra.def.typename == "record" ) {
ra = ra.def;
}
if( rb && is_typetype(rb) && rb.def.typename == "record" ) {
rb = rb.def;
}
if( node.op.op == "." ) {
a = ra;
if( a.typename == "map" ) {
if( is_a(a.keys, STRING) || is_a(a.keys, ANY) ) {
node.type = a.values;
} else {
node_error(node, "cannot use . index, expects keys of type %s", a.keys);
}
} else {
node.type = match_record_key(node, a, { y = node.e2.y, x = node.e2.x, kind = "string", tk = node.e2.tk }, orig_a);
if( node.type.needs_compat && opts.gen_compat != "off" ) {
// only apply to a literal use, not a propagated type
if( node.e1.kind == "variable" && node.e2.kind == "identifier" ) {
var key = node.e1.tk .. "." .. node.e2.tk;
node.kind = "variable";
node.tk = "_tl_" .. node.e1.tk .. "_" .. node.e2.tk;
all_needs_compat[key] = true;
}
}
}
} else if( node.op.op == "@funcall" ) {
node.type = type_check_funcall(node, a, b);
} else if( node.op.op == "@index" ) {
node.type = type_check_index(node, node.e2, a, b);
} else if( node.op.op == "as" ) {
node.type = b;
} else if( node.op.op == "is" ) {
if( rb.typename == "integer" ) {
all_needs_compat["math"] = true;
}
if( ra.typename == "typetype" ) {
node_error(node, "can only use 'is' on variables, not types");
} else if( node.e1.kind == "variable" ) {
node.known = Fact( { fact = "is", _v_var = node.e1.tk, typ = b, where = node });
} else {
node_error(node, "can only use 'is' on variables");
}
node.type = BOOLEAN;
} else if( node.op.op == ":" ) {
node.type = match_record_key(node, node.e1.type, node.e2, orig_a);
} else if( node.op.op == "not" ) {
node.known = facts_not(node.e1.known, node);
node.type = BOOLEAN;
} else if( node.op.op == "and" ) {
node.known = facts_and(node.e1.known, node.e2.known, node);
node.type = resolve_tuple(b);
} else if( node.op.op == "or" && is_lua_table_type(ra) && b.typename == "emptytable" ) {
node.known = null;
node.type = resolve_tuple(a);
} else if( node.op.op == "or" && is_a(rb, ra) ) {
node.known = facts_or(node.e1.known, node.e2.known);
node.type = resolve_tuple(a);
} else if( node.op.op == "or" && b.typename == "nil" ) {
node.known = null;
node.type = resolve_tuple(a);
} else if( node.op.op == "or"
&& ((ra.typename == "enum" && rb.typename == "string" && is_a(rb, ra))
|| (ra.typename == "string" && rb.typename == "enum" && is_a(ra, rb))) ) {
node.known = null;
node.type = (ra.typename == "enum" && ra || rb);
} else if( node.op.op == "or" && node.expected && node.expected.typename == "union" ) {
// must be checked after string/enum above
node.known = facts_or(node.e1.known, node.e2.known);
var u = unite({ra, rb}, true);
var valid, err = is_valid_union(u);
node.type = valid && u || node_error(node, err);
} else if( node.op.op == "==" || node.op.op == "~=" ) {
node.type = BOOLEAN;
if( is_a(b, a, true) || a.typename == "typevar" ) {
if( node.op.op == "==" && node.e1.kind == "variable" ) {
node.known = Fact( { fact = "==", _v_var = node.e1.tk, typ = b, where = node });
}
} else if( is_a(a, b, true) || b.typename == "typevar" ) {
if( node.op.op == "==" && node.e2.kind == "variable" ) {
node.known = Fact( { fact = "==", _v_var = node.e2.tk, typ = a, where = node });
}
} else if( lax && (is_unknown(a) || is_unknown(b)) ) {
node.type = UNKNOWN;
} else {
return node_error(node, "types are not comparable for equality: %s and %s", a, b);
}
} else if( node.op.arity == 1 && unop_types[node.op.op] ) {
a = ra;
if( a.typename == "union" ) {
a = unite(a.types, true); // squash unions of string constants
}
var types_op = unop_types[node.op.op];
node.type = types_op[a.typename];
var metamethod: Type;
if( node.type ) {
if( node.type.typename != "boolean" ) {
node.known = FACT_TRUTHY;
}
} else {
metamethod = a.meta_fields && a.meta_fields[unop_to_metamethod[node.op.op] || ""];
if( metamethod ) {
node.type = resolve_tuple_and_nominal(type_check_function_call(node, metamethod, {a}, false, 0));
} else if( lax && is_unknown(a) ) {
node.type = UNKNOWN;
} else {
return node_error(node, "cannot use operator '" .. node.op.op->gsub("%%", "%%%%") .. "' on type %s", resolve_tuple(orig_a));
}
}
if( node.op.op == "~" && env.gen_target == "5.1" ) {
if( metamethod ) {
all_needs_compat["mt"] = true;
convert_node_to_compat_mt_call(node, unop_to_metamethod[node.op.op], 1, node.e1);
} else {
all_needs_compat["bit32"] = true;
convert_node_to_compat_call(node, "bit32", "bnot", node.e1);
}
}
} else if( node.op.arity == 2 && binop_types[node.op.op] ) {
if( node.op.op == "or" ) {
node.known = facts_or(node.e1.known, node.e2.known);
}
a = ra;
b = rb;
if( a.typename == "union" ) {
a = unite(a.types, true); // squash unions of string constants
}
if( b.typename == "union" ) {
b = unite(b.types, true); // squash unions of string constants
}
var types_op = binop_types[node.op.op];
node.type = types_op[a.typename] && types_op[a.typename][b.typename];
var metamethod: Type;
var meta_self = 1;
if( node.type ) {
if( types_op == numeric_binop || node.op.op == ".." ) {
node.known = FACT_TRUTHY;
}
} else {
metamethod = a.meta_fields && a.meta_fields[binop_to_metamethod[node.op.op] || ""];
if( ! metamethod ) {
metamethod = b.meta_fields && b.meta_fields[binop_to_metamethod[node.op.op] || ""];
meta_self = 2;
}
if( metamethod ) {
node.type = resolve_tuple_and_nominal(type_check_function_call(node, metamethod, {a, b}, false, 0));
} else if( lax && (is_unknown(a) || is_unknown(b)) ) {
node.type = UNKNOWN;
} else {
return node_error(node, "cannot use operator '" .. node.op.op->gsub("%%", "%%%%") .. "' for types %s and %s", resolve_tuple(orig_a), resolve_tuple(orig_b));
}
}
if( node.op.op == "//" && env.gen_target == "5.1" ) {
if( metamethod ) {
all_needs_compat["mt"] = true;
convert_node_to_compat_mt_call(node, "__idiv", meta_self, node.e1, node.e2);
} else {
var div: Node = { y = node.y, x = node.x, kind = "op", op = an_operator(node, 2, "/"), e1 = node.e1, e2 = node.e2 };
convert_node_to_compat_call(node, "math", "floor", div);
}
} else if( bit_operators[node.op.op] && env.gen_target == "5.1" ) {
if( metamethod ) {
all_needs_compat["mt"] = true;
convert_node_to_compat_mt_call(node, binop_to_metamethod[node.op.op], meta_self, node.e1, node.e2);
} else {
all_needs_compat["bit32"] = true;
convert_node_to_compat_call(node, "bit32", bit_operators[node.op.op], node.e1, node.e2);
}
}
} else {
error("unknown node op " .. node.op.op);
}
return node.type;
},
},
["variable"] = {
after = function(node: Node, _children: {Type}): Type {
if( node.tk == "..." ) {
var va_sentinel = find_var_type("@is_va");
if( ! va_sentinel || va_sentinel.typename == "nil" ) {
return node_error(node, "cannot use '...' outside a vararg function");
}
}
if( node.tk == "_G" ) {
node.type, node.is_const = simulate_g();
} else {
node.type, node.is_const = find_var_type(node.tk);
}
if( node.type && is_typetype(node.type) ) {
node.type = a_type( {
y = node.y,
x = node.x,
typename = "nominal",
names = { node.tk },
found = node.type,
resolved = node.type,
});
}
if( node.type == null ) {
node.type = a_type( { typename = "unknown" });
if( lax ) {
add_unknown(node, node.tk);
} else {
return node_error(node, "unknown variable: " .. node.tk);
}
}
return node.type;
},
},
["type_identifier"] = {
after = function(node: Node, _children: {Type}): Type {
node.type, node.is_const = find_var_type(node.tk);
if( node.type == null ) {
if( lax ) {
node.type = UNKNOWN;
add_unknown(node, node.tk);
} else {
return node_error(node, "unknown variable: " .. node.tk);
}
}
return node.type;
},
},
["argument"] = {
after = function(node: Node, _children: {Type}): Type {
var t = node.decltype;
if( ! t ) {
t = UNKNOWN;
}
if( node.tk == "..." ) {
t = a_type( { typename = "tuple", is_va = true, t });
}
add_var(node, node.tk, t).is_func_arg = true;
return node.type;
},
},
["identifier"] = {
after = function(node: Node, _children: {Type}): Type {
node.type = node.type || NONE; // type is resolved elsewhere
return node.type;
},
},
["newtype"] = {
after = function(node: Node, _children: {Type}): Type {
node.type = node.type || node.newtype;
return node.type;
},
},
["error_node"] = {
after = function(node: Node, _children: {Type}): Type {
node.type = INVALID;
return node.type;
},
}
};
visit_node.cbs["string"] = {
after = function(node: Node, _children: {Type}): Type {
node.type = a_type( {
y = node.y,
x = node.x,
typename = node.kind as TypeName,
tk = node.tk,
});
node.known = FACT_TRUTHY;
return node.type;
},
};
visit_node.cbs["number"] = visit_node.cbs["string"];
visit_node.cbs["integer"] = visit_node.cbs["string"];
visit_node.cbs["boolean"] = {
after = function(node: Node, _children: {Type}): Type {
node.type = a_type( {
y = node.y,
x = node.x,
typename = node.kind as TypeName,
tk = node.tk,
});
if( node.tk == "true" ) {
node.known = FACT_TRUTHY;
}
return node.type;
},
};
visit_node.cbs["nil"] = visit_node.cbs["boolean"];
visit_node.cbs["do"] = visit_node.cbs["if"];
visit_node.cbs["..."] = visit_node.cbs["variable"];
visit_node.cbs["break"] = visit_node.cbs["if"];
visit_node.cbs["argument_list"] = visit_node.cbs["variable_list"];
visit_node.cbs["expression_list"] = visit_node.cbs["variable_list"];
visit_node.after = function(node: Node, _children: {Type}): Type {
if( type(node.type) != "table" ) {
error(node.kind .. " did not produce a type");
}
if( type(node.type.typename) != "string" ) {
error(node.kind .. " type does not have a typename");
}
return node.type;
};
var visit_type: Visitor<TypeName,Type,Type> = {
cbs = {
["string"] = {
after = function(typ: Type, _children: {Type}): Type {
return typ;
},
},
["function"] = {
before = function(_typ: Type, _children: {Type}) {
begin_scope();
},
after = function(typ: Type, _children: {Type}): Type {
end_scope();
return typ;
},
},
["record"] = {
before = function(typ: Type, _children: {Type}) {
begin_scope();
for( name, typ2 in fields_of(typ) ) {
if( typ2.typename == "typetype" ) {
typ2.typename = "nestedtype";
var resolved, is_alias = resolve_nominal_typetype(typ2);
if( is_alias ) {
typ2.is_alias = true;
typ2.def.resolved = resolved;
}
add_var(null, name, resolved);
}
}
},
after = function(typ: Type, _children: {Type}): Type {
end_scope();
for( _, typ2 in fields_of(typ) ) {
if( typ2.typename == "nestedtype" ) {
typ2.typename = "typetype";
}
}
return typ;
},
},
["typearg"] = {
after = function(typ: Type, _children: {Type}): Type {
add_var(null, typ.typearg, a_type( {
y = typ.y,
x = typ.x,
typename = "typearg",
typearg = typ.typearg,
}));
return typ;
},
},
["typevar"] = {
after = function(typ: Type, _children: {Type}): Type {
if( ! find_var_type(typ.typevar) ) {
type_error(typ, "undefined type variable " .. typ.typevar);
}
return typ;
},
},
["nominal"] = {
after = function(typ: Type, _children: {Type}): Type {
if( typ.found ) {
return typ;
}
var t = find_type(typ.names, true);
if( t ) {
if( t.typename == "typearg" ) {
// convert nominal into a typevar
typ.names = null;
typ.typename = "typevar";
typ.typevar = t.typearg;
} else {
typ.found = t;
}
} else {
var name = typ.names[1];
var unresolved = find_var_type("@unresolved");
if( ! unresolved ) {
unresolved = { typename = "unresolved", labels = {}, nominals = {} };
add_var(null, "@unresolved", unresolved);
}
unresolved.nominals[name] = unresolved.nominals[name] || {};
table.insert(unresolved.nominals[name], typ);
}
return typ;
},
},
["union"] = {
after = function(typ: Type, _children: {Type}): Type {
var valid, err = is_valid_union(typ);
if( ! valid ) {
type_error(typ, err, typ);
}
return typ;
}
},
},
after = function(typ: Type, _children: {Type}, ret: Type): Type {
if( type(ret) != "table" ) {
error(typ.typename .. " did not produce a type");
}
if( type(ret.typename) != "string" ) {
error("type node does not have a typename");
}
return ret;
}
};
if( ! opts.run_internal_compiler_checks ) {
visit_node.after = null;
visit_type.after = null;
}
visit_type.cbs["tupletable"] = visit_type.cbs["string"];
visit_type.cbs["typetype"] = visit_type.cbs["string"];
visit_type.cbs["nestedtype"] = visit_type.cbs["string"];
visit_type.cbs["array"] = visit_type.cbs["string"];
visit_type.cbs["map"] = visit_type.cbs["string"];
visit_type.cbs["arrayrecord"] = visit_type.cbs["record"];
visit_type.cbs["enum"] = visit_type.cbs["string"];
visit_type.cbs["boolean"] = visit_type.cbs["string"];
visit_type.cbs["nil"] = visit_type.cbs["string"];
visit_type.cbs["number"] = visit_type.cbs["string"];
visit_type.cbs["integer"] = visit_type.cbs["string"];
visit_type.cbs["thread"] = visit_type.cbs["string"];
visit_type.cbs["bad_nominal"] = visit_type.cbs["string"];
visit_type.cbs["emptytable"] = visit_type.cbs["string"];
visit_type.cbs["table_item"] = visit_type.cbs["string"];
visit_type.cbs["unresolved_emptytable_value"] = visit_type.cbs["string"];
visit_type.cbs["tuple"] = visit_type.cbs["string"];
visit_type.cbs["poly"] = visit_type.cbs["string"];
visit_type.cbs["any"] = visit_type.cbs["string"];
visit_type.cbs["unknown"] = visit_type.cbs["string"];
visit_type.cbs["invalid"] = visit_type.cbs["string"];
visit_type.cbs["unresolved"] = visit_type.cbs["string"];
visit_type.cbs["none"] = visit_type.cbs["string"];
assert(ast.kind == "statements");
recurse_node(ast, visit_node, visit_type);
close_types(st[1]);
check_for_unused_vars(st[1]);
clear_redundant_errors(errors);
add_compat_entries(ast, all_needs_compat, env.gen_compat);
var result = {
ast = ast,
env = env,
type = module_type,
filename = filename,
warnings = warnings,
type_errors = errors,
symbol_list = symbol_list,
dependencies = dependencies,
};
env.loaded[filename] = result;
table.insert(env.loaded_order, filename);
return result;
};
//------------------------------------------------------------------------------
// Report types
//------------------------------------------------------------------------------
var typename_to_typecode: {TypeName:integer} = {
["typevar"] = tl.typecodes.TYPE_VARIABLE,
["typearg"] = tl.typecodes.TYPE_VARIABLE,
["function"] = tl.typecodes.FUNCTION,
["array"] = tl.typecodes.ARRAY,
["map"] = tl.typecodes.MAP,
["tupletable"] = tl.typecodes.TUPLE,
["arrayrecord"] = tl.typecodes.ARRAYRECORD,
["record"] = tl.typecodes.RECORD,
["enum"] = tl.typecodes.ENUM,
["boolean"] = tl.typecodes.BOOLEAN,
["string"] = tl.typecodes.STRING,
["nil"] = tl.typecodes.NIL,
["thread"] = tl.typecodes.THREAD,
["number"] = tl.typecodes.NUMBER,
["integer"] = tl.typecodes.INTEGER,
["union"] = tl.typecodes.IS_UNION,
["nominal"] = tl.typecodes.NOMINAL,
["emptytable"] = tl.typecodes.EMPTY_TABLE,
["unresolved_emptytable_value"] = tl.typecodes.EMPTY_TABLE,
["poly"] = tl.typecodes.IS_POLY,
["any"] = tl.typecodes.ANY,
["unknown"] = tl.typecodes.UNKNOWN,
["invalid"] = tl.typecodes.INVALID,
};
function tl.get_types(result: Result, trenv: TypeReportEnv): TypeReport, TypeReportEnv {
var filename = result.filename || "?";
var function mark_array<T>(x: T): T {
var arr = x as {boolean};
arr[0] = false;
return x;
}
if( ! trenv ) {
trenv = {
next_num = 1,
typeid_to_num = {},
tr = {
by_pos = {},
types = {},
symbols = mark_array( {}),
globals = {},
},
};
}
var tr = trenv.tr;
var typeid_to_num = trenv.typeid_to_num;
var get_typenum: function(t: Type): integer;
var function store_function(ti: TypeInfo, rt: Type) {
var args: {{integer, string}} = {};
for( _, fnarg in ipairs(rt.args) ) {
table.insert(args, mark_array( { get_typenum(fnarg), null }));
}
ti.args = mark_array(args);
var rets: {{integer, string}} = {};
for( _, fnarg in ipairs(rt.rets) ) {
table.insert(rets, mark_array( { get_typenum(fnarg), null }));
}
ti.rets = mark_array(rets);
ti.vararg = ! ! rt.is_va;
}
get_typenum = function(t: Type): integer {
assert(t.typeid);
// try by typeid
var n = typeid_to_num[t.typeid];
if( n ) {
return n;
}
// it's a new entry: store and increment
n = trenv.next_num;
var rt = t;
if( rt.typename == "typetype" || rt.typename == "nestedtype" ) {
rt = rt.def;
} else if( rt.typename == "tuple" && #rt == 1 ) {
rt = rt[1];
}
var ti: TypeInfo = {
t = assert(typename_to_typecode[rt.typename]),
str = show_type(t, true),
file = t.filename,
y = t.y,
x = t.x,
};
tr.types[n] = ti;
typeid_to_num[t.typeid] = n;
trenv.next_num = trenv.next_num + 1;
if( t.found ) {
ti.ref = get_typenum(t.found);
}
if( t.resolved ) {
rt = t;
}
assert(rt.typename != "typetype");
if( is_record_type(rt) ) {
// store record field info
var r = {};
for( _, k in ipairs(rt.field_order) ) {
var v = rt.fields[k];
r[k] = get_typenum(v);
}
ti.fields = r;
}
if( is_array_type(rt) ) {
ti.elements = get_typenum(rt.elements);
}
if( rt.typename == "map" ) {
ti.keys = get_typenum(rt.keys);
ti.values = get_typenum(rt.values);
} else if( rt.typename == "enum" ) {
ti.enums = mark_array(sorted_keys(rt.enumset));
} else if( rt.typename == "function" ) {
store_function(ti, rt);
} else if( rt.typename == "poly" || rt.typename == "union" || rt.typename == "tupletable" ) {
var tis = {};
for( _, pt in ipairs(rt.types) ) {
table.insert(tis, get_typenum(pt));
}
ti.types = mark_array(tis);
}
return n;
};
var visit_node: Visitor<NodeKind, Node, null> = { allow_missing_cbs = true };
var visit_type: Visitor<TypeName, Type, null> = { allow_missing_cbs = true };
var skip: {TypeName: boolean} = {
["none"] = true,
["tuple"] = true,
["table_item"] = true,
};
var ft: {integer:{integer:integer}} = {};
tr.by_pos[filename] = ft;
var function store(y: integer, x: integer, typ: Type) {
if( ! typ || skip[typ.typename] ) {
return;
}
var yt = ft[y];
if( ! yt ) {
yt = {};
ft[y] = yt;
}
yt[x] = get_typenum(typ);
}
visit_node.after = function(node: Node): null {
store(node.y, node.x, node.type);
};
visit_type.after = function(typ: Type): null {
store(typ.y || 0, typ.x || 0, typ);
};
recurse_node(result.ast, visit_node, visit_type);
tr.by_pos[filename][0] = null;
// mark unneeded scope blocks to be skipped
{
var n = 0; // number of symbols in current scope
var p = 0; // opening position of current scope block
var n_stack, p_stack = {}, {};
var level = 0;
for( i, s in ipairs(result.symbol_list) ) {
if( s.typ ) {
n = n + 1;
} else if( s.name == "@{" ) {
level = level + 1;
n_stack[level], p_stack[level] = n, p; // push current scope
n, p = 0, i; // begin new scope
} else {
if( n == 0 ) { // nothing declared in this scope
result.symbol_list[p].skip = true; // skip @{
s.skip = true; // skip @}
}
n, p = n_stack[level], p_stack[level]; // pop previous scope
level = level - 1;
}
}
}
// resolve scope cross references, skipping unneeded scope blocks
{
var stack = {};
var level = 0;
var i = 0;
for( _, s in ipairs(result.symbol_list) ) {
if( ! s.skip ) {
i = i + 1;
var id: integer;
if( s.typ ) {
id = get_typenum(s.typ);
} else if( s.name == "@{" ) {
level = level + 1;
stack[level] = i;
id = -1; // will be overwritten
} else {
var other = stack[level];
level = level - 1;
tr.symbols[other][4] = i; // overwrite id from @{
id = other - 1;
}
var sym = mark_array({ s.y, s.x, s.name, id });
table.insert(tr.symbols, sym);
}
}
}
var gkeys = sorted_keys(result.env.globals);
for( _, name in ipairs(gkeys) ) {
if( name->sub(1, 1) != "@" ) {
var _v_var = result.env.globals[name];
tr.globals[name] = get_typenum(_v_var.t);
}
}
return tr, trenv;
}
function tl.symbols_in_scope(tr: TypeReport, y: integer, x: integer): {string:integer} {
var function find(symbols: {{integer, integer, string, integer}}, at_y: integer, at_x: integer): integer {
var function le(a: {integer, integer}, b: {integer, integer}): boolean {
return a[1] < b[1]
|| (a[1] == b[1] && a[2] <= b[2]);
}
return binary_search(symbols, {at_y, at_x}, le) || 0;
}
var ret: {string:integer} = {};
// local a, b = 0, 0
// for i, s in ipairs(tr.symbols) do
// print("["..i.."]", (a < s[1] or (a == s[1] and b <= s[2])) and " " or "x", s[1], s[2], s[3], s[4])
// a, b = s[1], s[2]
// end
// print()
var n = find(tr.symbols, y, x);
var symbols = tr.symbols;
while( n >= 1 ) {
var s = symbols[n];
if( s[3] == "@{" ) {
n = n - 1;
} else if( s[3] == "@}" ) {
n = s[4];
} else {
ret[s[3]] = s[4];
n = n - 1;
}
}
return ret;
}
//------------------------------------------------------------------------------
// High-level API
//------------------------------------------------------------------------------
tl.process = function(filename: string, env: Env): Result, string {
if( env && env.loaded && env.loaded[filename] ) {
return env.loaded[filename];
}
var fd, err = io.open(filename, "r");
if( ! fd ) {
return null, "could not open " .. filename .. ": " .. err;
}
var input: string; input, err = fd->read("*a");
fd->close();
if( ! input ) {
return null, "could not read " .. filename .. ": " .. err;
}
var _, extension = filename->match("(.*)%.([a-z]+)$");
extension = extension && extension->lower();
var is_lua: boolean;
if( extension == "tl" ) {
is_lua = false;
} else if( extension == "lua" ) {
is_lua = true;
} else {
is_lua = input->match("^#![^\n]*lua[^\n]*\n") as boolean;
}
return tl.process_string(input, is_lua, env, filename);
};
function tl.process_string(input: string, is_lua: boolean, env: Env, filename: string): Result {
env = env || tl.init_env(is_lua);
if( env.loaded && env.loaded[filename] ) {
return env.loaded[filename];
}
filename = filename || "";
var syntax_errors: {Error} = {};
var tokens, errs = tl.lex(input);
if( errs ) {
for( _, err in ipairs(errs) ) {
table.insert(syntax_errors, {
y = err.y,
x = err.x,
msg = "invalid token '" .. err.tk .. "'",
filename = filename,
});
}
}
var _, program = tl.parse_program(tokens, syntax_errors, filename);
if( (! env.keep_going) && #syntax_errors > 0 ) {
var result = {
ok = false,
filename = filename,
type_errors = {},
syntax_errors = syntax_errors,
env = env,
};
env.loaded[filename] = result;
table.insert(env.loaded_order, filename);
return result;
}
var opts: TypeCheckOptions = {
filename = filename,
lax = is_lua,
gen_compat = env.gen_compat,
env = env,
};
var result = tl.type_check(program, opts);
result.syntax_errors = syntax_errors;
return result;
}
tl.gen = function(input: string, env: Env): string, Result {
env = env || tl.init_env();
var result = tl.process_string(input, false, env);
if( (! result.ast) || #result.syntax_errors > 0 ) {
return null, result;
}
return tl.pretty_print_ast(result.ast), result;
};
var function tl_package_loader(module_name: string): any {
var found_filename, fd, tried = tl.search_module(module_name, false);
if( found_filename ) {
var input = fd->read("*a");
if( ! input ) {
return table.concat(tried, "\n\t");
}
fd->close();
var errs: {Error} = {};
var _, program = tl.parse_program(tl.lex(input), errs, module_name);
if( #errs > 0 ) {
error(found_filename .. ":" .. errs[1].y .. ":" .. errs[1].x .. ": " .. errs[1].msg);
}
var lax = ! ! found_filename->match("lua$");
if( ! tl.package_loader_env ) {
tl.package_loader_env = tl.init_env(lax);
}
tl.type_check(program, {
lax = lax,
filename = found_filename,
env = tl.package_loader_env,
run_internal_compiler_checks = false,
});
var code = tl.pretty_print_ast(program, true);
var chunk, err = load(code, module_name, "t");
if( chunk ) {
return function(): any {
var ret = chunk();
package.loaded[module_name] = ret;
return ret;
};
} else {
error("Internal Compiler Error: Teal generator produced invalid Lua. Please report a bug at https://github.com/teal-language/tl\n\n" .. err);
}
}
return table.concat(tried, "\n\t");
}
function tl.loader() {
if( package.searchers ) {
table.insert(package.searchers, 2, tl_package_loader);
} else {
table.insert(package.loaders, 2, tl_package_loader);
}
}
tl.load = function(input: string, chunkname: string, mode: LoadMode, ...: {any:any}): LoadFunction, string {
var tokens = tl.lex(input);
var errs: {Error} = {};
var _, program = tl.parse_program(tokens, errs, chunkname);
if( #errs > 0 ) {
return null, (chunkname || "") .. ":" .. errs[1].y .. ":" .. errs[1].x .. ": " .. errs[1].msg;
}
var code = tl.pretty_print_ast(program, true);
return load(code, chunkname, mode, ...);
};
return tl
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment