from sys import argv, stderr, float_info import sys from upstream.parsePolyglot import parseYAML from os import walk from os.path import join from re import sub, match, split, DOTALL from collections import namedtuple import ast verbosity = 1 try: NameConstant = ast.NameConstant except: NameConstant = lambda a: a class Discard(Exception): pass class Unhandled(Exception): pass failed = False Ctx = namedtuple('Ctx', ['vars', 'context', 'type']) def convert(python, prec, file, type): try: expr = ast.parse(python, filename=file, mode='eval').body cxx = to_cxx(expr, prec, Ctx(vars=[], type=type, context=None)) return sub('" \\+ "', '', cxx) except (Unhandled, AssertionError): print("While translating: " + python, file=stderr) raise except SyntaxError as e: raise Unhandled("syntax error: " + str(e) + ": " + repr(python)) def py_str(py): def maybe_unstr(s): if '(' in s: return s else: return repr(s) if type(py) is dict: return '{' + ', '.join([repr(k) + ': ' + maybe_str(py[k]) for k in py]) + '}' if not isinstance(py, "".__class__): return repr(py) return py def rename(id): return { 'R::default': 'R::default_', 'default': 'default_', 'R::do': 'R::do_', 'do': 'do_', 'union': 'union_', 'False': 'false', 'True': 'true', 'xrange': 'R::range', 'None': 'R::Nil()', 'null': 'R::Nil()', 'delete': 'delete_', 'float': 'double', 'int_cmp': 'int', 'float_cmp': 'double', 'range': 'R::range', 'list': '', 'R::union': 'R::union_' }.get(id, id) def to_cxx_str(expr): if type(expr) is ast.Str: return string(expr.s) if type(expr) is ast.Num: return string(str(expr.n)) if 'frozenset' in ast.dump(expr): raise Discard("frozenset not supported") if type(expr) is ast.Name: raise Discard("dict with non-string key") raise Unhandled("not string expr: " + ast.dump(expr)) def is_null(expr): return (type(expr) is ast.Name and expr.id in ['None', 'null'] or type(expr) is NameConstant and expr.value == None) def is_bool(expr): return (type(expr) is ast.Name and expr.id in ['true', 'false', 'True', 'False'] or type(expr) is NameConstant and expr.value in [True, False]) def to_cxx_expr(expr, prec, ctx): if ctx.type == 'query': if type(expr) in [ast.Str, ast.Num] or is_null(expr) or is_bool(expr): return "R::expr(" + to_cxx(expr, 17, ctx) + ")" return to_cxx(expr, prec, ctx) def to_cxx(expr, prec, ctx, parentType=None): context = ctx.context ctx = Ctx(vars=ctx.vars, type=ctx.type, context=None) try: t = type(expr) if t == ast.Num: if abs(expr.n) > 4503599627370496: f = repr(expr.n) if "e" in f: return f else: return f + ".0" else: return repr(expr.n) elif t == ast.Call: #assert not expr.kwargs #assert not expr.starargs return to_cxx(expr.func, 2, ctx_set(ctx, context='function')) + to_args(expr.func, expr.args, expr.keywords, ctx) elif t == ast.Attribute: if type(expr.value) is ast.Name: if expr.value.id == 'r': if expr.attr == 'error' and context != 'function': return "R::error()" if expr.attr == 'binary': if ctx.type == 'query': return 'R::binary' else: return 'R::Binary' return rename("R::" + expr.attr) elif expr.value.id == 'datetime': if expr.attr == 'fromtimestamp': return "R::Time" elif expr.attr == 'now': return "R::Time::now" if expr.attr == 'RqlTzinfo': return 'R::Time::parse_utc_offset' if expr.attr in ['encode', 'close']: raise Discard(expr.attr + " not supported") return to_cxx_expr(expr.value, 2, ctx) + "." + rename(expr.attr) elif t == ast.Name: if expr.id in ['frozenset']: raise Discard("frozenset not supported") elif expr.id in ctx.vars: if ctx.type == 'query': return parens(prec, 3, "*" + expr.id) else: return expr.id elif (expr.id == 'range' or expr.id == 'xrange') and ctx.type != 'query': return 'array_range' elif expr.id == 'nil' and ctx.type == 'query': return 'R::expr(nil)' return rename(expr.id) elif t == NameConstant: if expr.value == True: return "true" elif expr.value == False: return "false" elif expr.value == None: return "R::Nil()" else: raise Unhandled("constant: " + repr(expr.value)) elif t == ast.Subscript: st = type(expr.slice) if st == ast.Index: return to_cxx(expr.value, 2, ctx) + "[" + to_cxx(expr.slice.value, 17, ctx) + "]" if st == ast.Slice: assert not expr.slice.step if not expr.slice.upper: return to_cxx(expr.value, 2, ctx) + ".slice(" + to_cxx(expr.slice.lower, 17, ctx) + ")" if not expr.slice.lower: return to_cxx(expr.value, 2, ctx) + ".limit(" + to_cxx(expr.slice.upper, 17, ctx) + ")" return to_cxx(expr.value, 2, ctx) + ".slice(" + to_cxx(expr.slice.lower, 17, ctx) + ", " + to_cxx(expr.slice.upper, 17, ctx) + ")" else: raise Unhandled("slice type: " + repr(st)) elif t == ast.Dict: if ctx.type == 'query': return "R::object(" + ', '.join([to_cxx(k, 17, ctx) + ", " + to_cxx(v, 17, ctx) for k, v in zip(expr.keys, expr.values)]) + ")" else: return "R::Object{" + ', '.join(["{" + to_cxx_str(k) + ", " + to_cxx(v, 17, ctx) + "}" for k, v in zip(expr.keys, expr.values)]) + "}" elif t == ast.Str: return string(expr.s, ctx) elif t == ast.List: if ctx.type == 'query': return "R::array(" + ', '.join([to_cxx(el, 17, ctx) for el in expr.elts]) + ")" else: if parentType == ast.List: return "{ R::Array{" + ', '.join([to_cxx(el, 17, ctx, t) for el in expr.elts]) + "} }" else: return "R::Array{" + ', '.join([to_cxx(el, 17, ctx, t) for el in expr.elts]) + "}" elif t == ast.Lambda: assert not expr.args.vararg assert not expr.args.kwarg ctx = ctx_set(ctx, vars = ctx.vars + [arg.arg for arg in expr.args.args]) return "[=](" + ', '.join(['R::Var ' + arg.arg for arg in expr.args.args]) + "){ return " + to_cxx_expr(expr.body, 17, ctx_set(ctx, type='query')) + "; }" elif t == ast.BinOp: if type(expr.op) is ast.Mult and type(expr.left) is ast.Str: return "repeat(" + to_cxx(expr.left, 17, ctx) + ", " + to_cxx(expr.right, 17, ctx) + ")" ll = type(expr.left) is ast.List or type(expr.left) is ast.ListComp rl = type(expr.right) is ast.List or type(expr.right) is ast.ListComp op, op_prec = convert_op(expr.op) if type(expr.op) is ast.Add and ll and rl: return "append(" + to_cxx_expr(expr.left, op_prec, ctx) + ", " + to_cxx(expr.right, op_prec, ctx) + ")" if op_prec: return parens(prec, op_prec, to_cxx_expr(expr.left, op_prec, ctx) + " " + op + " " + to_cxx(expr.right, op_prec, ctx)) else: return op + "(" + to_cxx(expr.left, 17, ctx) + ", " + to_cxx(expr.right, 17, ctx) + ")" elif t == ast.ListComp: assert len(expr.generators) == 1 assert type(expr.generators[0]) == ast.comprehension assert type(expr.generators[0].target) == ast.Name assert expr.generators[0].ifs == [] seq = to_cxx(expr.generators[0].iter, 2, ctx) if ctx.type == 'query': var = expr.generators[0].target.id body = to_cxx(expr.elt, 17, ctx_set(ctx, vars = ctx.vars + [var])) return seq + ".map([=](R::Var " + var + "){ return " + body + "; })" else: var = expr.generators[0].target.id body = to_cxx(expr.elt, 17, ctx_set(ctx, vars = ctx.vars + [var])) # assume int return "array_map([=](int " + var + "){ return " + body + "; }, " + seq + ")" elif t == ast.Compare: assert len(expr.ops) == 1 assert len(expr.comparators) == 1 op, op_prec = convert_op(expr.ops[0]) return parens(prec, op_prec, to_cxx_expr(expr.left, op_prec, ctx) + op + to_cxx(expr.comparators[0], op_prec, ctx)) elif t == ast.UnaryOp: op, op_prec = convert_op(expr.op) return parens(prec, op_prec, op + to_cxx(expr.operand, op_prec, ctx)) elif t == ast.Bytes: return string(expr.s, ctx) elif t == ast.Tuple: if ctx.type == 'query': return "R::array(" + ', '.join([to_cxx(el, 17, ctx) for el in expr.elts]) + ")" else: return "R::Array{" + ', '.join([to_cxx(el, 17, ctx) for el in expr.elts]) + "}" else: raise Unhandled('ast type: ' + repr(t) + ', fields: ' + str(expr._fields)) except Unhandled: print("While translating: " + ast.dump(expr), file=stderr) raise def ctx_set(ctx, context=None, vars=None, type=None): if context is None: context = ctx.context if vars is None: vars = ctx.vars if type is None: type = ctx.type return Ctx(vars=vars, type=type, context=context) def convert_op(op): t = type(op) if t == ast.Add: return '+', 6 if t == ast.Sub: return '-', 6 if t == ast.Mod: return '%', 5 if t == ast.Mult: return '*', 5 if t == ast.Div: return '/', 5 if t == ast.Pow: return 'pow', 0 if t == ast.Eq: return '==', 9 if t == ast.NotEq: return '!=', 9 if t == ast.Lt: return '<', 8 if t == ast.Gt: return '>', 8 if t == ast.GtE: return '>=', 8 if t == ast.LtE: return '<=', 8 if t == ast.USub: return '-', 3 if t == ast.BitAnd: return '&&', 13 if t == ast.BitOr: return '||', 14 if t == ast.Invert: return '!', 3 else: raise Unhandled('op type: ' + repr(t)) def to_args(func, args, optargs, ctx): it = func while type(it) is ast.Attribute: it = it.value if type(it) is ast.Call: ctx = ctx_set(ctx, type='query') break if type(it) is ast.Name and it.id == 'r': ctx = ctx_set(ctx, type='query') ret = "(" ret = ret + ', '.join([to_cxx(arg, 17, ctx) for arg in args]) o = list(optargs) if o: out = [] for f in o: out.append("{" + string(f.arg) + ", R::expr(" + to_cxx(f.value, 17, ctx) + ")}") if args: ret = ret + ", " ret = ret + "R::OptArgs{" + ', '.join(out) + "}" return ret + ")" def string(s, ctx=None): was_hex = False wrap = ctx and ctx.type == 'string' if type(s) is str: s = s.encode('utf8') if type(s) is bytes: def string_escape(c): nonlocal wrap nonlocal was_hex if c == 0: wrap = True if c < 32 or c > 127 or (was_hex and chr(c) in "0123456789abcdefABCDEF"): was_hex = True return '\\x' + ('0' + hex(c)[2:])[-2:] was_hex = False if c == 34: return '\\"' if c == 92: return '\\\\' else: return chr(c) else: raise Unhandled("string type: " + repr(type(s))) e = '"' + ''.join([string_escape(c) for c in s]) + '"' if wrap: return "std::string(" + e + ", " + str(len(s)) + ")" return e def parens(prec, in_prec, cxx): if in_prec >= prec: return "(" + cxx + ")" else: return cxx print("// auto-generated by yaml_to_cxx.py from " + argv[1]) print("#include \"testlib.h\"") indent = 0 def p(s): print((indent * " ") + s); def enter(s = ""): if s: p(s) global indent indent = indent + 1 def exit(s = ""): global indent indent = indent - 1 if s: p(s) def get(o, ks, d): try: for k in ks: if k in o: return o[k] except: pass return d def python_tests(tests): for test in tests: runopts = get(test, ['runopts'], None) try: ot = py_str(get(test['ot'], ['py', 'cd'], test['ot'])) except: try: ot = py_str(test['py']['ot']) except: ot = None if 'def' in test: py = get(test['def'], ['py', 'cd'], test['def']) if py and type(py) is not dict: yield py_str(py), None, 'def', runopts py = get(test, ['py', 'cd'], None) if py: if isinstance(py, "".__class__): yield py, ot, 'query', runopts elif type(py) is dict and 'cd' in py: yield py_str(py['cd']), ot, 'query', runopts else: for t in py: yield py_str(t), ot, 'query', runopts def maybe_discard(py, ot): if ot is None: return if match(".*Expected .* argument", ot): raise Discard("argument checks not supported") if match(".*argument .* must", ot): raise Discard("argument checks not supported") if match(".*infix bitwise", ot): raise Discard("infix bitwise not supported") if match(".*Object keys must be strings", ot): raise Discard("string object keys tests not supported") if match(".*Got .* argument", ot): raise Discard("argument checks not supported") if match(".*AttributeError.*", ot): raise Discard("attribute checks not supported, will cause a compiler error") data = parseYAML(open(argv[1]).read()) name = sub('/', '_', argv[1].split('.')[0]) enter("void %s() {" % name) p("enter_section(\"%s: %s\");" % (name, data['desc'].replace('"', '\\"'))) if 'table_variable_name' in data: for var in split(" |, ", data['table_variable_name']): p("temp_table %s_table;" % var) p("R::Term %s = %s_table.table();" % (var, var)) defined = [] for py, ot, tp, runopts in python_tests(data["tests"]): try: maybe_discard(py, ot) assignment = match("\\A(\\w+) *= *([^=].*)\\Z", py, DOTALL) if runopts: args = ", R::optargs(" + ', '.join(['"' + k + '", ' + convert(py_str(runopts[k]), 17, name, 'value') for k in runopts]) + ")" else: args = '' if assignment: var = assignment.group(1) if var == 'float_max': p('auto float_max = ' + repr(float_info.max) + ";") elif var == 'float_min': p('auto float_min = ' + repr(float_info.min) + ";") else: if tp == 'def' and var not in ['bad_insert', 'trows']: val = convert(assignment.group(2), 15, name, 'string') post = "" else: val = convert(assignment.group(2), 15, name, 'query') post = ".run(*conn" + args + ")" if var in defined: dvar = var else: defined.append(var); dvar = "auto " + var p("TEST_DO(" + dvar + " = (" + val + post + "));") elif ot: p("TEST_EQ(maybe_run(%s, *conn%s), (%s));" % (convert(py, 2, name, 'query'), args, convert(ot, 17, name, 'datum'))) else: p("TEST_DO(maybe_run(%s, *conn%s));" % (convert(py, 2, name, 'query'), args)) except Discard as exc: if verbosity >= 1: print("Discarding %s (%s): %s" % (repr(py), repr(ot), str(exc)), file=stderr) pass except Unhandled as e: failed = True print(argv[1] + ": could not translate: " + str(e), file=stderr) p("section_cleanup();") p("exit_section();") exit("}") if failed: sys.exit(1)