OLD | NEW |
---|---|
(Empty) | |
1 #!/usr/bin/env python | |
2 # Copyright 2014 the V8 project authors. All rights reserved. | |
3 # Use of this source code is governed by a BSD-style license that can be | |
4 # found in the LICENSE file. | |
5 | |
6 import os | |
7 import re | |
8 import shutil | |
9 import sys | |
10 | |
11 # TODO(jkummerow): Support DATA_VIEW_{G,S}ETTER in runtime.cc | |
12 | |
13 FILENAME = "src/runtime.cc" | |
14 HEADERFILENAME = "src/runtime.h" | |
15 FUNCTION = re.compile("^RUNTIME_FUNCTION\(Runtime_(\w+)") | |
16 ARGSLENGTH = re.compile(".*ASSERT\(.*args\.length\(\) == (\d+)\);") | |
17 FUNCTIONEND = "}\n" | |
18 | |
19 WORKSPACE = os.path.abspath(os.path.join(os.path.dirname(sys.argv[0]), "..")) | |
20 BASEPATH = os.path.join(WORKSPACE, "test", "mjsunit", "runtime-gen") | |
21 THIS_SCRIPT = os.path.relpath(sys.argv[0]) | |
22 | |
23 # Counts of functions in each detection state. These are used to assert | |
24 # that the parser doesn't bit-rot. Change the values as needed when you add, | |
25 # remove or change runtime functions, but make sure we don't lose our ability | |
26 # to parse them! | |
27 EXPECTED_FUNCTION_COUNT = 334 | |
28 EXPECTED_FUZZABLE_COUNT = 312 | |
29 EXPECTED_CCTEST_COUNT = 6 | |
30 EXPECTED_UNKNOWN_COUNT = 7 | |
31 | |
32 | |
33 # Don't call these at all. | |
34 BLACKLISTED = [ | |
35 "Abort", # Kills the process. | |
36 "AbortJS", # Kills the process. | |
37 "CompileForOnStackReplacement", # Riddled with ASSERTs. | |
38 "IS_VAR", # Not implemented in the runtime. | |
39 "ListNatives", # Not available in Release mode. | |
40 "SetAllocationTimeout", # Too slow for fuzzing. | |
41 "SystemBreak", # Kills (int3) the process. | |
42 | |
43 # TODO(jkummerow): Fix these and un-blacklist them! | |
44 "CreateDateTimeFormat", | |
45 "CreateNumberFormat", | |
46 ] | |
47 | |
48 | |
49 # These will always throw. | |
50 THROWS = [ | |
51 "CheckExecutionState", # Needs to hit a break point. | |
52 "CheckIsBootstrapping", # Needs to be bootstrapping. | |
53 "DebugEvaluate", # Needs to hit a break point. | |
54 "DebugEvaluateGlobal", # Needs to hit a break point. | |
55 "DebugIndexedInterceptorElementValue", # Needs an indexed interceptor. | |
56 "DebugNamedInterceptorPropertyValue", # Needs a named interceptor. | |
57 "DebugSetScriptSource", # Checks compilation state of script. | |
58 "GetAllScopesDetails", # Needs to hit a break point. | |
59 "GetFrameCount", # Needs to hit a break point. | |
60 "GetFrameDetails", # Needs to hit a break point. | |
61 "GetRootNaN", # Needs to be bootstrapping. | |
62 "GetScopeCount", # Needs to hit a break point. | |
63 "GetScopeDetails", # Needs to hit a break point. | |
64 "GetStepInPositions", # Needs to hit a break point. | |
65 "GetTemplateField", # Needs a {Function,Object}TemplateInfo. | |
66 "GetThreadCount", # Needs to hit a break point. | |
67 "GetThreadDetails", # Needs to hit a break point. | |
68 "IsAccessAllowedForObserver", # Needs access-check-required object. | |
69 "LiveEditFunctionSourceUpdated", # Needs a SharedFunctionInfo. | |
70 "LiveEditPatchFunctionPositions", # Needs a SharedFunctionInfo. | |
71 "LiveEditReplaceFunctionCode", # Needs a SharedFunctionInfo. | |
72 "LiveEditReplaceRefToNestedFunction", # Needs a SharedFunctionInfo. | |
73 "LiveEditRestartFrame", # Needs to hit a break point. | |
74 "UnblockConcurrentRecompilation" # Needs --block-concurrent-recompilation. | |
75 ] | |
76 | |
77 | |
78 # Definitions used in CUSTOM_KNOWN_GOOD_INPUT below. | |
79 _BREAK_ITERATOR = ( | |
80 "%GetImplFromInitializedIntlObject(new Intl.v8BreakIterator())") | |
81 _COLLATOR = "%GetImplFromInitializedIntlObject(new Intl.Collator('en-US'))" | |
82 _DATETIME_FORMAT = ( | |
83 "%GetImplFromInitializedIntlObject(new Intl.DateTimeFormat('en-US'))") | |
84 _NUMBER_FORMAT = ( | |
85 "%GetImplFromInitializedIntlObject(new Intl.NumberFormat('en-US'))") | |
86 _SCRIPT = "%DebugGetLoadedScripts()[1]" | |
87 | |
88 | |
89 # Custom definitions for function input that does not throw. | |
90 # Format: "FunctionName": ["arg0", "arg1", ..., argslength]. | |
91 # None means "fall back to autodetected value". | |
92 CUSTOM_KNOWN_GOOD_INPUT = { | |
93 "Apply": ["function() {}", None, None, None, None, None], | |
94 "ArrayBufferSliceImpl": [None, None, 0, None], | |
95 "ArrayConcat": ["[1, 'a']", None], | |
96 "BreakIteratorAdoptText": [_BREAK_ITERATOR, None, None], | |
97 "BreakIteratorBreakType": [_BREAK_ITERATOR, None], | |
98 "BreakIteratorCurrent": [_BREAK_ITERATOR, None], | |
99 "BreakIteratorFirst": [_BREAK_ITERATOR, None], | |
100 "BreakIteratorNext": [_BREAK_ITERATOR, None], | |
101 "CompileString": [None, "false", None], | |
102 "CreateBreakIterator": ["'en-US'", "{type: 'string'}", None, None], | |
103 "CreateJSFunctionProxy": [None, "function() {}", None, None, None], | |
104 "CreatePrivateSymbol": ["\"foo\"", None], | |
105 "CreateSymbol": ["\"foo\"", None], | |
106 "DateParseString": [None, "new Array(8)", None], | |
107 "DebugSetScriptSource": [_SCRIPT, None, None], | |
108 "DefineOrRedefineAccessorProperty": [None, None, "function() {}", | |
109 "function() {}", 2, None], | |
110 "GetBreakLocations": [None, 0, None], | |
111 "GetDefaultReceiver": ["function() {}", None], | |
112 "GetImplFromInitializedIntlObject": ["new Intl.NumberFormat('en-US')", None], | |
113 "InternalCompare": [_COLLATOR, None, None, None], | |
114 "InternalDateFormat": [_DATETIME_FORMAT, None, None], | |
115 "InternalDateParse": [_DATETIME_FORMAT, None, None], | |
116 "InternalNumberFormat": [_NUMBER_FORMAT, None, None], | |
117 "InternalNumberParse": [_NUMBER_FORMAT, None, None], | |
118 "IsSloppyModeFunction": ["function() {}", None], | |
119 "LiveEditFindSharedFunctionInfosForScript": [_SCRIPT, None], | |
120 "LiveEditGatherCompileInfo": [_SCRIPT, None, None], | |
121 "LiveEditReplaceScript": [_SCRIPT, None, None, None], | |
122 "LoadMutableDouble": ["{foo: 1.2}", None, None], | |
123 "NewObjectFromBound": ["(function() {}).bind({})", None], | |
124 "NumberToRadixString": [None, "2", None], | |
125 "ParseJson": ["\"{}\"", 1], | |
126 "SetAccessorProperty": [None, None, "undefined", "undefined", None, None, | |
127 None], | |
128 "SetCreateIterator": [None, "2", None], | |
129 "SetDebugEventListener": ["undefined", None, None], | |
130 "SetScriptBreakPoint": [_SCRIPT, None, 0, None, None], | |
131 "StringBuilderConcat": ["[1, 2, 3]", 3, None, None], | |
132 "StringBuilderJoin": ["['a', 'b']", 4, None, None], | |
133 "TypedArrayInitialize": [None, None, "new ArrayBuffer(8)", None, None, None], | |
134 "TypedArraySetFastCases": [None, None, "0", None], | |
135 } | |
136 | |
137 | |
138 # Types of arguments that cannot be generated in a JavaScript testcase. | |
139 NON_JS_TYPES = [ | |
140 "Code", "Context", "FixedArray", "FunctionTemplateInfo", | |
141 "JSFunctionResultCache", "JSMessageObject", "Map", "ScopeInfo", | |
142 "SharedFunctionInfo"] | |
143 | |
144 | |
145 # Maps argument types to concrete example inputs of that type. | |
146 JS_TYPE_GENERATORS = { | |
147 "Boolean": "true", | |
148 "HeapObject": "new Object()", | |
149 "Int32": "32", | |
150 "JSArray": "new Array()", | |
151 "JSArrayBuffer": "new ArrayBuffer(8)", | |
152 "JSDataView": "new DataView(new ArrayBuffer(8))", | |
153 "JSDate": "new Date()", | |
154 "JSFunction": "function() {}", | |
155 "JSFunctionProxy": "Proxy.createFunction({}, function() {})", | |
156 "JSGeneratorObject": "(function*(){ yield 1; })()", | |
157 "JSMap": "new Map()", | |
158 "JSMapIterator": "%MapCreateIterator(new Map(), 3)", | |
159 "JSObject": "new Object()", | |
160 "JSProxy": "Proxy.create({})", | |
161 "JSReceiver": "new Object()", | |
162 "JSRegExp": "/ab/g", | |
163 "JSSet": "new Set()", | |
164 "JSSetIterator": "%SetCreateIterator(new Set(), 2)", | |
165 "JSTypedArray": "new Int32Array(2)", | |
166 "JSValue": "new String('foo')", | |
167 "JSWeakCollection": "new WeakMap()", | |
168 "Name": "\"name\"", | |
169 "Number": "1.5", | |
170 "Object": "new Object()", | |
171 "PropertyDetails": "513", | |
172 "SeqString": "\"seqstring\"", | |
173 "Smi": 1, | |
174 "StrictMode": "1", | |
175 "String": "\"foo\"", | |
176 "Symbol": "Symbol(\"symbol\")", | |
177 "Uint32": "32", | |
178 } | |
179 | |
180 | |
181 class ArgParser(object): | |
182 def __init__(self, regex, ctor): | |
183 self.regex = regex | |
184 self.ArgCtor = ctor | |
185 | |
186 | |
187 class Arg(object): | |
188 def __init__(self, typename, varname, index): | |
189 self.type = typename | |
190 self.name = "_%s" % varname | |
191 self.index = index | |
192 | |
193 | |
194 class Function(object): | |
195 def __init__(self, match): | |
196 self.name = match.group(1) | |
197 self.argslength = -1 | |
198 self.args = {} | |
199 self.inline = "" | |
200 | |
201 handle_arg_parser = ArgParser( | |
202 re.compile("^\s*CONVERT_ARG_HANDLE_CHECKED\((\w+), (\w+), (\d+)\)"), | |
203 lambda match: Arg(match.group(1), match.group(2), int(match.group(3)))) | |
204 | |
205 plain_arg_parser = ArgParser( | |
206 re.compile("^\s*CONVERT_ARG_CHECKED\((\w+), (\w+), (\d+)\)"), | |
207 lambda match: Arg(match.group(1), match.group(2), int(match.group(3)))) | |
208 | |
209 number_handle_arg_parser = ArgParser( | |
210 re.compile("^\s*CONVERT_NUMBER_ARG_HANDLE_CHECKED\((\w+), (\d+)\)"), | |
211 lambda match: Arg("Number", match.group(1), int(match.group(2)))) | |
212 | |
213 smi_arg_parser = ArgParser( | |
214 re.compile("^\s*CONVERT_SMI_ARG_CHECKED\((\w+), (\d+)\)"), | |
215 lambda match: Arg("Smi", match.group(1), int(match.group(2)))) | |
216 | |
217 double_arg_parser = ArgParser( | |
218 re.compile("^\s*CONVERT_DOUBLE_ARG_CHECKED\((\w+), (\d+)\)"), | |
219 lambda match: Arg("Number", match.group(1), int(match.group(2)))) | |
220 | |
221 number_arg_parser = ArgParser( | |
222 re.compile( | |
223 "^\s*CONVERT_NUMBER_CHECKED\(\w+, (\w+), (\w+), args\[(\d+)\]\)"), | |
224 lambda match: Arg(match.group(2), match.group(1), int(match.group(3)))) | |
225 | |
226 strict_mode_arg_parser = ArgParser( | |
227 re.compile("^\s*CONVERT_STRICT_MODE_ARG_CHECKED\((\w+), (\d+)\)"), | |
228 lambda match: Arg("StrictMode", match.group(1), int(match.group(2)))) | |
229 | |
230 boolean_arg_parser = ArgParser( | |
231 re.compile("^\s*CONVERT_BOOLEAN_ARG_CHECKED\((\w+), (\d+)\)"), | |
232 lambda match: Arg("Boolean", match.group(1), int(match.group(2)))) | |
233 | |
234 property_details_parser = ArgParser( | |
235 re.compile("^\s*CONVERT_PROPERTY_DETAILS_CHECKED\((\w+), (\d+)\)"), | |
236 lambda match: Arg("PropertyDetails", match.group(1), int(match.group(2)))) | |
237 | |
238 arg_parsers = [handle_arg_parser, plain_arg_parser, number_handle_arg_parser, | |
239 smi_arg_parser, | |
240 double_arg_parser, number_arg_parser, strict_mode_arg_parser, | |
241 boolean_arg_parser, property_details_parser] | |
242 | |
243 | |
244 def SetArgsLength(self, match): | |
245 self.argslength = int(match.group(1)) | |
246 | |
247 def TryParseArg(self, line): | |
248 for parser in Function.arg_parsers: | |
249 match = parser.regex.match(line) | |
250 if match: | |
251 arg = parser.ArgCtor(match) | |
252 self.args[arg.index] = arg | |
253 return True | |
254 return False | |
255 | |
256 def Filename(self): | |
257 return "%s.js" % self.name.lower() | |
Dmitry Lomov (no reviews)
2014/04/25 12:54:34
Suggestion: why lowercase names for tests? It's ok
Jakob Kummerow
2014/05/07 13:07:54
I don't care much either way, but http://google-st
| |
258 | |
259 def __str__(self): | |
260 s = [self.name, "("] | |
261 argcount = self.argslength | |
262 if argcount < 0: | |
263 print("WARNING: unknown argslength for function %s" % self.name) | |
264 if self.args: | |
265 argcount = max([self.args[i].index + 1 for i in self.args]) | |
266 else: | |
267 argcount = 0 | |
268 for i in range(argcount): | |
269 if i > 0: s.append(", ") | |
270 s.append(self.args[i].type if i in self.args else "<unknown>") | |
271 s.append(")") | |
272 return "".join(s) | |
273 | |
274 # Parses HEADERFILENAME to find out which runtime functions are "inline". | |
275 def FindInlineRuntimeFunctions(): | |
276 inline_functions = [] | |
277 with open(HEADERFILENAME, "r") as f: | |
278 inline_list = "#define INLINE_FUNCTION_LIST(F) \\\n" | |
279 inline_opt_list = "#define INLINE_OPTIMIZED_FUNCTION_LIST(F) \\\n" | |
280 inline_function = re.compile(r"^\s*F\((\w+), \d+, \d+\)\s*\\") | |
281 mode = "SEARCHING" | |
282 for line in f: | |
283 if mode == "ACTIVE": | |
284 match = inline_function.match(line) | |
285 if match: | |
286 inline_functions.append(match.group(1)) | |
287 if not line.endswith("\\\n"): | |
288 mode = "SEARCHING" | |
289 elif mode == "SEARCHING": | |
290 if line == inline_list or line == inline_opt_list: | |
291 mode = "ACTIVE" | |
292 return inline_functions | |
293 | |
294 | |
295 # Detects runtime functions by parsing FILENAME. | |
296 def FindRuntimeFunctions(): | |
297 inline_functions = FindInlineRuntimeFunctions() | |
298 functions = [] | |
299 with open(FILENAME, "r") as f: | |
300 function = None | |
301 partial_line = "" | |
302 for line in f: | |
303 # Multi-line definition support, ignoring macros. | |
304 if line.startswith("RUNTIME_FUNCTION") and not line.endswith("{\n"): | |
305 if line.endswith("\\\n"): continue | |
306 partial_line = line.rstrip() | |
307 continue | |
308 if partial_line: | |
309 partial_line += " " + line.strip() | |
310 if partial_line.endswith("{"): | |
311 line = partial_line | |
312 partial_line = "" | |
313 else: | |
314 continue | |
315 | |
316 match = FUNCTION.match(line) | |
317 if match: | |
318 function = Function(match) | |
319 if function.name in inline_functions: | |
320 function.inline = "_" | |
321 continue | |
322 if function is None: continue | |
323 | |
324 match = ARGSLENGTH.match(line) | |
325 if match: | |
326 function.SetArgsLength(match) | |
327 continue | |
328 | |
329 if function.TryParseArg(line): | |
330 continue | |
331 | |
332 if line == FUNCTIONEND: | |
333 if function is not None: | |
334 functions.append(function) | |
335 function = None | |
336 return functions | |
337 | |
338 # Classifies runtime functions. | |
339 def ClassifyFunctions(functions): | |
340 # Can be fuzzed with a JavaScript testcase. | |
341 js_fuzzable_functions = [] | |
342 # We have enough information to fuzz these, but they need inputs that | |
343 # cannot be created or passed around in JavaScript. | |
344 cctest_fuzzable_functions = [] | |
345 # This script does not have enough information about these. | |
346 unknown_functions = [] | |
347 | |
348 types = {} | |
349 for f in functions: | |
350 if f.name in BLACKLISTED: | |
351 continue | |
352 decision = js_fuzzable_functions | |
353 custom = CUSTOM_KNOWN_GOOD_INPUT.get(f.name, None) | |
354 if f.argslength < 0: | |
355 # Unknown length -> give up unless there's a custom definition. | |
356 if custom and custom[-1] is not None: | |
357 f.argslength = custom[-1] | |
358 assert len(custom) == f.argslength + 1, \ | |
359 ("%s: last custom definition must be argslength" % f.name) | |
360 else: | |
361 decision = unknown_functions | |
362 else: | |
363 if custom: | |
364 # Any custom definitions must match the known argslength. | |
365 assert len(custom) == f.argslength + 1, \ | |
366 ("%s should have %d custom definitions but has %d" % | |
367 (f.name, f.argslength + 1, len(custom))) | |
368 for i in range(f.argslength): | |
369 if custom and custom[i] is not None: | |
370 # All good, there's a custom definition. | |
371 pass | |
372 elif not i in f.args: | |
373 # No custom definition and no parse result -> give up. | |
374 decision = unknown_functions | |
375 else: | |
376 t = f.args[i].type | |
377 if t in NON_JS_TYPES: | |
378 decision = cctest_fuzzable_functions | |
379 else: | |
380 assert t in JS_TYPE_GENERATORS, \ | |
381 ("type generator not found for %s, function: %s" % (t, f)) | |
382 decision.append(f) | |
383 return (js_fuzzable_functions, cctest_fuzzable_functions, unknown_functions) | |
384 | |
385 | |
386 def GenerateJSTestcaseForFunction(f): | |
387 s = ["// Copyright 2014 the V8 project authors. All rights reserved.", | |
388 "// AUTO-GENERATED BY tools/generate-runtime-tests.py, DO NOT MODIFY", | |
389 "// Flags: --allow-natives-syntax --harmony"] | |
390 call = "%%%s%s(" % (f.inline, f.name) | |
391 custom = CUSTOM_KNOWN_GOOD_INPUT.get(f.name, None) | |
392 for i in range(f.argslength): | |
393 if custom and custom[i] is not None: | |
394 (name, value) = ("arg%d" % i, custom[i]) | |
395 else: | |
396 arg = f.args[i] | |
397 (name, value) = (arg.name, JS_TYPE_GENERATORS[arg.type]) | |
398 s.append("var %s = %s;" % (name, value)) | |
399 if i > 0: call += ", " | |
400 call += name | |
401 call += ");" | |
402 if f.name in THROWS: | |
403 s.append("try {") | |
404 s.append(call); | |
405 s.append("} catch(e) {}") | |
406 else: | |
407 s.append(call) | |
408 testcase = "\n".join(s) | |
409 path = os.path.join(BASEPATH, f.Filename()) | |
410 with open(path, "w") as f: | |
411 f.write("%s\n" % testcase) | |
412 | |
413 def GenerateTestcases(functions): | |
414 shutil.rmtree(BASEPATH) # Re-generate everything. | |
415 os.makedirs(BASEPATH) | |
416 for f in functions: | |
417 GenerateJSTestcaseForFunction(f) | |
418 | |
419 def PrintUsage(): | |
420 print """Usage: %(this_script)s ACTION | |
421 | |
422 where ACTION can be: | |
423 | |
424 info Print diagnostic info. | |
425 check Check that runtime functions can be parsed as expected, and that | |
426 test cases exist. | |
427 generate Parse source code for runtime functions, and auto-generate | |
428 test cases for them. Warning: this will nuke and re-create | |
429 %(path)s. | |
430 """ % {"path": os.path.relpath(BASEPATH), "this_script": THIS_SCRIPT} | |
431 | |
432 if __name__ == "__main__": | |
433 if len(sys.argv) != 2: | |
434 PrintUsage() | |
435 sys.exit(1) | |
436 action = sys.argv[1] | |
437 if action in ["-h", "--help", "help"]: | |
438 PrintUsage() | |
439 sys.exit(0) | |
440 | |
441 functions = FindRuntimeFunctions() | |
442 (js_fuzzable_functions, cctest_fuzzable_functions, unknown_functions) = \ | |
443 ClassifyFunctions(functions) | |
444 | |
445 if action == "info": | |
446 print("%d functions total; js_fuzzable_functions: %d, " | |
447 "cctest_fuzzable_functions: %d, unknown_functions: %d" | |
448 % (len(functions), len(js_fuzzable_functions), | |
449 len(cctest_fuzzable_functions), len(unknown_functions))) | |
450 print("unknown functions:") | |
451 for f in unknown_functions: | |
452 print(f) | |
453 sys.exit(0) | |
454 | |
455 if action == "check": | |
456 error = False | |
457 def CheckCount(actual, expected, description): | |
458 global error | |
459 if len(actual) != expected: | |
460 print("Expected to detect %d %s, but found %d." % ( | |
461 expected, description, len(actual))) | |
462 error = True | |
463 CheckCount(functions, EXPECTED_FUNCTION_COUNT, "functions in total") | |
464 CheckCount(js_fuzzable_functions, EXPECTED_FUZZABLE_COUNT, | |
465 "JavaScript-fuzzable functions") | |
466 CheckCount(cctest_fuzzable_functions, EXPECTED_CCTEST_COUNT, | |
467 "cctest-fuzzable functions") | |
468 CheckCount(unknown_functions, EXPECTED_UNKNOWN_COUNT, | |
469 "functions with incomplete type information") | |
470 | |
471 def CheckTestcasesExisting(functions): | |
472 global error | |
473 for f in functions: | |
474 if not os.path.isfile(os.path.join(BASEPATH, f.Filename())): | |
475 print("Missing testcase for %s, please run '%s generate'" % | |
Dmitry Lomov (no reviews)
2014/04/25 12:54:34
Suggestion: would also be awesome to check if file
Jakob Kummerow
2014/05/07 13:07:54
Yeah, that's a possible follow-up change.
| |
476 (f.name, THIS_SCRIPT)) | |
477 error = True | |
478 files = os.listdir(BASEPATH) | |
479 if (len(files) != len(functions)): | |
480 unexpected_files = set(files) - set([f.Filename() for f in functions]) | |
481 for f in unexpected_files: | |
482 print("Unexpected testcase: %s" % os.path.join(BASEPATH, f)) | |
483 error = True | |
484 CheckTestcasesExisting(js_fuzzable_functions) | |
485 | |
486 if error: | |
487 sys.exit(1) | |
488 print("All good.") | |
489 sys.exit(0) | |
490 | |
491 if action == "generate": | |
492 GenerateTestcases(js_fuzzable_functions) | |
493 sys.exit(0) | |
OLD | NEW |