diff options
author | Hidehiko Abe <hidehiko@google.com> | 2018-04-23 20:01:13 -0700 |
---|---|---|
committer | android-build-merger <android-build-merger@google.com> | 2018-04-23 20:01:13 -0700 |
commit | ed7128dca79cff94e99465e3c1bc31d91d83c76d (patch) | |
tree | bd2d04362f66c36d4279f7a9735ba21ea3a2a021 /mojo/public/tools/bindings/pylib/mojom | |
parent | d6187ab7d79d95d101c2ecb83aa98c05bcdcccd1 (diff) | |
parent | 0ab20ac2283987e63b0e7c1318db2a5cf7c668d2 (diff) | |
download | libchrome-ed7128dca79cff94e99465e3c1bc31d91d83c76d.tar.gz |
Migrate libmojo repository into libchrome, part 2. am: b268b43ac6
am: 0ab20ac228
Change-Id: I1b1afe0c902f1d122b100f5bf56d1df4a94eb090
Diffstat (limited to 'mojo/public/tools/bindings/pylib/mojom')
19 files changed, 3740 insertions, 0 deletions
diff --git a/mojo/public/tools/bindings/pylib/mojom/__init__.py b/mojo/public/tools/bindings/pylib/mojom/__init__.py new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/mojo/public/tools/bindings/pylib/mojom/__init__.py diff --git a/mojo/public/tools/bindings/pylib/mojom/error.py b/mojo/public/tools/bindings/pylib/mojom/error.py new file mode 100644 index 0000000000..99522b9507 --- /dev/null +++ b/mojo/public/tools/bindings/pylib/mojom/error.py @@ -0,0 +1,27 @@ +# Copyright 2014 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +class Error(Exception): + """Base class for Mojo IDL bindings parser/generator errors.""" + + def __init__(self, filename, message, lineno=None, addenda=None, **kwargs): + """|filename| is the (primary) file which caused the error, |message| is the + error message, |lineno| is the 1-based line number (or |None| if not + applicable/available), and |addenda| is a list of additional lines to append + to the final error message.""" + Exception.__init__(self, **kwargs) + self.filename = filename + self.message = message + self.lineno = lineno + self.addenda = addenda + + def __str__(self): + if self.lineno: + s = "%s:%d: Error: %s" % (self.filename, self.lineno, self.message) + else: + s = "%s: Error: %s" % (self.filename, self.message) + return "\n".join([s] + self.addenda) if self.addenda else s + + def __repr__(self): + return str(self) diff --git a/mojo/public/tools/bindings/pylib/mojom/fileutil.py b/mojo/public/tools/bindings/pylib/mojom/fileutil.py new file mode 100644 index 0000000000..b321e9f543 --- /dev/null +++ b/mojo/public/tools/bindings/pylib/mojom/fileutil.py @@ -0,0 +1,18 @@ +# Copyright 2015 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +import errno +import os.path + +def EnsureDirectoryExists(path, always_try_to_create=False): + """A wrapper for os.makedirs that does not error if the directory already + exists. A different process could be racing to create this directory.""" + + if not os.path.exists(path) or always_try_to_create: + try: + os.makedirs(path) + except OSError as e: + # There may have been a race to create this directory. + if e.errno != errno.EEXIST: + raise diff --git a/mojo/public/tools/bindings/pylib/mojom/generate/__init__.py b/mojo/public/tools/bindings/pylib/mojom/generate/__init__.py new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/mojo/public/tools/bindings/pylib/mojom/generate/__init__.py diff --git a/mojo/public/tools/bindings/pylib/mojom/generate/constant_resolver.py b/mojo/public/tools/bindings/pylib/mojom/generate/constant_resolver.py new file mode 100644 index 0000000000..c8b21f2629 --- /dev/null +++ b/mojo/public/tools/bindings/pylib/mojom/generate/constant_resolver.py @@ -0,0 +1,91 @@ +# Copyright 2015 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +"""Resolves the values used for constants and enums.""" + +from itertools import ifilter +import mojom.generate.module as mojom + +def ResolveConstants(module, expression_to_text): + in_progress = set() + computed = set() + + def GetResolvedValue(named_value): + assert isinstance(named_value, (mojom.EnumValue, mojom.ConstantValue)) + if isinstance(named_value, mojom.EnumValue): + field = next(ifilter(lambda field: field.name == named_value.name, + named_value.enum.fields), None) + if not field: + raise RuntimeError( + 'Unable to get computed value for field %s of enum %s' % + (named_value.name, named_value.enum.name)) + if field not in computed: + ResolveEnum(named_value.enum) + return field.resolved_value + else: + ResolveConstant(named_value.constant) + named_value.resolved_value = named_value.constant.resolved_value + return named_value.resolved_value + + def ResolveConstant(constant): + if constant in computed: + return + if constant in in_progress: + raise RuntimeError('Circular dependency for constant: %s' % constant.name) + in_progress.add(constant) + if isinstance(constant.value, (mojom.EnumValue, mojom.ConstantValue)): + resolved_value = GetResolvedValue(constant.value) + else: + resolved_value = expression_to_text(constant.value) + constant.resolved_value = resolved_value + in_progress.remove(constant) + computed.add(constant) + + def ResolveEnum(enum): + def ResolveEnumField(enum, field, default_value): + if field in computed: + return + if field in in_progress: + raise RuntimeError('Circular dependency for enum: %s' % enum.name) + in_progress.add(field) + if field.value: + if isinstance(field.value, mojom.EnumValue): + resolved_value = GetResolvedValue(field.value) + elif isinstance(field.value, str): + resolved_value = int(field.value, 0) + else: + raise RuntimeError('Unexpected value: %s' % field.value) + else: + resolved_value = default_value + field.resolved_value = resolved_value + in_progress.remove(field) + computed.add(field) + + current_value = 0 + for field in enum.fields: + ResolveEnumField(enum, field, current_value) + current_value = field.resolved_value + 1 + + for constant in module.constants: + ResolveConstant(constant) + + for enum in module.enums: + ResolveEnum(enum) + + for struct in module.structs: + for constant in struct.constants: + ResolveConstant(constant) + for enum in struct.enums: + ResolveEnum(enum) + for field in struct.fields: + if isinstance(field.default, (mojom.ConstantValue, mojom.EnumValue)): + field.default.resolved_value = GetResolvedValue(field.default) + + for interface in module.interfaces: + for constant in interface.constants: + ResolveConstant(constant) + for enum in interface.enums: + ResolveEnum(enum) + + return module diff --git a/mojo/public/tools/bindings/pylib/mojom/generate/generator.py b/mojo/public/tools/bindings/pylib/mojom/generate/generator.py new file mode 100644 index 0000000000..0e64af78a1 --- /dev/null +++ b/mojo/public/tools/bindings/pylib/mojom/generate/generator.py @@ -0,0 +1,153 @@ +# Copyright 2013 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +"""Code shared by the various language-specific code generators.""" + +from functools import partial +import os.path +import re + +import module as mojom +import mojom.fileutil as fileutil +import pack + +def ExpectedArraySize(kind): + if mojom.IsArrayKind(kind): + return kind.length + return None + +def StudlyCapsToCamel(studly): + return studly[0].lower() + studly[1:] + +def UnderToCamel(under): + """Converts underscore_separated strings to CamelCase strings.""" + return ''.join(word.capitalize() for word in under.split('_')) + +def WriteFile(contents, full_path): + # Make sure the containing directory exists. + full_dir = os.path.dirname(full_path) + fileutil.EnsureDirectoryExists(full_dir) + + # Dump the data to disk. + with open(full_path, "w+") as f: + f.write(contents) + +class Generator(object): + # Pass |output_dir| to emit files to disk. Omit |output_dir| to echo all + # files to stdout. + def __init__(self, module, output_dir=None, typemap=None, variant=None, + bytecode_path=None, for_blink=False, use_once_callback=False, + use_new_js_bindings=False, export_attribute=None, + export_header=None, generate_non_variant_code=False): + self.module = module + self.output_dir = output_dir + self.typemap = typemap or {} + self.variant = variant + self.bytecode_path = bytecode_path + self.for_blink = for_blink + self.use_once_callback = use_once_callback + self.use_new_js_bindings = use_new_js_bindings + self.export_attribute = export_attribute + self.export_header = export_header + self.generate_non_variant_code = generate_non_variant_code + + def GetStructsFromMethods(self): + result = [] + for interface in self.module.interfaces: + for method in interface.methods: + result.append(self._GetStructFromMethod(method)) + if method.response_parameters != None: + result.append(self._GetResponseStructFromMethod(method)) + return result + + def GetStructs(self): + return map(partial(self._AddStructComputedData, True), self.module.structs) + + def GetUnions(self): + return map(self._AddUnionComputedData, self.module.unions) + + def GetInterfaces(self): + return map(self._AddInterfaceComputedData, self.module.interfaces) + + # Prepend the filename with a directory that matches the directory of the + # original .mojom file, relative to the import root. + def MatchMojomFilePath(self, filename): + return os.path.join(os.path.dirname(self.module.path), filename) + + def Write(self, contents, filename): + if self.output_dir is None: + print contents + return + full_path = os.path.join(self.output_dir, filename) + WriteFile(contents, full_path) + + def GenerateFiles(self, args): + raise NotImplementedError("Subclasses must override/implement this method") + + def GetJinjaParameters(self): + """Returns default constructor parameters for the jinja environment.""" + return {} + + def GetGlobals(self): + """Returns global mappings for the template generation.""" + return {} + + def _AddStructComputedData(self, exported, struct): + """Adds computed data to the given struct. The data is computed once and + used repeatedly in the generation process.""" + struct.packed = pack.PackedStruct(struct) + struct.bytes = pack.GetByteLayout(struct.packed) + struct.versions = pack.GetVersionInfo(struct.packed) + struct.exported = exported + return struct + + def _AddUnionComputedData(self, union): + """Adds computed data to the given union. The data is computed once and + used repeatedly in the generation process.""" + ordinal = 0 + for field in union.fields: + if field.ordinal is not None: + ordinal = field.ordinal + field.ordinal = ordinal + ordinal += 1 + return union + + def _AddInterfaceComputedData(self, interface): + """Adds computed data to the given interface. The data is computed once and + used repeatedly in the generation process.""" + interface.version = 0 + for method in interface.methods: + if method.min_version is not None: + interface.version = max(interface.version, method.min_version) + + method.param_struct = self._GetStructFromMethod(method) + interface.version = max(interface.version, + method.param_struct.versions[-1].version) + + if method.response_parameters is not None: + method.response_param_struct = self._GetResponseStructFromMethod(method) + interface.version = max( + interface.version, + method.response_param_struct.versions[-1].version) + else: + method.response_param_struct = None + return interface + + def _GetStructFromMethod(self, method): + """Converts a method's parameters into the fields of a struct.""" + params_class = "%s_%s_Params" % (method.interface.name, method.name) + struct = mojom.Struct(params_class, module=method.interface.module) + for param in method.parameters: + struct.AddField(param.name, param.kind, param.ordinal, + attributes=param.attributes) + return self._AddStructComputedData(False, struct) + + def _GetResponseStructFromMethod(self, method): + """Converts a method's response_parameters into the fields of a struct.""" + params_class = "%s_%s_ResponseParams" % (method.interface.name, method.name) + struct = mojom.Struct(params_class, module=method.interface.module) + for param in method.response_parameters: + struct.AddField(param.name, param.kind, param.ordinal, + attributes=param.attributes) + return self._AddStructComputedData(False, struct) diff --git a/mojo/public/tools/bindings/pylib/mojom/generate/generator_unittest.py b/mojo/public/tools/bindings/pylib/mojom/generate/generator_unittest.py new file mode 100644 index 0000000000..9966b0b7f8 --- /dev/null +++ b/mojo/public/tools/bindings/pylib/mojom/generate/generator_unittest.py @@ -0,0 +1,24 @@ +# Copyright 2015 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +import unittest + +import module as mojom +import generator + +class TestGenerator(unittest.TestCase): + + def testGetUnionsAddsOrdinals(self): + module = mojom.Module() + union = module.AddUnion('a') + union.AddField('a', mojom.BOOL) + union.AddField('b', mojom.BOOL) + union.AddField('c', mojom.BOOL, ordinal=10) + union.AddField('d', mojom.BOOL) + + gen = generator.Generator(module) + union = gen.GetUnions()[0] + ordinals = [field.ordinal for field in union.fields] + + self.assertEquals([0, 1, 10, 11], ordinals) diff --git a/mojo/public/tools/bindings/pylib/mojom/generate/module.py b/mojo/public/tools/bindings/pylib/mojom/generate/module.py new file mode 100644 index 0000000000..3a5f188e75 --- /dev/null +++ b/mojo/public/tools/bindings/pylib/mojom/generate/module.py @@ -0,0 +1,891 @@ +# Copyright 2013 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +# This module's classes provide an interface to mojo modules. Modules are +# collections of interfaces and structs to be used by mojo ipc clients and +# servers. +# +# A simple interface would be created this way: +# module = mojom.generate.module.Module('Foo') +# interface = module.AddInterface('Bar') +# method = interface.AddMethod('Tat', 0) +# method.AddParameter('baz', 0, mojom.INT32) + + +# We use our own version of __repr__ when displaying the AST, as the +# AST currently doesn't capture which nodes are reference (e.g. to +# types) and which nodes are definitions. This allows us to e.g. print +# the definition of a struct when it's defined inside a module, but +# only print its name when it's referenced in e.g. a method parameter. +def Repr(obj, as_ref=True): + """A version of __repr__ that can distinguish references. + + Sometimes we like to print an object's full representation + (e.g. with its fields) and sometimes we just want to reference an + object that was printed in full elsewhere. This function allows us + to make that distinction. + + Args: + obj: The object whose string representation we compute. + as_ref: If True, use the short reference representation. + + Returns: + A str representation of |obj|. + """ + if hasattr(obj, 'Repr'): + return obj.Repr(as_ref=as_ref) + # Since we cannot implement Repr for existing container types, we + # handle them here. + elif isinstance(obj, list): + if not obj: + return '[]' + else: + return ('[\n%s\n]' % (',\n'.join(' %s' % Repr(elem, as_ref).replace( + '\n', '\n ') for elem in obj))) + elif isinstance(obj, dict): + if not obj: + return '{}' + else: + return ('{\n%s\n}' % (',\n'.join(' %s: %s' % ( + Repr(key, as_ref).replace('\n', '\n '), + Repr(val, as_ref).replace('\n', '\n ')) + for key, val in obj.iteritems()))) + else: + return repr(obj) + + +def GenericRepr(obj, names): + """Compute generic Repr for |obj| based on the attributes in |names|. + + Args: + obj: The object to compute a Repr for. + names: A dict from attribute names to include, to booleans + specifying whether those attributes should be shown as + references or not. + + Returns: + A str representation of |obj|. + """ + def ReprIndent(name, as_ref): + return ' %s=%s' % (name, Repr(getattr(obj, name), as_ref).replace( + '\n', '\n ')) + + return '%s(\n%s\n)' % ( + obj.__class__.__name__, + ',\n'.join(ReprIndent(name, as_ref) + for (name, as_ref) in names.iteritems())) + + +class Kind(object): + """Kind represents a type (e.g. int8, string). + + Attributes: + spec: A string uniquely identifying the type. May be None. + parent_kind: The enclosing type. For example, a struct defined + inside an interface has that interface as its parent. May be None. + """ + def __init__(self, spec=None): + self.spec = spec + self.parent_kind = None + + def Repr(self, as_ref=True): + return '<%s spec=%r>' % (self.__class__.__name__, self.spec) + + def __repr__(self): + # Gives us a decent __repr__ for all kinds. + return self.Repr() + + +class ReferenceKind(Kind): + """ReferenceKind represents pointer and handle types. + + A type is nullable if null (for pointer types) or invalid handle (for handle + types) is a legal value for the type. + + Attributes: + is_nullable: True if the type is nullable. + """ + + def __init__(self, spec=None, is_nullable=False): + assert spec is None or is_nullable == spec.startswith('?') + Kind.__init__(self, spec) + self.is_nullable = is_nullable + self.shared_definition = {} + + def Repr(self, as_ref=True): + return '<%s spec=%r is_nullable=%r>' % (self.__class__.__name__, self.spec, + self.is_nullable) + + def MakeNullableKind(self): + assert not self.is_nullable + + if self == STRING: + return NULLABLE_STRING + if self == HANDLE: + return NULLABLE_HANDLE + if self == DCPIPE: + return NULLABLE_DCPIPE + if self == DPPIPE: + return NULLABLE_DPPIPE + if self == MSGPIPE: + return NULLABLE_MSGPIPE + if self == SHAREDBUFFER: + return NULLABLE_SHAREDBUFFER + + nullable_kind = type(self)() + nullable_kind.shared_definition = self.shared_definition + if self.spec is not None: + nullable_kind.spec = '?' + self.spec + nullable_kind.is_nullable = True + + return nullable_kind + + @classmethod + def AddSharedProperty(cls, name): + """Adds a property |name| to |cls|, which accesses the corresponding item in + |shared_definition|. + + The reason of adding such indirection is to enable sharing definition + between a reference kind and its nullable variation. For example: + a = Struct('test_struct_1') + b = a.MakeNullableKind() + a.name = 'test_struct_2' + print b.name # Outputs 'test_struct_2'. + """ + def Get(self): + return self.shared_definition[name] + + def Set(self, value): + self.shared_definition[name] = value + + setattr(cls, name, property(Get, Set)) + + +# Initialize the set of primitive types. These can be accessed by clients. +BOOL = Kind('b') +INT8 = Kind('i8') +INT16 = Kind('i16') +INT32 = Kind('i32') +INT64 = Kind('i64') +UINT8 = Kind('u8') +UINT16 = Kind('u16') +UINT32 = Kind('u32') +UINT64 = Kind('u64') +FLOAT = Kind('f') +DOUBLE = Kind('d') +STRING = ReferenceKind('s') +HANDLE = ReferenceKind('h') +DCPIPE = ReferenceKind('h:d:c') +DPPIPE = ReferenceKind('h:d:p') +MSGPIPE = ReferenceKind('h:m') +SHAREDBUFFER = ReferenceKind('h:s') +NULLABLE_STRING = ReferenceKind('?s', True) +NULLABLE_HANDLE = ReferenceKind('?h', True) +NULLABLE_DCPIPE = ReferenceKind('?h:d:c', True) +NULLABLE_DPPIPE = ReferenceKind('?h:d:p', True) +NULLABLE_MSGPIPE = ReferenceKind('?h:m', True) +NULLABLE_SHAREDBUFFER = ReferenceKind('?h:s', True) + + +# Collection of all Primitive types +PRIMITIVES = ( + BOOL, + INT8, + INT16, + INT32, + INT64, + UINT8, + UINT16, + UINT32, + UINT64, + FLOAT, + DOUBLE, + STRING, + HANDLE, + DCPIPE, + DPPIPE, + MSGPIPE, + SHAREDBUFFER, + NULLABLE_STRING, + NULLABLE_HANDLE, + NULLABLE_DCPIPE, + NULLABLE_DPPIPE, + NULLABLE_MSGPIPE, + NULLABLE_SHAREDBUFFER +) + + +ATTRIBUTE_MIN_VERSION = 'MinVersion' +ATTRIBUTE_EXTENSIBLE = 'Extensible' +ATTRIBUTE_SYNC = 'Sync' + + +class NamedValue(object): + def __init__(self, module, parent_kind, name): + self.module = module + self.namespace = module.namespace + self.parent_kind = parent_kind + self.name = name + self.imported_from = None + + def GetSpec(self): + return (self.namespace + '.' + + (self.parent_kind and (self.parent_kind.name + '.') or "") + + self.name) + + +class BuiltinValue(object): + def __init__(self, value): + self.value = value + + +class ConstantValue(NamedValue): + def __init__(self, module, parent_kind, constant): + NamedValue.__init__(self, module, parent_kind, constant.name) + self.constant = constant + + +class EnumValue(NamedValue): + def __init__(self, module, enum, field): + NamedValue.__init__(self, module, enum.parent_kind, field.name) + self.enum = enum + + def GetSpec(self): + return (self.namespace + '.' + + (self.parent_kind and (self.parent_kind.name + '.') or "") + + self.enum.name + '.' + self.name) + + +class Constant(object): + def __init__(self, name=None, kind=None, value=None, parent_kind=None): + self.name = name + self.kind = kind + self.value = value + self.parent_kind = parent_kind + + +class Field(object): + def __init__(self, name=None, kind=None, ordinal=None, default=None, + attributes=None): + if self.__class__.__name__ == 'Field': + raise Exception() + self.name = name + self.kind = kind + self.ordinal = ordinal + self.default = default + self.attributes = attributes + + def Repr(self, as_ref=True): + # Fields are only referenced by objects which define them and thus + # they are always displayed as non-references. + return GenericRepr(self, {'name': False, 'kind': True}) + + @property + def min_version(self): + return self.attributes.get(ATTRIBUTE_MIN_VERSION) \ + if self.attributes else None + + +class StructField(Field): pass + + +class UnionField(Field): pass + + +class Struct(ReferenceKind): + """A struct with typed fields. + + Attributes: + name: {str} The name of the struct type. + native_only: {bool} Does the struct have a body (i.e. any fields) or is it + purely a native struct. + module: {Module} The defining module. + imported_from: {dict} Information about where this union was + imported from. + fields: {List[StructField]} The members of the struct. + attributes: {dict} Additional information about the struct, such as + if it's a native struct. + """ + + ReferenceKind.AddSharedProperty('name') + ReferenceKind.AddSharedProperty('native_only') + ReferenceKind.AddSharedProperty('module') + ReferenceKind.AddSharedProperty('imported_from') + ReferenceKind.AddSharedProperty('fields') + ReferenceKind.AddSharedProperty('attributes') + + def __init__(self, name=None, module=None, attributes=None): + if name is not None: + spec = 'x:' + name + else: + spec = None + ReferenceKind.__init__(self, spec) + self.name = name + self.native_only = False + self.module = module + self.imported_from = None + self.fields = [] + self.attributes = attributes + + def Repr(self, as_ref=True): + if as_ref: + return '<%s name=%r imported_from=%s>' % ( + self.__class__.__name__, self.name, + Repr(self.imported_from, as_ref=True)) + else: + return GenericRepr(self, {'name': False, 'fields': False, + 'imported_from': True}) + + def AddField(self, name, kind, ordinal=None, default=None, attributes=None): + field = StructField(name, kind, ordinal, default, attributes) + self.fields.append(field) + return field + + +class Union(ReferenceKind): + """A union of several kinds. + + Attributes: + name: {str} The name of the union type. + module: {Module} The defining module. + imported_from: {dict} Information about where this union was + imported from. + fields: {List[UnionField]} The members of the union. + attributes: {dict} Additional information about the union, such as + which Java class name to use to represent it in the generated + bindings. + """ + ReferenceKind.AddSharedProperty('name') + ReferenceKind.AddSharedProperty('module') + ReferenceKind.AddSharedProperty('imported_from') + ReferenceKind.AddSharedProperty('fields') + ReferenceKind.AddSharedProperty('attributes') + + def __init__(self, name=None, module=None, attributes=None): + if name is not None: + spec = 'x:' + name + else: + spec = None + ReferenceKind.__init__(self, spec) + self.name = name + self.module = module + self.imported_from = None + self.fields = [] + self.attributes = attributes + + def Repr(self, as_ref=True): + if as_ref: + return '<%s spec=%r is_nullable=%r fields=%s>' % ( + self.__class__.__name__, self.spec, self.is_nullable, + Repr(self.fields)) + else: + return GenericRepr(self, {'fields': True, 'is_nullable': False}) + + def AddField(self, name, kind, ordinal=None, attributes=None): + field = UnionField(name, kind, ordinal, None, attributes) + self.fields.append(field) + return field + + +class Array(ReferenceKind): + """An array. + + Attributes: + kind: {Kind} The type of the elements. May be None. + length: The number of elements. None if unknown. + """ + + ReferenceKind.AddSharedProperty('kind') + ReferenceKind.AddSharedProperty('length') + + def __init__(self, kind=None, length=None): + if kind is not None: + if length is not None: + spec = 'a%d:%s' % (length, kind.spec) + else: + spec = 'a:%s' % kind.spec + + ReferenceKind.__init__(self, spec) + else: + ReferenceKind.__init__(self) + self.kind = kind + self.length = length + + def Repr(self, as_ref=True): + if as_ref: + return '<%s spec=%r is_nullable=%r kind=%s length=%r>' % ( + self.__class__.__name__, self.spec, self.is_nullable, Repr(self.kind), + self.length) + else: + return GenericRepr(self, {'kind': True, 'length': False, + 'is_nullable': False}) + + +class Map(ReferenceKind): + """A map. + + Attributes: + key_kind: {Kind} The type of the keys. May be None. + value_kind: {Kind} The type of the elements. May be None. + """ + ReferenceKind.AddSharedProperty('key_kind') + ReferenceKind.AddSharedProperty('value_kind') + + def __init__(self, key_kind=None, value_kind=None): + if (key_kind is not None and value_kind is not None): + ReferenceKind.__init__(self, + 'm[' + key_kind.spec + '][' + value_kind.spec + + ']') + if IsNullableKind(key_kind): + raise Exception("Nullable kinds cannot be keys in maps.") + if IsAnyHandleKind(key_kind): + raise Exception("Handles cannot be keys in maps.") + if IsAnyInterfaceKind(key_kind): + raise Exception("Interfaces cannot be keys in maps.") + if IsArrayKind(key_kind): + raise Exception("Arrays cannot be keys in maps.") + else: + ReferenceKind.__init__(self) + + self.key_kind = key_kind + self.value_kind = value_kind + + def Repr(self, as_ref=True): + if as_ref: + return '<%s spec=%r is_nullable=%r key_kind=%s value_kind=%s>' % ( + self.__class__.__name__, self.spec, self.is_nullable, + Repr(self.key_kind), Repr(self.value_kind)) + else: + return GenericRepr(self, {'key_kind': True, 'value_kind': True}) + + +class InterfaceRequest(ReferenceKind): + ReferenceKind.AddSharedProperty('kind') + + def __init__(self, kind=None): + if kind is not None: + if not isinstance(kind, Interface): + raise Exception( + "Interface request requires %r to be an interface." % kind.spec) + ReferenceKind.__init__(self, 'r:' + kind.spec) + else: + ReferenceKind.__init__(self) + self.kind = kind + + +class AssociatedInterfaceRequest(ReferenceKind): + ReferenceKind.AddSharedProperty('kind') + + def __init__(self, kind=None): + if kind is not None: + if not isinstance(kind, InterfaceRequest): + raise Exception( + "Associated interface request requires %r to be an interface " + "request." % kind.spec) + assert not kind.is_nullable + ReferenceKind.__init__(self, 'asso:' + kind.spec) + else: + ReferenceKind.__init__(self) + self.kind = kind.kind if kind is not None else None + + +class Parameter(object): + def __init__(self, name=None, kind=None, ordinal=None, default=None, + attributes=None): + self.name = name + self.ordinal = ordinal + self.kind = kind + self.default = default + self.attributes = attributes + + def Repr(self, as_ref=True): + return '<%s name=%r kind=%s>' % (self.__class__.__name__, self.name, + self.kind.Repr(as_ref=True)) + + @property + def min_version(self): + return self.attributes.get(ATTRIBUTE_MIN_VERSION) \ + if self.attributes else None + + +class Method(object): + def __init__(self, interface, name, ordinal=None, attributes=None): + self.interface = interface + self.name = name + self.ordinal = ordinal + self.parameters = [] + self.response_parameters = None + self.attributes = attributes + + def Repr(self, as_ref=True): + if as_ref: + return '<%s name=%r>' % (self.__class__.__name__, self.name) + else: + return GenericRepr(self, {'name': False, 'parameters': True, + 'response_parameters': True}) + + def AddParameter(self, name, kind, ordinal=None, default=None, + attributes=None): + parameter = Parameter(name, kind, ordinal, default, attributes) + self.parameters.append(parameter) + return parameter + + def AddResponseParameter(self, name, kind, ordinal=None, default=None, + attributes=None): + if self.response_parameters == None: + self.response_parameters = [] + parameter = Parameter(name, kind, ordinal, default, attributes) + self.response_parameters.append(parameter) + return parameter + + @property + def min_version(self): + return self.attributes.get(ATTRIBUTE_MIN_VERSION) \ + if self.attributes else None + + @property + def sync(self): + return self.attributes.get(ATTRIBUTE_SYNC) \ + if self.attributes else None + + +class Interface(ReferenceKind): + ReferenceKind.AddSharedProperty('module') + ReferenceKind.AddSharedProperty('name') + ReferenceKind.AddSharedProperty('imported_from') + ReferenceKind.AddSharedProperty('methods') + ReferenceKind.AddSharedProperty('attributes') + + def __init__(self, name=None, module=None, attributes=None): + if name is not None: + spec = 'x:' + name + else: + spec = None + ReferenceKind.__init__(self, spec) + self.module = module + self.name = name + self.imported_from = None + self.methods = [] + self.attributes = attributes + + def Repr(self, as_ref=True): + if as_ref: + return '<%s name=%r>' % (self.__class__.__name__, self.name) + else: + return GenericRepr(self, {'name': False, 'attributes': False, + 'methods': False}) + + def AddMethod(self, name, ordinal=None, attributes=None): + method = Method(self, name, ordinal, attributes) + self.methods.append(method) + return method + + # TODO(451323): Remove when the language backends no longer rely on this. + @property + def client(self): + return None + + +class AssociatedInterface(ReferenceKind): + ReferenceKind.AddSharedProperty('kind') + + def __init__(self, kind=None): + if kind is not None: + if not isinstance(kind, Interface): + raise Exception( + "Associated interface requires %r to be an interface." % kind.spec) + assert not kind.is_nullable + ReferenceKind.__init__(self, 'asso:' + kind.spec) + else: + ReferenceKind.__init__(self) + self.kind = kind + + +class EnumField(object): + def __init__(self, name=None, value=None, attributes=None, + numeric_value=None): + self.name = name + self.value = value + self.attributes = attributes + self.numeric_value = numeric_value + + @property + def min_version(self): + return self.attributes.get(ATTRIBUTE_MIN_VERSION) \ + if self.attributes else None + + +class Enum(Kind): + def __init__(self, name=None, module=None, attributes=None): + self.module = module + self.name = name + self.native_only = False + self.imported_from = None + if name is not None: + spec = 'x:' + name + else: + spec = None + Kind.__init__(self, spec) + self.fields = [] + self.attributes = attributes + + def Repr(self, as_ref=True): + if as_ref: + return '<%s name=%r>' % (self.__class__.__name__, self.name) + else: + return GenericRepr(self, {'name': False, 'fields': False}) + + @property + def extensible(self): + return self.attributes.get(ATTRIBUTE_EXTENSIBLE, False) \ + if self.attributes else False + + +class Module(object): + def __init__(self, name=None, namespace=None, attributes=None): + self.name = name + self.path = name + self.namespace = namespace + self.structs = [] + self.unions = [] + self.interfaces = [] + self.kinds = {} + self.attributes = attributes + + def __repr__(self): + # Gives us a decent __repr__ for modules. + return self.Repr() + + def Repr(self, as_ref=True): + if as_ref: + return '<%s name=%r namespace=%r>' % ( + self.__class__.__name__, self.name, self.namespace) + else: + return GenericRepr(self, {'name': False, 'namespace': False, + 'attributes': False, 'structs': False, + 'interfaces': False, 'unions': False}) + + def AddInterface(self, name, attributes=None): + interface = Interface(name, self, attributes) + self.interfaces.append(interface) + return interface + + def AddStruct(self, name, attributes=None): + struct = Struct(name, self, attributes) + self.structs.append(struct) + return struct + + def AddUnion(self, name, attributes=None): + union = Union(name, self, attributes) + self.unions.append(union) + return union + + +def IsBoolKind(kind): + return kind.spec == BOOL.spec + + +def IsFloatKind(kind): + return kind.spec == FLOAT.spec + + +def IsDoubleKind(kind): + return kind.spec == DOUBLE.spec + + +def IsIntegralKind(kind): + return (kind.spec == BOOL.spec or + kind.spec == INT8.spec or + kind.spec == INT16.spec or + kind.spec == INT32.spec or + kind.spec == INT64.spec or + kind.spec == UINT8.spec or + kind.spec == UINT16.spec or + kind.spec == UINT32.spec or + kind.spec == UINT64.spec) + + +def IsStringKind(kind): + return kind.spec == STRING.spec or kind.spec == NULLABLE_STRING.spec + + +def IsGenericHandleKind(kind): + return kind.spec == HANDLE.spec or kind.spec == NULLABLE_HANDLE.spec + + +def IsDataPipeConsumerKind(kind): + return kind.spec == DCPIPE.spec or kind.spec == NULLABLE_DCPIPE.spec + + +def IsDataPipeProducerKind(kind): + return kind.spec == DPPIPE.spec or kind.spec == NULLABLE_DPPIPE.spec + + +def IsMessagePipeKind(kind): + return kind.spec == MSGPIPE.spec or kind.spec == NULLABLE_MSGPIPE.spec + + +def IsSharedBufferKind(kind): + return (kind.spec == SHAREDBUFFER.spec or + kind.spec == NULLABLE_SHAREDBUFFER.spec) + + +def IsStructKind(kind): + return isinstance(kind, Struct) + + +def IsUnionKind(kind): + return isinstance(kind, Union) + + +def IsArrayKind(kind): + return isinstance(kind, Array) + + +def IsInterfaceKind(kind): + return isinstance(kind, Interface) + + +def IsAssociatedInterfaceKind(kind): + return isinstance(kind, AssociatedInterface) + + +def IsInterfaceRequestKind(kind): + return isinstance(kind, InterfaceRequest) + + +def IsAssociatedInterfaceRequestKind(kind): + return isinstance(kind, AssociatedInterfaceRequest) + + +def IsEnumKind(kind): + return isinstance(kind, Enum) + + +def IsReferenceKind(kind): + return isinstance(kind, ReferenceKind) + + +def IsNullableKind(kind): + return IsReferenceKind(kind) and kind.is_nullable + + +def IsMapKind(kind): + return isinstance(kind, Map) + + +def IsObjectKind(kind): + return IsPointerKind(kind) or IsUnionKind(kind) + + +def IsPointerKind(kind): + return (IsStructKind(kind) or IsArrayKind(kind) or IsStringKind(kind) or + IsMapKind(kind)) + + +# Please note that it doesn't include any interface kind. +def IsAnyHandleKind(kind): + return (IsGenericHandleKind(kind) or + IsDataPipeConsumerKind(kind) or + IsDataPipeProducerKind(kind) or + IsMessagePipeKind(kind) or + IsSharedBufferKind(kind)) + + +def IsAnyInterfaceKind(kind): + return (IsInterfaceKind(kind) or IsInterfaceRequestKind(kind) or + IsAssociatedKind(kind)) + + +def IsAnyHandleOrInterfaceKind(kind): + return IsAnyHandleKind(kind) or IsAnyInterfaceKind(kind) + + +def IsAssociatedKind(kind): + return (IsAssociatedInterfaceKind(kind) or + IsAssociatedInterfaceRequestKind(kind)) + + +def HasCallbacks(interface): + for method in interface.methods: + if method.response_parameters != None: + return True + return False + + +# Finds out whether an interface passes associated interfaces and associated +# interface requests. +def PassesAssociatedKinds(interface): + def _ContainsAssociatedKinds(kind, visited_kinds): + if kind in visited_kinds: + # No need to examine the kind again. + return False + visited_kinds.add(kind) + if IsAssociatedKind(kind): + return True + if IsArrayKind(kind): + return _ContainsAssociatedKinds(kind.kind, visited_kinds) + if IsStructKind(kind) or IsUnionKind(kind): + for field in kind.fields: + if _ContainsAssociatedKinds(field.kind, visited_kinds): + return True + if IsMapKind(kind): + # No need to examine the key kind, only primitive kinds and non-nullable + # string are allowed to be key kinds. + return _ContainsAssociatedKinds(kind.value_kind, visited_kinds) + return False + + visited_kinds = set() + for method in interface.methods: + for param in method.parameters: + if _ContainsAssociatedKinds(param.kind, visited_kinds): + return True + if method.response_parameters != None: + for param in method.response_parameters: + if _ContainsAssociatedKinds(param.kind, visited_kinds): + return True + return False + + +def HasSyncMethods(interface): + for method in interface.methods: + if method.sync: + return True + return False + + +def ContainsHandlesOrInterfaces(kind): + """Check if the kind contains any handles. + + This check is recursive so it checks all struct fields, containers elements, + etc. + + Args: + struct: {Kind} The kind to check. + + Returns: + {bool}: True if the kind contains handles. + """ + # We remember the types we already checked to avoid infinite recursion when + # checking recursive (or mutually recursive) types: + checked = set() + def Check(kind): + if kind.spec in checked: + return False + checked.add(kind.spec) + if IsStructKind(kind): + return any(Check(field.kind) for field in kind.fields) + elif IsUnionKind(kind): + return any(Check(field.kind) for field in kind.fields) + elif IsAnyHandleKind(kind): + return True + elif IsAnyInterfaceKind(kind): + return True + elif IsArrayKind(kind): + return Check(kind.kind) + elif IsMapKind(kind): + return Check(kind.key_kind) or Check(kind.value_kind) + else: + return False + return Check(kind) diff --git a/mojo/public/tools/bindings/pylib/mojom/generate/module_tests.py b/mojo/public/tools/bindings/pylib/mojom/generate/module_tests.py new file mode 100644 index 0000000000..a887686e1b --- /dev/null +++ b/mojo/public/tools/bindings/pylib/mojom/generate/module_tests.py @@ -0,0 +1,34 @@ +# Copyright 2013 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +import sys + +import test_support + +EXPECT_EQ = test_support.EXPECT_EQ +EXPECT_TRUE = test_support.EXPECT_TRUE +RunTest = test_support.RunTest +ModulesAreEqual = test_support.ModulesAreEqual +BuildTestModule = test_support.BuildTestModule +TestTestModule = test_support.TestTestModule + + +def BuildAndTestModule(): + return TestTestModule(BuildTestModule()) + + +def TestModulesEqual(): + return EXPECT_TRUE(ModulesAreEqual(BuildTestModule(), BuildTestModule())) + + +def Main(args): + errors = 0 + errors += RunTest(BuildAndTestModule) + errors += RunTest(TestModulesEqual) + + return errors + + +if __name__ == '__main__': + sys.exit(Main(sys.argv[1:])) diff --git a/mojo/public/tools/bindings/pylib/mojom/generate/pack.py b/mojo/public/tools/bindings/pylib/mojom/generate/pack.py new file mode 100644 index 0000000000..37dc8f396b --- /dev/null +++ b/mojo/public/tools/bindings/pylib/mojom/generate/pack.py @@ -0,0 +1,250 @@ +# Copyright 2013 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +import module as mojom + +# This module provides a mechanism for determining the packed order and offsets +# of a mojom.Struct. +# +# ps = pack.PackedStruct(struct) +# ps.packed_fields will access a list of PackedField objects, each of which +# will have an offset, a size and a bit (for mojom.BOOLs). + +# Size of struct header in bytes: num_bytes [4B] + version [4B]. +HEADER_SIZE = 8 + +class PackedField(object): + kind_to_size = { + mojom.BOOL: 1, + mojom.INT8: 1, + mojom.UINT8: 1, + mojom.INT16: 2, + mojom.UINT16: 2, + mojom.INT32: 4, + mojom.UINT32: 4, + mojom.FLOAT: 4, + mojom.HANDLE: 4, + mojom.MSGPIPE: 4, + mojom.SHAREDBUFFER: 4, + mojom.DCPIPE: 4, + mojom.DPPIPE: 4, + mojom.NULLABLE_HANDLE: 4, + mojom.NULLABLE_MSGPIPE: 4, + mojom.NULLABLE_SHAREDBUFFER: 4, + mojom.NULLABLE_DCPIPE: 4, + mojom.NULLABLE_DPPIPE: 4, + mojom.INT64: 8, + mojom.UINT64: 8, + mojom.DOUBLE: 8, + mojom.STRING: 8, + mojom.NULLABLE_STRING: 8 + } + + @classmethod + def GetSizeForKind(cls, kind): + if isinstance(kind, (mojom.Array, mojom.Map, mojom.Struct, + mojom.Interface, mojom.AssociatedInterface)): + return 8 + if isinstance(kind, mojom.Union): + return 16 + if isinstance(kind, mojom.InterfaceRequest): + kind = mojom.MSGPIPE + if isinstance(kind, mojom.AssociatedInterfaceRequest): + return 4 + if isinstance(kind, mojom.Enum): + # TODO(mpcomplete): what about big enums? + return cls.kind_to_size[mojom.INT32] + if not kind in cls.kind_to_size: + raise Exception("Invalid kind: %s" % kind.spec) + return cls.kind_to_size[kind] + + @classmethod + def GetAlignmentForKind(cls, kind): + if isinstance(kind, (mojom.Interface, mojom.AssociatedInterface)): + return 4 + if isinstance(kind, mojom.Union): + return 8 + return cls.GetSizeForKind(kind) + + def __init__(self, field, index, ordinal): + """ + Args: + field: the original field. + index: the position of the original field in the struct. + ordinal: the ordinal of the field for serialization. + """ + self.field = field + self.index = index + self.ordinal = ordinal + self.size = self.GetSizeForKind(field.kind) + self.alignment = self.GetAlignmentForKind(field.kind) + self.offset = None + self.bit = None + self.min_version = None + + +def GetPad(offset, alignment): + """Returns the pad necessary to reserve space so that |offset + pad| equals to + some multiple of |alignment|.""" + return (alignment - (offset % alignment)) % alignment + + +def GetFieldOffset(field, last_field): + """Returns a 2-tuple of the field offset and bit (for BOOLs).""" + if (field.field.kind == mojom.BOOL and + last_field.field.kind == mojom.BOOL and + last_field.bit < 7): + return (last_field.offset, last_field.bit + 1) + + offset = last_field.offset + last_field.size + pad = GetPad(offset, field.alignment) + return (offset + pad, 0) + + +def GetPayloadSizeUpToField(field): + """Returns the payload size (not including struct header) if |field| is the + last field. + """ + if not field: + return 0 + offset = field.offset + field.size + pad = GetPad(offset, 8) + return offset + pad + + +class PackedStruct(object): + def __init__(self, struct): + self.struct = struct + # |packed_fields| contains all the fields, in increasing offset order. + self.packed_fields = [] + # |packed_fields_in_ordinal_order| refers to the same fields as + # |packed_fields|, but in ordinal order. + self.packed_fields_in_ordinal_order = [] + + # No fields. + if (len(struct.fields) == 0): + return + + # Start by sorting by ordinal. + src_fields = self.packed_fields_in_ordinal_order + ordinal = 0 + for index, field in enumerate(struct.fields): + if field.ordinal is not None: + ordinal = field.ordinal + src_fields.append(PackedField(field, index, ordinal)) + ordinal += 1 + src_fields.sort(key=lambda field: field.ordinal) + + # Set |min_version| for each field. + next_min_version = 0 + for packed_field in src_fields: + if packed_field.field.min_version is None: + assert next_min_version == 0 + else: + assert packed_field.field.min_version >= next_min_version + next_min_version = packed_field.field.min_version + packed_field.min_version = next_min_version + + if (packed_field.min_version != 0 and + mojom.IsReferenceKind(packed_field.field.kind) and + not packed_field.field.kind.is_nullable): + raise Exception("Non-nullable fields are only allowed in version 0 of " + "a struct. %s.%s is defined with [MinVersion=%d]." + % (self.struct.name, packed_field.field.name, + packed_field.min_version)) + + src_field = src_fields[0] + src_field.offset = 0 + src_field.bit = 0 + dst_fields = self.packed_fields + dst_fields.append(src_field) + + # Then find first slot that each field will fit. + for src_field in src_fields[1:]: + last_field = dst_fields[0] + for i in xrange(1, len(dst_fields)): + next_field = dst_fields[i] + offset, bit = GetFieldOffset(src_field, last_field) + if offset + src_field.size <= next_field.offset: + # Found hole. + src_field.offset = offset + src_field.bit = bit + dst_fields.insert(i, src_field) + break + last_field = next_field + if src_field.offset is None: + # Add to end + src_field.offset, src_field.bit = GetFieldOffset(src_field, last_field) + dst_fields.append(src_field) + + +class ByteInfo(object): + def __init__(self): + self.is_padding = False + self.packed_fields = [] + + +def GetByteLayout(packed_struct): + total_payload_size = GetPayloadSizeUpToField( + packed_struct.packed_fields[-1] if packed_struct.packed_fields else None) + bytes = [ByteInfo() for i in xrange(total_payload_size)] + + limit_of_previous_field = 0 + for packed_field in packed_struct.packed_fields: + for i in xrange(limit_of_previous_field, packed_field.offset): + bytes[i].is_padding = True + bytes[packed_field.offset].packed_fields.append(packed_field) + limit_of_previous_field = packed_field.offset + packed_field.size + + for i in xrange(limit_of_previous_field, len(bytes)): + bytes[i].is_padding = True + + for byte in bytes: + # A given byte cannot both be padding and have a fields packed into it. + assert not (byte.is_padding and byte.packed_fields) + + return bytes + + +class VersionInfo(object): + def __init__(self, version, num_fields, num_bytes): + self.version = version + self.num_fields = num_fields + self.num_bytes = num_bytes + + +def GetVersionInfo(packed_struct): + """Get version information for a struct. + + Args: + packed_struct: A PackedStruct instance. + + Returns: + A non-empty list of VersionInfo instances, sorted by version in increasing + order. + Note: The version numbers may not be consecutive. + """ + versions = [] + last_version = 0 + last_num_fields = 0 + last_payload_size = 0 + + for packed_field in packed_struct.packed_fields_in_ordinal_order: + if packed_field.min_version != last_version: + versions.append( + VersionInfo(last_version, last_num_fields, + last_payload_size + HEADER_SIZE)) + last_version = packed_field.min_version + + last_num_fields += 1 + # The fields are iterated in ordinal order here. However, the size of a + # version is determined by the last field of that version in pack order, + # instead of ordinal order. Therefore, we need to calculate the max value. + last_payload_size = max(GetPayloadSizeUpToField(packed_field), + last_payload_size) + + assert len(versions) == 0 or last_num_fields != versions[-1].num_fields + versions.append(VersionInfo(last_version, last_num_fields, + last_payload_size + HEADER_SIZE)) + return versions diff --git a/mojo/public/tools/bindings/pylib/mojom/generate/pack_tests.py b/mojo/public/tools/bindings/pylib/mojom/generate/pack_tests.py new file mode 100644 index 0000000000..14f699da34 --- /dev/null +++ b/mojo/public/tools/bindings/pylib/mojom/generate/pack_tests.py @@ -0,0 +1,193 @@ +# Copyright 2013 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +import sys + +import module as mojom +import pack +import test_support + + +EXPECT_EQ = test_support.EXPECT_EQ +EXPECT_TRUE = test_support.EXPECT_TRUE +RunTest = test_support.RunTest + + +def TestOrdinalOrder(): + errors = 0 + struct = mojom.Struct('test') + struct.AddField('testfield1', mojom.INT32, 2) + struct.AddField('testfield2', mojom.INT32, 1) + ps = pack.PackedStruct(struct) + + errors += EXPECT_EQ(2, len(ps.packed_fields)) + errors += EXPECT_EQ('testfield2', ps.packed_fields[0].field.name) + errors += EXPECT_EQ('testfield1', ps.packed_fields[1].field.name) + + return errors + +def TestZeroFields(): + errors = 0 + struct = mojom.Struct('test') + ps = pack.PackedStruct(struct) + errors += EXPECT_EQ(0, len(ps.packed_fields)) + return errors + + +def TestOneField(): + errors = 0 + struct = mojom.Struct('test') + struct.AddField('testfield1', mojom.INT8) + ps = pack.PackedStruct(struct) + errors += EXPECT_EQ(1, len(ps.packed_fields)) + return errors + +# Pass three tuples. +# |kinds| is a sequence of mojom.Kinds that specify the fields that are to +# be created. +# |fields| is the expected order of the resulting fields, with the integer +# "1" first. +# |offsets| is the expected order of offsets, with the integer "0" first. +def TestSequence(kinds, fields, offsets): + errors = 0 + struct = mojom.Struct('test') + index = 1 + for kind in kinds: + struct.AddField("%d" % index, kind) + index += 1 + ps = pack.PackedStruct(struct) + num_fields = len(ps.packed_fields) + errors += EXPECT_EQ(len(kinds), num_fields) + for i in xrange(num_fields): + EXPECT_EQ("%d" % fields[i], ps.packed_fields[i].field.name) + EXPECT_EQ(offsets[i], ps.packed_fields[i].offset) + + return errors + + +def TestPaddingPackedInOrder(): + return TestSequence( + (mojom.INT8, mojom.UINT8, mojom.INT32), + (1, 2, 3), + (0, 1, 4)) + + +def TestPaddingPackedOutOfOrder(): + return TestSequence( + (mojom.INT8, mojom.INT32, mojom.UINT8), + (1, 3, 2), + (0, 1, 4)) + + +def TestPaddingPackedOverflow(): + kinds = (mojom.INT8, mojom.INT32, mojom.INT16, mojom.INT8, mojom.INT8) + # 2 bytes should be packed together first, followed by short, then by int. + fields = (1, 4, 3, 2, 5) + offsets = (0, 1, 2, 4, 8) + return TestSequence(kinds, fields, offsets) + + +def TestNullableTypes(): + kinds = (mojom.STRING.MakeNullableKind(), + mojom.HANDLE.MakeNullableKind(), + mojom.Struct('test_struct').MakeNullableKind(), + mojom.DCPIPE.MakeNullableKind(), + mojom.Array().MakeNullableKind(), + mojom.DPPIPE.MakeNullableKind(), + mojom.Array(length=5).MakeNullableKind(), + mojom.MSGPIPE.MakeNullableKind(), + mojom.Interface('test_inteface').MakeNullableKind(), + mojom.SHAREDBUFFER.MakeNullableKind(), + mojom.InterfaceRequest().MakeNullableKind()) + fields = (1, 2, 4, 3, 5, 6, 8, 7, 9, 10, 11) + offsets = (0, 8, 12, 16, 24, 32, 36, 40, 48, 52, 56) + return TestSequence(kinds, fields, offsets) + + +def TestAllTypes(): + return TestSequence( + (mojom.BOOL, mojom.INT8, mojom.STRING, mojom.UINT8, + mojom.INT16, mojom.DOUBLE, mojom.UINT16, + mojom.INT32, mojom.UINT32, mojom.INT64, + mojom.FLOAT, mojom.STRING, mojom.HANDLE, + mojom.UINT64, mojom.Struct('test'), mojom.Array(), + mojom.STRING.MakeNullableKind()), + (1, 2, 4, 5, 7, 3, 6, 8, 9, 10, 11, 13, 12, 14, 15, 16, 17, 18), + (0, 1, 2, 4, 6, 8, 16, 24, 28, 32, 40, 44, 48, 56, 64, 72, 80, 88)) + + +def TestPaddingPackedOutOfOrderByOrdinal(): + errors = 0 + struct = mojom.Struct('test') + struct.AddField('testfield1', mojom.INT8) + struct.AddField('testfield3', mojom.UINT8, 3) + struct.AddField('testfield2', mojom.INT32, 2) + ps = pack.PackedStruct(struct) + errors += EXPECT_EQ(3, len(ps.packed_fields)) + + # Second byte should be packed in behind first, altering order. + errors += EXPECT_EQ('testfield1', ps.packed_fields[0].field.name) + errors += EXPECT_EQ('testfield3', ps.packed_fields[1].field.name) + errors += EXPECT_EQ('testfield2', ps.packed_fields[2].field.name) + + # Second byte should be packed with first. + errors += EXPECT_EQ(0, ps.packed_fields[0].offset) + errors += EXPECT_EQ(1, ps.packed_fields[1].offset) + errors += EXPECT_EQ(4, ps.packed_fields[2].offset) + + return errors + + +def TestBools(): + errors = 0 + struct = mojom.Struct('test') + struct.AddField('bit0', mojom.BOOL) + struct.AddField('bit1', mojom.BOOL) + struct.AddField('int', mojom.INT32) + struct.AddField('bit2', mojom.BOOL) + struct.AddField('bit3', mojom.BOOL) + struct.AddField('bit4', mojom.BOOL) + struct.AddField('bit5', mojom.BOOL) + struct.AddField('bit6', mojom.BOOL) + struct.AddField('bit7', mojom.BOOL) + struct.AddField('bit8', mojom.BOOL) + ps = pack.PackedStruct(struct) + errors += EXPECT_EQ(10, len(ps.packed_fields)) + + # First 8 bits packed together. + for i in xrange(8): + pf = ps.packed_fields[i] + errors += EXPECT_EQ(0, pf.offset) + errors += EXPECT_EQ("bit%d" % i, pf.field.name) + errors += EXPECT_EQ(i, pf.bit) + + # Ninth bit goes into second byte. + errors += EXPECT_EQ("bit8", ps.packed_fields[8].field.name) + errors += EXPECT_EQ(1, ps.packed_fields[8].offset) + errors += EXPECT_EQ(0, ps.packed_fields[8].bit) + + # int comes last. + errors += EXPECT_EQ("int", ps.packed_fields[9].field.name) + errors += EXPECT_EQ(4, ps.packed_fields[9].offset) + + return errors + + +def Main(args): + errors = 0 + errors += RunTest(TestZeroFields) + errors += RunTest(TestOneField) + errors += RunTest(TestPaddingPackedInOrder) + errors += RunTest(TestPaddingPackedOutOfOrder) + errors += RunTest(TestPaddingPackedOverflow) + errors += RunTest(TestNullableTypes) + errors += RunTest(TestAllTypes) + errors += RunTest(TestPaddingPackedOutOfOrderByOrdinal) + errors += RunTest(TestBools) + + return errors + + +if __name__ == '__main__': + sys.exit(Main(sys.argv[1:])) diff --git a/mojo/public/tools/bindings/pylib/mojom/generate/run_tests.py b/mojo/public/tools/bindings/pylib/mojom/generate/run_tests.py new file mode 100755 index 0000000000..41f11a2b71 --- /dev/null +++ b/mojo/public/tools/bindings/pylib/mojom/generate/run_tests.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python +# Copyright 2013 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +""" Test runner for Mojom """ + +import subprocess +import sys + +def TestMojom(testname, args): + print '\nRunning unit tests for %s.' % testname + try: + args = [sys.executable, testname] + args + subprocess.check_call(args, stdout=sys.stdout) + print 'Succeeded' + return 0 + except subprocess.CalledProcessError as err: + print 'Failed with %s.' % str(err) + return 1 + + +def main(args): + errors = 0 + errors += TestMojom('data_tests.py', ['--test']) + errors += TestMojom('module_tests.py', ['--test']) + errors += TestMojom('pack_tests.py', ['--test']) + + if errors: + print '\nFailed tests.' + return min(errors, 127) # Make sure the return value doesn't "wrap". + + +if __name__ == '__main__': + sys.exit(main(sys.argv[1:])) diff --git a/mojo/public/tools/bindings/pylib/mojom/generate/template_expander.py b/mojo/public/tools/bindings/pylib/mojom/generate/template_expander.py new file mode 100644 index 0000000000..66f8954012 --- /dev/null +++ b/mojo/public/tools/bindings/pylib/mojom/generate/template_expander.py @@ -0,0 +1,67 @@ +# Copyright 2013 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +# Based on third_party/WebKit/Source/build/scripts/template_expander.py. + +import imp +import os.path +import sys + +# Disable lint check for finding modules: +# pylint: disable=F0401 + +def _GetDirAbove(dirname): + """Returns the directory "above" this file containing |dirname| (which must + also be "above" this file).""" + path = os.path.abspath(__file__) + while True: + path, tail = os.path.split(path) + assert tail + if tail == dirname: + return path + +try: + imp.find_module("jinja2") +except ImportError: + sys.path.append(os.path.join(_GetDirAbove("mojo"), "third_party")) +import jinja2 + + +def ApplyTemplate(mojo_generator, path_to_template, params, **kwargs): + loader = jinja2.ModuleLoader(os.path.join( + mojo_generator.bytecode_path, "%s.zip" % mojo_generator.GetTemplatePrefix( + ))) + final_kwargs = dict(mojo_generator.GetJinjaParameters()) + final_kwargs.update(kwargs) + jinja_env = jinja2.Environment(loader=loader, + keep_trailing_newline=True, + **final_kwargs) + jinja_env.globals.update(mojo_generator.GetGlobals()) + jinja_env.filters.update(mojo_generator.GetFilters()) + template = jinja_env.get_template(path_to_template) + return template.render(params) + + +def UseJinja(path_to_template, **kwargs): + def RealDecorator(generator): + def GeneratorInternal(*args, **kwargs2): + parameters = generator(*args, **kwargs2) + return ApplyTemplate(args[0], path_to_template, parameters, **kwargs) + GeneratorInternal.func_name = generator.func_name + return GeneratorInternal + return RealDecorator + + +def PrecompileTemplates(generator_modules, output_dir): + for module in generator_modules.values(): + generator = module.Generator(None) + jinja_env = jinja2.Environment(loader=jinja2.FileSystemLoader([os.path.join( + os.path.dirname(module.__file__), generator.GetTemplatePrefix())])) + jinja_env.filters.update(generator.GetFilters()) + jinja_env.compile_templates( + os.path.join(output_dir, "%s.zip" % generator.GetTemplatePrefix()), + extensions=["tmpl"], + zip="stored", + py_compile=True, + ignore_errors=False) diff --git a/mojo/public/tools/bindings/pylib/mojom/generate/test_support.py b/mojo/public/tools/bindings/pylib/mojom/generate/test_support.py new file mode 100644 index 0000000000..eb394619d2 --- /dev/null +++ b/mojo/public/tools/bindings/pylib/mojom/generate/test_support.py @@ -0,0 +1,193 @@ +# Copyright 2013 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +import sys +import traceback + +import module as mojom + +# Support for writing mojom test cases. +# RunTest(fn) will execute fn, catching any exceptions. fn should return +# the number of errors that are encountered. +# +# EXPECT_EQ(a, b) and EXPECT_TRUE(b) will print error information if the +# expectations are not true and return a non zero value. This allows test cases +# to be written like this +# +# def Foo(): +# errors = 0 +# errors += EXPECT_EQ('test', test()) +# ... +# return errors +# +# RunTest(foo) + +def FieldsAreEqual(field1, field2): + if field1 == field2: + return True + return field1.name == field2.name and \ + KindsAreEqual(field1.kind, field2.kind) and \ + field1.ordinal == field2.ordinal and \ + field1.default == field2.default + + +def KindsAreEqual(kind1, kind2): + if kind1 == kind2: + return True + if kind1.__class__ != kind2.__class__ or kind1.spec != kind2.spec: + return False + if kind1.__class__ == mojom.Kind: + return kind1.spec == kind2.spec + if kind1.__class__ == mojom.Struct: + if kind1.name != kind2.name or \ + kind1.spec != kind2.spec or \ + len(kind1.fields) != len(kind2.fields): + return False + for i in range(len(kind1.fields)): + if not FieldsAreEqual(kind1.fields[i], kind2.fields[i]): + return False + return True + if kind1.__class__ == mojom.Array: + return KindsAreEqual(kind1.kind, kind2.kind) + print 'Unknown Kind class: ', kind1.__class__.__name__ + return False + + +def ParametersAreEqual(parameter1, parameter2): + if parameter1 == parameter2: + return True + return parameter1.name == parameter2.name and \ + parameter1.ordinal == parameter2.ordinal and \ + parameter1.default == parameter2.default and \ + KindsAreEqual(parameter1.kind, parameter2.kind) + + +def MethodsAreEqual(method1, method2): + if method1 == method2: + return True + if method1.name != method2.name or \ + method1.ordinal != method2.ordinal or \ + len(method1.parameters) != len(method2.parameters): + return False + for i in range(len(method1.parameters)): + if not ParametersAreEqual(method1.parameters[i], method2.parameters[i]): + return False + return True + + +def InterfacesAreEqual(interface1, interface2): + if interface1 == interface2: + return True + if interface1.name != interface2.name or \ + len(interface1.methods) != len(interface2.methods): + return False + for i in range(len(interface1.methods)): + if not MethodsAreEqual(interface1.methods[i], interface2.methods[i]): + return False + return True + + +def ModulesAreEqual(module1, module2): + if module1 == module2: + return True + if module1.name != module2.name or \ + module1.namespace != module2.namespace or \ + len(module1.structs) != len(module2.structs) or \ + len(module1.interfaces) != len(module2.interfaces): + return False + for i in range(len(module1.structs)): + if not KindsAreEqual(module1.structs[i], module2.structs[i]): + return False + for i in range(len(module1.interfaces)): + if not InterfacesAreEqual(module1.interfaces[i], module2.interfaces[i]): + return False + return True + + +# Builds and returns a Module suitable for testing/ +def BuildTestModule(): + module = mojom.Module('test', 'testspace') + struct = module.AddStruct('teststruct') + struct.AddField('testfield1', mojom.INT32) + struct.AddField('testfield2', mojom.Array(mojom.INT32), 42) + + interface = module.AddInterface('Server') + method = interface.AddMethod('Foo', 42) + method.AddParameter('foo', mojom.INT32) + method.AddParameter('bar', mojom.Array(struct)) + + return module + + +# Tests if |module| is as built by BuildTestModule(). Returns the number of +# errors +def TestTestModule(module): + errors = 0 + + errors += EXPECT_EQ('test', module.name) + errors += EXPECT_EQ('testspace', module.namespace) + errors += EXPECT_EQ(1, len(module.structs)) + errors += EXPECT_EQ('teststruct', module.structs[0].name) + errors += EXPECT_EQ(2, len(module.structs[0].fields)) + errors += EXPECT_EQ('testfield1', module.structs[0].fields[0].name) + errors += EXPECT_EQ(mojom.INT32, module.structs[0].fields[0].kind) + errors += EXPECT_EQ('testfield2', module.structs[0].fields[1].name) + errors += EXPECT_EQ(mojom.Array, module.structs[0].fields[1].kind.__class__) + errors += EXPECT_EQ(mojom.INT32, module.structs[0].fields[1].kind.kind) + + errors += EXPECT_EQ(1, len(module.interfaces)) + errors += EXPECT_EQ('Server', module.interfaces[0].name) + errors += EXPECT_EQ(1, len(module.interfaces[0].methods)) + errors += EXPECT_EQ('Foo', module.interfaces[0].methods[0].name) + errors += EXPECT_EQ(2, len(module.interfaces[0].methods[0].parameters)) + errors += EXPECT_EQ('foo', module.interfaces[0].methods[0].parameters[0].name) + errors += EXPECT_EQ(mojom.INT32, + module.interfaces[0].methods[0].parameters[0].kind) + errors += EXPECT_EQ('bar', module.interfaces[0].methods[0].parameters[1].name) + errors += EXPECT_EQ( + mojom.Array, + module.interfaces[0].methods[0].parameters[1].kind.__class__) + errors += EXPECT_EQ( + module.structs[0], + module.interfaces[0].methods[0].parameters[1].kind.kind) + return errors + + +def PrintFailure(string): + stack = traceback.extract_stack() + frame = stack[len(stack)-3] + sys.stderr.write("ERROR at %s:%d, %s\n" % (frame[0], frame[1], string)) + print "Traceback:" + for line in traceback.format_list(stack[:len(stack)-2]): + sys.stderr.write(line) + + +def EXPECT_EQ(a, b): + if a != b: + PrintFailure("%s != %s" % (a, b)) + return 1 + return 0 + + +def EXPECT_TRUE(a): + if not a: + PrintFailure('Expecting True') + return 1 + return 0 + + +def RunTest(fn): + sys.stdout.write('Running %s...' % fn.__name__) + try: + errors = fn() + except: + traceback.print_exc(sys.stderr) + errors = 1 + if errors == 0: + sys.stdout.write('OK\n') + elif errors == 1: + sys.stdout.write('1 ERROR\n') + else: + sys.stdout.write('%d ERRORS\n' % errors) + return errors diff --git a/mojo/public/tools/bindings/pylib/mojom/generate/translate.py b/mojo/public/tools/bindings/pylib/mojom/generate/translate.py new file mode 100644 index 0000000000..ffad7447a9 --- /dev/null +++ b/mojo/public/tools/bindings/pylib/mojom/generate/translate.py @@ -0,0 +1,639 @@ +# Copyright 2013 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +"""Convert parse tree to AST. + +This module converts the parse tree to the AST we use for code generation. The +main entry point is OrderedModule, which gets passed the parser +representation of a mojom file. When called it's assumed that all imports have +already been parsed and converted to ASTs before. +""" + +import copy +import re + +import module as mojom +from mojom.parse import ast + +def _DuplicateName(values): + """Returns the 'name' of the first entry in |values| whose 'name' has already + been encountered. If there are no duplicates, returns None.""" + names = set() + for value in values: + if value.name in names: + return value.name + names.add(value.name) + return None + +def _ElemsOfType(elems, elem_type, scope): + """Find all elements of the given type. + + Args: + elems: {Sequence[Any]} Sequence of elems. + elem_type: {Type[C]} Extract all elems of this type. + scope: {str} The name of the surrounding scope (e.g. struct + definition). Used in error messages. + + Returns: + {List[C]} All elems of matching type. + """ + assert isinstance(elem_type, type) + result = [elem for elem in elems if isinstance(elem, elem_type)] + duplicate_name = _DuplicateName(result) + if duplicate_name: + raise Exception('Names in mojom must be unique within a scope. The name ' + '"%s" is used more than once within the scope "%s".' % + (duplicate_name, scope)) + return result + +def _MapKind(kind): + map_to_kind = {'bool': 'b', + 'int8': 'i8', + 'int16': 'i16', + 'int32': 'i32', + 'int64': 'i64', + 'uint8': 'u8', + 'uint16': 'u16', + 'uint32': 'u32', + 'uint64': 'u64', + 'float': 'f', + 'double': 'd', + 'string': 's', + 'handle': 'h', + 'handle<data_pipe_consumer>': 'h:d:c', + 'handle<data_pipe_producer>': 'h:d:p', + 'handle<message_pipe>': 'h:m', + 'handle<shared_buffer>': 'h:s'} + if kind.endswith('?'): + base_kind = _MapKind(kind[0:-1]) + # NOTE: This doesn't rule out enum types. Those will be detected later, when + # cross-reference is established. + reference_kinds = ('m', 's', 'h', 'a', 'r', 'x', 'asso') + if re.split('[^a-z]', base_kind, 1)[0] not in reference_kinds: + raise Exception( + 'A type (spec "%s") cannot be made nullable' % base_kind) + return '?' + base_kind + if kind.endswith('}'): + lbracket = kind.rfind('{') + value = kind[0:lbracket] + return 'm[' + _MapKind(kind[lbracket+1:-1]) + '][' + _MapKind(value) + ']' + if kind.endswith(']'): + lbracket = kind.rfind('[') + typename = kind[0:lbracket] + return 'a' + kind[lbracket+1:-1] + ':' + _MapKind(typename) + if kind.endswith('&'): + return 'r:' + _MapKind(kind[0:-1]) + if kind.startswith('asso<'): + assert kind.endswith('>') + return 'asso:' + _MapKind(kind[5:-1]) + if kind in map_to_kind: + return map_to_kind[kind] + return 'x:' + kind + +def _AttributeListToDict(attribute_list): + if attribute_list is None: + return None + assert isinstance(attribute_list, ast.AttributeList) + # TODO(vtl): Check for duplicate keys here. + return dict([(attribute.key, attribute.value) + for attribute in attribute_list]) + +builtin_values = frozenset([ + "double.INFINITY", + "double.NEGATIVE_INFINITY", + "double.NAN", + "float.INFINITY", + "float.NEGATIVE_INFINITY", + "float.NAN"]) + +def _IsBuiltinValue(value): + return value in builtin_values + +def _LookupKind(kinds, spec, scope): + """Tries to find which Kind a spec refers to, given the scope in which its + referenced. Starts checking from the narrowest scope to most general. For + example, given a struct field like + Foo.Bar x; + Foo.Bar could refer to the type 'Bar' in the 'Foo' namespace, or an inner + type 'Bar' in the struct 'Foo' in the current namespace. + + |scope| is a tuple that looks like (namespace, struct/interface), referring + to the location where the type is referenced.""" + if spec.startswith('x:'): + name = spec[2:] + for i in xrange(len(scope), -1, -1): + test_spec = 'x:' + if i > 0: + test_spec += '.'.join(scope[:i]) + '.' + test_spec += name + kind = kinds.get(test_spec) + if kind: + return kind + + return kinds.get(spec) + +def _LookupValue(values, name, scope, kind): + """Like LookupKind, but for constant values.""" + # If the type is an enum, the value can be specified as a qualified name, in + # which case the form EnumName.ENUM_VALUE must be used. We use the presence + # of a '.' in the requested name to identify this. Otherwise, we prepend the + # enum name. + if isinstance(kind, mojom.Enum) and '.' not in name: + name = '%s.%s' % (kind.spec.split(':', 1)[1], name) + for i in reversed(xrange(len(scope) + 1)): + test_spec = '.'.join(scope[:i]) + if test_spec: + test_spec += '.' + test_spec += name + value = values.get(test_spec) + if value: + return value + + return values.get(name) + +def _FixupExpression(module, value, scope, kind): + """Translates an IDENTIFIER into a built-in value or structured NamedValue + object.""" + if isinstance(value, tuple) and value[0] == 'IDENTIFIER': + # Allow user defined values to shadow builtins. + result = _LookupValue(module.values, value[1], scope, kind) + if result: + if isinstance(result, tuple): + raise Exception('Unable to resolve expression: %r' % value[1]) + return result + if _IsBuiltinValue(value[1]): + return mojom.BuiltinValue(value[1]) + return value + +def _Kind(kinds, spec, scope): + """Convert a type name into a mojom.Kind object. + + As a side-effect this function adds the result to 'kinds'. + + Args: + kinds: {Dict[str, mojom.Kind]} All known kinds up to this point, indexed by + their names. + spec: {str} A name uniquely identifying a type. + scope: {Tuple[str, str]} A tuple that looks like (namespace, + struct/interface), referring to the location where the type is + referenced. + + Returns: + {mojom.Kind} The type corresponding to 'spec'. + """ + kind = _LookupKind(kinds, spec, scope) + if kind: + return kind + + if spec.startswith('?'): + kind = _Kind(kinds, spec[1:], scope).MakeNullableKind() + elif spec.startswith('a:'): + kind = mojom.Array(_Kind(kinds, spec[2:], scope)) + elif spec.startswith('asso:'): + inner_kind = _Kind(kinds, spec[5:], scope) + if isinstance(inner_kind, mojom.InterfaceRequest): + kind = mojom.AssociatedInterfaceRequest(inner_kind) + else: + kind = mojom.AssociatedInterface(inner_kind) + elif spec.startswith('a'): + colon = spec.find(':') + length = int(spec[1:colon]) + kind = mojom.Array(_Kind(kinds, spec[colon+1:], scope), length) + elif spec.startswith('r:'): + kind = mojom.InterfaceRequest(_Kind(kinds, spec[2:], scope)) + elif spec.startswith('m['): + # Isolate the two types from their brackets. + + # It is not allowed to use map as key, so there shouldn't be nested ']'s + # inside the key type spec. + key_end = spec.find(']') + assert key_end != -1 and key_end < len(spec) - 1 + assert spec[key_end+1] == '[' and spec[-1] == ']' + + first_kind = spec[2:key_end] + second_kind = spec[key_end+2:-1] + + kind = mojom.Map(_Kind(kinds, first_kind, scope), + _Kind(kinds, second_kind, scope)) + else: + kind = mojom.Kind(spec) + + kinds[spec] = kind + return kind + +def _KindFromImport(original_kind, imported_from): + """Used with 'import module' - clones the kind imported from the given + module's namespace. Only used with Structs, Unions, Interfaces and Enums.""" + kind = copy.copy(original_kind) + # |shared_definition| is used to store various properties (see + # |AddSharedProperty()| in module.py), including |imported_from|. We don't + # want the copy to share these with the original, so copy it if necessary. + if hasattr(original_kind, 'shared_definition'): + kind.shared_definition = copy.copy(original_kind.shared_definition) + kind.imported_from = imported_from + return kind + +def _Import(module, import_module): + import_item = {} + import_item['module_name'] = import_module.name + import_item['namespace'] = import_module.namespace + import_item['module'] = import_module + + # Copy the struct kinds from our imports into the current module. + importable_kinds = (mojom.Struct, mojom.Union, mojom.Enum, mojom.Interface) + for kind in import_module.kinds.itervalues(): + if (isinstance(kind, importable_kinds) and + kind.imported_from is None): + kind = _KindFromImport(kind, import_item) + module.kinds[kind.spec] = kind + # Ditto for values. + for value in import_module.values.itervalues(): + if value.imported_from is None: + # Values don't have shared definitions (since they're not nullable), so no + # need to do anything special. + value = copy.copy(value) + value.imported_from = import_item + module.values[value.GetSpec()] = value + + return import_item + +def _Struct(module, parsed_struct): + """ + Args: + module: {mojom.Module} Module currently being constructed. + parsed_struct: {ast.Struct} Parsed struct. + + Returns: + {mojom.Struct} AST struct. + """ + struct = mojom.Struct(module=module) + struct.name = parsed_struct.name + struct.native_only = parsed_struct.body is None + struct.spec = 'x:' + module.namespace + '.' + struct.name + module.kinds[struct.spec] = struct + if struct.native_only: + struct.enums = [] + struct.constants = [] + struct.fields_data = [] + else: + struct.enums = map( + lambda enum: _Enum(module, enum, struct), + _ElemsOfType(parsed_struct.body, ast.Enum, parsed_struct.name)) + struct.constants = map( + lambda constant: _Constant(module, constant, struct), + _ElemsOfType(parsed_struct.body, ast.Const, parsed_struct.name)) + # Stash fields parsed_struct here temporarily. + struct.fields_data = _ElemsOfType( + parsed_struct.body, ast.StructField, parsed_struct.name) + struct.attributes = _AttributeListToDict(parsed_struct.attribute_list) + + # Enforce that a [Native] attribute is set to make native-only struct + # declarations more explicit. + if struct.native_only: + if not struct.attributes or not struct.attributes.get('Native', False): + raise Exception("Native-only struct declarations must include a " + + "Native attribute.") + + return struct + +def _Union(module, parsed_union): + """ + Args: + module: {mojom.Module} Module currently being constructed. + parsed_union: {ast.Union} Parsed union. + + Returns: + {mojom.Union} AST union. + """ + union = mojom.Union(module=module) + union.name = parsed_union.name + union.spec = 'x:' + module.namespace + '.' + union.name + module.kinds[union.spec] = union + # Stash fields parsed_union here temporarily. + union.fields_data = _ElemsOfType( + parsed_union.body, ast.UnionField, parsed_union.name) + union.attributes = _AttributeListToDict(parsed_union.attribute_list) + return union + +def _StructField(module, parsed_field, struct): + """ + Args: + module: {mojom.Module} Module currently being constructed. + parsed_field: {ast.StructField} Parsed struct field. + struct: {mojom.Struct} Struct this field belongs to. + + Returns: + {mojom.StructField} AST struct field. + """ + field = mojom.StructField() + field.name = parsed_field.name + field.kind = _Kind( + module.kinds, _MapKind(parsed_field.typename), + (module.namespace, struct.name)) + field.ordinal = parsed_field.ordinal.value if parsed_field.ordinal else None + field.default = _FixupExpression( + module, parsed_field.default_value, (module.namespace, struct.name), + field.kind) + field.attributes = _AttributeListToDict(parsed_field.attribute_list) + return field + +def _UnionField(module, parsed_field, union): + """ + Args: + module: {mojom.Module} Module currently being constructed. + parsed_field: {ast.UnionField} Parsed union field. + union: {mojom.Union} Union this fields belong to. + + Returns: + {mojom.UnionField} AST union. + """ + field = mojom.UnionField() + field.name = parsed_field.name + field.kind = _Kind( + module.kinds, _MapKind(parsed_field.typename), + (module.namespace, union.name)) + field.ordinal = parsed_field.ordinal.value if parsed_field.ordinal else None + field.default = _FixupExpression( + module, None, (module.namespace, union.name), field.kind) + field.attributes = _AttributeListToDict(parsed_field.attribute_list) + return field + +def _Parameter(module, parsed_param, interface): + """ + Args: + module: {mojom.Module} Module currently being constructed. + parsed_param: {ast.Parameter} Parsed parameter. + union: {mojom.Interface} Interface this parameter belongs to. + + Returns: + {mojom.Parameter} AST parameter. + """ + parameter = mojom.Parameter() + parameter.name = parsed_param.name + parameter.kind = _Kind( + module.kinds, _MapKind(parsed_param.typename), + (module.namespace, interface.name)) + parameter.ordinal = ( + parsed_param.ordinal.value if parsed_param.ordinal else None) + parameter.default = None # TODO(tibell): We never have these. Remove field? + parameter.attributes = _AttributeListToDict(parsed_param.attribute_list) + return parameter + +def _Method(module, parsed_method, interface): + """ + Args: + module: {mojom.Module} Module currently being constructed. + parsed_method: {ast.Method} Parsed method. + interface: {mojom.Interface} Interface this method belongs to. + + Returns: + {mojom.Method} AST method. + """ + method = mojom.Method( + interface, parsed_method.name, + ordinal=parsed_method.ordinal.value if parsed_method.ordinal else None) + method.parameters = map( + lambda parameter: _Parameter(module, parameter, interface), + parsed_method.parameter_list) + if parsed_method.response_parameter_list is not None: + method.response_parameters = map( + lambda parameter: _Parameter(module, parameter, interface), + parsed_method.response_parameter_list) + method.attributes = _AttributeListToDict(parsed_method.attribute_list) + + # Enforce that only methods with response can have a [Sync] attribute. + if method.sync and method.response_parameters is None: + raise Exception("Only methods with response can include a [Sync] " + "attribute. If no response parameters are needed, you " + "could use an empty response parameter list, i.e., " + "\"=> ()\".") + + return method + +def _Interface(module, parsed_iface): + """ + Args: + module: {mojom.Module} Module currently being constructed. + parsed_iface: {ast.Interface} Parsed interface. + + Returns: + {mojom.Interface} AST interface. + """ + interface = mojom.Interface(module=module) + interface.name = parsed_iface.name + interface.spec = 'x:' + module.namespace + '.' + interface.name + module.kinds[interface.spec] = interface + interface.enums = map( + lambda enum: _Enum(module, enum, interface), + _ElemsOfType(parsed_iface.body, ast.Enum, parsed_iface.name)) + interface.constants = map( + lambda constant: _Constant(module, constant, interface), + _ElemsOfType(parsed_iface.body, ast.Const, parsed_iface.name)) + # Stash methods parsed_iface here temporarily. + interface.methods_data = _ElemsOfType( + parsed_iface.body, ast.Method, parsed_iface.name) + interface.attributes = _AttributeListToDict(parsed_iface.attribute_list) + return interface + +def _EnumField(module, enum, parsed_field, parent_kind): + """ + Args: + module: {mojom.Module} Module currently being constructed. + enum: {mojom.Enum} Enum this field belongs to. + parsed_field: {ast.EnumValue} Parsed enum value. + parent_kind: {mojom.Kind} The enclosing type. + + Returns: + {mojom.EnumField} AST enum field. + """ + field = mojom.EnumField() + field.name = parsed_field.name + # TODO(mpcomplete): FixupExpression should be done in the second pass, + # so constants and enums can refer to each other. + # TODO(mpcomplete): But then, what if constants are initialized to an enum? Or + # vice versa? + if parent_kind: + field.value = _FixupExpression( + module, parsed_field.value, (module.namespace, parent_kind.name), enum) + else: + field.value = _FixupExpression( + module, parsed_field.value, (module.namespace, ), enum) + field.attributes = _AttributeListToDict(parsed_field.attribute_list) + value = mojom.EnumValue(module, enum, field) + module.values[value.GetSpec()] = value + return field + +def _ResolveNumericEnumValues(enum_fields): + """ + Given a reference to a list of mojom.EnumField, resolves and assigns their + values to EnumField.numeric_value. + """ + + # map of <name> -> integral value + resolved_enum_values = {} + prev_value = -1 + for field in enum_fields: + # This enum value is +1 the previous enum value (e.g: BEGIN). + if field.value is None: + prev_value += 1 + + # Integral value (e.g: BEGIN = -0x1). + elif type(field.value) is str: + prev_value = int(field.value, 0) + + # Reference to a previous enum value (e.g: INIT = BEGIN). + elif type(field.value) is mojom.EnumValue: + prev_value = resolved_enum_values[field.value.name] + else: + raise Exception("Unresolved enum value.") + + resolved_enum_values[field.name] = prev_value + field.numeric_value = prev_value + +def _Enum(module, parsed_enum, parent_kind): + """ + Args: + module: {mojom.Module} Module currently being constructed. + parsed_enum: {ast.Enum} Parsed enum. + + Returns: + {mojom.Enum} AST enum. + """ + enum = mojom.Enum(module=module) + enum.name = parsed_enum.name + enum.native_only = parsed_enum.enum_value_list is None + name = enum.name + if parent_kind: + name = parent_kind.name + '.' + name + enum.spec = 'x:%s.%s' % (module.namespace, name) + enum.parent_kind = parent_kind + enum.attributes = _AttributeListToDict(parsed_enum.attribute_list) + if enum.native_only: + enum.fields = [] + else: + enum.fields = map( + lambda field: _EnumField(module, enum, field, parent_kind), + parsed_enum.enum_value_list) + _ResolveNumericEnumValues(enum.fields) + + module.kinds[enum.spec] = enum + + # Enforce that a [Native] attribute is set to make native-only enum + # declarations more explicit. + if enum.native_only: + if not enum.attributes or not enum.attributes.get('Native', False): + raise Exception("Native-only enum declarations must include a " + + "Native attribute.") + + return enum + +def _Constant(module, parsed_const, parent_kind): + """ + Args: + module: {mojom.Module} Module currently being constructed. + parsed_const: {ast.Const} Parsed constant. + + Returns: + {mojom.Constant} AST constant. + """ + constant = mojom.Constant() + constant.name = parsed_const.name + if parent_kind: + scope = (module.namespace, parent_kind.name) + else: + scope = (module.namespace, ) + # TODO(mpcomplete): maybe we should only support POD kinds. + constant.kind = _Kind(module.kinds, _MapKind(parsed_const.typename), scope) + constant.parent_kind = parent_kind + constant.value = _FixupExpression(module, parsed_const.value, scope, None) + + value = mojom.ConstantValue(module, parent_kind, constant) + module.values[value.GetSpec()] = value + return constant + +def _Module(tree, name, imports): + """ + Args: + tree: {ast.Mojom} The parse tree. + name: {str} The mojom filename, excluding the path. + imports: {Dict[str, mojom.Module]} Mapping from filenames, as they appear in + the import list, to already processed modules. Used to process imports. + + Returns: + {mojom.Module} An AST for the mojom. + """ + module = mojom.Module() + module.kinds = {} + for kind in mojom.PRIMITIVES: + module.kinds[kind.spec] = kind + + module.values = {} + + module.name = name + module.namespace = tree.module.name[1] if tree.module else '' + # Imports must come first, because they add to module.kinds which is used + # by by the others. + module.imports = [ + _Import(module, imports[imp.import_filename]) + for imp in tree.import_list] + if tree.module and tree.module.attribute_list: + assert isinstance(tree.module.attribute_list, ast.AttributeList) + # TODO(vtl): Check for duplicate keys here. + module.attributes = dict((attribute.key, attribute.value) + for attribute in tree.module.attribute_list) + + # First pass collects kinds. + module.enums = map( + lambda enum: _Enum(module, enum, None), + _ElemsOfType(tree.definition_list, ast.Enum, name)) + module.structs = map( + lambda struct: _Struct(module, struct), + _ElemsOfType(tree.definition_list, ast.Struct, name)) + module.unions = map( + lambda union: _Union(module, union), + _ElemsOfType(tree.definition_list, ast.Union, name)) + module.interfaces = map( + lambda interface: _Interface(module, interface), + _ElemsOfType(tree.definition_list, ast.Interface, name)) + module.constants = map( + lambda constant: _Constant(module, constant, None), + _ElemsOfType(tree.definition_list, ast.Const, name)) + + # Second pass expands fields and methods. This allows fields and parameters + # to refer to kinds defined anywhere in the mojom. + for struct in module.structs: + struct.fields = map(lambda field: + _StructField(module, field, struct), struct.fields_data) + del struct.fields_data + for union in module.unions: + union.fields = map(lambda field: + _UnionField(module, field, union), union.fields_data) + del union.fields_data + for interface in module.interfaces: + interface.methods = map(lambda method: + _Method(module, method, interface), interface.methods_data) + del interface.methods_data + + return module + +def OrderedModule(tree, name, imports): + """Convert parse tree to AST module. + + Args: + tree: {ast.Mojom} The parse tree. + name: {str} The mojom filename, excluding the path. + imports: {Dict[str, mojom.Module]} Mapping from filenames, as they appear in + the import list, to already processed modules. Used to process imports. + + Returns: + {mojom.Module} An AST for the mojom. + """ + module = _Module(tree, name, imports) + for interface in module.interfaces: + next_ordinal = 0 + for method in interface.methods: + if method.ordinal is None: + method.ordinal = next_ordinal + next_ordinal = method.ordinal + 1 + return module diff --git a/mojo/public/tools/bindings/pylib/mojom/parse/__init__.py b/mojo/public/tools/bindings/pylib/mojom/parse/__init__.py new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/mojo/public/tools/bindings/pylib/mojom/parse/__init__.py diff --git a/mojo/public/tools/bindings/pylib/mojom/parse/ast.py b/mojo/public/tools/bindings/pylib/mojom/parse/ast.py new file mode 100644 index 0000000000..2c6b5fcdf8 --- /dev/null +++ b/mojo/public/tools/bindings/pylib/mojom/parse/ast.py @@ -0,0 +1,410 @@ +# Copyright 2014 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +"""Node classes for the AST for a Mojo IDL file.""" + +# Note: For convenience of testing, you probably want to define __eq__() methods +# for all node types; it's okay to be slightly lax (e.g., not compare filename +# and lineno). You may also define __repr__() to help with analyzing test +# failures, especially for more complex types. + + +class NodeBase(object): + """Base class for nodes in the AST.""" + + def __init__(self, filename=None, lineno=None): + self.filename = filename + self.lineno = lineno + + def __eq__(self, other): + return type(self) == type(other) + + # Make != the inverse of ==. (Subclasses shouldn't have to override this.) + def __ne__(self, other): + return not self == other + + +# TODO(vtl): Some of this is complicated enough that it should be tested. +class NodeListBase(NodeBase): + """Represents a list of other nodes, all having the same type. (This is meant + to be subclassed, with subclasses defining _list_item_type to be the class (or + classes, in a tuple) of the members of the list.)""" + + def __init__(self, item_or_items=None, **kwargs): + super(NodeListBase, self).__init__(**kwargs) + self.items = [] + if item_or_items is None: + pass + elif isinstance(item_or_items, list): + for item in item_or_items: + assert isinstance(item, self._list_item_type) + self.Append(item) + else: + assert isinstance(item_or_items, self._list_item_type) + self.Append(item_or_items) + + # Support iteration. For everything else, users should just access |items| + # directly. (We intentionally do NOT supply |__len__()| or |__nonzero__()|, so + # |bool(NodeListBase())| is true.) + def __iter__(self): + return self.items.__iter__() + + def __eq__(self, other): + return super(NodeListBase, self).__eq__(other) and \ + self.items == other.items + + # Implement this so that on failure, we get slightly more sensible output. + def __repr__(self): + return self.__class__.__name__ + "([" + \ + ", ".join([repr(elem) for elem in self.items]) + "])" + + def Insert(self, item): + """Inserts item at the front of the list.""" + + assert isinstance(item, self._list_item_type) + self.items.insert(0, item) + self._UpdateFilenameAndLineno() + + def Append(self, item): + """Appends item to the end of the list.""" + + assert isinstance(item, self._list_item_type) + self.items.append(item) + self._UpdateFilenameAndLineno() + + def _UpdateFilenameAndLineno(self): + if self.items: + self.filename = self.items[0].filename + self.lineno = self.items[0].lineno + + +class Definition(NodeBase): + """Represents a definition of anything that has a global name (e.g., enums, + enum values, consts, structs, struct fields, interfaces). (This does not + include parameter definitions.) This class is meant to be subclassed.""" + + def __init__(self, name, **kwargs): + assert isinstance(name, str) + NodeBase.__init__(self, **kwargs) + self.name = name + + +################################################################################ + + +class Attribute(NodeBase): + """Represents an attribute.""" + + def __init__(self, key, value, **kwargs): + assert isinstance(key, str) + super(Attribute, self).__init__(**kwargs) + self.key = key + self.value = value + + def __eq__(self, other): + return super(Attribute, self).__eq__(other) and \ + self.key == other.key and \ + self.value == other.value + + +class AttributeList(NodeListBase): + """Represents a list attributes.""" + + _list_item_type = Attribute + + +class Const(Definition): + """Represents a const definition.""" + + def __init__(self, name, typename, value, **kwargs): + # The typename is currently passed through as a string. + assert isinstance(typename, str) + # The value is either a literal (currently passed through as a string) or a + # "wrapped identifier". + assert isinstance(value, str) or isinstance(value, tuple) + super(Const, self).__init__(name, **kwargs) + self.typename = typename + self.value = value + + def __eq__(self, other): + return super(Const, self).__eq__(other) and \ + self.typename == other.typename and \ + self.value == other.value + + +class Enum(Definition): + """Represents an enum definition.""" + + def __init__(self, name, attribute_list, enum_value_list, **kwargs): + assert attribute_list is None or isinstance(attribute_list, AttributeList) + assert enum_value_list is None or isinstance(enum_value_list, EnumValueList) + super(Enum, self).__init__(name, **kwargs) + self.attribute_list = attribute_list + self.enum_value_list = enum_value_list + + def __eq__(self, other): + return super(Enum, self).__eq__(other) and \ + self.attribute_list == other.attribute_list and \ + self.enum_value_list == other.enum_value_list + + +class EnumValue(Definition): + """Represents a definition of an enum value.""" + + def __init__(self, name, attribute_list, value, **kwargs): + # The optional value is either an int (which is current a string) or a + # "wrapped identifier". + assert attribute_list is None or isinstance(attribute_list, AttributeList) + assert value is None or isinstance(value, (str, tuple)) + super(EnumValue, self).__init__(name, **kwargs) + self.attribute_list = attribute_list + self.value = value + + def __eq__(self, other): + return super(EnumValue, self).__eq__(other) and \ + self.attribute_list == other.attribute_list and \ + self.value == other.value + + +class EnumValueList(NodeListBase): + """Represents a list of enum value definitions (i.e., the "body" of an enum + definition).""" + + _list_item_type = EnumValue + + +class Import(NodeBase): + """Represents an import statement.""" + + def __init__(self, import_filename, **kwargs): + assert isinstance(import_filename, str) + super(Import, self).__init__(**kwargs) + self.import_filename = import_filename + + def __eq__(self, other): + return super(Import, self).__eq__(other) and \ + self.import_filename == other.import_filename + + +class ImportList(NodeListBase): + """Represents a list (i.e., sequence) of import statements.""" + + _list_item_type = Import + + +class Interface(Definition): + """Represents an interface definition.""" + + def __init__(self, name, attribute_list, body, **kwargs): + assert attribute_list is None or isinstance(attribute_list, AttributeList) + assert isinstance(body, InterfaceBody) + super(Interface, self).__init__(name, **kwargs) + self.attribute_list = attribute_list + self.body = body + + def __eq__(self, other): + return super(Interface, self).__eq__(other) and \ + self.attribute_list == other.attribute_list and \ + self.body == other.body + + +class Method(Definition): + """Represents a method definition.""" + + def __init__(self, name, attribute_list, ordinal, parameter_list, + response_parameter_list, **kwargs): + assert attribute_list is None or isinstance(attribute_list, AttributeList) + assert ordinal is None or isinstance(ordinal, Ordinal) + assert isinstance(parameter_list, ParameterList) + assert response_parameter_list is None or \ + isinstance(response_parameter_list, ParameterList) + super(Method, self).__init__(name, **kwargs) + self.attribute_list = attribute_list + self.ordinal = ordinal + self.parameter_list = parameter_list + self.response_parameter_list = response_parameter_list + + def __eq__(self, other): + return super(Method, self).__eq__(other) and \ + self.attribute_list == other.attribute_list and \ + self.ordinal == other.ordinal and \ + self.parameter_list == other.parameter_list and \ + self.response_parameter_list == other.response_parameter_list + + +# This needs to be declared after |Method|. +class InterfaceBody(NodeListBase): + """Represents the body of (i.e., list of definitions inside) an interface.""" + + _list_item_type = (Const, Enum, Method) + + +class Module(NodeBase): + """Represents a module statement.""" + + def __init__(self, name, attribute_list, **kwargs): + # |name| is either none or a "wrapped identifier". + assert name is None or isinstance(name, tuple) + assert attribute_list is None or isinstance(attribute_list, AttributeList) + super(Module, self).__init__(**kwargs) + self.name = name + self.attribute_list = attribute_list + + def __eq__(self, other): + return super(Module, self).__eq__(other) and \ + self.name == other.name and \ + self.attribute_list == other.attribute_list + + +class Mojom(NodeBase): + """Represents an entire .mojom file. (This is the root node.)""" + + def __init__(self, module, import_list, definition_list, **kwargs): + assert module is None or isinstance(module, Module) + assert isinstance(import_list, ImportList) + assert isinstance(definition_list, list) + super(Mojom, self).__init__(**kwargs) + self.module = module + self.import_list = import_list + self.definition_list = definition_list + + def __eq__(self, other): + return super(Mojom, self).__eq__(other) and \ + self.module == other.module and \ + self.import_list == other.import_list and \ + self.definition_list == other.definition_list + + def __repr__(self): + return "%s(%r, %r, %r)" % (self.__class__.__name__, self.module, + self.import_list, self.definition_list) + + +class Ordinal(NodeBase): + """Represents an ordinal value labeling, e.g., a struct field.""" + + def __init__(self, value, **kwargs): + assert isinstance(value, int) + super(Ordinal, self).__init__(**kwargs) + self.value = value + + def __eq__(self, other): + return super(Ordinal, self).__eq__(other) and \ + self.value == other.value + + +class Parameter(NodeBase): + """Represents a method request or response parameter.""" + + def __init__(self, name, attribute_list, ordinal, typename, **kwargs): + assert isinstance(name, str) + assert attribute_list is None or isinstance(attribute_list, AttributeList) + assert ordinal is None or isinstance(ordinal, Ordinal) + assert isinstance(typename, str) + super(Parameter, self).__init__(**kwargs) + self.name = name + self.attribute_list = attribute_list + self.ordinal = ordinal + self.typename = typename + + def __eq__(self, other): + return super(Parameter, self).__eq__(other) and \ + self.name == other.name and \ + self.attribute_list == other.attribute_list and \ + self.ordinal == other.ordinal and \ + self.typename == other.typename + + +class ParameterList(NodeListBase): + """Represents a list of (method request or response) parameters.""" + + _list_item_type = Parameter + + +class Struct(Definition): + """Represents a struct definition.""" + + def __init__(self, name, attribute_list, body, **kwargs): + assert attribute_list is None or isinstance(attribute_list, AttributeList) + assert isinstance(body, StructBody) or body is None + super(Struct, self).__init__(name, **kwargs) + self.attribute_list = attribute_list + self.body = body + + def __eq__(self, other): + return super(Struct, self).__eq__(other) and \ + self.attribute_list == other.attribute_list and \ + self.body == other.body + + +class StructField(Definition): + """Represents a struct field definition.""" + + def __init__(self, name, attribute_list, ordinal, typename, default_value, + **kwargs): + assert isinstance(name, str) + assert attribute_list is None or isinstance(attribute_list, AttributeList) + assert ordinal is None or isinstance(ordinal, Ordinal) + assert isinstance(typename, str) + # The optional default value is currently either a value as a string or a + # "wrapped identifier". + assert default_value is None or isinstance(default_value, (str, tuple)) + super(StructField, self).__init__(name, **kwargs) + self.attribute_list = attribute_list + self.ordinal = ordinal + self.typename = typename + self.default_value = default_value + + def __eq__(self, other): + return super(StructField, self).__eq__(other) and \ + self.attribute_list == other.attribute_list and \ + self.ordinal == other.ordinal and \ + self.typename == other.typename and \ + self.default_value == other.default_value + + +# This needs to be declared after |StructField|. +class StructBody(NodeListBase): + """Represents the body of (i.e., list of definitions inside) a struct.""" + + _list_item_type = (Const, Enum, StructField) + + +class Union(Definition): + """Represents a union definition.""" + + def __init__(self, name, attribute_list, body, **kwargs): + assert attribute_list is None or isinstance(attribute_list, AttributeList) + assert isinstance(body, UnionBody) + super(Union, self).__init__(name, **kwargs) + self.attribute_list = attribute_list + self.body = body + + def __eq__(self, other): + return super(Union, self).__eq__(other) and \ + self.attribute_list == other.attribute_list and \ + self.body == other.body + + +class UnionField(Definition): + + def __init__(self, name, attribute_list, ordinal, typename, **kwargs): + assert isinstance(name, str) + assert attribute_list is None or isinstance(attribute_list, AttributeList) + assert ordinal is None or isinstance(ordinal, Ordinal) + assert isinstance(typename, str) + super(UnionField, self).__init__(name, **kwargs) + self.attribute_list = attribute_list + self.ordinal = ordinal + self.typename = typename + + def __eq__(self, other): + return super(UnionField, self).__eq__(other) and \ + self.attribute_list == other.attribute_list and \ + self.ordinal == other.ordinal and \ + self.typename == other.typename + + +class UnionBody(NodeListBase): + + _list_item_type = UnionField diff --git a/mojo/public/tools/bindings/pylib/mojom/parse/lexer.py b/mojo/public/tools/bindings/pylib/mojom/parse/lexer.py new file mode 100644 index 0000000000..06354b1d85 --- /dev/null +++ b/mojo/public/tools/bindings/pylib/mojom/parse/lexer.py @@ -0,0 +1,254 @@ +# Copyright 2014 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +import imp +import os.path +import sys + +def _GetDirAbove(dirname): + """Returns the directory "above" this file containing |dirname| (which must + also be "above" this file).""" + path = os.path.abspath(__file__) + while True: + path, tail = os.path.split(path) + assert tail + if tail == dirname: + return path + +try: + imp.find_module("ply") +except ImportError: + sys.path.append(os.path.join(_GetDirAbove("mojo"), "third_party")) +from ply.lex import TOKEN + +from ..error import Error + + +class LexError(Error): + """Class for errors from the lexer.""" + + def __init__(self, filename, message, lineno): + Error.__init__(self, filename, message, lineno=lineno) + + +# We have methods which look like they could be functions: +# pylint: disable=R0201 +class Lexer(object): + + def __init__(self, filename): + self.filename = filename + + ######################-- PRIVATE --###################### + + ## + ## Internal auxiliary methods + ## + def _error(self, msg, token): + raise LexError(self.filename, msg, token.lineno) + + ## + ## Reserved keywords + ## + keywords = ( + 'HANDLE', + + 'IMPORT', + 'MODULE', + 'STRUCT', + 'UNION', + 'INTERFACE', + 'ENUM', + 'CONST', + 'TRUE', + 'FALSE', + 'DEFAULT', + 'ARRAY', + 'MAP', + 'ASSOCIATED' + ) + + keyword_map = {} + for keyword in keywords: + keyword_map[keyword.lower()] = keyword + + ## + ## All the tokens recognized by the lexer + ## + tokens = keywords + ( + # Identifiers + 'NAME', + + # Constants + 'ORDINAL', + 'INT_CONST_DEC', 'INT_CONST_HEX', + 'FLOAT_CONST', + + # String literals + 'STRING_LITERAL', + + # Operators + 'MINUS', + 'PLUS', + 'AMP', + 'QSTN', + + # Assignment + 'EQUALS', + + # Request / response + 'RESPONSE', + + # Delimiters + 'LPAREN', 'RPAREN', # ( ) + 'LBRACKET', 'RBRACKET', # [ ] + 'LBRACE', 'RBRACE', # { } + 'LANGLE', 'RANGLE', # < > + 'SEMI', # ; + 'COMMA', 'DOT' # , . + ) + + ## + ## Regexes for use in tokens + ## + + # valid C identifiers (K&R2: A.2.3) + identifier = r'[a-zA-Z_][0-9a-zA-Z_]*' + + hex_prefix = '0[xX]' + hex_digits = '[0-9a-fA-F]+' + + # integer constants (K&R2: A.2.5.1) + decimal_constant = '0|([1-9][0-9]*)' + hex_constant = hex_prefix+hex_digits + # Don't allow octal constants (even invalid octal). + octal_constant_disallowed = '0[0-9]+' + + # character constants (K&R2: A.2.5.2) + # Note: a-zA-Z and '.-~^_!=&;,' are allowed as escape chars to support #line + # directives with Windows paths as filenames (..\..\dir\file) + # For the same reason, decimal_escape allows all digit sequences. We want to + # parse all correct code, even if it means to sometimes parse incorrect + # code. + # + simple_escape = r"""([a-zA-Z._~!=&\^\-\\?'"])""" + decimal_escape = r"""(\d+)""" + hex_escape = r"""(x[0-9a-fA-F]+)""" + bad_escape = r"""([\\][^a-zA-Z._~^!=&\^\-\\?'"x0-7])""" + + escape_sequence = \ + r"""(\\("""+simple_escape+'|'+decimal_escape+'|'+hex_escape+'))' + + # string literals (K&R2: A.2.6) + string_char = r"""([^"\\\n]|"""+escape_sequence+')' + string_literal = '"'+string_char+'*"' + bad_string_literal = '"'+string_char+'*'+bad_escape+string_char+'*"' + + # floating constants (K&R2: A.2.5.3) + exponent_part = r"""([eE][-+]?[0-9]+)""" + fractional_constant = r"""([0-9]*\.[0-9]+)|([0-9]+\.)""" + floating_constant = \ + '(((('+fractional_constant+')'+ \ + exponent_part+'?)|([0-9]+'+exponent_part+')))' + + # Ordinals + ordinal = r'@[0-9]+' + missing_ordinal_value = r'@' + # Don't allow ordinal values in octal (even invalid octal, like 09) or + # hexadecimal. + octal_or_hex_ordinal_disallowed = r'@((0[0-9]+)|('+hex_prefix+hex_digits+'))' + + ## + ## Rules for the normal state + ## + t_ignore = ' \t\r' + + # Newlines + def t_NEWLINE(self, t): + r'\n+' + t.lexer.lineno += len(t.value) + + # Operators + t_MINUS = r'-' + t_PLUS = r'\+' + t_AMP = r'&' + t_QSTN = r'\?' + + # = + t_EQUALS = r'=' + + # => + t_RESPONSE = r'=>' + + # Delimiters + t_LPAREN = r'\(' + t_RPAREN = r'\)' + t_LBRACKET = r'\[' + t_RBRACKET = r'\]' + t_LBRACE = r'\{' + t_RBRACE = r'\}' + t_LANGLE = r'<' + t_RANGLE = r'>' + t_COMMA = r',' + t_DOT = r'\.' + t_SEMI = r';' + + t_STRING_LITERAL = string_literal + + # The following floating and integer constants are defined as + # functions to impose a strict order (otherwise, decimal + # is placed before the others because its regex is longer, + # and this is bad) + # + @TOKEN(floating_constant) + def t_FLOAT_CONST(self, t): + return t + + @TOKEN(hex_constant) + def t_INT_CONST_HEX(self, t): + return t + + @TOKEN(octal_constant_disallowed) + def t_OCTAL_CONSTANT_DISALLOWED(self, t): + msg = "Octal values not allowed" + self._error(msg, t) + + @TOKEN(decimal_constant) + def t_INT_CONST_DEC(self, t): + return t + + # unmatched string literals are caught by the preprocessor + + @TOKEN(bad_string_literal) + def t_BAD_STRING_LITERAL(self, t): + msg = "String contains invalid escape code" + self._error(msg, t) + + # Handle ordinal-related tokens in the right order: + @TOKEN(octal_or_hex_ordinal_disallowed) + def t_OCTAL_OR_HEX_ORDINAL_DISALLOWED(self, t): + msg = "Octal and hexadecimal ordinal values not allowed" + self._error(msg, t) + + @TOKEN(ordinal) + def t_ORDINAL(self, t): + return t + + @TOKEN(missing_ordinal_value) + def t_BAD_ORDINAL(self, t): + msg = "Missing ordinal value" + self._error(msg, t) + + @TOKEN(identifier) + def t_NAME(self, t): + t.type = self.keyword_map.get(t.value, "NAME") + return t + + # Ignore C and C++ style comments + def t_COMMENT(self, t): + r'(/\*(.|\n)*?\*/)|(//.*(\n[ \t]*//.*)*)' + t.lexer.lineno += t.value.count("\n") + + def t_error(self, t): + msg = "Illegal character %s" % repr(t.value[0]) + self._error(msg, t) diff --git a/mojo/public/tools/bindings/pylib/mojom/parse/parser.py b/mojo/public/tools/bindings/pylib/mojom/parse/parser.py new file mode 100644 index 0000000000..868fb45f33 --- /dev/null +++ b/mojo/public/tools/bindings/pylib/mojom/parse/parser.py @@ -0,0 +1,461 @@ +# Copyright 2014 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +"""Generates a syntax tree from a Mojo IDL file.""" + +import imp +import os.path +import sys + +def _GetDirAbove(dirname): + """Returns the directory "above" this file containing |dirname| (which must + also be "above" this file).""" + path = os.path.abspath(__file__) + while True: + path, tail = os.path.split(path) + assert tail + if tail == dirname: + return path + +try: + imp.find_module("ply") +except ImportError: + sys.path.append(os.path.join(_GetDirAbove("mojo"), "third_party")) +from ply import lex +from ply import yacc + +from ..error import Error +from . import ast +from .lexer import Lexer + + +_MAX_ORDINAL_VALUE = 0xffffffff +_MAX_ARRAY_SIZE = 0xffffffff + + +class ParseError(Error): + """Class for errors from the parser.""" + + def __init__(self, filename, message, lineno=None, snippet=None): + Error.__init__(self, filename, message, lineno=lineno, + addenda=([snippet] if snippet else None)) + + +# We have methods which look like they could be functions: +# pylint: disable=R0201 +class Parser(object): + + def __init__(self, lexer, source, filename): + self.tokens = lexer.tokens + self.source = source + self.filename = filename + + # Names of functions + # + # In general, we name functions after the left-hand-side of the rule(s) that + # they handle. E.g., |p_foo_bar| for a rule |foo_bar : ...|. + # + # There may be multiple functions handling rules for the same left-hand-side; + # then we name the functions |p_foo_bar_N| (for left-hand-side |foo_bar|), + # where N is a number (numbered starting from 1). Note that using multiple + # functions is actually more efficient than having single functions handle + # multiple rules (and, e.g., distinguishing them by examining |len(p)|). + # + # It's also possible to have a function handling multiple rules with different + # left-hand-sides. We do not do this. + # + # See http://www.dabeaz.com/ply/ply.html#ply_nn25 for more details. + + # TODO(vtl): Get rid of the braces in the module "statement". (Consider + # renaming "module" -> "package".) Then we'll be able to have a single rule + # for root (by making module "optional"). + def p_root_1(self, p): + """root : """ + p[0] = ast.Mojom(None, ast.ImportList(), []) + + def p_root_2(self, p): + """root : root module""" + if p[1].module is not None: + raise ParseError(self.filename, + "Multiple \"module\" statements not allowed:", + p[2].lineno, snippet=self._GetSnippet(p[2].lineno)) + if p[1].import_list.items or p[1].definition_list: + raise ParseError( + self.filename, + "\"module\" statements must precede imports and definitions:", + p[2].lineno, snippet=self._GetSnippet(p[2].lineno)) + p[0] = p[1] + p[0].module = p[2] + + def p_root_3(self, p): + """root : root import""" + if p[1].definition_list: + raise ParseError(self.filename, + "\"import\" statements must precede definitions:", + p[2].lineno, snippet=self._GetSnippet(p[2].lineno)) + p[0] = p[1] + p[0].import_list.Append(p[2]) + + def p_root_4(self, p): + """root : root definition""" + p[0] = p[1] + p[0].definition_list.append(p[2]) + + def p_import(self, p): + """import : IMPORT STRING_LITERAL SEMI""" + # 'eval' the literal to strip the quotes. + # TODO(vtl): This eval is dubious. We should unquote/unescape ourselves. + p[0] = ast.Import(eval(p[2]), filename=self.filename, lineno=p.lineno(2)) + + def p_module(self, p): + """module : attribute_section MODULE identifier_wrapped SEMI""" + p[0] = ast.Module(p[3], p[1], filename=self.filename, lineno=p.lineno(2)) + + def p_definition(self, p): + """definition : struct + | union + | interface + | enum + | const""" + p[0] = p[1] + + def p_attribute_section_1(self, p): + """attribute_section : """ + p[0] = None + + def p_attribute_section_2(self, p): + """attribute_section : LBRACKET attribute_list RBRACKET""" + p[0] = p[2] + + def p_attribute_list_1(self, p): + """attribute_list : """ + p[0] = ast.AttributeList() + + def p_attribute_list_2(self, p): + """attribute_list : nonempty_attribute_list""" + p[0] = p[1] + + def p_nonempty_attribute_list_1(self, p): + """nonempty_attribute_list : attribute""" + p[0] = ast.AttributeList(p[1]) + + def p_nonempty_attribute_list_2(self, p): + """nonempty_attribute_list : nonempty_attribute_list COMMA attribute""" + p[0] = p[1] + p[0].Append(p[3]) + + def p_attribute_1(self, p): + """attribute : NAME EQUALS evaled_literal + | NAME EQUALS NAME""" + p[0] = ast.Attribute(p[1], p[3], filename=self.filename, lineno=p.lineno(1)) + + def p_attribute_2(self, p): + """attribute : NAME""" + p[0] = ast.Attribute(p[1], True, filename=self.filename, lineno=p.lineno(1)) + + def p_evaled_literal(self, p): + """evaled_literal : literal""" + # 'eval' the literal to strip the quotes. Handle keywords "true" and "false" + # specially since they cannot directly be evaluated to python boolean + # values. + if p[1] == "true": + p[0] = True + elif p[1] == "false": + p[0] = False + else: + p[0] = eval(p[1]) + + def p_struct_1(self, p): + """struct : attribute_section STRUCT NAME LBRACE struct_body RBRACE SEMI""" + p[0] = ast.Struct(p[3], p[1], p[5]) + + def p_struct_2(self, p): + """struct : attribute_section STRUCT NAME SEMI""" + p[0] = ast.Struct(p[3], p[1], None) + + def p_struct_body_1(self, p): + """struct_body : """ + p[0] = ast.StructBody() + + def p_struct_body_2(self, p): + """struct_body : struct_body const + | struct_body enum + | struct_body struct_field""" + p[0] = p[1] + p[0].Append(p[2]) + + def p_struct_field(self, p): + """struct_field : attribute_section typename NAME ordinal default SEMI""" + p[0] = ast.StructField(p[3], p[1], p[4], p[2], p[5]) + + def p_union(self, p): + """union : attribute_section UNION NAME LBRACE union_body RBRACE SEMI""" + p[0] = ast.Union(p[3], p[1], p[5]) + + def p_union_body_1(self, p): + """union_body : """ + p[0] = ast.UnionBody() + + def p_union_body_2(self, p): + """union_body : union_body union_field""" + p[0] = p[1] + p[1].Append(p[2]) + + def p_union_field(self, p): + """union_field : attribute_section typename NAME ordinal SEMI""" + p[0] = ast.UnionField(p[3], p[1], p[4], p[2]) + + def p_default_1(self, p): + """default : """ + p[0] = None + + def p_default_2(self, p): + """default : EQUALS constant""" + p[0] = p[2] + + def p_interface(self, p): + """interface : attribute_section INTERFACE NAME LBRACE interface_body \ + RBRACE SEMI""" + p[0] = ast.Interface(p[3], p[1], p[5]) + + def p_interface_body_1(self, p): + """interface_body : """ + p[0] = ast.InterfaceBody() + + def p_interface_body_2(self, p): + """interface_body : interface_body const + | interface_body enum + | interface_body method""" + p[0] = p[1] + p[0].Append(p[2]) + + def p_response_1(self, p): + """response : """ + p[0] = None + + def p_response_2(self, p): + """response : RESPONSE LPAREN parameter_list RPAREN""" + p[0] = p[3] + + def p_method(self, p): + """method : attribute_section NAME ordinal LPAREN parameter_list RPAREN \ + response SEMI""" + p[0] = ast.Method(p[2], p[1], p[3], p[5], p[7]) + + def p_parameter_list_1(self, p): + """parameter_list : """ + p[0] = ast.ParameterList() + + def p_parameter_list_2(self, p): + """parameter_list : nonempty_parameter_list""" + p[0] = p[1] + + def p_nonempty_parameter_list_1(self, p): + """nonempty_parameter_list : parameter""" + p[0] = ast.ParameterList(p[1]) + + def p_nonempty_parameter_list_2(self, p): + """nonempty_parameter_list : nonempty_parameter_list COMMA parameter""" + p[0] = p[1] + p[0].Append(p[3]) + + def p_parameter(self, p): + """parameter : attribute_section typename NAME ordinal""" + p[0] = ast.Parameter(p[3], p[1], p[4], p[2], + filename=self.filename, lineno=p.lineno(3)) + + def p_typename(self, p): + """typename : nonnullable_typename QSTN + | nonnullable_typename""" + if len(p) == 2: + p[0] = p[1] + else: + p[0] = p[1] + "?" + + def p_nonnullable_typename(self, p): + """nonnullable_typename : basictypename + | array + | fixed_array + | associative_array + | interfacerequest""" + p[0] = p[1] + + def p_basictypename(self, p): + """basictypename : identifier + | ASSOCIATED identifier + | handletype""" + if len(p) == 2: + p[0] = p[1] + else: + p[0] = "asso<" + p[2] + ">" + + def p_handletype(self, p): + """handletype : HANDLE + | HANDLE LANGLE NAME RANGLE""" + if len(p) == 2: + p[0] = p[1] + else: + if p[3] not in ('data_pipe_consumer', + 'data_pipe_producer', + 'message_pipe', + 'shared_buffer'): + # Note: We don't enable tracking of line numbers for everything, so we + # can't use |p.lineno(3)|. + raise ParseError(self.filename, "Invalid handle type %r:" % p[3], + lineno=p.lineno(1), + snippet=self._GetSnippet(p.lineno(1))) + p[0] = "handle<" + p[3] + ">" + + def p_array(self, p): + """array : ARRAY LANGLE typename RANGLE""" + p[0] = p[3] + "[]" + + def p_fixed_array(self, p): + """fixed_array : ARRAY LANGLE typename COMMA INT_CONST_DEC RANGLE""" + value = int(p[5]) + if value == 0 or value > _MAX_ARRAY_SIZE: + raise ParseError(self.filename, "Fixed array size %d invalid:" % value, + lineno=p.lineno(5), + snippet=self._GetSnippet(p.lineno(5))) + p[0] = p[3] + "[" + p[5] + "]" + + def p_associative_array(self, p): + """associative_array : MAP LANGLE identifier COMMA typename RANGLE""" + p[0] = p[5] + "{" + p[3] + "}" + + def p_interfacerequest(self, p): + """interfacerequest : identifier AMP + | ASSOCIATED identifier AMP""" + if len(p) == 3: + p[0] = p[1] + "&" + else: + p[0] = "asso<" + p[2] + "&>" + + def p_ordinal_1(self, p): + """ordinal : """ + p[0] = None + + def p_ordinal_2(self, p): + """ordinal : ORDINAL""" + value = int(p[1][1:]) + if value > _MAX_ORDINAL_VALUE: + raise ParseError(self.filename, "Ordinal value %d too large:" % value, + lineno=p.lineno(1), + snippet=self._GetSnippet(p.lineno(1))) + p[0] = ast.Ordinal(value, filename=self.filename, lineno=p.lineno(1)) + + def p_enum_1(self, p): + """enum : attribute_section ENUM NAME LBRACE enum_value_list \ + RBRACE SEMI + | attribute_section ENUM NAME LBRACE nonempty_enum_value_list \ + COMMA RBRACE SEMI""" + p[0] = ast.Enum(p[3], p[1], p[5], filename=self.filename, + lineno=p.lineno(2)) + + def p_enum_2(self, p): + """enum : attribute_section ENUM NAME SEMI""" + p[0] = ast.Enum(p[3], p[1], None, filename=self.filename, + lineno=p.lineno(2)) + + def p_enum_value_list_1(self, p): + """enum_value_list : """ + p[0] = ast.EnumValueList() + + def p_enum_value_list_2(self, p): + """enum_value_list : nonempty_enum_value_list""" + p[0] = p[1] + + def p_nonempty_enum_value_list_1(self, p): + """nonempty_enum_value_list : enum_value""" + p[0] = ast.EnumValueList(p[1]) + + def p_nonempty_enum_value_list_2(self, p): + """nonempty_enum_value_list : nonempty_enum_value_list COMMA enum_value""" + p[0] = p[1] + p[0].Append(p[3]) + + def p_enum_value(self, p): + """enum_value : attribute_section NAME + | attribute_section NAME EQUALS int + | attribute_section NAME EQUALS identifier_wrapped""" + p[0] = ast.EnumValue(p[2], p[1], p[4] if len(p) == 5 else None, + filename=self.filename, lineno=p.lineno(2)) + + def p_const(self, p): + """const : CONST typename NAME EQUALS constant SEMI""" + p[0] = ast.Const(p[3], p[2], p[5]) + + def p_constant(self, p): + """constant : literal + | identifier_wrapped""" + p[0] = p[1] + + def p_identifier_wrapped(self, p): + """identifier_wrapped : identifier""" + p[0] = ('IDENTIFIER', p[1]) + + # TODO(vtl): Make this produce a "wrapped" identifier (probably as an + # |ast.Identifier|, to be added) and get rid of identifier_wrapped. + def p_identifier(self, p): + """identifier : NAME + | NAME DOT identifier""" + p[0] = ''.join(p[1:]) + + def p_literal(self, p): + """literal : int + | float + | TRUE + | FALSE + | DEFAULT + | STRING_LITERAL""" + p[0] = p[1] + + def p_int(self, p): + """int : int_const + | PLUS int_const + | MINUS int_const""" + p[0] = ''.join(p[1:]) + + def p_int_const(self, p): + """int_const : INT_CONST_DEC + | INT_CONST_HEX""" + p[0] = p[1] + + def p_float(self, p): + """float : FLOAT_CONST + | PLUS FLOAT_CONST + | MINUS FLOAT_CONST""" + p[0] = ''.join(p[1:]) + + def p_error(self, e): + if e is None: + # Unexpected EOF. + # TODO(vtl): Can we figure out what's missing? + raise ParseError(self.filename, "Unexpected end of file") + + raise ParseError(self.filename, "Unexpected %r:" % e.value, lineno=e.lineno, + snippet=self._GetSnippet(e.lineno)) + + def _GetSnippet(self, lineno): + return self.source.split('\n')[lineno - 1] + + +def Parse(source, filename): + """Parse source file to AST. + + Args: + source: The source text as a str. + filename: The filename that |source| originates from. + + Returns: + The AST as a mojom.parse.ast.Mojom object. + """ + lexer = Lexer(filename) + parser = Parser(lexer, source, filename) + + lex.lex(object=lexer) + yacc.yacc(module=parser, debug=0, write_tables=0) + + tree = yacc.parse(source) + return tree |