Created
March 22, 2022 09:18
-
-
Save mingodad/1a5b0ed6c25232cb29db9e3fa7de61b1 to your computer and use it in GitHub Desktop.
Teal Language first attempt to Tealjs language
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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