cc9af440c32859d40e6b4cd4ee7516cf4a5f6914
[goals.git] / src / run.ml
1 (* Goalfile run
2  * Copyright (C) 2019 Richard W.M. Jones
3  * Copyright (C) 2019 Red Hat Inc.
4  *
5  * This program is free software; you can redistribute it and/or modify
6  * it under the terms of the GNU General Public License as published by
7  * the Free Software Foundation; either version 2 of the License, or
8  * (at your option) any later version.
9  *
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  * GNU General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License along
16  * with this program; if not, write to the Free Software Foundation, Inc.,
17  * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
18  *)
19
20 open Printf
21
22 open Utils
23
24 let rec run_targets env exprs =
25   List.iter (run_target env) exprs
26
27 and run_target env = function
28   | Ast.EGoalDefn _ | Ast.ETacticDefn _ -> assert false
29
30   (* Call a goal. *)
31   | Ast.ECallGoal (loc, name, args) ->
32      let goal = Ast.getgoal env loc name in
33      run_goal env loc name args goal []
34
35   (* Call a tactic. *)
36   | Ast.ETacticCtor (loc, name, args) ->
37      (* All parameters of tactics must be simple constant expressions
38       * (strings, in future booleans, numbers, etc).
39       *)
40      let args = List.map (Eval.to_constant env) args in
41      run_tactic env loc name args
42
43   (* If this is a goal then it's the same as calling goal().  If not
44    * then look up the variable and substitute it.
45    *)
46   | Ast.EVar (loc, name) ->
47      let expr = Ast.getvar env loc name in
48      (match expr with
49       | EGoalDefn (loc, ([], _, _, _)) ->
50          run_target env (Ast.ECallGoal (loc, name, []))
51       | EGoalDefn _ ->
52          failwithf "%a: cannot call %s() since this goal has parameters"
53            Ast.string_loc loc name
54       | _ ->
55          run_target env expr
56      )
57
58   (* Lists are inlined when found as a target. *)
59   | Ast.EList (loc, exprs) ->
60      run_targets env exprs
61
62   (* A string (with or without substitutions) implies *file(filename). *)
63   | Ast.ESubsts (loc, str) ->
64      let str = Eval.substitute env loc str in
65      run_tactic env loc "*file" [Ast.CString str]
66
67   | Ast.EConstant (loc, c) ->
68      run_tactic env loc "*file" [c]
69
70 (* Run a goal by name. *)
71 and run_goal env loc name args (params, patterns, deps, code) extra_deps =
72   Cmdline.debug "%a: running goal %s %a"
73     Ast.string_loc loc name Ast.string_expr (Ast.EList (Ast.noloc, args));
74
75   (* This is the point where we evaluate the goal arguments.  We must
76    * do this before creating the new environment, because variables
77    * appearing in goal arguments don't refer to goal parameters.
78    *)
79   let args = List.map (Eval.evaluate_goal_arg env) args in
80
81   (* Create a new environment which maps the parameter names to
82    * the args.
83    *)
84   let env =
85     let params =
86       try List.combine params args
87       with Invalid_argument _ ->
88         failwithf "%a: calling goal ‘%s’ with wrong number of arguments"
89           Ast.string_loc loc name in
90     List.fold_left (fun env (k, v) -> Ast.Env.add k v env) env params in
91
92   (* Check all dependencies have been updated. *)
93   run_targets env (deps @ extra_deps);
94
95   (* Check if any target (ie. pattern) needs to be rebuilt.
96    * As with make, a goal with no targets is always run.
97    *)
98   let rebuild =
99     patterns = [] ||
100     List.exists (needs_rebuild env loc deps extra_deps) patterns in
101
102   if rebuild then (
103     (* Run the code (if any). *)
104     (match code with
105      | None -> () (* No { CODE } section. *)
106
107      | Some code ->
108         (* Add some standard variables to the environment. *)
109         let expr_of_substs s = Ast.ESubsts (Ast.noloc, s) in
110         let expr_of_pattern = function
111           | Ast.PTactic (loc, tactic, targs) ->
112              Ast.ETacticCtor (loc, tactic, List.map expr_of_substs targs)
113         in
114         let pexprs = List.map expr_of_pattern patterns in
115         let env = Ast.Env.add "@" (Ast.EList (Ast.noloc, pexprs)) env in
116         let env = Ast.Env.add "<" (Ast.EList (Ast.noloc, deps)) env in
117         let env =
118           (* NB: extra_deps are not added to %^ *)
119           match deps with
120           | [] -> env
121           | d :: _ -> Ast.Env.add "^" d env in
122         let code = Eval.to_shell_script env loc code in
123         let code = "set -e\nset -x\n\n" ^ code in
124         let r = Sys.command code in
125         if r <> 0 then (
126           eprintf "*** goal ‘%s’ failed with exit code %d\n" name r;
127           exit 1
128         );
129
130         (* Check all targets were updated after the code was
131          * run (else it's an error).
132          *)
133         let pattern_still_needs_rebuild =
134           try
135             Some (List.find (needs_rebuild env loc deps extra_deps) patterns)
136           with
137             Not_found -> None in
138         match pattern_still_needs_rebuild with
139         | None -> ()
140         | Some pattern ->
141            failwithf "%a: goal ‘%s’ ran successfully but it did not rebuild %a"
142              Ast.string_loc loc
143              name
144              Ast.string_pattern pattern
145     )
146   )
147
148 (* Return whether the target (pattern) needs to be rebuilt. *)
149 and needs_rebuild env loc deps extra_deps pattern =
150   Cmdline.debug "%a: testing if %a needs rebuild"
151     Ast.string_loc loc Ast.string_pattern pattern;
152
153   match pattern with
154   | Ast.PTactic (loc, tactic, targs) ->
155      (* Look up the tactic. *)
156      let params, code = Ast.gettactic env loc tactic in
157
158      (* Resolve the targs down to constants.  Since needs_rebuild
159       * should be called with env containing the goal params, this
160       * should substitute any parameters in the tactic arguments.
161       *)
162      let targs = List.map (Ast.substitute env loc) targs in
163      let targs =
164        List.map (fun targ ->
165            Ast.EConstant (Ast.noloc, Ast.CString targ)) targs in
166
167      (* Create a new environment binding parameter names
168       * to tactic args.
169       *)
170      let env =
171        let params =
172          try List.combine params targs
173          with Invalid_argument _ ->
174            failwithf "%a: calling tactic ‘%s’ with wrong number of arguments"
175              Ast.string_loc loc tactic in
176        List.fold_left (fun env (k, v) -> Ast.Env.add k v env) env params in
177
178      (* Add some standard variables to the environment. *)
179      let env = Ast.Env.add "<" (Ast.EList (Ast.noloc, deps)) env in
180      let env =
181        (* NB: extra_deps are not added to %^ *)
182        match deps with
183        | [] -> env
184        | d :: _ -> Ast.Env.add "^" d env in
185      let code = Eval.to_shell_script env loc code in
186      let code = "set -e\n" (*^ "set -x\n" *) ^ "\n" ^ code in
187      let r = Sys.command code in
188      if r = 99 (* means "needs rebuild" *) then true
189      else if r = 0 (* means "doesn't need rebuild" *) then false
190      else (
191        eprintf "*** tactic ‘%s’ failed with exit code %d\n" tactic r;
192        exit 1
193      )
194
195 (* Find the goal which matches the given tactic and run it.
196  * cargs is a list of parameters (all constants).
197  *)
198 and run_tactic env loc tactic cargs =
199   Cmdline.debug "%a: running tactic %s" Ast.string_loc loc tactic;
200
201   (* Find all goals in the environment.  Returns a list of (name, goal). *)
202   let goals =
203     let env = Ast.Env.bindings env in
204     filter_map
205       (function
206        | name, Ast.EGoalDefn (loc, goal) -> Some (name, goal)
207        | _ -> None) env in
208
209   (* Find all patterns.  Returns a list of (pattern, name, goal). *)
210   let patterns : (Ast.pattern * Ast.id * Ast.goal) list =
211     List.flatten
212       (List.map (fun (name, ((_, patterns, _, _) as goal)) ->
213            List.map (fun pattern -> (pattern, name, goal)) patterns) goals) in
214
215   (* Find any patterns (ie. tactics) which match the one we
216    * are searching for.  This returns a binding for the goal args,
217    * so we end up with a list of (pattern, name, goal, args).
218    *)
219   let patterns : (Ast.pattern * Ast.id * Ast.goal * Ast.expr list) list =
220     filter_map (
221       fun (pattern, name, ((params, _, _, _) as goal)) ->
222         match matching_pattern env loc tactic cargs pattern params with
223         | None -> None
224         | Some args -> Some (pattern, name, goal, args)
225     ) patterns in
226
227   match patterns with
228   | [] ->
229      (* There's no matching goal, but we don't need one if
230       * the tactic doesn't need to be rebuilt.
231       *)
232      let targs = List.map (function Ast.CString s -> [Ast.SString s]) cargs in
233      let p = Ast.PTactic (loc, tactic, targs) in
234      if needs_rebuild env loc [] [] p then (
235        let t = Ast.ETacticCtor (loc, tactic,
236                                 List.map (fun c -> Ast.EConstant (loc, c))
237                                   cargs) in
238        failwithf "%a: don't know how to build %a"
239          Ast.string_loc loc Ast.string_expr t
240      )
241
242   | [_, name, goal, args] ->
243      (* Single goal matches, run it. *)
244      run_goal env loc name args goal []
245
246   | goals ->
247      (* Two or more goals match.  Only one must have a CODE section,
248       * and we combine the dependencies into a "supergoal".
249       *)
250      let with_code, without_code =
251        List.partition (
252          fun (_, _, (_, _, _, code), _) -> code <> None
253        ) goals in
254
255      let (_, name, goal, args), extra_deps =
256        match with_code with
257        | [g] ->
258           let extra_deps =
259             List.flatten (
260               List.map (fun (_, _, (_, _, deps, _), _) -> deps) without_code
261             ) in
262           (g, extra_deps)
263
264        | [] ->
265           (* This is OK, it means we'll rebuild all dependencies
266            * but there is no code to run.  Pick the first goal
267            * without code and the dependencies from the other goals.
268            *)
269           let g = List.hd without_code in
270           let extra_deps =
271             List.flatten (
272               List.map (fun (_, _, (_, _, deps, _), _) -> deps)
273                 (List.tl without_code)
274             ) in
275           (g, extra_deps)
276
277        | _ :: _ ->
278           failwithf "%a: multiple goals with {code} sections match tactic %s"
279             Ast.string_loc loc tactic in
280
281      run_goal env loc name args goal extra_deps
282
283 (* Test if pattern matches *tactic(cargs).  If it does
284  * then we return Some args where args is the arguments that must
285  * be passed to the matching goal.  The params parameter is
286  * the names of the parameters of that goal.
287  *)
288 and matching_pattern env loc tactic cargs pattern params =
289   match pattern with
290   | Ast.PTactic (loc, ttactic, targs)
291        when tactic <> ttactic ||
292             List.length cargs <> List.length targs ->
293      None (* Can't possibly match if tactic name or #args is different. *)
294   | Ast.PTactic (loc, ttactic, targs) ->
295      (* Do the args match with a possible params binding? *)
296      try Some (matching_params env loc params targs cargs)
297      with Not_found -> None
298
299 (* Return a possible binding.  For example the goal is:
300  *   goal compile (name) = "%name.o": "%name.c" {}
301  * which means that params = ["name"] and targs = ["%name.o"].
302  *
303  * If we are called with cargs = ["file1.o"], we would
304  * return ["file1"].
305  *
306  * On non-matching this raises Not_found.
307  *)
308 and matching_params env loc params targs cargs =
309   (* This is going to record the resulting binding. *)
310   let res = ref Ast.Env.empty in
311   List.iter2 (matching_param env loc params res) targs cargs;
312
313   (* Rearrange the result into goal parameter order.  Also this
314    * checks that every parameter got a binding.
315    *)
316   let res = !res in
317   List.map (
318     (* Allow the Not_found exception to escape if no binding for this param. *)
319     fun param -> Ast.Env.find param res
320   ) params
321
322 (* If targ = "%name.o" and carg = "file.o" then this would set
323  * name => "file" in !res.  If they don't match, raises Not_found.
324  *)
325 and matching_param env loc params res targ carg =
326   match carg with
327   | Ast.CString carg ->
328      (* Substitute any non parameters in targ from the environment. *)
329      let targ =
330        List.map (
331          function
332          | Ast.SString _ as s -> s
333          | Ast.SVar name ->
334             if not (List.mem name params) then (
335               try
336                 let expr = Ast.getvar env loc name in
337                 match Eval.to_constant env expr with
338                 | Ast.CString s -> Ast.SString s
339               with Failure _ -> raise Not_found
340             )
341             else
342               Ast.SVar name
343        ) targ in
344
345      (* Do the actual pattern matching.  Any remaining SVar elements
346       * must refer to goal parameters.
347       *)
348      let carg = ref carg in
349      let rec loop = function
350        | [] ->
351           (* End of targ, we must have matched all of carg. *)
352           if !carg <> "" then raise Not_found
353        | Ast.SString s :: rest ->
354           (* Does this match the first part of !carg? *)
355           let clen = String.length !carg in
356           let slen = String.length s in
357           if slen > clen || s <> String.sub !carg 0 slen then
358             raise Not_found;
359           (* Yes, so continue after the matching prefix. *)
360           carg := String.sub !carg slen (clen-slen);
361           loop rest
362        | Ast.SVar name :: Ast.SString s :: rest ->
363           (* This is a goal parameter.  Find s later in !carg. *)
364           let i = string_find !carg s in
365           if i = -1 then raise Not_found;
366           (* Set the binding in !res. *)
367           let r = Ast.EConstant (Ast.noloc,
368                                  Ast.CString (String.sub !carg 0 i)) in
369           res := Ast.Env.add name r !res;
370           (* Continue after the match. *)
371           let skip = i + String.length s in
372           carg := String.sub !carg skip (String.length !carg - skip);
373           loop rest
374        | Ast.SVar name :: [] ->
375           (* Matches the whole remainder of the string. *)
376           let r = Ast.EConstant (Ast.noloc, Ast.CString !carg) in
377           res := Ast.Env.add name r !res
378        | Ast.SVar x :: Ast.SVar y :: _ ->
379           (* TODO! We cannot match a target like "%x%y". *)
380           assert false
381      in
382      loop targ