diff --git a/emcc.py b/emcc.py index f099c5b3379d1..858b04c8edcfd 100755 --- a/emcc.py +++ b/emcc.py @@ -1909,8 +1909,8 @@ def get_full_import_name(name): shared.Settings.MAXIMUM_MEMORY > 2 * 1024 * 1024 * 1024)): shared.Settings.CAN_ADDRESS_2GB = 1 - if shared.Settings.EXCEPTION_HANDLING and shared.Settings.OPT_LEVEL >= 2 and not compile_only: - exit_with_error('wasm exception handling support is still experimental, and linking with -O2+ is not supported yet') + #if shared.Settings.EXCEPTION_HANDLING and shared.Settings.OPT_LEVEL >= 2 and not compile_only: + # exit_with_error('wasm exception handling support is still experimental, and linking with -O2+ is not supported yet') shared.Settings.EMSCRIPTEN_VERSION = shared.EMSCRIPTEN_VERSION shared.Settings.PROFILING_FUNCS = options.profiling_funcs diff --git a/emscripten.py b/emscripten.py index e6045033a48f1..a8ee27b48c19b 100644 --- a/emscripten.py +++ b/emscripten.py @@ -1,3 +1,4 @@ +import sys # Copyright 2010 The Emscripten Authors. All rights reserved. # Emscripten is available under two separate licenses, the MIT license and the # University of Illinois/NCSA Open Source License. Both these licenses can be @@ -430,6 +431,7 @@ def finalize_wasm(infile, outfile, memfile, DEBUG): if shared.Settings.DEBUG_LEVEL >= 3: args.append('--dwarf') +# sys.exit(0) stdout = building.run_binaryen_command('wasm-emscripten-finalize', infile=infile, outfile=outfile if modify_wasm else None, diff --git a/tests/fuzz/fuzz_exceptions.py b/tests/fuzz/fuzz_exceptions.py new file mode 100644 index 0000000000000..b5df1f816b066 --- /dev/null +++ b/tests/fuzz/fuzz_exceptions.py @@ -0,0 +1,973 @@ +import json +import os +import random +import subprocess +import sys +import time + +''' +Structural fuzz generator. + +Like the AFL and Binaryen etc. fuzzers, we start with random bytes as the input. +However, we start with a tree structure of random bytes, something like + +[ + 1, + [ + 0.42, + 0.501, + 0.17 + ], + [ + [ + ], + [ + 0.2, + 0.12 + ] + ] +] + +The grammar here is simply + +Node = Number or Array +Array = Node^K, K >= 0 +Number = [0..1) + +Starting from structured random data has the benefit of making it easy to reduce +on the random input. Consider if the structure of the random input gets mapped +to something else with structure, like a source program, then pruning the input +can lead to similar pruned source programs. (In comparison, unstructured random +data allows for truncation easily, but changing bytes earlier can lead to +dramatic differences in the output.) + +To get this benefit, the translator of the random structured data must convert +it to the output in a structured manner. That is, one node should be converted +to a corresponding node, and without looking at other nodes as much as possible, +so that if they are altered, that one will not be. + +A downside to this approach is that a very large random input may lead to a very +small output, for example, if a huge nested tree of data is consumed in a place +that just wants a bool. +''' + + +class StructuredRandomData: + NUM_TOPLEVEL = 5 + + # The range of widths. + MIN_WIDTH = 1 + MAX_WIDTH = 9 + + # The range of depths. + MIN_DEPTH = 2 + MAX_DEPTH = 10 + + # The chance to just emit a number instead of a list. + NUM_PROB = 0.25 + + def __init__(self): + self.root = [self.make_toplevel() for x in range(self.NUM_TOPLEVEL)] + + def make_toplevel(self): + depth_left = random.randint(self.MIN_DEPTH, self.MAX_DEPTH) + return self.make_array(0, depth_left) + + def make_array(self, depth, depth_left): + width = random.randint(self.MIN_WIDTH, self.MAX_WIDTH) + # When there is almost no depth left, emit fewer things. + width = min(width, depth_left + 1) + return [self.make(depth + 1, depth_left - 1) for i in range(width)] + + def make_num(self): + return random.random() + + def make(self, depth, depth_left): + if depth_left == 0 or random.random() < self.NUM_PROB: + return self.make_num() + return self.make_array(depth, depth_left) + + +def numify(node): + if type(node) == list: + if len(node) == 0: + return 0 + return numify(node[0]) + return node + + +def arrayify(node): + if type(node) != list: + return [node] + return node + + +def indent(code): + return '\n'.join([' ' + line for line in code.splitlines() if line]) + + +class Cursor: + ''' + A cursor over an array, allowing gradual consumption of it. If we run out, we + return simple values. + ''' + def __init__(self, array): + self.array = arrayify(array) + self.pos = 0 + + def get(self): + if self.pos >= len(self.array): + return 0 + self.pos += 1 + return self.array[self.pos - 1] + + def get_num(self): + return numify(self.get()) + + def get_array(self): + return arrayify(self.get()) + + def remaining(self): + return max(0, len(self.array) - self.pos) + + def has_more(self): + return self.remaining() > 0 + + def get_int(self): + return int(1000 * self.get_num()) + + +def pick(options, value): + ''' + Given a list of options (weight, choice), and a value in [0, 1) to help pick from + them, pick one. + The options can also not have a weight, in which case the weight is 1. + ''' + # Scale the value by the total weight. + assert 0 <= value < 1, value + options = [option if type(option) in (tuple, list) and len(option) == 2 else (1, option) for option in options] + total = 0 + for weight, choice in options: + total += weight + value *= total + + for weight, choice in options: + if value < weight: + return choice + value -= weight + + raise Exception('inconceivable') + + +class CppTranslator: + ''' + Translates random structured data into a random C++ program that uses C++ + exceptions. + ''' + + PREAMBLE = '''\ +#include <stdio.h> // avoid iostream C++ code, just test libc++abi, not libc++ +#include <stdint.h> + +extern void refuel(); +extern void checkRecursion(); +extern bool getBoolean(); + +struct Class { + Class(); + ~Class(); +}; +''' + + SUPPORT = '''\ +#include <stdio.h> +#include <stdlib.h> + +const int INIIAL_FUEL = 100; + +static int fuel = INIIAL_FUEL; + +void refuel() { + fuel = INIIAL_FUEL; +} + +void checkRecursion() { + if (fuel == 0) { + puts("out of fuel"); + abort(); + } + fuel--; +} + +// TODO random data +static bool boolean = true; + +bool getBoolean() { + // If we are done, exit all loops etc. + if (fuel == 0) { + return false; + } + fuel--; + boolean = !boolean; + return boolean; +} + +struct Class { + Class(); + ~Class(); +}; + +Class::Class() { + puts("class-instance"); +} + +Class::~Class() { + puts("~class-instance"); +} +''' + + def __init__(self, data): + self.toplevel = Cursor(data) + self.logging_index = 0 + self.try_nesting = 0 + self.loop_nesting = 0 + self.func_names = [] + + # The output is a list of strings which will be concatenated when + # writing. + self.output = [self.PREAMBLE] + self.make_functions() + + ''' + Outputs the main file and the support file on the side. Support code is not + in the main file so that the optimizer cannot see it all. + ''' + def write(self, main, support): + with open(main, 'w') as f: + f.write('\n'.join(self.output)) + with open(support, 'w') as f: + f.write(self.SUPPORT) + + def make_structs(self): + array = arrayify(self.toplevel.get()) + # Global mapping of struct name to its array of fields. + self.structs = {} + structs = [] + for node in array: + name = f'Struct{len(structs)}' + sig = self.get_types(node) + self.structs[name] = sig + fields = '\n'.join([f' {t} f{i};' for i, t in enumerate(sig)]) + structs.append('''\ +struct %(name)s { +%(fields)s +}; +''' % locals()) + self.output.append('\n'.join(structs)) + + def make_functions(self): + funcs = [] + main = '''\ +int main() { +''' + while self.toplevel.has_more(): + name = f'func_{len(funcs)}' + body = indent(self.make_statements(self.toplevel.get())) + self.func_names.append(name) + funcs.append('''\ +void %(name)s() { +%(body)s +} +''' % locals()) + main += '''\ + // %(name)s + puts("calling %(name)s"); + refuel(); + try { + %(name)s(); + } catch (...) { + puts("main caught from %(name)s"); + } +''' % locals() + + main += '''\ + return 0; +} +''' + funcs.append(main) + self.output.append('\n'.join(funcs)) + + def make_statements(self, node): + statements = [self.make_statement(n) for n in arrayify(node)] + return '\n'.join(statements) + + def make_statement(self, node): + cursor = Cursor(node) + options = [ + (1, self.make_nothing), + (10, self.make_logging), + (10, self.make_try), + (10, self.make_if), + (5, self.make_loop), + (5, self.make_call), + (5, self.make_raii), + ] + if self.try_nesting: + options.append((10, self.make_throw)) + else: + # Only rarely emit throws outside of a try. + options.append((2, self.make_throw)) + if self.loop_nesting: + options.append((10, self.make_branch)) + return pick(options, cursor.get_num())(cursor) + + def make_nothing(self, cursor): + return '' + + def make_logging(self, cursor): + if cursor.has_more(): + return f'puts("log(-{cursor.get_int()})");' + self.logging_index += 1 + return f'puts("log({self.logging_index})");' + + def make_throw(self, cursor): + return f'throw {cursor.get_int()};' + + def make_raii(self, cursor): + self.logging_index += 1 + return f'Class instance{self.logging_index};' + + def make_try(self, cursor): + self.try_nesting += 1 + body = indent(self.make_statements(cursor.get_array())) + self.try_nesting -= 1 + catch_types = ['int32_t', 'int64_t', 'float', 'double', 'Class'] + catches = [] + + def add_catch(ty): + catch = indent(self.make_statements(cursor.get_array())) + catches.append('''\ +} catch (%(ty)s) { +%(catch)s +''' % locals()) + + num = cursor.get_num() + if num < 0.5: + add_catch(pick(catch_types, num * 2)) + if not catches or num > 0.5: + add_catch('...') + catches = '\n'.join(catches) + return '''\ +try { +%(body)s +%(catches)s +} +''' % locals() + + def make_if(self, cursor): + if_arm = indent(self.make_statements(cursor.get_array())) + + else_ = '' + if cursor.get_num() >= 0.5: + else_arm = indent(self.make_statements(cursor.get_array())) + else_ = '''\ + else { +%(else_arm)s +}''' % locals() + + return '''\ +if (getBoolean()) { +%(if_arm)s +}%(else_)s +''' % locals() + + def make_loop(self, cursor): + self.loop_nesting += 1 + body = indent(self.make_statements(cursor.get_array())) + self.loop_nesting -= 1 + + return '''\ +while (getBoolean()) { +%(body)s +} +''' % locals() + + def make_call(self, cursor): + if not self.func_names: + return self.make_nothing(cursor) + return f'{random.choice(self.func_names)}();' + + def make_branch(self, cursor): + assert self.loop_nesting + if cursor.get_num() < 0.5: + return 'break;' + else: + return 'continue;' + + def get_types(self, node): + return [self.get_type(x) for x in arrayify(node)] + + def get_type(self, node): + if numify(node) < 0.5: + return 'uint32_t' + return 'double' + + + +class LLVMTranslator: + ''' + Translates random structured data into a random LLVM IR uses wasm + exceptions. + ''' + + PREAMBLE = '''\ +; ModuleID = 'a.cpp' +source_filename = "a.cpp" +target datalayout = "e-m:e-p:32:32-i64:64-n32:64-S128" +target triple = "wasm32-unknown-emscripten" + +%struct.Class = type { i8 } + +@_ZTIi = external constant i8* +@_ZTId = external constant i8* +''' + + START = '''\ +define hidden i32 @main() personality i8* bitcast (i32 (...)* @__gxx_wasm_personality_v0 to i8*) { +entry: + br label %b0 +''' + + POSTAMBLE = '''\ + +normal_exit: + ret i32 0 +} + +declare i32 @puts(i8*) + +declare %struct.Class* @_ZN5ClassC1Ev(%struct.Class* nonnull returned dereferenceable(1)) unnamed_addr +declare %struct.Class* @_ZN5ClassD1Ev(%struct.Class* nonnull returned dereferenceable(1)) unnamed_addr + +declare zeroext i1 @_Z10getBooleanv() + +declare i32 @__gxx_wasm_personality_v0(...) +declare i32 @llvm.eh.typeid.for(i8*) + +declare i8* @__cxa_begin_catch(i8*) +declare void @__cxa_end_catch() +declare i8* @__cxa_allocate_exception(i32) +declare void @__cxa_throw(i8*, i8*, i8*) + +declare i8* @llvm.wasm.get.exception(token) +declare i32 @llvm.wasm.get.ehselector(token) +declare void @llvm.wasm.rethrow() +''' + + SUPPORT = '''\ +#include <stdio.h> +#include <stdlib.h> + +const int INIIAL_FUEL = 100; + +static int fuel = INIIAL_FUEL; + +void refuel() { + fuel = INIIAL_FUEL; +} + +// TODO random data +static bool boolean = true; + +bool getBoolean() { + // If we are done, exit all loops etc. + if (fuel == 0) { + return false; + } + fuel--; + boolean = !boolean; + return boolean; +} + +struct Class { + Class(); + ~Class(); +}; + +Class::Class() { + puts("class-instance"); +} + +Class::~Class() { + puts("~class-instance"); +} +''' + + def __init__(self, data): + self.cursors = [Cursor(data)] + self.global_index = 1 + self.global_defs = '' + + # The output is a list of strings which will be concatenated when + # writing. + main = self.make_main() + self.output = [self.PREAMBLE, self.global_defs, self.START, + main, self.POSTAMBLE] + + ''' + Outputs the main file and the support file on the side. Support code is not + in the main file so that the optimizer cannot see it all. + ''' + def write(self, main, support): + with open(main, 'w') as f: + f.write('\n'.join(self.output)) + with open(support, 'w') as f: + f.write(self.SUPPORT) + + def get_global_index(self): + ret = self.global_index + self.global_index += 1 + return ret + + def cursor(self): + return self.cursors[-1] + + def push_cursor(self): + self.cursors.append(Cursor(self.cursor().get_array())) + + def pop_cursor(self): + self.cursors.pop() + + def pick(self, options): + return pick(options, self.cursor().get_num()) + + def make_main(self): + return self.make_blocks(start='b0', backs=[], forwards=['normal_exit']) + + def make_blocks(self, start, backs, forwards): + ''' + Make a block or blocks, starting with a block of name 'start', and with + given possible backedges and forward edges that are outside of what we + create in this call. + ''' + assert forwards, 'There must be a place to fall through to.' + + contents = self.pick([ + self.make_basic_block, + self.make_loop, + self.make_if, + self.make_if_else, + self.make_invoke, + ])(backs, forwards) + + return f'''\ +{start}: +{contents}''' + + def make_basic_block_contents(self): + return self.pick([ + self.make_nothing, + self.make_logging, + self.make_call, + ])() + + def make_basic_block(self, backs, forwards): + contents = self.make_basic_block_contents() + terminator = self.pick([ + (10, self.make_forward_branch), + ( 1, self.make_unreachable), + ])(backs, forwards) + + return contents + '\n' + terminator + + def make_loop(self, backs, forwards): + start = f'b{self.get_global_index()}' + end = f'b{self.get_global_index()}' + self.push_cursor() + body = self.make_blocks(start, backs + [start], forwards + [end]) + self.pop_cursor() + call = f'%c{self.get_global_index()}' + + return f'''\ + br label %{start} + +;; loop +{body} + +{end}: + ;; loop end + {call} = call zeroext i1 @_Z10getBooleanv() + br i1 {call}, label %{start}, label %{self.pick(forwards)}''' + + def make_if(self, backs, forwards): + call = f'%c{self.get_global_index()}' + middle = f'b{self.get_global_index()}' + self.push_cursor() + body = self.make_blocks(middle, backs, forwards) + self.pop_cursor() + + return f'''\ + ;; if + {call} = call zeroext i1 @_Z10getBooleanv() + br i1 {call}, label %{middle}, label %{self.pick(forwards)} + +{body}''' + + def make_if_else(self, backs, forwards): + call = f'%c{self.get_global_index()}' + left = f'b{self.get_global_index()}' + right = f'b{self.get_global_index()}' + self.push_cursor() + left_body = self.make_blocks(left, backs, forwards) + self.pop_cursor() + self.push_cursor() + right_body = self.make_blocks(right, backs, forwards) + self.pop_cursor() + + return f'''\ + ;; if-else + {call} = call zeroext i1 @_Z10getBooleanv() + br i1 {call}, label %{left}, label %{right} + +{left_body} + +{right_body}''' + + def make_invoke(self, backs, forwards): + call = f'%c{self.get_global_index()}' + ok = f'b{self.get_global_index()}' + bad = f'b{self.get_global_index()}' + self.push_cursor() + ok_body = self.make_blocks(ok, backs, forwards) + self.pop_cursor() + bad_body = self.make_basic_block_contents() + + return f'''\ + {call} = invoke zeroext i1 @_Z10getBooleanv() + to label %{ok} unwind label %{bad}.dispatch + +{bad}.dispatch: + %catch.{bad}.0 = catchswitch within none [label %{bad}.start] unwind to caller + +{bad}.start: + %catch.{bad}.1 = catchpad within %catch.{bad}.0 [i8* bitcast (i8** @_ZTId to i8*)] + %catch.{bad}.2 = call i8* @llvm.wasm.get.exception(token %catch.{bad}.1) + %catch.{bad}.3 = call i32 @llvm.wasm.get.ehselector(token %catch.{bad}.1) + %catch.{bad}.4 = call i32 @llvm.eh.typeid.for(i8* bitcast (i8** @_ZTId to i8*)) + %catch.{bad}.matches = icmp eq i32 %catch.{bad}.3, %catch.{bad}.4 + br i1 %catch.{bad}.matches, label %{bad}.catch, label %{bad}.rethrow + +{bad}.catch: + %catch.{bad}.5 = call i8* @__cxa_begin_catch(i8* %catch.{bad}.2) [ "funclet"(token %catch.{bad}.1) ] +{bad_body} + call void @__cxa_end_catch() [ "funclet"(token %catch.{bad}.1) ] + catchret from %catch.{bad}.1 to label %{ok} + +{bad}.rethrow: + call void @llvm.wasm.rethrow() [ "funclet"(token %catch.{bad}.1) ] + unreachable + +{ok_body}''' + + def make_nothing(self): + return ' ;; nothing' + + def make_logging(self): + num = self.get_global_index() + global_name = f'@.str.{num}' + num_len = len(str(num)) + full_len = 6 + num_len + global_def = f'{global_name} = private unnamed_addr constant [{full_len} x i8] c"log({num})\\00", align 1\n' + self.global_defs += global_def + logging = f' %call{num} = call i32 @puts(i8* getelementptr inbounds ([{full_len} x i8], [{full_len} x i8]* {global_name}, i32 0, i32 0))' + return logging + + def make_call(self): + num = self.get_global_index() + return f'''\ + %instance.{num} = alloca %struct.Class, align 1 + %call.{num} = call %struct.Class* @_ZN5ClassC1Ev(%struct.Class* nonnull dereferenceable(1) %instance.{num})''' + + def make_forward_branch(self, backs, forwards): + return f' br label %{self.pick(forwards)}' + + def make_unreachable(self, backs, forwards): + return f' unreachable' + + +# Main harness + + +def known(err, silent): + if 'Delegate destination should be in scope' in err: + # https://github.com/emscripten-core/emscripten/issues/13514 + return True + #if 'Branch destination should be in scope' in err: + # # https://github.com/emscripten-core/emscripten/issues/13515 + # return True + if 'Active sort region list not finished' in err: + # https://github.com/emscripten-core/emscripten/issues/13554 + return True + if 'Allocation failed' in err: + # Looks related to "Active sort region", testcases fluctuate. + return True + if not silent: + print('unknown compile error') + return False + + +def check_testcase(data, silent=True): + ''' + Checks if a testcase is valid. Returns True if so. + ''' + # Generate C++ + CppTranslator(data).write(main='a.cpp', support='b.cpp') + + # Compile with emcc, looking for a compilation error. + + # TODO: also compile b.cpp, and remove -c so that we test linking. + open('a.sh', 'w').write('''\ +ulimit -v 500000 +./em++ a.cpp b.cpp -s WASM_BIGINT -fwasm-exceptions -O0 -o a.out.js +''') + try: + result = subprocess.run(['bash', 'a.sh'], + stderr=subprocess.PIPE, text=True, timeout=2) + except subprocess.TimeoutExpired: + if not silent: + print('timeout error') + return False + + if not silent: + print(result.stderr) + + if result.returncode != 0: + # Ignore if the problem is known, and halt here regardless (as we cannot + # continue to do any more checks). + return known(result.stderr, silent) + + # Optimize with binaryen + ''' + debug_env = os.environ.copy() + debug_env['BINARYEN_PASS_DEBUG'] = '1' + # read from a.out.wasm and also write to it, so that the execution below + # runs on optimized code. + result = subprocess.run(['/home/azakai/Dev/binaryen/bin/wasm-opt', + 'a.out.wasm', '-o', 'a.out.wasm', '-Os'], + stdout=subprocess.PIPE, stderr=subprocess.PIPE, + text=True, env=debug_env) + + if not silent: + print(result.stderr) + + if result.returncode != 0: + if not silent: + print('binaryen opt error') + return False + ''' + # Compile normally, run normally and with wasm, and compare the results. + + subprocess.check_call(['c++', 'a.cpp', 'b.cpp']) + normal = subprocess.run(['./a.out'], stdout=subprocess.PIPE, text=True) + assert normal.returncode == 0 + wasm = subprocess.run([os.path.expanduser('~/.jsvu/v8'), + '--experimental-wasm-eh', 'a.out.js'], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True) + if wasm.returncode != 0: + if not silent: + print('runtime error') + return False + + if normal.stdout != wasm.stdout: + if not silent: + print('comparison error') + return False + + return True + +''' +TODO: LLVM fuzzing + + result = subprocess.run(['/home/azakai/Dev/sdk/upstream/bin/llc', 'a.ll', + '-exception-model=wasm', + '-mattr=+exception-handling', + '-o', '/dev/null'], + stderr=subprocess.PIPE, text=True) + + if not silent: + print(result.stderr) + + if result.returncode != 0: + return known(result.stderr, silent) + + return True # XXX +''' + +def reduce(data): + ''' + Given a failing testcase, reduce the input data to create a reduced C++ + testcase. + ''' + + assert not check_testcase(data) + + # The input is structured. The simplest thing is to reduce on it in text + # form, so that we do not need to have references to nested things etc. + text = json.dumps(data) + assert not check_testcase(json.loads(text)) + print(f'[reducing, starting from size {len(text)}]') + + def iteration(text): + # Find ',' delimiters, and reduce using it it, starting from the start + # and going to the end (doing this on the text lets us handle all + # nested structure in a single loop) + print(f'[reduction iteration begins]') + + def find_delimiter_at_same_scope(text, delimiters, i): + ''' + Given a reference to the first comma here: + [1,2,3] + ^ + We return a reference to the next one, or to a ] if there is none. + This handles scoping, that is, + [1,[5,6],3] + ^ + That will return the comma before the '3'. + ''' + nesting = 0 + while True: + curr = text[i] + if curr in delimiters and nesting == 0: + return i + elif curr == '[': + nesting += 1 + elif curr == ']': + nesting -= 1 + i += 1 + + # Reduce starting from commas. + i = 0 + while True: + i = text.find(',', i) + if i < 0: + break + # Look for the ], which might allow us to reduce all the tail of the + # current array. Often the tails are ignored, so this is a big + # speedup potentially. + j = find_delimiter_at_same_scope(text, ']', i + 1) + + # We now have something like + # ,..., + # i j + # or + # ,...] + # i j + # And we can try a reduction by removing up to j. + new_text = text[:i] + text[j:] + if not check_testcase(json.loads(new_text)): + # This is a successful reduction! + text = new_text + print(f'[reduced (large) to {len(text)}]') + # Note that i can stay where it is. + continue + + # The reduction failed. Try a smaller reduction, not all the way + # to the end of the tail. + j = find_delimiter_at_same_scope(text, ',]', i + 1) + if text[j] == ',': + new_text = text[:i] + text[j:] + if not check_testcase(json.loads(new_text)): + text = new_text + print(f'[reduced (small) to {len(text)}]') + continue + i += 1 + + # Reduce starting from open braces. This handles removing the very first + # element. + i = 0 + while True: + i = text.find('[', i) + if i < 0: + break + j = find_delimiter_at_same_scope(text, ',]', i + 1) + if text[j] == ',': + # [.., => [ + new_text = text[:i + 1] + text[j + 1:] + else: + # [..] => [] + new_text = text[:i + 1] + text[j:] + if not check_testcase(json.loads(new_text)): + text = new_text + print(f'[reduced (open) to {len(text)}]') + i += 1 + + # Reduce a singleton parent to a child, + # [[x]] => [x] + i = 0 + while True: + i = text.find('[', i) + if i < 0: + break + if text[i + 1] != '[': + i += 1 + continue + j = find_delimiter_at_same_scope(text, ']', i + 2) + if text[j + 1] != ']': + i += 1 + continue + # We now have + # [[..]] + # i j + new_text = text[:i] + text[i + 1:j + 1] + text[j + 2:] + if not check_testcase(json.loads(new_text)): + text = new_text + print(f'[reduced (singleton) to {len(text)}]') + continue + i += 1 + + return text + + # Main loop: do iterations while we are still reducing. + while True: + reduced = iteration(text) + if reduced == text: + break + text = reduced + + # Run and verify the final reduction. + assert not check_testcase(json.loads(text)) + print('[reduction complete, reduced testcase written out]') + + +def main(): + given_seed = None + if len(sys.argv) == 2: + given_seed = int(sys.argv[1]) + + total = 0 + seed = time.time() * os.getpid() + random.seed(seed) + + while 1: + seed = random.randint(0, 1 << 64) + if given_seed is not None: + seed = given_seed + random.seed(seed) + print(f'[iteration {total} (seed = {seed})]') + total += 1 + + # Generate a testcase. + data = StructuredRandomData().root + + # Test it. + if not check_testcase(data, silent=False): + print('[testcase failed]') + reduce(data) + sys.exit(1) + + if given_seed is not None: + break + +main() diff --git a/tools/system_libs.py b/tools/system_libs.py index ce370de898c74..c50732897f61e 100644 --- a/tools/system_libs.py +++ b/tools/system_libs.py @@ -1482,8 +1482,8 @@ def add_library(lib): add_library(system_libs_map['libc']) add_library(system_libs_map['libcompiler_rt']) - if cxx: - add_library(system_libs_map['libc++']) + #if cxx: + # add_library(system_libs_map['libc++']) if cxx or sanitize: add_library(system_libs_map['libc++abi']) if shared.Settings.EXCEPTION_HANDLING: