]> git.lizzy.rs Git - metalua.git/blob - src/lib/metalua/extension/types.mlua
fixed types extension
[metalua.git] / src / lib / metalua / extension / types.mlua
1 -- This extension inserts type-checking code at approriate place in the code,
2 -- thanks to annotations based on "::" keyword:
3 --
4 -- * function declarations can be annotated with a returned type. When they
5 --   are, type-checking code is inserted in each of their return statements,
6 --   to make sure they return the expected type.
7 --
8 -- * function parameters can also be annotated. If they are, type-checking
9 --   code is inserted in the function body, which checks the arguments' types
10 --   and cause an explicit error upon incorrect calls. Moreover, if a new value
11 --   is assigned to the parameter in the function's body, the new value's type
12 --   is checked before the assignment is performed.
13 --
14 -- * Local variables can also be annotated. If they are, type-checking
15 --   code is inserted before any value assignment or re-assignment is
16 --   performed on them.
17 --
18 -- Type checking can be disabled with:
19 --
20 -- -{stat: types.enabled = false }
21 --
22 -- Code transformation is performed at the chunk level, i.e. file by
23 -- file.  Therefore, it the value of compile-time variable
24 -- [types.enabled] changes in the file, the only value that counts is
25 -- its value once the file is entirely parsed.
26 --
27 -- Syntax
28 -- ======
29 --
30 -- Syntax annotations consist of "::" followed by a type
31 -- specifier. They can appear after a function parameter name, after
32 -- the closing parameter parenthese of a function, or after a local
33 -- variable name in the declaration. See example in samples.
34 --
35 -- Type specifiers are expressions, in which identifiers are taken
36 -- from table types. For instance, [number] is transformed into
37 -- [types.number]. These [types.xxx] fields must contain functions,
38 -- which generate an error when they receive an argument which doesn't
39 -- belong to the type they represent. It is perfectly acceptible for a
40 -- type-checking function to return another type-checking function,
41 -- thus defining parametric/generic types. Parameters can be
42 -- identifiers (they're then considered as indexes in table [types])
43 -- or literals.
44 --
45 -- Design hints
46 -- ============
47 --
48 -- This extension uses the code walking library [walk] to globally
49 -- transform the chunk AST. See [chunk_transformer()] for details
50 -- about the walker.
51 --
52 -- During parsing, type informations are stored in string-indexed
53 -- fields, in the AST nodes of tags `Local and `Function. They are
54 -- used by the walker to generate code only if [types.enabled] is
55 -- true.
56 --
57 -- TODO
58 -- ====
59 --
60 -- It's easy to add global vars type-checking, by declaring :: as an
61 -- assignment operator.  It's easy to add arbitrary expr
62 -- type-checking, by declaring :: as an infix operator. How to make
63 -- both cohabit?
64
65 --------------------------------------------------------------------------------
66 --
67 -- Function chunk_transformer()
68 --
69 --------------------------------------------------------------------------------
70 --
71 -- Takes a block annotated with extra fields, describing typing
72 -- constraints, and returns a normal AST where these constraints have
73 -- been turned into type-checking instructions.
74 --
75 -- It relies on the following annotations:
76 --
77 --  * [`Local{ }] statements may have a [types] field, which contains a
78 --    id name ==> type name map.
79 --
80 --  * [Function{ }] expressions may have an [param_types] field, also a
81 --    id name ==> type name map. They may also have a [ret_type] field
82 --    containing the type of the returned value.
83 --
84 -- Design hints:
85 -- =============
86 --
87 -- It relies on the code walking library, and two states:
88 --
89 --  * [return_types] is a stack of the expected return values types for
90 --    the functions currently in scope, the most deeply nested one
91 --    having the biggest index.
92 --
93 --  * [scopes] is a stack of id name ==> type name scopes, one per
94 --    currently active variables scope.
95 --
96 -- What's performed by the walker:
97 --
98 --  * Assignments to a typed variable involve a type checking of the
99 --    new value;
100 --
101 --  * Local declarations are checked for additional type declarations.
102 --
103 --  * Blocks create and destroy variable scopes in [scopes]
104 --
105 --  * Functions create an additional scope (around its body block's scope)
106 --    which retains its argument type associations, and stacks another
107 --    return type (or [false] if no type constraint is given)
108 --
109 --  * Return statements get the additional type checking statement if
110 --    applicable.
111 --
112 --------------------------------------------------------------------------------
113
114 -- TODO: unify scopes handling with free variables detector
115 -- FIXME: scopes are currently incorrect anyway, only functions currently define a scope.
116
117 require "metalua.walk"
118
119 -{ extension 'match' }
120
121 module("types", package.seeall)
122
123 enabled = true
124
125 local function chunk_transformer (block)
126    if not enabled then return end
127    local return_types, scopes = { }, { }
128    local cfg = { block = { }; stat = { }; expr = { } }
129
130    function cfg.stat.down (x)
131       match x with
132       | `Local{ lhs, rhs, types = x_types } ->
133          -- Add new types declared by lhs in current scope.
134          local myscope = scopes [#scopes]
135          for var, type in pairs (x_types) do
136             myscope [var] = process_type (type)
137          end
138          -- Type-check each rhs value with the type of the
139          -- corresponding lhs declaration, if any.  Check backward, in
140          -- case a local var name is used more than once.
141          for i = 1, max (#lhs, #rhs) do
142             local type, new_val = myscope[lhs[i][1]], rhs[i]
143             if type and new_val then
144                rhs[i] = checktype_builder (type, new_val, 'expr')
145             end
146          end
147       | `Set{ lhs, rhs } ->
148          for i=1, #lhs do
149             match lhs[i] with
150             | `Id{ v } ->
151                -- Retrieve the type associated with the variable, if any:
152                local  j, type = #scopes, nil
153                repeat j, type = j-1, scopes[j][v] until type or j==0
154                -- If a type constraint is found, apply it:
155                if type then rhs[i] = checktype_builder(type, rhs[i] or `Nil, 'expr') end
156             | _ -> -- assignment to a non-variable, pass
157             end
158          end
159       | `Return{ r_val } ->
160          local r_type = return_types[#return_types]
161          if r_type then
162             x <- `Return{ checktype_builder (r_type, r_val, 'expr') }
163          end
164       | _ -> -- pass
165       end
166    end
167
168    function cfg.expr.down (x)
169       if x.tag ~= 'Function' then return end
170       local new_scope = { }
171       table.insert (scopes, new_scope)
172       for var, type in pairs (x.param_types or { }) do
173          new_scope[var] = process_type (type)
174       end
175       local r_type = x.ret_type and process_type (x.ret_type) or false
176       table.insert (return_types, r_type)
177    end
178
179    -------------------------------------------------------------------
180    -- Unregister the returned type and the variable scope in which
181    -- arguments are registered;
182    -- then, adds the parameters type checking instructions at the
183    -- beginning of the function, if applicable.
184    -------------------------------------------------------------------
185    function cfg.expr.up (x)
186       if x.tag ~= 'Function' then return end
187       -- Unregister stuff going out of scope:
188       table.remove (return_types)
189       table.remove (scopes)
190       -- Add initial type checking:
191       for v, t in pairs(x.param_types or { }) do
192          table.insert(x[2], 1, checktype_builder(t, `Id{v}, 'stat'))
193       end
194    end
195
196    cfg.block.down = || table.insert (scopes, { })
197    cfg.block.up   = || table.remove (scopes)
198
199    walk.block(cfg, block)
200 end
201
202 --------------------------------------------------------------------------
203 -- Perform required transformations to change a raw type expression into
204 -- a callable function:
205 --
206 --  * identifiers are changed into indexes in [types], unless they're
207 --    allready indexed, or into parentheses;
208 --
209 --  * literal tables are embedded into a call to types.__table
210 --
211 -- This transformation is not performed when type checking is disabled:
212 -- types are stored under their raw form in the AST; the transformation is
213 -- only performed when they're put in the stacks (scopes and return_types)
214 -- of the main walker.
215 --------------------------------------------------------------------------
216 function process_type (type_term)
217    -- Transform the type:
218    cfg = { expr = { } }
219
220    function cfg.expr.down(x)
221       match x with
222       | `Index{...} | `Paren{...} -> return 'break'
223       | _ -> -- pass
224       end
225    end
226    function cfg.expr.up (x)
227       match x with
228       | `Id{i} -> x <- `Index{ `Id "types", `String{ i } }
229       | `Table{...} | `String{...} | `Op{...} ->
230          local xcopy, name = table.shallow_copy(x)
231          match x.tag with
232          | 'Table'  -> name = '__table'
233          | 'String' -> name = '__string'
234          | 'Op'     -> name = '__'..x[1]
235          end
236          x <- `Call{ `Index{ `Id "types", `String{ name } }, xcopy }
237       | `Function{ params, { results } } if results.tag=='Return' ->
238          results.tag = nil
239          x <- `Call{ +{types.__function}, params, results }
240       | `Function{...} -> error "malformed function type"
241       | _ -> -- pass
242       end
243    end
244    walk.expr(cfg, type_term)
245    return type_term
246 end
247
248 --------------------------------------------------------------------------
249 -- Insert a type-checking function call on [term] before returning
250 -- [term]'s value. Only legal in an expression context.
251 --------------------------------------------------------------------------
252 local non_const_tags = table.transpose
253    { 'Dots', 'Op', 'Index', 'Call', 'Invoke', 'Table' }
254 function checktype_builder(type, term, kind)
255    -- Shove type-checking code into the term to check:
256    match kind with
257    | 'expr' if non_const_tags [term.tag] ->
258       local  v = mlp.gensym()
259       return `Stat{ { `Local{ {v}, {term} }; `Call{ type, v } }, v }
260    | 'expr' ->
261       return `Stat{ { `Call{ type, term } }, term }
262    | 'stat' ->
263       return `Call{ type, term }
264    end
265 end
266
267 --------------------------------------------------------------------------
268 -- Parse the typechecking tests in a function definition, and adds the
269 -- corresponding tests at the beginning of the function's body.
270 --------------------------------------------------------------------------
271 local function func_val_builder (x)
272    local typed_params, ret_type, body = unpack(x)
273    local e = `Function{ { }, body; param_types = { }; ret_type = ret_type }
274
275    -- Build [untyped_params] list, and [e.param_types] dictionary.
276    for i, y in ipairs (typed_params) do
277       if y.tag=="Dots" then
278          assert(i==#typed_params, "`...' must be the last parameter")
279          break
280       end
281       local param, type = unpack(y)
282       e[1][i] = param
283       if type then e.param_types[param[1]] = type end
284    end
285    return e
286 end
287
288 --------------------------------------------------------------------------
289 -- Parse ":: type" annotation if next token is "::", or return false.
290 -- Called by function parameters parser
291 --------------------------------------------------------------------------
292 local opt_type = gg.onkeyword{ "::", mlp.expr }
293
294 --------------------------------------------------------------------------
295 -- Updated function definition parser, which accepts typed vars as
296 -- parameters.
297 --------------------------------------------------------------------------
298
299 -- Parameters parsing:
300 local id_or_dots = gg.multisequence{ { "...", builder = "Dots" }, default = mlp.id }
301
302 -- Function parsing:
303 mlp.func_val = gg.sequence{
304    "(", gg.list{
305       gg.sequence{ id_or_dots, opt_type }, terminators = ")", separators  = "," },
306    ")",  opt_type, mlp.block, "end",
307    builder = func_val_builder }
308
309 mlp.lexer:add { "::", "newtype" }
310 mlp.chunk.transformers:add (chunk_transformer)
311
312 -- Local declarations parsing:
313 local local_decl_parser = mlp.stat:get "local" [2].default
314
315 local_decl_parser[1].primary = gg.sequence{ mlp.id, opt_type }
316
317 function local_decl_parser.builder(x)
318    local lhs, rhs = unpack(x)
319    local s, stypes = `Local{ { }, rhs or { } }, { }
320    for i = 1, #lhs do
321       local id, type = unpack(lhs[i])
322       s[1][i] = id
323       if type then stypes[id[1]]=type end
324    end
325    if next(stypes) then s.types = stypes end
326    return s
327 end
328
329 function newtype_builder(x)
330    local lhs, rhs = unpack(x)
331    match lhs with
332    | `Id{ x } -> t = process_type (rhs)
333    | `Call{ `Id{ x }, ... } ->
334       t = `Function{ { }, rhs }
335       for i = 2, #lhs do
336          if lhs[i].tag ~= "Id" then error "Invalid newtype parameter" end
337          t[1][i-1] = lhs[i]
338       end
339    | _ -> error "Invalid newtype definition"
340    end
341    return `Let{ { `Index{ `Id "types", `String{ x } } }, { t } }
342 end
343
344 mlp.stat:add{ "newtype", mlp.expr, "=", mlp.expr, builder = newtype_builder }
345
346
347 --------------------------------------------------------------------------
348 -- Register as an operator
349 --------------------------------------------------------------------------
350 --mlp.expr.infix:add{ "::", prec=100, builder = |a, _, b| insert_test(a,b) }
351
352 return +{ require (-{ `String{ package.metalua_extension_prefix .. 'types-runtime' } }) }