yace 0.5.7__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- yace/__init__.py +1 -0
- yace/__main__.py +3 -0
- yace/cli/__init__.py +0 -0
- yace/cli/yace.py +111 -0
- yace/compiler.py +84 -0
- yace/emitters.py +99 -0
- yace/errors.py +125 -0
- yace/ir/__init__.py +0 -0
- yace/ir/base.py +121 -0
- yace/ir/constants.py +138 -0
- yace/ir/cparser.py +586 -0
- yace/ir/datatypes.py +719 -0
- yace/ir/derivedtypes.py +115 -0
- yace/ir/directives.py +16 -0
- yace/ir/functiontypes.py +49 -0
- yace/model.py +220 -0
- yace/targets/__init__.py +0 -0
- yace/targets/capi/__init__.py +0 -0
- yace/targets/capi/target.py +147 -0
- yace/targets/collector.py +37 -0
- yace/targets/ctypes/__init__.py +0 -0
- yace/targets/ctypes/ctypes_sugar.py +101 -0
- yace/targets/ctypes/target.py +93 -0
- yace/targets/target.py +63 -0
- yace/tools.py +166 -0
- yace/transformations.py +97 -0
- yace-0.5.7.dist-info/LICENSE +339 -0
- yace-0.5.7.dist-info/METADATA +415 -0
- yace-0.5.7.dist-info/RECORD +32 -0
- yace-0.5.7.dist-info/WHEEL +5 -0
- yace-0.5.7.dist-info/entry_points.txt +2 -0
- yace-0.5.7.dist-info/top_level.txt +1 -0
yace/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.5.7"
|
yace/__main__.py
ADDED
yace/cli/__init__.py
ADDED
|
File without changes
|
yace/cli/yace.py
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
The is the command-line interface to **yace**.
|
|
4
|
+
|
|
5
|
+
TODO
|
|
6
|
+
====
|
|
7
|
+
|
|
8
|
+
* Discover 'adhoc' targets, e.g. targets available in the
|
|
9
|
+
current-workding-directory
|
|
10
|
+
* Discover targets via packages under the namespace 'yace.targets.{NAME}`
|
|
11
|
+
|
|
12
|
+
"""
|
|
13
|
+
import argparse
|
|
14
|
+
import logging as log
|
|
15
|
+
import sys
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
from yace import __version__ as version
|
|
19
|
+
from yace.compiler import Compiler
|
|
20
|
+
from yace.ir.cparser import c_to_yace
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def parse_args():
|
|
24
|
+
"""Parse command-line arguments"""
|
|
25
|
+
|
|
26
|
+
parser = argparse.ArgumentParser()
|
|
27
|
+
|
|
28
|
+
parser.add_argument(
|
|
29
|
+
"filepath",
|
|
30
|
+
nargs="+",
|
|
31
|
+
type=Path,
|
|
32
|
+
help="path to one or more Yace-file(s) or C Header(s)",
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
parser.add_argument(
|
|
36
|
+
"--emit",
|
|
37
|
+
nargs="+",
|
|
38
|
+
choices=sorted([target.NAME for target in Compiler.TARGETS]),
|
|
39
|
+
default="capi",
|
|
40
|
+
help="treat filepath(s) as Yace-file, and emit code using target(s), then exit",
|
|
41
|
+
)
|
|
42
|
+
parser.add_argument(
|
|
43
|
+
"--version",
|
|
44
|
+
action="version",
|
|
45
|
+
version=f"%(prog)s {version}",
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
parser.add_argument(
|
|
49
|
+
"--output",
|
|
50
|
+
type=Path,
|
|
51
|
+
default=Path.cwd(),
|
|
52
|
+
help="path to output directory, for emitted code / artifacts",
|
|
53
|
+
)
|
|
54
|
+
parser.add_argument(
|
|
55
|
+
"--log-level",
|
|
56
|
+
"-l",
|
|
57
|
+
action="append_const",
|
|
58
|
+
const=1,
|
|
59
|
+
default=[],
|
|
60
|
+
help="increase log-level.",
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
return parser.parse_args()
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def main():
|
|
67
|
+
"""Emit enums, structs, and pretty-printer functions for them"""
|
|
68
|
+
|
|
69
|
+
ok = False
|
|
70
|
+
try:
|
|
71
|
+
args = parse_args()
|
|
72
|
+
args.filepath = [filepath.resolve() for filepath in args.filepath]
|
|
73
|
+
|
|
74
|
+
levels = [log.ERROR, log.INFO, log.DEBUG]
|
|
75
|
+
log.basicConfig(
|
|
76
|
+
format="%(levelname)s:%(module)s:%(funcName)s(): %(message)s",
|
|
77
|
+
level=levels[min(sum(args.log_level), len(levels) - 1)],
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
suffixes = list(
|
|
81
|
+
set(path.suffix for path in args.filepath if path.suffix in [".h", ".yaml"])
|
|
82
|
+
)
|
|
83
|
+
if len(suffixes) != 1:
|
|
84
|
+
log.error(f"Suffixes({suffixes}); provide either .h or .yaml")
|
|
85
|
+
return sys.exit(1)
|
|
86
|
+
|
|
87
|
+
missing = [str(path) for path in args.filepath if not path.exists()]
|
|
88
|
+
if missing:
|
|
89
|
+
log.error(f"Does not exist: {missing}")
|
|
90
|
+
return sys.exit(1)
|
|
91
|
+
|
|
92
|
+
if suffixes[0] == ".h": # C to Yace-file Compiler
|
|
93
|
+
log.info("Got .h will convert to Yace IR")
|
|
94
|
+
|
|
95
|
+
errors = c_to_yace(args.filepath, args.output)
|
|
96
|
+
for error in errors:
|
|
97
|
+
log.error(error)
|
|
98
|
+
|
|
99
|
+
return sys.exit(1 if errors else 0)
|
|
100
|
+
|
|
101
|
+
log.info(f"Got .yaml, will do '{args.emit}'")
|
|
102
|
+
yace = Compiler(args.emit, args.output)
|
|
103
|
+
ok = all([yace.process(path, yace.STAGES) for path in args.filepath])
|
|
104
|
+
return sys.exit(0 if ok else 1)
|
|
105
|
+
|
|
106
|
+
except Exception as exc:
|
|
107
|
+
log.error("Unhandled Exception: message(%s)", exc)
|
|
108
|
+
log.error("Unhandled Exception: increase log-level (-ll) for trace")
|
|
109
|
+
log.error("Unhandled Exception(%s)", exc, exc_info=True)
|
|
110
|
+
|
|
111
|
+
return sys.exit(1)
|
yace/compiler.py
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import copy
|
|
2
|
+
import logging as log
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import List, Optional
|
|
5
|
+
|
|
6
|
+
from yace.model import Model
|
|
7
|
+
from yace.targets.capi.target import CAPI
|
|
8
|
+
from yace.targets.collector import collect
|
|
9
|
+
from yace.targets.ctypes.target import Ctypes
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Compiler(object):
|
|
13
|
+
"""
|
|
14
|
+
Encapsulation of **yace** compiler stages
|
|
15
|
+
|
|
16
|
+
* Parse
|
|
17
|
+
* Lint
|
|
18
|
+
* Transform
|
|
19
|
+
* Emit
|
|
20
|
+
* Format
|
|
21
|
+
* Check
|
|
22
|
+
|
|
23
|
+
The first two stages are generic, the third is generic however is usually performed
|
|
24
|
+
for target-specific-reasons such as re-structuring the IR to make the code-emitter
|
|
25
|
+
simpler, the last three are target-specific.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
STAGES = ["parse", "lint", "transform", "emit", "format", "check"]
|
|
29
|
+
TARGETS = list(set([CAPI, Ctypes] + collect()))
|
|
30
|
+
|
|
31
|
+
def __init__(self, targets: List[str], output: Path):
|
|
32
|
+
self.targets = [target for target in Compiler.TARGETS if target.NAME in targets]
|
|
33
|
+
self.output = output.resolve()
|
|
34
|
+
|
|
35
|
+
def process(self, path: Path, stages: Optional[List[str]] = None) -> bool:
|
|
36
|
+
"""
|
|
37
|
+
Take 'path' through the given compiler 'stages'
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
if stages is None:
|
|
41
|
+
stages = Compiler.STAGES
|
|
42
|
+
|
|
43
|
+
log.info("Path: '%s', stages: '%s'", path, stages)
|
|
44
|
+
self.output.mkdir(parents=True, exist_ok=True)
|
|
45
|
+
|
|
46
|
+
log.info("Stage: 'parse'")
|
|
47
|
+
model_orig = Model.from_path(path)
|
|
48
|
+
|
|
49
|
+
targets = [cls(self.output) for cls in self.targets]
|
|
50
|
+
if not all([tgt.is_ready() for tgt in targets]):
|
|
51
|
+
log.error("One or more targets !ready; see above/log. stopping.")
|
|
52
|
+
return False
|
|
53
|
+
|
|
54
|
+
for target in targets:
|
|
55
|
+
log.info("Target: %s", target.NAME)
|
|
56
|
+
|
|
57
|
+
model = copy.deepcopy(model_orig)
|
|
58
|
+
|
|
59
|
+
if "transform" in stages:
|
|
60
|
+
log.info("Stage: 'transform'")
|
|
61
|
+
model = target.transform(model)
|
|
62
|
+
|
|
63
|
+
if "emit" in stages:
|
|
64
|
+
log.info("Stage: 'emit'")
|
|
65
|
+
err = target.emit(model)
|
|
66
|
+
if err:
|
|
67
|
+
log.error("Got error, stopping.")
|
|
68
|
+
break
|
|
69
|
+
|
|
70
|
+
if "format" in stages:
|
|
71
|
+
log.info("Stage: 'format'")
|
|
72
|
+
err = target.format()
|
|
73
|
+
if err:
|
|
74
|
+
log.error("Got error, stopping.")
|
|
75
|
+
break
|
|
76
|
+
|
|
77
|
+
if "check" in stages:
|
|
78
|
+
log.info("Stage: 'check'")
|
|
79
|
+
err = target.check()
|
|
80
|
+
if err:
|
|
81
|
+
log.error("Got error, stopping.")
|
|
82
|
+
break
|
|
83
|
+
|
|
84
|
+
return True
|
yace/emitters.py
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""
|
|
2
|
+
YAML ==> Interface(List[Entities]) ==> Emitter() ==> CodeTarget
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from jinja2 import Environment, FileSystemLoader
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def camelcase(symbol, pascalcase=True):
|
|
11
|
+
"""Format the given 'symbol' as (C|c)amelCase"""
|
|
12
|
+
|
|
13
|
+
camelized = "".join(
|
|
14
|
+
[part[0].upper() + part[1:].lower() for part in symbol.split("_")]
|
|
15
|
+
)
|
|
16
|
+
if pascalcase:
|
|
17
|
+
return camelized
|
|
18
|
+
|
|
19
|
+
return camelized[0].lower() + camelized[1:]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Emitter(object):
|
|
23
|
+
"""
|
|
24
|
+
The default approach to transforming :class:`.Entity` in :class:`.Model`:
|
|
25
|
+
to source-code-text is inheriting **this** class and utilizing the Jinja
|
|
26
|
+
templates made available in self.templates. It might seem naive to utilize
|
|
27
|
+
templating-engine like Jinja for a code-emitter, however, so far it seems
|
|
28
|
+
to be the an incredibly simple approach.
|
|
29
|
+
|
|
30
|
+
This class populates ``self.templates`` with templates loaded for a given
|
|
31
|
+
emitter-implementation. They work as Jinja usually does, a feature-addition
|
|
32
|
+
allowing you to render a Jinja template **within** a Jinja template, via a
|
|
33
|
+
custom-filter-function. For example, like so::
|
|
34
|
+
|
|
35
|
+
{{ entity | emit_entity }}
|
|
36
|
+
|
|
37
|
+
This works by seperating Jinja templates by filename-convention,
|
|
38
|
+
template-names starting with "entity_{name}.template" are usable by this
|
|
39
|
+
in-template renderer. Thus, for the example above, the template file is
|
|
40
|
+
named ``entity_typedecl``.
|
|
41
|
+
|
|
42
|
+
It assumes that 'entity' is an instance of :class:`.Entity` and uses a
|
|
43
|
+
Jinja template to render it. This behavior is added as a means to keep
|
|
44
|
+
**all** text-formating code inside the Jinja-templates, one does not need
|
|
45
|
+
to goto the Python to see a different technique. Thus, **all**::
|
|
46
|
+
|
|
47
|
+
Entity ==> SourceCodeText
|
|
48
|
+
|
|
49
|
+
Is by default handled in one place: the Jinja templates.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
def __init__(self, searchpath: Path):
|
|
53
|
+
self.searchpath = searchpath.resolve()
|
|
54
|
+
|
|
55
|
+
def render(self, template, args, filters):
|
|
56
|
+
"""Renders the given template, passing args..."""
|
|
57
|
+
|
|
58
|
+
# Emitter jinja-environment, setup here such that the filter-function
|
|
59
|
+
# 'emit_entity' can be utilized in the other jinja-environment
|
|
60
|
+
entity_jenv = Environment(
|
|
61
|
+
loader=FileSystemLoader(self.searchpath),
|
|
62
|
+
extensions=["jinja2.ext.do"],
|
|
63
|
+
)
|
|
64
|
+
entity_jenv.globals.update(zip=zip, len=len)
|
|
65
|
+
entity_jenv.filters["camelcase"] = camelcase
|
|
66
|
+
for name, filter in filters.items():
|
|
67
|
+
entity_jenv.filters[name] = filter
|
|
68
|
+
|
|
69
|
+
entity_templates = {
|
|
70
|
+
Path(f).stem: entity_jenv.get_template(f)
|
|
71
|
+
for f in entity_jenv.list_templates()
|
|
72
|
+
if f.endswith(".template") and f.startswith("entity_")
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
def emit_entity(entity, depth: int = 0):
|
|
76
|
+
args["entity"] = entity
|
|
77
|
+
args["depth"] = depth
|
|
78
|
+
|
|
79
|
+
return entity_templates[f"entity_{entity.key}.h"].render(**args)
|
|
80
|
+
|
|
81
|
+
# This is the jinja environment for files, e.g. the one that renders
|
|
82
|
+
# "file_<thing>.jinja"
|
|
83
|
+
file_jenv = Environment(
|
|
84
|
+
loader=FileSystemLoader(self.searchpath),
|
|
85
|
+
extensions=["jinja2.ext.do"],
|
|
86
|
+
)
|
|
87
|
+
file_jenv.globals.update(zip=zip, len=len)
|
|
88
|
+
file_jenv.filters["camelcase"] = camelcase
|
|
89
|
+
file_jenv.filters["emit_entity"] = emit_entity
|
|
90
|
+
for name, filter in filters.items():
|
|
91
|
+
file_jenv.filters[name] = filter
|
|
92
|
+
|
|
93
|
+
file_templates = {
|
|
94
|
+
Path(f).stem: file_jenv.get_template(f)
|
|
95
|
+
for f in file_jenv.list_templates()
|
|
96
|
+
if f.endswith(".template") and not f.startswith("entity_")
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return file_templates[template].render(**args)
|
yace/errors.py
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
YACE returns errors except for the truly exceptional cases.
|
|
4
|
+
"""
|
|
5
|
+
from pydantic import BaseModel
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Error(BaseModel):
|
|
9
|
+
message: str
|
|
10
|
+
|
|
11
|
+
def __str__(self):
|
|
12
|
+
return f"error.message: {self.message}"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ParseError(Error):
|
|
16
|
+
filename: str
|
|
17
|
+
line: int
|
|
18
|
+
column: int
|
|
19
|
+
|
|
20
|
+
@classmethod
|
|
21
|
+
def from_exception(cls, exception, cursor):
|
|
22
|
+
errors = "\n".join(
|
|
23
|
+
[f"{count}: {error}" for count, error in enumerate(exception.errors())]
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
return cls(
|
|
27
|
+
message=errors,
|
|
28
|
+
filename=cursor.location.file.name if cursor.location.file else "unknown",
|
|
29
|
+
line=cursor.location.line if cursor.location.line else -1,
|
|
30
|
+
column=cursor.location.column if cursor.location.column else -1,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
@classmethod
|
|
34
|
+
def from_cursor(cls, message, cursor):
|
|
35
|
+
return cls(
|
|
36
|
+
message=message,
|
|
37
|
+
filename=cursor.location.file.name if cursor.location.file else "unknown",
|
|
38
|
+
line=cursor.location.line if cursor.location.line else -1,
|
|
39
|
+
column=cursor.location.column if cursor.location.column else -1,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
def __str__(self):
|
|
43
|
+
return (
|
|
44
|
+
f"{self.filename}; line: {self.line}, "
|
|
45
|
+
f"column: {self.column}, error: {self.message}"
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class UnsupportedDatatype(ParseError):
|
|
50
|
+
@classmethod
|
|
51
|
+
def from_cursor(cls, cursor, message=None):
|
|
52
|
+
if not message:
|
|
53
|
+
message = f"Unsupported datatype({cursor.type.spelling})"
|
|
54
|
+
|
|
55
|
+
return cls(
|
|
56
|
+
message=message,
|
|
57
|
+
filename=cursor.location.file.name if cursor.location.file else "unknown",
|
|
58
|
+
line=cursor.location.line if cursor.location.line else -1,
|
|
59
|
+
column=cursor.location.column if cursor.location.column else -1,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class TransformationError(Exception):
|
|
64
|
+
"""Something went wrong during a yid-transformation"""
|
|
65
|
+
|
|
66
|
+
pass
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class ToolError(Exception):
|
|
70
|
+
"""Exception raised when a tool exists with non-zero returncode"""
|
|
71
|
+
|
|
72
|
+
pass
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class InvalidModelData(Exception):
|
|
76
|
+
"""The data given to entity instantion is not valid"""
|
|
77
|
+
|
|
78
|
+
pass
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class CSubsetIntegrityError(ParseError):
|
|
82
|
+
"""
|
|
83
|
+
To provide meaningful error messages when a C file uses unsupported language
|
|
84
|
+
features or violates Yace conventions, errors are captured by C Subset Integrity
|
|
85
|
+
Errors, which inherit from this class.
|
|
86
|
+
|
|
87
|
+
This error itself is typically not instantiated directly; instead, errors inherit
|
|
88
|
+
from it. The error message is generated by reusing the description from the
|
|
89
|
+
docstring.
|
|
90
|
+
|
|
91
|
+
Integrity errors inherit from ParseError and use the docstring to provide the error
|
|
92
|
+
message.
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
@classmethod
|
|
96
|
+
def from_cursor(cls, message, cursor):
|
|
97
|
+
return cls.from_cursor(cls.__doc__, cursor)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class InvalidFunctionPointerSymbol(CSubsetIntegrityError):
|
|
101
|
+
"""
|
|
102
|
+
Function pointers must be named with a suffix of: '_cb', '_callback', '_fn', '_fun',
|
|
103
|
+
'_func', or '_function'. The function-pointer found here breaks this convention.
|
|
104
|
+
"""
|
|
105
|
+
|
|
106
|
+
pass
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class MissingFunctionPointerArgumentSymbol(CSubsetIntegrityError):
|
|
110
|
+
"""
|
|
111
|
+
Function pointers arguments must have symbol names. The function-pointer found here
|
|
112
|
+
break this convention.
|
|
113
|
+
"""
|
|
114
|
+
|
|
115
|
+
pass
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class MissingDocstring(CSubsetIntegrityError):
|
|
119
|
+
"""
|
|
120
|
+
To provide usable APIs and their bindings, proper documentation of data structures,
|
|
121
|
+
magic values, functions, and their arguments is required. This error occurs when a
|
|
122
|
+
docstring for the entity in question is missing.
|
|
123
|
+
"""
|
|
124
|
+
|
|
125
|
+
pass
|
yace/ir/__init__.py
ADDED
|
File without changes
|
yace/ir/base.py
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"""
|
|
2
|
+
The entities in the **yace** Interface Definition are enherit the
|
|
3
|
+
:class:`.Entity` and mix-in common attributes.
|
|
4
|
+
|
|
5
|
+
* Base-classes: :class:`.Entity` / :class:`.Typespec`
|
|
6
|
+
|
|
7
|
+
* Mixin-attributes: :class:`.Named` / :class:`.Documented` / :class:`.Typed`
|
|
8
|
+
|
|
9
|
+
The above are defined below.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import re
|
|
13
|
+
from typing import Dict, Optional
|
|
14
|
+
|
|
15
|
+
from pydantic import BaseModel, Field
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Docstring(BaseModel):
|
|
19
|
+
"""
|
|
20
|
+
An enrichment of a raw-comment-text
|
|
21
|
+
|
|
22
|
+
The raw comment-text is parsed into the components of brief, description, and tags.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
brief: str
|
|
26
|
+
description: str
|
|
27
|
+
tags: Dict[str, Dict[str, str]]
|
|
28
|
+
|
|
29
|
+
@classmethod
|
|
30
|
+
def from_cursor(cls, cursor):
|
|
31
|
+
data = {"brief": "", "description": "", "tags": {}}
|
|
32
|
+
|
|
33
|
+
raw_comment = ""
|
|
34
|
+
if cursor.raw_comment:
|
|
35
|
+
raw_comment = ("".join(list(cursor.raw_comment))).strip()
|
|
36
|
+
|
|
37
|
+
if raw_comment.startswith("///<"):
|
|
38
|
+
data["brief"] = raw_comment[4:].strip()
|
|
39
|
+
return cls(**data)
|
|
40
|
+
|
|
41
|
+
# Remove comment markers (/**, */, and leading *)
|
|
42
|
+
cleaned_docstring = re.sub(r"\/\*\*|\*\/", "", raw_comment)
|
|
43
|
+
cleaned_docstring = re.sub(
|
|
44
|
+
r"^\s*\*\s?", "", cleaned_docstring, flags=re.MULTILINE
|
|
45
|
+
).strip()
|
|
46
|
+
|
|
47
|
+
# Split into lines
|
|
48
|
+
lines = cleaned_docstring.split("\n")
|
|
49
|
+
|
|
50
|
+
# Extract brief description (first non-empty line)
|
|
51
|
+
brief_description = lines[0].strip()
|
|
52
|
+
data["brief"] = brief_description
|
|
53
|
+
|
|
54
|
+
# Extract full description (everything after the first line,
|
|
55
|
+
# before any tag like @param/@return
|
|
56
|
+
description_lines = []
|
|
57
|
+
for line in lines[1:]:
|
|
58
|
+
if line.strip().startswith("@"):
|
|
59
|
+
break
|
|
60
|
+
if line.strip():
|
|
61
|
+
description_lines.append(line.strip())
|
|
62
|
+
|
|
63
|
+
data["description"] = " ".join(description_lines)
|
|
64
|
+
|
|
65
|
+
# Generic tag pattern to match any tag starting with @
|
|
66
|
+
tag_pattern = re.compile(r"@(\w+)\s+(\w+)?\s*(.*?)\n\s*\*", re.DOTALL)
|
|
67
|
+
matches = re.findall(tag_pattern, raw_comment)
|
|
68
|
+
|
|
69
|
+
tags = {}
|
|
70
|
+
for tag, param, desc in matches:
|
|
71
|
+
if tag not in tags:
|
|
72
|
+
tags[tag] = {}
|
|
73
|
+
if param:
|
|
74
|
+
tags[tag][param] = desc.strip()
|
|
75
|
+
else:
|
|
76
|
+
tags[tag] = desc.strip()
|
|
77
|
+
|
|
78
|
+
data["tags"] = tags
|
|
79
|
+
|
|
80
|
+
return data
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class Entity(BaseModel):
|
|
84
|
+
"""
|
|
85
|
+
A baseclass for all entities in the Yace Interface Definition Language.
|
|
86
|
+
|
|
87
|
+
* label; list of labels
|
|
88
|
+
* annotations; User-defined annotations of any kind
|
|
89
|
+
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
key: str # The Yace-IR constructor keyword
|
|
93
|
+
ant: Optional[dict] = Field(default_factory=dict) # User annotations
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class Documented(BaseModel):
|
|
97
|
+
"""
|
|
98
|
+
Attribute-mixin; adding a required 'doc' describing the :class:`.Entity`
|
|
99
|
+
"""
|
|
100
|
+
|
|
101
|
+
doc: Docstring
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class Named(BaseModel):
|
|
105
|
+
"""
|
|
106
|
+
Attribute-mixin; adding a required 'sym' to be used as identifier
|
|
107
|
+
"""
|
|
108
|
+
|
|
109
|
+
sym: str
|
|
110
|
+
|
|
111
|
+
def is_valid_sym(self):
|
|
112
|
+
"""Check whether the symbol is a valid C identifier"""
|
|
113
|
+
|
|
114
|
+
if self.sym is None:
|
|
115
|
+
return False, f"Invalid attr: sym == None; for {self.as_dict()}"
|
|
116
|
+
|
|
117
|
+
match = re.match(r"[_a-z][_a-z0-9]{0,30}", str(self.sym))
|
|
118
|
+
if match:
|
|
119
|
+
return True, "OK"
|
|
120
|
+
|
|
121
|
+
return False, f"Invalid attr: '{self.sym}'; for {self.as_dict()}"
|
yace/ir/constants.py
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
The **yace** constants consists of:
|
|
4
|
+
|
|
5
|
+
* Constant Values:
|
|
6
|
+
|
|
7
|
+
* :class:`yace.ir.constants.String`
|
|
8
|
+
* :class:`yace.ir.constants.Dec`
|
|
9
|
+
* :class:`yace.ir.constants.Hex`
|
|
10
|
+
|
|
11
|
+
* Symbolic Constants:
|
|
12
|
+
|
|
13
|
+
* :class:`yace.ir.constants.Define`
|
|
14
|
+
|
|
15
|
+
* Enum Types:
|
|
16
|
+
|
|
17
|
+
* :class:`yace.ir.constants.Enum`
|
|
18
|
+
* :class:`yace.ir.constants.EnumValue`
|
|
19
|
+
|
|
20
|
+
Their ir representation follows below.
|
|
21
|
+
"""
|
|
22
|
+
from typing import List, Union
|
|
23
|
+
|
|
24
|
+
from .base import Documented, Entity, Named
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class String(Entity):
|
|
28
|
+
"""
|
|
29
|
+
Representation of a literal string e.g. in C::
|
|
30
|
+
|
|
31
|
+
"this is a string-literal"
|
|
32
|
+
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
key: str = "str"
|
|
36
|
+
lit: str
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class Hex(Entity):
|
|
40
|
+
"""
|
|
41
|
+
Representation of a hexadecimal constant
|
|
42
|
+
|
|
43
|
+
The emitted C code should be hexadecimal notation of literal integer
|
|
44
|
+
values, such as those used by :class:`yace.ir.constants.Define` and
|
|
45
|
+
:class:`yace.ir.enumtypes.Enum`::
|
|
46
|
+
|
|
47
|
+
#define FOO 0xACDC
|
|
48
|
+
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
key: str = "hex"
|
|
52
|
+
lit: int
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class Dec(Entity):
|
|
56
|
+
"""Representation of a an integer literal in decimal"""
|
|
57
|
+
|
|
58
|
+
key: str = "dec"
|
|
59
|
+
lit: int
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class Define(Entity, Named):
|
|
63
|
+
"""
|
|
64
|
+
C MACROS, one of biggest headaches when it comes to FFI interfaces,
|
|
65
|
+
especially for interpreted languages such as Python, since
|
|
66
|
+
MACRO-expressions have no symbols to "call" as these are all
|
|
67
|
+
pre-preprocossed and gone thus, no longer available.
|
|
68
|
+
|
|
69
|
+
Thus, **yace** only support the definition of MACRO values, such as::
|
|
70
|
+
|
|
71
|
+
#define SOME_GLOBALLY_CONSTANT 0xACDC
|
|
72
|
+
|
|
73
|
+
Definitions such as these to translate fairly when into equivalent globally
|
|
74
|
+
available and constant constructs in other languages
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
key: str = "define"
|
|
78
|
+
val: Union[String, Hex, Dec]
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class EnumValue(Entity, Named, Documented):
|
|
82
|
+
"""
|
|
83
|
+
Representation of values in :class:`yace.ir.constants.Enum`.
|
|
84
|
+
|
|
85
|
+
By default, the C emitter produces::
|
|
86
|
+
|
|
87
|
+
SYMBOL = lit.val
|
|
88
|
+
|
|
89
|
+
for the :class:`yace.ir.constants.EnumValue`.
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
key: str = "enum_value"
|
|
93
|
+
val: Union[Hex, Dec]
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class Enum(Entity, Named, Documented):
|
|
97
|
+
"""
|
|
98
|
+
Representation of enumeration values, note that literals on the
|
|
99
|
+
:class:`yace.ir.constants.EnumValue` are optional, an
|
|
100
|
+
:class:`yace.ir.constants.Enum` with **non** literal
|
|
101
|
+
:class:`yace.ir.constants.EnumValue` looks like::
|
|
102
|
+
|
|
103
|
+
enum example {
|
|
104
|
+
FOO,
|
|
105
|
+
BAR,
|
|
106
|
+
BAZ,
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
Whereas, an :class:`yace.ir.constants.Enum` with **literal**
|
|
110
|
+
:class:`yace.ir.constants.EnumValue` looks like::
|
|
111
|
+
|
|
112
|
+
enum example {
|
|
113
|
+
FOO = 0xACDC,
|
|
114
|
+
BAR = 0x1337,
|
|
115
|
+
BAZ = 0xBEEF,
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
for the :class:`yace.ir.constants.EnumValue``.
|
|
119
|
+
"""
|
|
120
|
+
|
|
121
|
+
key: str = "enum"
|
|
122
|
+
members: List[EnumValue]
|
|
123
|
+
|
|
124
|
+
def is_valid_members(self):
|
|
125
|
+
"""Checks whether the :class:`yace.ir.constants.Enum` is valid"""
|
|
126
|
+
|
|
127
|
+
invalid_types = [type(m) for m in self.members if not isinstance(m, EnumValue)]
|
|
128
|
+
if len(invalid_types):
|
|
129
|
+
return (
|
|
130
|
+
False,
|
|
131
|
+
f"Invalid types in {self.sym}.members; "
|
|
132
|
+
"expecting: EnumValue, got: [{invalid_types}]",
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
if String in [type(m) for m in self.members]:
|
|
136
|
+
return (False, "Enum-values must be numeral-types")
|
|
137
|
+
|
|
138
|
+
return (True, None)
|