]> git.lizzy.rs Git - metalua.git/blob - src/compiler/metalua.mlua
265f06b8e79f37c80bccea964c2a397a89a44955
[metalua.git] / src / compiler / metalua.mlua
1 --*-lua-*- Set as a metalua file because it requires some metalua libs
2
3 --require 'verbose_require'
4
5 require 'metalua.compiler'
6 require 'metalua.clopts'
7 require 'serialize'
8
9 AST_COMPILE_ERROR_NUMBER        = -1
10 RUNTIME_ERROR_NUMBER            = -3
11 BYTECODE_SYNTHESE_ERROR_NUMBER  = -100
12
13 -{ extension 'match' }
14
15 function spring_pcall (f, arg, name)
16    -- FIXME: won't work if `arg' or `name' contain a backslash-escaped quot char.
17    arg  = arg  :gsub ("'", "\\'")
18    name = name :gsub ("'", "\\'")
19    local pattern = [[lua -l metalua.mlc -l serialize -e "print(serialize(%s('%s', '%s')))"]]
20    local cmd = string.format (pattern, f, arg, name)
21    --print ("Running the following process: " .. cmd)
22    local fd = io.popen (cmd)
23    local data = fd:read '*a'
24    fd:close()
25    --print (data)
26    local ast_builder, msg = lua_loadstring(data)
27    if not ast_builder then error ("can't compile data: "..msg) end
28    local ast = ast_builder()
29    return true, ast
30 end
31
32 local chunks  = { }
33 local runargs = { }
34
35 local acc_chunk = |kind| |arg| table.insert (chunks, { tag=kind, arg })
36
37 parser = clopts {
38    -- Chunk loading
39    {  short = 'f', long = 'file', type = 'string', action = acc_chunk 'File',
40       usage = 'load a file to compile and/or run'
41    },
42    {  short = 'l', long = 'library', type = 'string', action = acc_chunk 'Library',
43       usage = 'load a libary from the standard paths'
44    },
45    {  short = 'e', long = 'literal', type = 'string', action = acc_chunk 'Literal',
46       usage = 'load a literal piece of source code'
47    },
48    -- What to do with chunks
49    {  short = 'o', long = 'output', type = 'string',
50       usage = 'set the target name of the next compiled file'
51    },
52    {  short = 'x', long = 'run', type = 'boolean',
53       usage = 'execute the compiled file instead of saving it (unless -o is also used)'
54    },
55    {  short = 'i', long = 'interactive', type = 'boolean',
56       usage = 'run an interactive loop after having run other files'
57    },
58    -- Advanced stuff
59    {  short = 'v', long = 'verbose', type = 'boolean',
60       usage = 'verbose mode'
61    },
62    {  short = 'a', long = 'print-ast',  type = 'boolean',
63       usage = 'print the AST resulting from file compilation'
64    },
65    {  short = 'A', long = 'print-ast-lineinfo',  type = 'boolean',
66       usage = 'print the AST resulting from file compilation, including lineinfo data'
67    },
68    {  short = 'b', long = 'metabugs', type = 'boolean',
69       usage = 'show syntax errors as compile-time execution errors'
70    },
71    {  short = 's', long = 'sharpbang', type = 'string',
72       usage = 'set a first line to add to compiled file, typically "#!/bin/env mlr"'
73    },
74    {  long  = 'no-runtime', type = 'boolean',
75       usage = "prevent the automatic requirement of metalua runtime"
76    },
77    {  long  = '', short = 'p', type = '*',
78       action= function (newargs) runargs=table.icat(runargs, newargs) end,
79       usage = "pass all remaining arguments to the program"
80    },
81 usage=[[
82
83 Compile and/or execute metalua programs. Parameters passed to the
84 compiler should be prefixed with an option flag, hinting what must be
85 done with them: take tham as file names to compile, as library names
86 to load, as parameters passed to the running program... When option
87 flags lack, metalua tries to adopt a "Do What I Mean" approach:
88
89 - if no code (no library, no literal expression and no file) is
90   specified, the first flag-less parameter is taken as a file name to
91   load.
92
93 - if no code and no parameter is passed, an interactive loop is
94   started.
95
96 - if a target file is specified with --output, the program is not
97   executed by default, unless a --run flag forces it to. Conversely,
98   if no --output target is specified, the code is run unless ++run
99   forbids it.
100 ]]}
101
102 local function main (...)
103
104    local cfg = parser(...)
105
106    -------------------------------------------------------------------
107    -- Print messages if in verbose mode
108    -------------------------------------------------------------------
109    local function verb_print (fmt, ...)
110       if cfg.verbose then
111          return printf ("[ "..fmt.." ]", ...)
112       end
113    end
114
115    if cfg.verbose then
116       verb_print("raw options: %s", table.tostring(cfg))
117    end
118
119    -------------------------------------------------------------------
120    -- If there's no chunk but there are params, interpret the first
121    -- param as a file name.
122    if #chunks==0 and cfg.params then
123       local the_file = table.remove(cfg.params, 1)
124       verb_print("Param %q considered as a source file", the_file)
125       chunks = { `File{ the_file } }
126    end
127
128    -------------------------------------------------------------------
129    -- If nothing to do, run REPL loop
130    if #chunks==0 and cfg.interactive==nil then
131       verb_print "Nothing to compile nor run, force interactive loop"
132       cfg.interactive=true
133    end
134
135
136    -------------------------------------------------------------------
137    -- Run if asked to, or if no --output has been given
138    -- if cfg.run==false it's been *forced* to false, don't override.
139    if cfg.run==nil and not cfg.output then
140       verb_print("No output file specified; I'll run the program")
141       cfg.run = true
142    end
143
144    local code = { }
145
146    -------------------------------------------------------------------
147    -- Get ASTs from sources
148    local last_file
149    for x in values(chunks) do
150       verb_print("Compiling %s", table.tostring(x))
151       local st, ast
152       match x with
153       | `Library{ l } -> st, ast = true, `Call{ `Id 'require', `String{ l } }
154       | `Literal{ e } -> st, ast = spring_pcall('mlc.ast_of_luastring', e, 'literal')
155       | `File{ f } ->
156          st, ast = spring_pcall('mlc.ast_of_luafile', f, '@'..f)
157          -- Isolate each file in a separate fenv
158          if st then
159             ast = +{ function (...) -{ast} end (...)  }
160             ast.source  = '@'..f -- TODO [EVE]
161             code.source = '@'..f -- TODO [EVE]
162             last_file = ast
163          end
164       end
165       if not st then
166          printf ("Cannot compile %s: %s", table.tostring(x), ast or "no msg")
167          os.exit (AST_COMPILE_ERROR_NUMBER)
168       end
169       ast.origin = x
170       table.insert(code, ast)
171    end
172    -- The last file returns the whole chunk's result
173    if last_file then
174       local c = table.shallow_copy(last_file)
175       last_file <- `Return{ source = c.source, c }
176    end
177
178    -------------------------------------------------------------------
179    -- AST printing
180    if cfg['print-ast'] or cfg['print-ast-lineinfo'] then
181       verb_print "Resulting AST:"
182       for x in ivalues(code) do
183          printf("--- AST From %s: ---", table.tostring(x.source, 'nohash'))
184          if x.origin and x.origin.tag=='File' then x=x[1][1][2][1] end
185          if cfg['print-ast-lineinfo'] then table.print(x, 80, "indent1")
186          else table.print(x, 80, 'nohash') end
187       end
188    end
189
190    -------------------------------------------------------------------
191    -- Insert runtime loader
192    if cfg['no-runtime'] then
193       verb_print "Prevent insertion of command \"require 'metalua.runtime'\""
194    else
195       table.insert(code, 1, +{require'metalua.runtime'})
196    end
197
198    -- FIXME: check for failures
199    mlc.metabugs = cfg.metabugs
200    local bytecode = mlc.luacstring_of_ast (code)
201    code = nil
202
203    -------------------------------------------------------------------
204    -- Insert #!... command
205    if cfg.sharpbang then
206       verb_print ("Adding sharp-bang directive %q", cfg.sharpbang)
207       if not cfg.sharpbang:strmatch'^#!' then cfg.sharpbang='#!'..cfg.sharpbang end
208       if not cfg.sharpbang:strmatch'\n$' then cfg.sharpbang=cfg.sharpbang..'\n' end
209       bytecode = cfg.sharpbang..bytecode
210    end
211
212    -------------------------------------------------------------------
213    -- Save to file
214    if cfg.output then
215       -- FIXME: handle '-'
216       verb_print ("Saving to file %q", cfg.output)
217       local file, err_msg = io.open(cfg.output, 'wb')
218       if not file then error("can't open output file: "..err_msg) end
219       file:write(bytecode)
220       file:close()
221       if cfg.sharpbang and os.getenv "OS" ~= "Windows_NT" then
222          pcall(os.execute, 'chmod a+x "'..cfg.output..'"')
223       end
224    end
225
226    -------------------------------------------------------------------
227    -- Run compiled code
228    if cfg.run then
229       verb_print "Running"
230       local f = mlc.function_of_luacstring (bytecode)
231       bytecode = nil
232       -- FIXME: isolate execution in a ring
233       -- FIXME: check for failures
234
235       runargs = table.icat(cfg.params or { }, runargs)
236       local st, msg = pcall(f, unpack (runargs))
237       if not st then
238          io.stderr:write(msg)
239          os.exit(RUNTIME_ERROR_NUMBER)
240       end
241    end
242
243    -------------------------------------------------------------------
244    -- Run REPL loop
245    if cfg.interactive then
246       verb_print "Starting REPL loop"
247       require 'metalua.metaloop'
248       metaloop.run()
249    end
250
251    verb_print "Done"
252
253 end
254
255 main(...)