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 ADDED
@@ -0,0 +1 @@
1
+ __version__ = "0.5.7"
yace/__main__.py ADDED
@@ -0,0 +1,3 @@
1
+ from yace.cli.yace import main
2
+
3
+ main()
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)