stdlib/fedora: Use grep -F when matching %fedora-rebuild-name
[goals.git] / src / eval.ml
1 (* Goalfile Abstract Syntax Tree
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 pure_cache = Hashtbl.create 13
25
26 let rec to_constant env = function
27   | Ast.EConstant (loc, c) -> c
28
29   | EVar (loc, name) ->
30      let expr = Ast.getvar env loc name in
31      to_constant env expr
32
33   | ESubsts (loc, str) ->
34      CString (substitute env loc str)
35
36   | EList (loc, _) ->
37      failwithf "%a: list found where constant expression expected"
38        Ast.string_loc loc
39
40   | ECall (loc, name, args) ->
41      let expr = Ast.getvar env loc name in
42      (match expr with
43       | EGoalDefn _ ->
44          (* Goals don't return anything so they cannot be used in
45           * constant expressions.  Use a function instead.
46           *)
47          failwithf "%a: cannot use goal call ‘%s’ in shell expansion"
48            Ast.string_loc loc name
49
50       | EFuncDefn (loc, func) ->
51          to_constant env (call_function env loc name args func)
52
53       | _ ->
54          failwithf "%a: cannot use ‘%s’ in constant expression"
55            Ast.string_loc loc name
56      )
57
58   | EPredCtor (loc, name, _) ->
59      failwithf "%a: cannot use predicate ‘%s’ in constant expression"
60        Ast.string_loc loc name
61
62   | EGoalDefn (loc, _) ->
63      failwithf "%a: cannot use goal in constant expression"
64        Ast.string_loc loc
65
66   | EFuncDefn (loc, _) ->
67      failwithf "%a: cannot use function in constant expression"
68        Ast.string_loc loc
69
70   | EPredDefn (loc, _) ->
71      failwithf "%a: cannot use predicate in constant expression"
72        Ast.string_loc loc
73
74 and substitute env loc substs =
75   let b = Buffer.create 13 in
76   List.iter (
77     function
78     | Ast.SString s -> Buffer.add_string b s
79     | SVar name ->
80        let expr = Ast.getvar env loc name in
81        match to_constant env expr with
82        | Ast.CString s -> Buffer.add_string b s
83   ) substs;
84   Buffer.contents b
85
86 and to_shell_script env loc substs =
87   let b = Buffer.create 13 in
88   List.iter (
89     function
90     | Ast.SString s -> Buffer.add_string b s
91     | SVar name ->
92        let expr = Ast.getvar env loc name in
93        let s = expr_to_shell_string env expr in
94        Buffer.add_string b s
95   ) substs;
96   Buffer.contents b
97
98 and expr_to_shell_string env = function
99   | Ast.EConstant (loc, CString s) -> Filename.quote s
100
101   | EVar (loc, name) ->
102      let expr = Ast.getvar env loc name in
103      expr_to_shell_string env expr
104
105   | ESubsts (loc, str) ->
106      Filename.quote (substitute env loc str)
107
108   | EList (loc, exprs) ->
109      let strs = List.map (expr_to_shell_string env) exprs in
110      (* These are shell quoted so we can just concat them with space. *)
111      String.concat " " strs
112
113   | ECall (loc, name, args) ->
114      let expr = Ast.getvar env loc name in
115      (match expr with
116       | EGoalDefn _ ->
117          (* Goals don't return anything so they cannot be used in
118           * shell script expansions.  Use a function instead.
119           *)
120          failwithf "%a: cannot use goal call ‘%s’ in shell expansion"
121            Ast.string_loc loc name
122
123       | EFuncDefn (loc, func) ->
124          expr_to_shell_string env (call_function env loc name args func)
125
126       | _ ->
127          failwithf "%a: cannot call ‘%s’ which is not a function"
128            Ast.string_loc loc name
129      )
130
131   (* Predicates expand to the first parameter. *)
132   | EPredCtor (loc, _, []) -> Filename.quote ""
133   | EPredCtor (loc, _, (arg :: _)) -> expr_to_shell_string env arg
134
135   | EGoalDefn (loc, _) ->
136      failwithf "%a: cannot use goal in shell expansion"
137        Ast.string_loc loc
138
139   | EFuncDefn (loc, _) ->
140      failwithf "%a: cannot use function in shell expansion"
141        Ast.string_loc loc
142
143   | EPredDefn (loc, _) ->
144      failwithf "%a: cannot use predicate in shell expansion"
145        Ast.string_loc loc
146
147 and run_code env loc code =
148   let code = prepare_code env loc code in
149   Sys.command code
150
151 and run_code_to_string env loc code =
152   let code = prepare_code env loc code in
153   let chan = Unix.open_process_in code in
154   let b = Buffer.create 1024 in
155   (try
156      while true do
157        Buffer.add_string b (input_line chan);
158        Buffer.add_char b '\n'
159      done
160    with End_of_file -> ());
161   let st = Unix.close_process_in chan in
162   let i =
163     match st with
164     | Unix.WEXITED i -> i
165     | Unix.WSIGNALED i ->
166        failwithf "%a: killed by signal %d" Ast.string_loc loc i
167     | Unix.WSTOPPED i ->
168        failwithf "%a: stopped by signal %d" Ast.string_loc loc i in
169   i, Buffer.contents b
170
171 and run_code_to_string_list env loc code =
172   let code = prepare_code env loc code in
173   let chan = Unix.open_process_in code in
174   let lines = ref [] in
175   (try while true do lines := input_line chan :: !lines done
176    with End_of_file -> ());
177   let st = Unix.close_process_in chan in
178   let i =
179     match st with
180     | Unix.WEXITED i -> i
181     | Unix.WSIGNALED i ->
182        failwithf "%a: killed by signal %d" Ast.string_loc loc i
183     | Unix.WSTOPPED i ->
184        failwithf "%a: stopped by signal %d" Ast.string_loc loc i in
185   let lines = List.rev !lines in
186   i, lines
187
188 and prepare_code env loc (code, quiet) =
189   let quiet = if Cmdline.debug_flag () then false else quiet in
190   let code = to_shell_script env loc code in
191   "source " ^ Filename.quote Cmdline.prelude_sh_file ^ "\n" ^
192   "set -e\n" ^
193   (if not (Cmdline.silent ()) && not quiet then "set -x\n" else "") ^
194   "\n" ^
195   code
196
197 and evaluate_goal_arg env = function
198   | Ast.EVar (loc, name) ->
199      let expr = Ast.getvar env loc name in
200      evaluate_goal_arg env expr
201
202   | ESubsts (loc, str) ->
203      let str = substitute env loc str in
204      Ast.EConstant (loc, Ast.CString str)
205
206   | EList (loc, exprs) ->
207      Ast.EList (loc, List.map (evaluate_goal_arg env) exprs)
208
209   | EPredCtor (loc, name, exprs) ->
210      Ast.EPredCtor (loc, name, List.map (evaluate_goal_arg env) exprs)
211
212   | ECall (loc, name, args) ->
213      let expr = Ast.getvar env loc name in
214      (match expr with
215       | EGoalDefn _ ->
216          (* Goals don't return anything so they cannot be used in
217           * goal args.  Use a function instead.
218           *)
219          failwithf "%a: cannot use goal call ‘%s’ in goal argument"
220            Ast.string_loc loc name
221
222       | EFuncDefn (loc, func) ->
223          call_function env loc name args func
224
225       | _ ->
226          failwithf "%a: cannot call ‘%s’ which is not a function"
227            Ast.string_loc loc name
228      )
229
230   | EConstant _
231   | EGoalDefn _
232   | EFuncDefn _
233   | EPredDefn _ as e -> e
234
235 (* Functions are only called from goal args or when substituting
236  * into a shell script or constant expression (this may change if we
237  * implement ‘:=’ assignment for variables).  This evaluates a
238  * function by running the associated shell script and parsing
239  * the output as an expression, string or list of strings.
240  *)
241 and call_function env loc name args (params, returning, pure, code) =
242   (* This is used to print the function in debug and error messages only. *)
243   let debug_func =
244     sprintf "%s (%s) returning %s" name
245       (String.concat ", " (List.map (Ast.string_expr ()) args))
246       (match returning with RetExpr -> "expression"
247                           | RetString -> "string"
248                           | RetStrings -> "strings") in
249   Cmdline.debug "%a: running function %s" Ast.string_loc loc debug_func;
250
251   (* Evaluate function args.  Must be done before updating the environment. *)
252   let args = List.map (evaluate_goal_arg env) args in
253
254   (* Create a new environment which maps the parameter names to
255    * the args.
256    *)
257   let env =
258     let params =
259       try List.combine params args
260       with Invalid_argument _ ->
261         failwithf "%a: calling function %s with wrong number of arguments, expecting %d args but got %d args"
262           Ast.string_loc loc debug_func
263           (List.length params) (List.length args) in
264     List.fold_left (fun env (k, v) -> Ast.Env.add k v env) env params in
265
266   if pure then call_function_pure env loc name returning code
267   else call_function_really env loc name returning code
268
269 and call_function_really env loc name returning code =
270   match returning with
271   | RetExpr ->
272      let r, b = run_code_to_string env loc code in
273      if r <> 0 then
274        failwithf "function ‘%s’ failed with exit code %d" name r;
275      Parse.parse_expr (sprintf "function:%s" name) b
276
277   | RetString ->
278      let r, b = run_code_to_string env loc code in
279      if r <> 0 then
280        failwithf "function ‘%s’ failed with exit code %d" name r;
281      (* Remove a single trailing \n if present. *)
282      let b =
283        let len = String.length b in
284        if len > 0 && b.[len-1] = '\n' then String.sub b 0 (len-1) else b in
285      Ast.EConstant (loc, Ast.CString b)
286
287   | RetStrings ->
288      let r, lines = run_code_to_string_list env loc code in
289      let strs = List.map (fun s -> Ast.EConstant (loc, Ast.CString s)) lines in
290      EList (loc, strs)
291
292 (* For pure functions, check if the function can be matched to
293  * a previously memoized result, but don't fail if this goes wrong.
294  *)
295 and call_function_pure env loc name returning code =
296   let code_key =
297     try Some (to_shell_script env loc (fst code))
298     with Failure _ -> None in
299   match code_key with
300   | None -> call_function_really env loc name returning code
301   | Some code_key ->
302      let r =
303        try Some (Hashtbl.find pure_cache code_key)
304        with Not_found -> None in
305      match r with
306      | Some expr -> expr
307      | None ->
308         let expr = call_function_really env loc name returning code in
309         Hashtbl.add pure_cache code_key expr;
310         expr