forked from teal-language/tl
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathtl
executable file
·306 lines (237 loc) · 7.34 KB
/
tl
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
#!/usr/bin/env lua
local function script_path()
local str = debug.getinfo(2, "S").source:sub(2)
return str:match("(.*[/\\])") or "."
end
local function printerr(s)
io.stderr:write(s .. "\n")
end
local function trim(str)
return str:gsub("^%s*(.-)%s*$", "%1")
end
local function die(msg)
printerr(msg)
os.exit(1)
end
local function is_nil_or_whitespace(str)
return str == nil or trim(str) == ""
end
local function find_in_sequence(seq, value)
for _, v in ipairs(seq) do
if trim(v) == trim(value) then
return true
end
end
return false
end
-- FIXME
local function validate_config(config)
local valid_keys = {
preload_modules = true,
include = true
}
for k, _ in pairs(config) do
if not valid_keys[k] then
print(string.format("Warning: unknown key '%s' in tlconfig.lua", k))
end
end
-- TODO: could we type-check the config file using tl?
return nil
end
local function get_config()
local config = {
preload_modules = {},
include = {}
}
local status, user_config = pcall(require, "tlconfig")
if not status then
return config
end
-- Merge tlconfig with the default config
for k, v in pairs(user_config) do
config[k] = v
end
local err = validate_config(config)
if err then
die("Error while loading config: " .. err)
end
return config
end
package.path = script_path() .. "/?.lua;" .. package.path
local tl = require("tl")
local argparse = require("argparse")
local function get_args_parser()
local parser = argparse("tl", "A minimalistic typed dialect of Lua.")
parser:option("-l --preload", "Execute the equivalent of require('modulename') before executing the tl script(s).")
:argname("<modulename>")
:count("*")
parser:option("-I --include", "Prepend this directory to the module search path.")
:argname("<directory>")
:count("*")
parser:flag("--skip-compat53", "Skip compat53 insertions.")
parser:command_target("command")
local check_command = parser:command("check", "Type-check one or more tl script.")
check_command:argument("script", "The tl script."):args("+")
local gen_command = parser:command("gen", "Generate a Lua file for one or more tl script.")
gen_command:argument("script", "The tl script."):args("+")
local run_command = parser:command("run", "Run a tl script.")
run_command:argument("script", "The tl script."):args("+")
return parser
end
local parser = get_args_parser()
local args = parser:parse()
local tlconfig = get_config()
local cmd = args["command"]
for _, preload_module_cli in ipairs(args["preload"]) do
if not find_in_sequence(tlconfig.preload_modules, preload_module_cli) then
table.insert(tlconfig.preload_modules, preload_module_cli)
end
end
for _, include_dir_cli in ipairs(args["include"]) do
if not find_in_sequence(tlconfig.include, include_dir_cli) then
table.insert(tlconfig.include, include_dir_cli)
end
end
local function report_errors(category, errors)
if not errors then
return false
end
if #errors > 0 then
local n = #errors
printerr("========================================")
printerr(n .. " " .. category .. (n ~= 1 and "s" or "") .. ":")
for _, err in ipairs(errors) do
printerr(err.filename .. ":" .. err.y .. ":" .. err.x .. ": " .. (err.msg or ""))
end
return true
end
return false
end
local exit = 0
local function report_type_errors(result)
local has_type_errors = report_errors("error", result.type_errors)
report_errors("unknown variable", result.unknowns)
return not has_type_errors
end
local env = nil
local function get_shared_library_ext()
if is_nil_or_whitespace(package.cpath) then
return "so" -- FIXME
end
return package.cpath:match("%.(%w+)%s*$")
end
local function prepend_to_path(directory)
local path_separator = package.config:sub(1, 1)
local path_str = directory
if string.sub(path_str, -1) == path_separator then
path_str = path_str:sub(1, -2)
end
path_str = path_str .. path_separator
local lib_path_str = path_str .. "?." .. get_shared_library_ext() .. ";"
local lua_path_str = path_str .. "?.lua;"
package.path = lua_path_str .. package.path
package.cpath = lib_path_str .. package.cpath
end
for _, include in ipairs(tlconfig["include"]) do
prepend_to_path(include)
end
for i, filename in ipairs(args["script"]) do
local modules = i == 1 and tlconfig.preload_modules
if not env then
local basename, extension = filename:match("(.*)%.([a-z]+)$")
extension = extension and extension:lower()
local lax_mode
if extension == "tl" then
lax_mode = false
elseif extension == "lua" then
lax_mode = true
else
-- if we can't decide based on the file extension, default to strict
-- mode
lax_mode = false
end
local skip_compat53 = args["skip_compat53"]
env = tl.init_env(lax_mode, skip_compat53)
end
local result, err = tl.process(filename, env, nil, modules)
if err then
die(err)
end
env = result.env
local has_syntax_errors = report_errors("syntax error", result.syntax_errors)
if has_syntax_errors then
exit = 1
break
end
local lua_name = filename:gsub(".tl$", ".lua")
if cmd == "run" then
if filename:match("%.tl$") then
local ok = report_type_errors(result)
if not ok then
os.exit(1)
end
end
local chunk = (loadstring or load)(tl.pretty_print_ast(result.ast), "@" .. filename)
-- collect all non-arguments including negative arg values
local neg_arg = {}
local nargs = #args["script"]
local j = #arg
local p = nargs
local n = 1
while arg[j] do
if arg[j] == args["script"][p] then
p = p - 1
else
neg_arg[n] = arg[j]
n = n + 1
end
j = j - 1
end
-- shift back all non-arguments to negative positions
for p, a in ipairs(neg_arg) do
arg[-p] = a
end
-- put script in arg[0] and arguments in positive positions
for p, a in ipairs(args["script"]) do
arg[p - 1] = a
end
-- cleanup the rest
n = nargs
while arg[n] do
arg[n] = nil
n = n + 1
end
tl.loader()
return chunk()
elseif cmd == "check" then
local ok = report_type_errors(result)
if not ok then
exit = 1
end
if exit == 0 and #args["script"] == 1 then
print("========================================")
print("Type checked " .. filename)
print("0 errors detected -- you can use:")
print()
print(" tl run " .. filename)
print()
print(" to run " .. filename .. " as a program")
print()
print(" tl gen " .. filename)
print()
print(" to generate " .. lua_name)
end
elseif cmd == "gen" then
local ofd, err = io.open(lua_name, "w")
if not ofd then
die("cannot write " .. lua_name .. ": " .. err)
end
local ok, err = ofd:write(tl.pretty_print_ast(result.ast) .. "\n")
if err then
die("error writing " .. lua_name .. ": " .. err)
end
ofd:close()
print("Wrote: " .. lua_name)
end
end
os.exit(exit)