#!/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()