| #!/usr/bin/python |
| # |
| # Copyright (C) 2009 Chia-I Wu <olv@0xlab.org> |
| # |
| # Permission is hereby granted, free of charge, to any person obtaining a |
| # copy of this software and associated documentation files (the "Software"), |
| # to deal in the Software without restriction, including without limitation |
| # on the rights to use, copy, modify, merge, publish, distribute, sub |
| # license, and/or sell copies of the Software, and to permit persons to whom |
| # the Software is furnished to do so, subject to the following conditions: |
| # |
| # The above copyright notice and this permission notice (including the next |
| # paragraph) shall be included in all copies or substantial portions of the |
| # Software. |
| # |
| # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
| # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
| # FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL |
| # IBM AND/OR ITS SUPPLIERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
| # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING |
| # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS |
| # IN THE SOFTWARE. |
| """ |
| A parser for APIspec. |
| """ |
| |
| class SpecError(Exception): |
| """Error in the spec file.""" |
| |
| |
| class Spec(object): |
| """A Spec is an abstraction of the API spec.""" |
| |
| def __init__(self, doc): |
| self.doc = doc |
| |
| self.spec_node = doc.getRootElement() |
| self.tmpl_nodes = {} |
| self.api_nodes = {} |
| self.impl_node = None |
| |
| # parse <apispec> |
| node = self.spec_node.children |
| while node: |
| if node.type == "element": |
| if node.name == "template": |
| self.tmpl_nodes[node.prop("name")] = node |
| elif node.name == "api": |
| self.api_nodes[node.prop("name")] = node |
| else: |
| raise SpecError("unexpected node %s in apispec" % |
| node.name) |
| node = node.next |
| |
| # find an implementation |
| for name, node in self.api_nodes.iteritems(): |
| if node.prop("implementation") == "true": |
| self.impl_node = node |
| break |
| if not self.impl_node: |
| raise SpecError("unable to find an implementation") |
| |
| def get_impl(self): |
| """Return the implementation.""" |
| return API(self, self.impl_node) |
| |
| def get_api(self, name): |
| """Return an API.""" |
| return API(self, self.api_nodes[name]) |
| |
| |
| class API(object): |
| """An API consists of categories and functions.""" |
| |
| def __init__(self, spec, api_node): |
| self.name = api_node.prop("name") |
| self.is_impl = (api_node.prop("implementation") == "true") |
| |
| self.categories = [] |
| self.functions = [] |
| |
| # parse <api> |
| func_nodes = [] |
| node = api_node.children |
| while node: |
| if node.type == "element": |
| if node.name == "category": |
| cat = node.prop("name") |
| self.categories.append(cat) |
| elif node.name == "function": |
| func_nodes.append(node) |
| else: |
| raise SpecError("unexpected node %s in api" % node.name) |
| node = node.next |
| |
| # realize functions |
| for func_node in func_nodes: |
| tmpl_node = spec.tmpl_nodes[func_node.prop("template")] |
| try: |
| func = Function(tmpl_node, func_node, self.is_impl, |
| self.categories) |
| except SpecError, e: |
| func_name = func_node.prop("name") |
| raise SpecError("failed to parse %s: %s" % (func_name, e)) |
| self.functions.append(func) |
| |
| def match(self, func, conversions={}): |
| """Find a matching function in the API.""" |
| match = None |
| need_conv = False |
| for f in self.functions: |
| matched, conv = f.match(func, conversions) |
| if matched: |
| match = f |
| need_conv = conv |
| # exact match |
| if not need_conv: |
| break |
| return (match, need_conv) |
| |
| |
| class Function(object): |
| """Parse and realize a <template> node.""" |
| |
| def __init__(self, tmpl_node, func_node, force_skip_desc=False, categories=[]): |
| self.tmpl_name = tmpl_node.prop("name") |
| self.direction = tmpl_node.prop("direction") |
| |
| self.name = func_node.prop("name") |
| self.prefix = func_node.prop("default_prefix") |
| self.is_external = (func_node.prop("external") == "true") |
| |
| if force_skip_desc: |
| self._skip_desc = True |
| else: |
| self._skip_desc = (func_node.prop("skip_desc") == "true") |
| |
| self._categories = categories |
| |
| # these attributes decide how the template is realized |
| self._gltype = func_node.prop("gltype") |
| if func_node.hasProp("vector_size"): |
| self._vector_size = int(func_node.prop("vector_size")) |
| else: |
| self._vector_size = 0 |
| self._expand_vector = (func_node.prop("expand_vector") == "true") |
| |
| self.return_type = "void" |
| param_nodes = [] |
| |
| # find <proto> |
| proto_node = tmpl_node.children |
| while proto_node: |
| if proto_node.type == "element" and proto_node.name == "proto": |
| break |
| proto_node = proto_node.next |
| if not proto_node: |
| raise SpecError("no proto") |
| # and parse it |
| node = proto_node.children |
| while node: |
| if node.type == "element": |
| if node.name == "return": |
| self.return_type = node.prop("type") |
| elif node.name == "param" or node.name == "vector": |
| if self.support_node(node): |
| # make sure the node is not hidden |
| if not (self._expand_vector and |
| (node.prop("hide_if_expanded") == "true")): |
| param_nodes.append(node) |
| else: |
| raise SpecError("unexpected node %s in proto" % node.name) |
| node = node.next |
| |
| self._init_params(param_nodes) |
| self._init_descs(tmpl_node, param_nodes) |
| |
| def __str__(self): |
| return "%s %s%s(%s)" % (self.return_type, self.prefix, self.name, |
| self.param_string(True)) |
| |
| def _init_params(self, param_nodes): |
| """Parse and initialize parameters.""" |
| self.params = [] |
| |
| for param_node in param_nodes: |
| size = self.param_node_size(param_node) |
| # when no expansion, vector is just like param |
| if param_node.name == "param" or not self._expand_vector: |
| param = Parameter(param_node, self._gltype, size) |
| self.params.append(param) |
| continue |
| |
| if not size or size > param_node.lsCountNode(): |
| raise SpecError("could not expand %s with unknown or " |
| "mismatch sizes" % param.name) |
| |
| # expand the vector |
| expanded_params = [] |
| child = param_node.children |
| while child: |
| if (child.type == "element" and child.name == "param" and |
| self.support_node(child)): |
| expanded_params.append(Parameter(child, self._gltype)) |
| if len(expanded_params) == size: |
| break |
| child = child.next |
| # just in case that lsCountNode counts unknown nodes |
| if len(expanded_params) < size: |
| raise SpecError("not enough named parameters") |
| |
| self.params.extend(expanded_params) |
| |
| def _init_descs(self, tmpl_node, param_nodes): |
| """Parse and initialize parameter descriptions.""" |
| self.checker = Checker() |
| if self._skip_desc: |
| return |
| |
| node = tmpl_node.children |
| while node: |
| if node.type == "element" and node.name == "desc": |
| if self.support_node(node): |
| # parse <desc> |
| desc = Description(node, self._categories) |
| self.checker.add_desc(desc) |
| node = node.next |
| |
| self.checker.validate(self, param_nodes) |
| |
| def support_node(self, node): |
| """Return true if a node is in the supported category.""" |
| return (not node.hasProp("category") or |
| node.prop("category") in self._categories) |
| |
| def get_param(self, name): |
| """Return the named parameter.""" |
| for param in self.params: |
| if param.name == name: |
| return param |
| return None |
| |
| def param_node_size(self, param): |
| """Return the size of a vector.""" |
| if param.name != "vector": |
| return 0 |
| |
| size = param.prop("size") |
| if size.isdigit(): |
| size = int(size) |
| else: |
| size = 0 |
| if not size: |
| size = self._vector_size |
| if not size and self._expand_vector: |
| # return the number of named parameters |
| size = param.lsCountNode() |
| return size |
| |
| def param_string(self, declaration): |
| """Return the C code of the parameters.""" |
| args = [] |
| if declaration: |
| for param in self.params: |
| sep = "" if param.type.endswith("*") else " " |
| args.append("%s%s%s" % (param.type, sep, param.name)) |
| if not args: |
| args.append("void") |
| else: |
| for param in self.params: |
| args.append(param.name) |
| return ", ".join(args) |
| |
| def match(self, other, conversions={}): |
| """Return true if the functions match, probably with a conversion.""" |
| if (self.tmpl_name != other.tmpl_name or |
| self.return_type != other.return_type or |
| len(self.params) != len(other.params)): |
| return (False, False) |
| |
| need_conv = False |
| for i in xrange(len(self.params)): |
| src = other.params[i] |
| dst = self.params[i] |
| if (src.is_vector != dst.is_vector or src.size != dst.size): |
| return (False, False) |
| if src.type != dst.type: |
| if dst.base_type() in conversions.get(src.base_type(), []): |
| need_conv = True |
| else: |
| # unable to convert |
| return (False, False) |
| |
| return (True, need_conv) |
| |
| |
| class Parameter(object): |
| """A parameter of a function.""" |
| |
| def __init__(self, param_node, gltype=None, size=0): |
| self.is_vector = (param_node.name == "vector") |
| |
| self.name = param_node.prop("name") |
| self.size = size |
| |
| type = param_node.prop("type") |
| if gltype: |
| type = type.replace("GLtype", gltype) |
| elif type.find("GLtype") != -1: |
| raise SpecError("parameter %s has unresolved type" % self.name) |
| |
| self.type = type |
| |
| def base_type(self): |
| """Return the base GL type by stripping qualifiers.""" |
| return [t for t in self.type.split(" ") if t.startswith("GL")][0] |
| |
| |
| class Checker(object): |
| """A checker is the collection of all descriptions on the same level. |
| Descriptions of the same parameter are concatenated. |
| """ |
| |
| def __init__(self): |
| self.switches = {} |
| self.switch_constants = {} |
| |
| def add_desc(self, desc): |
| """Add a description.""" |
| # TODO allow index to vary |
| const_attrs = ["index", "error", "convert", "size_str"] |
| if desc.name not in self.switches: |
| self.switches[desc.name] = [] |
| self.switch_constants[desc.name] = {} |
| for attr in const_attrs: |
| self.switch_constants[desc.name][attr] = None |
| |
| # some attributes, like error code, should be the same for all descs |
| consts = self.switch_constants[desc.name] |
| for attr in const_attrs: |
| if getattr(desc, attr) is not None: |
| if (consts[attr] is not None and |
| consts[attr] != getattr(desc, attr)): |
| raise SpecError("mismatch %s for %s" % (attr, desc.name)) |
| consts[attr] = getattr(desc, attr) |
| |
| self.switches[desc.name].append(desc) |
| |
| def validate(self, func, param_nodes): |
| """Validate the checker against a function.""" |
| tmp = Checker() |
| |
| for switch in self.switches.itervalues(): |
| valid_descs = [] |
| for desc in switch: |
| if desc.validate(func, param_nodes): |
| valid_descs.append(desc) |
| # no possible values |
| if not valid_descs: |
| return False |
| for desc in valid_descs: |
| if not desc._is_noop: |
| tmp.add_desc(desc) |
| |
| self.switches = tmp.switches |
| self.switch_constants = tmp.switch_constants |
| return True |
| |
| def flatten(self, name=None): |
| """Return a flat list of all descriptions of the named parameter.""" |
| flat_list = [] |
| for switch in self.switches.itervalues(): |
| for desc in switch: |
| if not name or desc.name == name: |
| flat_list.append(desc) |
| flat_list.extend(desc.checker.flatten(name)) |
| return flat_list |
| |
| def always_check(self, name): |
| """Return true if the parameter is checked in all possible pathes.""" |
| if name in self.switches: |
| return True |
| |
| # a param is always checked if any of the switch always checks it |
| for switch in self.switches.itervalues(): |
| # a switch always checks it if all of the descs always check it |
| always = True |
| for desc in switch: |
| if not desc.checker.always_check(name): |
| always = False |
| break |
| if always: |
| return True |
| return False |
| |
| def _c_switch(self, name, indent="\t"): |
| """Output C switch-statement for the named parameter, for debug.""" |
| switch = self.switches.get(name, []) |
| # make sure there are valid values |
| need_switch = False |
| for desc in switch: |
| if desc.values: |
| need_switch = True |
| if not need_switch: |
| return [] |
| |
| stmts = [] |
| var = switch[0].name |
| if switch[0].index >= 0: |
| var += "[%d]" % switch[0].index |
| stmts.append("switch (%s) { /* assume GLenum */" % var) |
| |
| for desc in switch: |
| if desc.values: |
| for val in desc.values: |
| stmts.append("case %s:" % val) |
| for dep_name in desc.checker.switches.iterkeys(): |
| dep_stmts = [indent + s for s in desc.checker._c_switch(dep_name, indent)] |
| stmts.extend(dep_stmts) |
| stmts.append(indent + "break;") |
| |
| stmts.append("default:") |
| stmts.append(indent + "ON_ERROR(%s);" % switch[0].error); |
| stmts.append(indent + "break;") |
| stmts.append("}") |
| |
| return stmts |
| |
| def dump(self, indent="\t"): |
| """Dump the descriptions in C code.""" |
| stmts = [] |
| for name in self.switches.iterkeys(): |
| c_switch = self._c_switch(name) |
| print "\n".join(c_switch) |
| |
| |
| class Description(object): |
| """A description desribes a parameter and its relationship with other |
| parameters. |
| """ |
| |
| def __init__(self, desc_node, categories=[]): |
| self._categories = categories |
| self._is_noop = False |
| |
| self.name = desc_node.prop("name") |
| self.index = -1 |
| |
| self.error = desc_node.prop("error") or "GL_INVALID_ENUM" |
| # vector_size may be C code |
| self.size_str = desc_node.prop("vector_size") |
| |
| self._has_enum = False |
| self.values = [] |
| dep_nodes = [] |
| |
| # parse <desc> |
| valid_names = ["value", "range", "desc"] |
| node = desc_node.children |
| while node: |
| if node.type == "element": |
| if node.name in valid_names: |
| # ignore nodes that require unsupported categories |
| if (node.prop("category") and |
| node.prop("category") not in self._categories): |
| node = node.next |
| continue |
| else: |
| raise SpecError("unexpected node %s in desc" % node.name) |
| |
| if node.name == "value": |
| val = node.prop("name") |
| if not self._has_enum and val.startswith("GL_"): |
| self._has_enum = True |
| self.values.append(val) |
| elif node.name == "range": |
| first = int(node.prop("from")) |
| last = int(node.prop("to")) |
| base = node.prop("base") or "" |
| if not self._has_enum and base.startswith("GL_"): |
| self._has_enum = True |
| # expand range |
| for i in xrange(first, last + 1): |
| self.values.append("%s%d" % (base, i)) |
| else: # dependent desc |
| dep_nodes.append(node) |
| node = node.next |
| |
| # default to convert if there is no enum |
| self.convert = not self._has_enum |
| if desc_node.hasProp("convert"): |
| self.convert = (desc_node.prop("convert") == "true") |
| |
| self._init_deps(dep_nodes) |
| |
| def _init_deps(self, dep_nodes): |
| """Parse and initialize dependents.""" |
| self.checker = Checker() |
| |
| for dep_node in dep_nodes: |
| # recursion! |
| dep = Description(dep_node, self._categories) |
| self.checker.add_desc(dep) |
| |
| def _search_param_node(self, param_nodes, name=None): |
| """Search the template parameters for the named node.""" |
| param_node = None |
| param_index = -1 |
| |
| if not name: |
| name = self.name |
| for node in param_nodes: |
| if name == node.prop("name"): |
| param_node = node |
| elif node.name == "vector": |
| child = node.children |
| idx = 0 |
| while child: |
| if child.type == "element" and child.name == "param": |
| if name == child.prop("name"): |
| param_node = node |
| param_index = idx |
| break |
| idx += 1 |
| child = child.next |
| if param_node: |
| break |
| return (param_node, param_index) |
| |
| def _find_final(self, func, param_nodes): |
| """Find the final parameter.""" |
| param = func.get_param(self.name) |
| param_index = -1 |
| |
| # the described param is not in the final function |
| if not param: |
| # search the template parameters |
| node, index = self._search_param_node(param_nodes) |
| if not node: |
| raise SpecError("invalid desc %s in %s" % |
| (self.name, func.name)) |
| |
| # a named parameter of a vector |
| if index >= 0: |
| param = func.get_param(node.prop("name")) |
| param_index = index |
| elif node.name == "vector": |
| # must be an expanded vector, check its size |
| if self.size_str and self.size_str.isdigit(): |
| size = int(self.size_str) |
| expanded_size = func.param_node_size(node) |
| if size != expanded_size: |
| return (False, None, -1) |
| # otherwise, it is a valid, but no-op, description |
| |
| return (True, param, param_index) |
| |
| def validate(self, func, param_nodes): |
| """Validate a description against certain function.""" |
| if self.checker.switches and not self.values: |
| raise SpecError("no valid values for %s" % self.name) |
| |
| valid, param, param_index = self._find_final(func, param_nodes) |
| if not valid: |
| return False |
| |
| # the description is valid, but the param is gone |
| # mark it no-op so that it will be skipped |
| if not param: |
| self._is_noop = True |
| return True |
| |
| if param.is_vector: |
| # if param was known, this should have been done in __init__ |
| if self._has_enum: |
| self.size_str = "1" |
| # size mismatch |
| if (param.size and self.size_str and self.size_str.isdigit() and |
| param.size != int(self.size_str)): |
| return False |
| elif self.size_str: |
| # only vector accepts vector_size |
| raise SpecError("vector_size is invalid for %s" % param.name) |
| |
| if not self.checker.validate(func, param_nodes): |
| return False |
| |
| # update the description |
| self.name = param.name |
| self.index = param_index |
| |
| return True |
| |
| |
| def main(): |
| import libxml2 |
| |
| filename = "APIspec.xml" |
| apinames = ["GLES1.1", "GLES2.0"] |
| |
| doc = libxml2.readFile(filename, None, |
| libxml2.XML_PARSE_DTDLOAD + |
| libxml2.XML_PARSE_DTDVALID + |
| libxml2.XML_PARSE_NOBLANKS) |
| |
| spec = Spec(doc) |
| impl = spec.get_impl() |
| for apiname in apinames: |
| spec.get_api(apiname) |
| |
| doc.freeDoc() |
| |
| print "%s is successfully parsed" % filename |
| |
| |
| if __name__ == "__main__": |
| main() |