#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# Copyright 2016 ScyllaDB
#
#
# This file is part of Scylla.
#
# Scylla is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Scylla is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Scylla. If not, see .
import argparse
import pyparsing as pp
from functools import reduce
import textwrap
from numbers import Number
from pprint import pformat
from copy import copy
EXTENSION = '.idl.hh'
READ_BUFF = 'input_buffer'
WRITE_BUFF = 'output_buffer'
SERIALIZER = 'serialize'
DESERIALIZER = 'deserialize'
SETSIZE = 'set_size'
SIZETYPE = 'size_type'
def reindent(indent, text):
return textwrap.indent(textwrap.dedent(text), ' ' * indent)
def fprint(f, *args):
for arg in args:
f.write(arg)
def fprintln(f, *args):
for arg in args:
f.write(arg)
f.write('\n')
def print_cw(f):
fprintln(f, """
/*
* Copyright 2016 ScyllaDB
*/
/*
* This file is part of Scylla.
*
* Scylla is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Scylla is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Scylla. If not, see .
*/
/*
* This is an auto-generated code, do not modify directly.
*/
#pragma once
""")
###
### AST Nodes
###
class BasicType:
'''AST node that represents terminal grammar nodes for the non-template
types, defined either inside or outside the IDL.
These can appear either in the definition of the class fields or as a part of
template types (template arguments).
Basic type nodes can also be marked as `const` when used inside a template type,
e.g. `lw_shared_ptr`. When an IDL-defined type `T` appears somewhere
with a `const` specifier, an additional `serializer` specialization
is generated for it.'''
def __init__(self, name, is_const=False):
self.name = name
self.is_const = is_const
def __str__(self):
return f""
def __repr__(self):
return self.__str__()
class TemplateType:
'''AST node representing template types, for example: `std::vector`.
These can appear either in the definition of the class fields or as a part of
template types (template arguments).
Such types can either be defined inside or outside the IDL.'''
def __init__(self, name, template_parameters):
self.name = name
# FIXME: dirty hack to translate non-type template parameters (numbers) to BasicType objects
self.template_parameters = [
t if isinstance(t, BasicType) or isinstance(t, TemplateType) else BasicType(name=str(t)) \
for t in template_parameters]
def __str__(self):
return f""
def __repr__(self):
return self.__str__()
class EnumValue:
'''AST node representing a single `name=value` enumerator in the enum.
Initializer part is optional, the same as in C++ enums.'''
def __init__(self, name, initializer=None):
self.name = name
self.initializer = initializer
def __str__(self):
return f""
def __repr__(self):
return self.__str__()
class EnumDef:
'''AST node representing C++ `enum class` construct.
Consists of individual initializers in form of `EnumValue` objects.
Should have an underlying type explicitly specified.'''
def __init__(self, name, underlying_type, members):
self.name = name
self.underlying_type = underlying_type
self.members = members
def __str__(self):
return f"";
def __repr__(self):
return self.__str__()
def serializer_write_impl(self, cout, template_decl, namespaces):
name = ns_qualified_name(self.name, namespaces)
fprintln(cout, f"""
{template_decl}
template
void serializer<{name}>::write(Output& buf, const {name}& v) {{
serialize(buf, static_cast<{self.underlying_type}>(v));
}}""")
def serializer_read_impl(self, cout, template_decl, namespaces):
name = ns_qualified_name(self.name, namespaces)
fprintln(cout, f"""
{template_decl}
template
{name} serializer<{name}>::read(Input& buf) {{
return static_cast<{name}>(deserialize(buf, boost::type<{self.underlying_type}>()));
}}""")
class Attribute:
''' AST node for representing class and field attributes.
The following attributes are supported:
- `[[writable]]` class attribute, triggers generation of writers and views
for a class.
- `[[version id]] field attribute, marks that a field is available starting
from a specific version.'''
def __init__(self, name):
self.name = name
def __str__(self):
return f"[[{self.name}]]"
def __repr__(self):
return self.__str__()
class DataClassMember:
'''AST node representing a data field in a class.
Can optionally have a version attribute and a default value specified.'''
def __init__(self, type, name, attribute=None, default_value=None):
self.type = type
self.name = name
self.attribute = attribute
self.default_value = default_value
def __str__(self):
return f""
def __repr__(self):
return self.__str__()
class FunctionClassMember:
'''AST node representing getter function in a class definition.
Can optionally have a version attribute and a default value specified.
Getter functions should be used whenever it's needed to access private
members of a class.'''
def __init__(self, type, name, attribute=None, default_value=None):
self.type = type
self.name = name
self.attribute = attribute
self.default_value = default_value
def __str__(self):
return f""
def __repr__(self):
return self.__str__()
class ClassTemplateParam:
'''AST node representing a single template argument of a class template
definition, such as `typename T`.'''
def __init__(self, typename, name):
self.typename = typename
self.name = name
def __str__(self):
return f""
def __repr__(self):
return self.__str__()
class ClassDef:
'''AST node representing a class definition. Can use either `class` or `struct`
keyword to define a class.
The following specifiers are allowed in a class declaration:
- `final` -- if a class is marked with this keyword it will not contain a
size argument. Final classes cannot be extended by a future version, so
it should be used with care.
- `stub` -- no code will be generated for the class, it's only there for
documentation.
Also it's possible to specify a `[[writable]]` attribute for a class, which
means that writers and views will be generated for the class.
Classes are also can be declared as template classes, much the same as in C++.
In this case the template declaration syntax mimics C++ templates.'''
def __init__(self, name, members, final, stub, attribute, template_params):
self.name = name
self.members = members
self.final = final
self.stub = stub
self.attribute = attribute
self.template_params = template_params
def __str__(self):
return f""
def __repr__(self):
return self.__str__()
def serializer_write_impl(self, cout, template_decl, template_class_param, namespaces):
name = ns_qualified_name(self.name, namespaces)
full_name = name + template_class_param
fprintln(cout, f"""
{template_decl}
template
void serializer<{full_name}>::write(Output& buf, const {full_name}& obj) {{""")
if not self.final:
fprintln(cout, f""" {SETSIZE}(buf, obj);""")
for member in self.members:
if isinstance(member, ClassDef) or isinstance(member, EnumDef):
continue
fprintln(cout, f""" static_assert(is_equivalent::value, "member value has a wrong type");
{SERIALIZER}(buf, obj.{member.name});""")
fprintln(cout, "}")
def serializer_read_impl(self, cout, template_decl, template_class_param, namespaces):
is_final = self.final
name = ns_qualified_name(self.name, namespaces)
fprintln(cout, f"""
{template_decl}
template
{name}{template_class_param} serializer<{name}{template_class_param}>::read(Input& buf) {{
return seastar::with_serialized_stream(buf, [] (auto& buf) {{""")
if not self.members:
if not is_final:
fprintln(cout, f""" {SIZETYPE} size = {DESERIALIZER}(buf, boost::type<{SIZETYPE}>());
buf.skip(size - sizeof({SIZETYPE}));""")
elif not is_final:
fprintln(cout, f""" {SIZETYPE} size = {DESERIALIZER}(buf, boost::type<{SIZETYPE}>());
auto in = buf.read_substream(size - sizeof({SIZETYPE}));""")
else:
fprintln(cout, """ auto& in = buf;""")
params = []
local_names = {}
for index, param in enumerate(self.members):
if isinstance(param, ClassDef) or isinstance(param, EnumDef):
continue
local_param = "__local_" + str(index)
local_names[param.name] = local_param
if param.attribute:
deflt = param_type(param.type) + "()"
if param.default_value:
deflt = param.default_value
if deflt in local_names:
deflt = local_names[deflt]
fprintln(cout, f""" auto {local_param} = (in.size()>0) ?
{DESERIALIZER}(in, boost::type<{param_type(param.type)}>()) : {deflt};""")
else:
fprintln(cout, f""" auto {local_param} = {DESERIALIZER}(in, boost::type<{param_type(param.type)}>());""")
params.append("std::move(" + local_param + ")")
fprintln(cout, f"""
{name}{template_class_param} res {{{", ".join(params)}}};
return res;
}});
}}""")
def serializer_skip_impl(self, cout, template_decl, template_class_param, namespaces):
is_final = self.final
name = ns_qualified_name(self.name, namespaces)
fprintln(cout, f"""
{template_decl}
template
void serializer<{name}{template_class_param}>::skip(Input& buf) {{
seastar::with_serialized_stream(buf, [] (auto& buf) {{""")
if not is_final:
fprintln(cout, f""" {SIZETYPE} size = {DESERIALIZER}(buf, boost::type<{SIZETYPE}>());
buf.skip(size - sizeof({SIZETYPE}));""")
else:
for m in get_members(self):
full_type = param_view_type(m.type)
fprintln(cout, f" ser::skip(buf, boost::type<{full_type}>());")
fprintln(cout, """ });\n}""")
class NamespaceDef:
'''AST node representing a namespace scope.
It has the same meaning as in C++ or other languages with similar facilities.
A namespace can contain one of the following top-level constructs:
- namespaces
- class definitions
- enum definitions'''
def __init__(self, name, members):
self.name = name
self.members = members
def __str__(self):
return f""
def __repr__(self):
return self.__str__()
###
### Parse actions, which transform raw tokens into structured representation: specialized AST nodes
###
def basic_type_parse_action(tokens):
return BasicType(name=tokens[0])
def template_type_parse_action(tokens):
return TemplateType(name=tokens['template_name'], template_parameters=tokens["template_parameters"].asList())
# Will be used after parsing is complete to determine which local types
# have usages with `const` specifiers: depending on that we should generate
# a serializer specialization for `const` type too.
types_with_const_appearances = set()
def type_parse_action(tokens):
if len(tokens) == 1:
return tokens[0]
# If we have two tokens in type parse action then
# it's because we have BasicType production with `const`
# NOTE: template types cannot have `const` modifier at the moment,
# this wouldn't parse.
tokens[1].is_const = True
types_with_const_appearances.add(tokens[1].name)
return tokens[1]
def enum_value_parse_action(tokens):
initializer = None
if len(tokens) == 2:
initializer = tokens[1]
return EnumValue(name=tokens[0], initializer=initializer)
def enum_def_parse_action(tokens):
return EnumDef(name=tokens['name'], underlying_type=tokens['underlying_type'], members=tokens['enum_values'].asList())
def attribute_parse_action(tokens):
return Attribute(name=tokens[0])
def class_member_parse_action(tokens):
member_name = tokens['name']
attribute = tokens['attribute'][0] if 'attribute' in tokens else None
default = tokens['default'][0] if 'default' in tokens else None
if not isinstance(member_name, str): # accessor function declaration
return FunctionClassMember(type=tokens["type"], name=member_name[0], attribute=attribute, default_value=default)
# data member
return DataClassMember(type=tokens["type"], name=member_name, attribute=attribute, default_value=default)
def class_def_parse_action(tokens):
is_final = 'final' in tokens
is_stub = 'stub' in tokens
class_members = tokens['members'].asList() if 'members' in tokens else []
attribute = tokens['attribute'][0] if 'attribute' in tokens else None
template_params = None
if 'template' in tokens:
template_params = [ClassTemplateParam(typename=tp[0], name=tp[1]) for tp in tokens['template']]
return ClassDef(name=tokens['name'], members=class_members, final=is_final, stub=is_stub, attribute=attribute, template_params=template_params)
def namespace_parse_action(tokens):
return NamespaceDef(name=tokens['name'], members=tokens['ns_members'].asList())
def parse_file(file_name):
number = pp.pyparsing_common.signed_integer
identifier = pp.pyparsing_common.identifier
lbrace = pp.Literal('{').suppress()
rbrace = pp.Literal('}').suppress()
cls = pp.Keyword('class').suppress()
colon = pp.Literal(":").suppress()
semi = pp.Literal(";").suppress()
langle = pp.Literal("<").suppress()
rangle = pp.Literal(">").suppress()
equals = pp.Literal("=").suppress()
comma = pp.Literal(",").suppress()
lparen = pp.Literal("(")
rparen = pp.Literal(")")
lbrack = pp.Literal("[").suppress()
rbrack = pp.Literal("]").suppress()
struct = pp.Keyword('struct').suppress()
template = pp.Keyword('template').suppress()
final = pp.Keyword('final')
stub = pp.Keyword('stub')
const = pp.Keyword('const')
ns_qualified_ident = pp.delimitedList(identifier, "::", combine=True)
enum_lit = pp.Keyword('enum').suppress()
ns = pp.Keyword("namespace").suppress()
btype = ns_qualified_ident.copy()
btype.setParseAction(basic_type_parse_action)
type = pp.Forward()
tmpl = ns_qualified_ident("template_name") + langle + pp.Group(pp.delimitedList(type | number))("template_parameters") + rangle
tmpl.setParseAction(template_type_parse_action)
type <<= tmpl | (pp.Optional(const) + btype)
type.setParseAction(type_parse_action)
enum_class = enum_lit - cls
enum_init = equals - number
enum_value = identifier - pp.Optional(enum_init)
enum_value.setParseAction(enum_value_parse_action)
enum_values = lbrace - pp.delimitedList(enum_value) - pp.Optional(comma) - rbrace
enum = enum_class - identifier("name") - colon - identifier("underlying_type") - enum_values("enum_values") + pp.Optional(semi)
enum.setParseAction(enum_def_parse_action)
content = pp.Forward()
attrib = lbrack - lbrack - pp.SkipTo(']') - rbrack - rbrack
attrib.setParseAction(attribute_parse_action)
opt_attribute = pp.Optional(attrib)("attribute")
default_value = equals - pp.SkipTo(';')
member_name = pp.Combine(identifier - pp.Optional(lparen - rparen)("function_marker"))
class_member = type("type") - member_name("name") - opt_attribute - pp.Optional(default_value)("default") - semi
class_member.setParseAction(class_member_parse_action)
template_param = pp.Group(identifier("type") - identifier("name"))
template_def = template - langle - pp.delimitedList(template_param)("params") - rangle
class_content = pp.Forward()
class_def = pp.Optional(template_def)("template") + (cls | struct) - ns_qualified_ident("name") - \
pp.Optional(final)("final") - pp.Optional(stub)("stub") - opt_attribute - \
lbrace - pp.ZeroOrMore(class_content)("members") - rbrace - pp.Optional(semi)
class_content <<= enum | class_def | class_member
class_def.setParseAction(class_def_parse_action)
namespace = ns - identifier("name") - lbrace - pp.OneOrMore(content)("ns_members") - rbrace
namespace.setParseAction(namespace_parse_action)
content <<= enum | class_def | namespace
for varname in ("enum", "class_def", "class_member", "content", "namespace", "template_def"):
locals()[varname].setName(varname)
rt = pp.OneOrMore(content)
rt.ignore(pp.cppStyleComment)
return rt.parseFile(file_name, parseAll=True)
def combine_ns(namespaces):
return "::".join(namespaces)
def declare_methods(hout, name, template_param=""):
fprintln(hout, f"""
template <{template_param}>
struct serializer<{name}> {{
template
static void write(Output& buf, const {name}& v);
template
static {name} read(Input& buf);
template
static void skip(Input& buf);
}};
""")
if name in types_with_const_appearances:
fprintln(hout, f"""
template <{template_param}>
struct serializer : public serializer<{name}>
{{}};
""")
def ns_qualified_name(name, namespaces):
return name if not namespaces else combine_ns(namespaces) + "::" + name
def template_params_str(template_params):
if not template_params:
return ""
return ", ".join(map(lambda param: param.typename + " " + param.name, template_params))
def handle_enum(enum, hout, cout, namespaces, parent_template_param=[]):
temp_def = template_params_str(parent_template_param)
template_decl = "template <" + temp_def + ">" if temp_def else ""
name = ns_qualified_name(enum.name, namespaces)
declare_methods(hout, name, temp_def)
enum.serializer_write_impl(cout, template_decl, namespaces)
enum.serializer_read_impl(cout, template_decl, namespaces)
def join_template(template_params):
return "<" + ", ".join([param_type(p) for p in template_params]) + ">"
def param_type(t):
if isinstance(t, BasicType):
return 'const ' + t.name if t.is_const else t.name
elif isinstance(t, TemplateType):
return t.name + join_template(t.template_parameters)
def flat_type(t):
if isinstance(t, BasicType):
return t.name
elif isinstance(t, TemplateType):
return (t.name + "__" + "_".join([flat_type(p) for p in t.template_parameters])).replace('::', '__')
local_types = {}
def list_types(t):
if isinstance(t, BasicType):
return [t.name]
elif isinstance(t, TemplateType):
return reduce(lambda a, b: a + b, [list_types(p) for p in t.template_parameters])
def list_local_types(t):
return {l for l in list_types(t) if l in local_types}
def is_basic_type(t):
return isinstance(t, BasicType) and t.name not in local_types
def is_local_type(t):
if isinstance(t, str): # e.g. `t` is a local class name
return t in local_types
return t.name in local_types
def get_template_name(lst):
return lst["template_name"] if not isinstance(lst, str) and len(lst) > 1 else None
def is_vector(t):
return isinstance(t, TemplateType) and (t.name == "std::vector" or t.name == "utils::chunked_vector")
def is_variant(t):
return isinstance(t, TemplateType) and (t.name == "boost::variant" or t.name == "std::variant")
def is_optional(t):
return isinstance(t, TemplateType) and t.name == "std::optional"
created_writers = set()
def get_member_name(name):
return name if not name.endswith('()') else name[:-2]
def get_members(cls):
return [p for p in cls.members if not isinstance(p, ClassDef) and not isinstance(p, EnumDef)]
def is_final(cls):
return cls.final
def get_variant_type(t):
if is_variant(t):
return "variant"
return param_type(t)
def variant_to_member(template_parameters):
return [DataClassMember(name=get_variant_type(x), type=x) for x in template_parameters if is_local_type(x) or is_variant(x)]
def variant_info(info, template_parameters):
[cls, namespaces, parent_template_param] = info
variant_info_cls = copy(cls) # shallow copy of cls
variant_info_cls.members = variant_to_member(template_parameters)
return [variant_info_cls, namespaces, parent_template_param]
stubs = set()
def is_stub(cls):
return cls in stubs
def handle_visitors_state(info, cout, classes=[]):
[cls, namespaces, parent_template_param] = info
name = "__".join(classes) if classes else cls.name
frame = "empty_frame" if cls.final else "frame"
fprintln(cout, f"""
template
struct state_of_{name} {{
{frame}