[sourcegen] Implement CLib variable getter

This commit is contained in:
Ingmar Schoegl 2025-02-02 12:18:37 -06:00
parent eddc33fbc0
commit 0cd6629f97
5 changed files with 133 additions and 56 deletions

View File

@ -25,14 +25,19 @@ _XML_PATH = _TAG_PATH / "doxygen" / "xml"
@dataclass(frozen=True)
@with_unpack_iter
class TagInfo:
"""Represents information parsed from a doxygen tag file."""
"""
Represents information parsed from a doxygen tag file.
May represent a member function or a variable.
"""
base: str = "" #: Qualified scope (skipping Cantera namespace)
type: str = "" #: Return type
name: str = "" #: Function name
name: str = "" #: Function/variable name
arglist: str = "" #: Function argument list (original XML string)
anchorfile: str = "" #: doxygen anchor file
anchor: str = "" #: doxygen anchor
kind: str = "" #: Member kind
@classmethod
def from_xml(cls: Self, qualified_name: str, xml: str) -> Self:
@ -47,7 +52,8 @@ class TagInfo:
xml_tree.find("name").text,
xml_tree.find("arglist").text,
xml_tree.find("anchorfile").text.replace(".html", ".xml"),
xml_tree.find("anchor").text)
xml_tree.find("anchor").text,
xml_tree.attrib.get("kind", ""))
def __bool__(self) -> bool:
return all([self.type, self.name, self.arglist, self.anchorfile, self.anchor])
@ -154,13 +160,15 @@ class TagFileParser:
# Get known functions from namespace and methods from classes
self._known = xml_members("function", namespace)
self._known.update(xml_members("variable", namespace))
for name, cls in classes.items():
prefix = f"{name}::"
self._known.update(xml_members("function", cls, prefix))
self._known.update(xml_members("variable", cls, prefix))
def exists(self, cxx_func: str) -> bool:
def exists(self, cxx_member: str) -> bool:
"""Check whether doxygen tag exists."""
return cxx_func in self._known
return cxx_member in self._known
def detect(self, name: str, bases: Iterable[str], permissive: bool = True) -> str:
"""Detect qualified method name."""
@ -178,17 +186,17 @@ class TagFileParser:
def tag_info(self, func_string: str) -> TagInfo:
"""Look up tag information based on (partial) function signature."""
cxx_func = func_string.split("(")[0].split(" ")[-1]
if cxx_func not in self._known:
msg = f"Could not find {cxx_func!r} in doxygen tag file."
cxx_member = func_string.split("(")[0].split(" ")[-1]
if cxx_member not in self._known:
msg = f"Could not find {cxx_member!r} in doxygen tag file."
_LOGGER.critical(msg)
sys.exit(1)
ix = 0
if len(self._known[cxx_func]) > 1:
if len(self._known[cxx_member]) > 1:
# Disambiguate functions with same name
# TODO: current approach does not use information on default arguments
known_args = [ET.fromstring(xml).find("arglist").text
for xml in self._known[cxx_func]]
for xml in self._known[cxx_member]]
known_args = [ArgList.from_xml(al).short_str() for al in known_args]
args = re.findall(re.compile(r"(?<=\().*(?=\))"), func_string)
if not args and "()" in known_args:
@ -214,13 +222,17 @@ class TagFileParser:
_LOGGER.critical(msg)
sys.exit(1)
return TagInfo.from_xml(cxx_func, self._known[cxx_func][ix])
return TagInfo.from_xml(cxx_member, self._known[cxx_member][ix])
def cxx_func(self, func_string: str) -> CFunc:
"""Generate annotated C++ function specification."""
def cxx_member(self, func_string: str) -> CFunc | Param:
"""Generate annotated C++ function/variable specification."""
details = tag_lookup(self.tag_info(func_string))
ret_param = Param.from_xml(details.type)
if details.kind == "variable":
return Param(ret_param.p_type, details.name,
details.briefdescription, "", None, details.base)
# Merge attributes from doxygen signature and doxygen annotations
args = ArgList.from_xml(details.arglist).params # from signature
args_annotated = details.parameterlist # from documentation
@ -250,7 +262,8 @@ def tag_lookup(tag_info: TagInfo) -> TagDetails:
xml_details = xml_file.read_text()
id_ = tag_info.id
regex = re.compile(rf'<memberdef kind="function" id="{id_}"[\s\S]*?</memberdef>')
kind_ = tag_info.kind
regex = re.compile(rf'<memberdef kind="{kind_}" id="{id_}"[\s\S]*?</memberdef>')
matches = re.findall(regex, xml_details)
if not matches:

