forked from jorgecasar/flow
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathengine.ml
317 lines (263 loc) · 11.3 KB
/
engine.ml
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
307
308
309
310
311
312
313
314
315
316
317
(**
* Copyright (c) 2013-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*)
module Utils = Flowtestgen_utils;;
module Logging = Flowtestgen_logging;;
module Config = Flowtestgen_config;;
(* A virtual class that defines the framework for ocaml-stype rules.
A rule is an ordinary ocaml function of the type
env_t -> syntax_t * env_t
meaning that it will take an environment e, generate some
syntax and produce a new environment e' by adding more stuffs into
the old environment. e is used to check the precondition for
running a rule. If any precondition is not met, the engine will
raise a Fail exception and it will run other rules to populate the
environment. For example, if "rule_prop_write" throws a Fail
exception, we will need to run "rule_empty_object".
Suppose we have a rule for reading properties to an object:
has_var(o); obj_type(o); is_number_prop(o, p);
------------------------------------
o.p : number
We will want to have a rule that first checks
1. We have a variable o
2. Var o is of an object type
3. o has a property p
4. property p is a number
After all the preconditions have been satisfied, the engine will
produce a piece of syntax, "o.p" and also populate the environment
by adding the expression "o.p" which has type number so that it can
run other rules later on which use the expression "o.p" as a
precondition.
A rule is typically written as a function like this:
let rule_prop_read env =
let obj = engine#choose require_var env in
engine#assert is_obj_type obj;
let prop = engine#choose require_prop obj in
engine#assert is_number prop;
let syntax = mk_syntax in
let new_env = mk_new_env env in
syntax, new_env
The first line contains a call named "require_var" meaning that it
requires the environment to have a variable. The second line
asserts that the variable is an object. The engine ensures that the
rule continues executing iff the assert call passes and in this
case if the execution passes the second line the "obj" variable
created in the first line is guaranteed to be an object type. It
does this by throwing exceptions and reruning the function.
Another power of the engine comes from the engine#choose method.It
is very likely that the "obj" variable we get from the environment
in the first line is not an object and thus the assertion will
fail.Then a backtracking is exercised to ensure that eventually we
will get an object from the environment if there's one. The
engine#choose method does this backtracking behind the
scene. Whenever an assertion fails, the engine will rerun the rule
and make sure the results enclosed by engine#choose method will
return a different result.
Notice that a rule can be written in many other ways and we could
totally write the property read rule like this:
let rule_prop_read env =
let obj, prop = engine#choose require_obj_with_number_property env in
let syntax = mk_syntax in
let new_env = mk_new_env env in
syntax, new_env
where the first four lines of the original rule is condensed into a
single require call. Nothing prevents a user from writing this
and no backtracking is even required in this case, because a user
could write "require_obj_with_number_property" in a way that it
only returns an object variable and its number property. The
major downside with this style is that it doesn't conform with
the style of the traditional type rule where preconditions are
written separately. With the power of backtracking, users could
write rules with simple require and assert functions and we
recommend users writing rules this way. *)
(* An exception indicating we want to backtrack *)
exception Backtrack
(* An exception indicating a rule fails *)
exception Fail
(* 'a - type of environment element
'b - type of the environment
'c - type of the syntax *)
class virtual ['a, 'b, 'c] engine = object(self)
(* The backtracking is implemented using a hash table
with a "size" integer which simulates a stack. Whenever
a user writes a require function or any other functions
that returns multiple values and backtracking is desired,
we will put all the candidate values onto the stack.
Whenever the function at the top of the stack is called,
we will update the "pointer" so that we will give a different
candidate value.
Since a rule can be rerun and thus we could totally run
a function that is not at the top of the stack. In that case,
we will simply return its old value, because we need to
iterate all its children values if we think of the combinatorial
search in terms of a tree.
OCaml's stack doesn't provide the functionality of accessing
the values other than its top. So we are using a hash table
where the key is an integer serving as an array index if we want
to access the values other than the top. The "size" value keeps
track of the size of the stack.
*)
val stack = Array.init 100 (fun _ -> [])
val mutable size = 0
(* This is used to keep track of the most recent used stack
level in order to do the correct forward at the right level *)
val mutable last_stack_lvl = -1
(* Main methods for getting all the rules for generating programs *)
method virtual get_all_rules : unit -> ('b -> ('c * 'b)) array
(* The assert function that provides backtracking. A strong
assertion is guaranteed to be satisfied using backtracking. *)
method backtrack_on_false (b : bool) : unit =
if not b then raise Backtrack
(* The assert function that will abort a rule *)
method virtual weak_assert : bool -> unit
(* Shuffle a list. This is from
https://stackoverflow.com/questions/15095541/how-to-shuffle-list-in-on-in-ocaml *)
method shuffle (d : 'a list) : 'a list =
let nd = List.map (fun c -> (Random.bits (), c)) d in
let sond = List.sort compare nd in
List.map snd sond
(* A method for printing the stack *)
method virtual print_stack : unit -> unit
method virtual print_env : 'b -> unit
method virtual print_syntax : 'c -> unit
method virtual combine_syntax : 'c list -> string
(* A mehod for getting the name of an engine *)
method virtual get_name : unit -> string
(* Choose a element from a list using combinatorial search.
id : This is index in the stack
func : This is the function that produces the data
Whenver this function is called, it will return the "current"
candidate value. if we encounter a "Backtrack" exception,
we will call "forward" method to move the "pointer" to the
next candidate value. If we cannot move the pointer,
we throws a Fail exception meaning that the rule fails.
The id param indicates the level in the stack. This is very
important and necessary, because inside a rule the same
require functions might be called multiple times. This id
param ensures that we don't return the same value for
these identical require functions.
*)
method choose
(id : int)
(func : unit -> 'a list) : 'a =
(* The depth is larger than the stack right now. We need to
push the candidate values onto the stack
*)
if id >= size then begin
(* push the data onto the stack *)
stack.(id) <- (func ());
size <- size + 1;
end;
(* Get the current value from the stack *)
match stack.(id) with
| [] -> raise Fail
| hd :: _ -> last_stack_lvl <- id; hd
(* We move the pointer forward so that next time "choose" is
called, we give a different result *)
method forward () =
(* The stack is empty. Abort the rule *)
if size = 0 || last_stack_lvl = -1 then
raise Fail
else begin
size <- min (last_stack_lvl + 1) size;
(* remove the old value *)
let all_vals = stack.(size - 1) in
stack.(size - 1) <- (List.tl all_vals);
(* If there's no more new candidate value,
we pop the function and move the pointer for
the next level *)
if stack.(size - 1) = [] then begin
(* pop the empty candidate value list *)
size <- size - 1;
(* Move the pointer for the next candidate value list *)
self#forward ()
end;
end
(* Clear the stack *)
method clear () =
size <- 0;
last_stack_lvl <- -1
(* method for running a single rule *)
method run
(rule : 'b -> ('c * 'b))
(env : 'b) : ('c * 'b) =
(* run the rule *)
try rule env with
| Backtrack ->
self#forward ();
self#run rule env
| Fail -> raise Fail
(* Run the rule until we run out of choices *)
method run_exhaustive
(rule : 'b -> ('c * 'b))
(env : 'b) : ('c * 'b) list =
self#clear ();
let rec helper all_result =
let r = try Some (self#run rule env) with
| Fail -> None in
match r with
| None -> all_result
| Some r ->
if size > 0 then begin
try
self#forward ();
helper (r :: all_result)
with
| Fail -> r :: all_result
end
else r :: all_result in
helper []
(* Main function for generating programs exhaustively.
Limit is an integer used to limit the number of programs at the end.
*)
method gen_prog (limit : int) : ('c list * 'b) list =
let rules = self#get_all_rules () in
(* This is the main queue for storying environments and corresponding syntax *)
let queue = Queue.create () in
(* This is used to store temporary results *)
let tmp_queue = Queue.create () in
(* We start with empty syntax and empty environment *)
Queue.push ([], []) queue;
(* Run a rule through all the results in the queue *)
let helper (rule : 'b -> ('c * 'b)) : unit =
(*
Printf.printf "Queue size : %d\n" (Queue.length queue);
Queue.iter (fun (slist, env) ->
self#print_env env;
Printf.printf "Syntax:\n";
List.iter (fun s -> self#print_syntax s) slist) queue;
*)
Queue.iter (fun (slist, env) ->
(* type check the program *)
let prog = self#combine_syntax slist in
let type_check_result =
if Utils.is_typecheck (self#get_name ()) || Config.(config.random) then
Utils.type_check prog
else
None in
match type_check_result with
| Some msg -> Logging.log_early_type_error prog msg
| None ->
let result = self#run_exhaustive rule env in
if result = [] then
(* We failed. Put the old syntax and env back into the queue *)
Queue.push (slist, env) tmp_queue
else
List.iter (fun (s, e) -> Queue.push (s :: slist, e) tmp_queue) result)
queue;
(* transfer the run results into the queue *)
Queue.clear queue;
Queue.transfer tmp_queue queue in
let rec limit_result count acc all_result = match all_result with
| [] -> acc
| _ when count >= limit -> acc
| hd :: tl -> limit_result (count + 1) (hd :: acc) tl in
(* Run all the rules *)
Array.iter (fun rule -> helper rule) rules;
(* We limit the number of results at the end *)
Queue.fold (fun acc elt -> elt :: acc) [] queue
|> limit_result 0 []
end;;