View File

@ -14,7 +14,9 @@ recipes:
- name: type # previously: ctkin_getReactionType
- name: usesThirdBody
- name: valid
# - name: id <--- member variable (access not yet implemented)
- name: id
- name: allowNonreactantOrders
implements: allow_nonreactant_orders
- name: del
- name: cabinetSize
- name: parentHandle

View File

@ -23,6 +23,7 @@ class Param:
description: str = "" #: Parameter description (optional annotation)
direction: str = "" #: Direction of parameter (optional annotation)
default: Any = None #: Default value (optional)
base: str = "" #: Base (optional). Only used if param represents a member variable
@classmethod
def from_str(cls: Self, param: str, doc: str = "") -> Self:
@ -181,7 +182,10 @@ class CFunc(Func):
def short_declaration(self) -> str:
"""Return a short string representation."""
ret = (f"{self.name}{self.arglist.short_str()}").strip()
if self.arglist is None:
ret = (f"{self.name}").strip()
else:
ret = (f"{self.name}{self.arglist.short_str()}").strip()
if self.base:
return f"{self.ret_type} {self.base}::{ret}"
return f"{self.ret_type} {ret}"

View File

@ -164,8 +164,8 @@ class CLibSourceGenerator(SourceGenerator):
sys.exit(1)
return params
@staticmethod
def _reverse_crosswalk(c_func: CFunc, base: str) -> tuple[dict[str, str], set[str]]:
def _reverse_crosswalk(
self, c_func: CFunc, base: str) -> tuple[dict[str, str], set[str]]:
"""Translate CLib arguments back to Jinja argument list."""
handle = ""
args = []
@ -180,28 +180,37 @@ class CLibSourceGenerator(SourceGenerator):
return cxx_type.split("<")[-1].split(">")[0]
c_args = c_func.arglist
cxx_func = c_func.implements
if not cxx_func:
cxx_member = c_func.implements
if not cxx_member:
if c_func.name.endswith("new"):
# Default constructor
cxx_func = CFunc("auto", f"make_shared<{base}>", ArgList([]), "", None)
cxx_func = CFunc("auto", f"make_shared<{base}>", ArgList([]))
elif len(c_args) and "char*" in c_args[-1].p_type:
cxx_func = CFunc("string", "dummy", ArgList([]), "", None, "", "base")
else:
cxx_func = CFunc("void", "dummy", ArgList([]), "", None, "", "base")
elif isinstance(cxx_member, Param):
if len(c_args) and "char*" in c_args[-1].p_type:
cxx_func = CFunc("string", cxx_member.name, None,
"", None, "", cxx_member.base)
else:
cxx_func = CFunc(cxx_member.p_type, cxx_member.name, None,
"", None, "", cxx_member.base)
else:
cxx_func = cxx_member
cxx_ix = 0
check_array = False
for c_ix, c_par in enumerate(c_func.arglist):
c_name = c_par.name
if cxx_ix >= len(cxx_func.arglist):
if isinstance(cxx_member, Param) or cxx_ix >= len(cxx_func.arglist):
if c_ix == 0 and cxx_func.base and "len" not in c_name.lower():
handle = c_name
c_ix += 1
if c_ix == len(c_args):
if c_ix == len(c_args) and not isinstance(cxx_member, Param):
break
cxx_type = cxx_func.ret_type
# Handle output buffer
# Handle output buffer and/or variable assignments
cxx_type = cxx_func.ret_type
if "string" in cxx_type:
buffer = ["string out",
f"copyString(out, {c_args[c_ix+1].name}, "
@ -212,6 +221,13 @@ class CLibSourceGenerator(SourceGenerator):
"std::copy(out.begin(), out.end(), "
f"{c_args[c_ix+1].name});",
"int(out.size())"]
elif "bool" in cxx_type:
buffer = [f"{cxx_type} out",
"",
"int(out)"]
elif cxx_type in self._config.ret_type_crosswalk:
# can pass values directly
buffer = []
else:
msg = (f"Scaffolding failed for {c_func.name!r}: reverse crosswalk "
f"not implemented for {cxx_type!r}:\n{c_func.declaration()}")
@ -333,6 +349,9 @@ class CLibSourceGenerator(SourceGenerator):
elif recipe.what == "function":
template = loader.from_string(self._templates["clib-function"])
elif recipe.what == "variable-getter":
template = loader.from_string(self._templates["clib-variable-getter"])
elif recipe.what == "constructor":
template = loader.from_string(self._templates["clib-constructor"])
@ -376,7 +395,8 @@ class CLibSourceGenerator(SourceGenerator):
def _resolve_recipe(self, recipe: Recipe) -> CFunc:
"""Build CLib header from recipe and doxygen annotations."""
def merge_params(implements: str, cxx_func: CFunc) -> tuple[list[Param], int]:
def merge_params(
implements: str, cxx_member: CFunc | Param) -> tuple[list[Param], int]:
"""Create preliminary CLib argument list."""
obj_handle = []
if "::" in implements:
@ -384,18 +404,20 @@ class CLibSourceGenerator(SourceGenerator):
what = implements.split("::")[0]
obj_handle.append(
Param("int", "handle", f"Handle to queried {what} object."))
if isinstance(cxx_member, Param):
return obj_handle, cxx_member
if "(" not in implements:
return obj_handle + cxx_func.arglist.params, cxx_func
return obj_handle + cxx_member.arglist.params, cxx_member
# Signature may skip C++ default parameters
args_short = CFunc.from_str(implements).arglist
if len(args_short) < len(cxx_func.arglist):
cxx_arglist = ArgList(cxx_func.arglist[:len(args_short)])
cxx_func = CFunc(cxx_func.ret_type, cxx_func.name,
cxx_arglist, cxx_func.brief, cxx_func.implements,
cxx_func.returns, cxx_func.base, cxx_func.uses)
if len(args_short) < len(cxx_member.arglist):
cxx_arglist = ArgList(cxx_member.arglist[:len(args_short)])
cxx_member = CFunc(cxx_member.ret_type, cxx_member.name,
cxx_arglist, cxx_member.brief, cxx_member.implements,
cxx_member.returns, cxx_member.base, cxx_member.uses)
return obj_handle + cxx_func.arglist.params, cxx_func
return obj_handle + cxx_member.arglist.params, cxx_member
func_name = f"{recipe.prefix}_{recipe.name}"
reserved = ["cabinetSize", "parentHandle",
@ -417,7 +439,7 @@ class CLibSourceGenerator(SourceGenerator):
recipe.uses = [self._doxygen_tags.detect(uu.split("(")[0], bases, False)
for uu in recipe.uses]
cxx_func = None
cxx_member = None
ret_param = Param("void")
args = []
brief = ""
@ -429,36 +451,48 @@ class CLibSourceGenerator(SourceGenerator):
if not self._doxygen_tags.exists(parts[0]):
parts[0] = self._doxygen_tags.detect(parts[0], bases, False)
recipe.implements = "".join(parts)
cxx_func = self._doxygen_tags.cxx_func(recipe.implements)
cxx_member = self._doxygen_tags.cxx_member(recipe.implements)
# Convert C++ return type to format suitable for crosswalk:
# Incompatible return parameters are buffered and appended to back
ret_param, buffer_params = self._ret_crosswalk(
cxx_func.ret_type, recipe.derived)
par_list, cxx_func = merge_params(recipe.implements, cxx_func)
prop_params = self._prop_crosswalk(par_list)
brief = cxx_func.brief
args = prop_params + buffer_params
if isinstance(cxx_member, CFunc):
# Convert C++ return type to format suitable for crosswalk:
# Incompatible return parameters are buffered and appended to back
ret_param, buffer_params = self._ret_crosswalk(
cxx_member.ret_type, recipe.derived)
par_list, cxx_member = merge_params(recipe.implements, cxx_member)
prop_params = self._prop_crosswalk(par_list)
brief = cxx_member.brief
args = prop_params + buffer_params
else:
# Variable getter
prop_params, cxx_member = merge_params(recipe.implements, cxx_member)
ret_param, buffer_params = self._ret_crosswalk(
cxx_member.p_type, recipe.derived)
args = prop_params + buffer_params
brief = cxx_member.description
if recipe.what and cxx_func:
if recipe.what and cxx_member:
# Recipe type and corresponding C++ function are known
pass
elif cxx_func:
elif cxx_member and isinstance(cxx_member, Param):
# Recipe represents a variable getter/setter
recipe.what = "variable-getter"
elif cxx_member:
# Autodetection of CLib function purpose ("what")
cxx_arglen = len(cxx_func.arglist)
if not cxx_func.base:
if (cxx_func.name.startswith("new") and
any(base in cxx_func.ret_type
cxx_arglen = len(cxx_member.arglist)
if not cxx_member.base:
if (cxx_member.name.startswith("new") and
any(base in cxx_member.ret_type
for base in [recipe.base] + recipe.derived)):
recipe.what = "constructor"
else:
recipe.what = "function"
elif "void" not in cxx_func.ret_type and cxx_arglen == 0:
elif "void" not in cxx_member.ret_type and cxx_arglen == 0:
recipe.what = "getter"
elif "void" in cxx_func.ret_type and cxx_arglen == 1:
p_type = cxx_func.arglist[0].p_type
if cxx_func.name.startswith("get"):
elif "void" in cxx_member.ret_type and cxx_arglen == 1:
p_type = cxx_member.arglist[0].p_type
if cxx_member.name.startswith("get"):
recipe.what = "getter"
elif "*" in p_type and not p_type.startswith("const"):
recipe.what = "getter" # getter assigns to existing array
@ -505,8 +539,8 @@ class CLibSourceGenerator(SourceGenerator):
if recipe.brief:
brief = recipe.brief
uses = [self._doxygen_tags.cxx_func(uu) for uu in recipe.uses]
return CFunc(ret_param.p_type, func_name, ArgList(args), brief, cxx_func,
uses = [self._doxygen_tags.cxx_member(uu) for uu in recipe.uses]
return CFunc(ret_param.p_type, func_name, ArgList(args), brief, cxx_member,
ret_param.description, None, uses)
def _write_header(self, headers: HeaderFile) -> None:

View File

@ -133,6 +133,30 @@ clib-function: |-
return handleAllExceptions({{ error[0] }}, {{ error[1] }});
}
clib-variable-getter: |-
// variable (getter): {{ cxx_implements }}
try {
{% if cxx_base == base %}
## object can be accessed directly
auto& obj = {{ base }}Cabinet::at({{ handle }});
{% else %}
## object needs a cast as method is defined for specialization
auto obj = {{ base }}Cabinet::as<{{ cxx_base }}>({{ handle }});
{% endif %}
{% if buffer and buffer[0] %}
{{ buffer[0] }} = obj->{{ cxx_name }};
{% if buffer[1] %}
{{ buffer[1] }}
{% endif %}
return {{ buffer[2] }};
{% else %}
obj->{{ cxx_name }};
return 0;
{% endif %}
} catch (...) {
return handleAllExceptions({{ error[0] }}, {{ error[1] }});
}
clib-constructor: |-
## CLib constructor template: instantiates new object
// constructor: {{ cxx_implements }}
@ -295,7 +319,7 @@ clib-array-getter: |-
// no size checking specified
{% endif %}
{% if buffer and buffer[0] %}
{{ buffer[0] }} = obj->{{ cxx_name }}({{ cxx_args[0] }});;
{{ buffer[0] }} = obj->{{ cxx_name }}({{ cxx_args[0] }});
{% if buffer[1] %}
{{ buffer[1] }}
{% endif %}