Skip to content

Commit 96897a0

Browse files
committed
Swift: implement python schema
The information that was contained in `schema.yml` is now in `swift/schema.py`, which allows a more integrated IDE experience for writing and navigating it. Another minor change is that `schema.Class` now has a `str` `group` field instead of a `pathlib.Path` `dir` one.
1 parent caaf9e7 commit 96897a0

File tree

20 files changed

+1026
-1711
lines changed

20 files changed

+1026
-1711
lines changed

swift/.pep8

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[pep8]
2+
ignore = E302
3+
max_line_length = 120

swift/BUILD.bazel

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,18 @@ load("@rules_pkg//:install.bzl", "pkg_install")
33
load("//:defs.bzl", "codeql_platform")
44
load("//misc/bazel:pkg_runfiles.bzl", "pkg_runfiles")
55

6+
filegroup(
7+
name = "schema",
8+
srcs = ["schema.py"],
9+
visibility = ["//swift:__subpackages__"],
10+
)
11+
12+
filegroup(
13+
name = "schema_includes",
14+
srcs = glob(["*.dbscheme"]),
15+
visibility = ["//swift:__subpackages__"],
16+
)
17+
618
pkg_files(
719
name = "dbscheme_files",
820
srcs = [

swift/codegen/.pep8

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,2 @@
11
[pep8]
22
max_line_length = 120
3-
ignore = E302

swift/codegen/BUILD.bazel

Lines changed: 5 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,16 @@
11
load("@swift_codegen_deps//:requirements.bzl", "requirement")
22

3-
filegroup(
4-
name = "schema",
5-
srcs = ["schema.yml"],
6-
visibility = ["//swift:__subpackages__"],
7-
)
8-
9-
filegroup(
10-
name = "schema_includes",
11-
srcs = glob(["*.dbscheme"]),
12-
visibility = ["//swift:__subpackages__"],
13-
)
14-
153
py_binary(
164
name = "codegen",
175
srcs = ["codegen.py"],
186
data = [
19-
":schema",
20-
":schema_includes",
7+
"//swift:schema",
8+
"//swift:schema_includes",
219
"//swift/codegen/templates:cpp",
2210
"//swift/codegen/templates:trap",
2311
],
2412
visibility = ["//swift:__subpackages__"],
25-
deps = ["//swift/codegen/generators"],
13+
deps = [
14+
"//swift/codegen/generators",
15+
],
2616
)

swift/codegen/codegen.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ def _parse_args() -> argparse.Namespace:
2323
p.add_argument("--swift-dir", type=_abspath, default=paths.swift_dir,
2424
help="the directory that should be regarded as the root of the swift codebase. Used to compute QL "
2525
"imports and in some comments (default %(default)s)")
26-
p.add_argument("--schema", type=_abspath, default=paths.swift_dir / "codegen/schema.yml",
26+
p.add_argument("--schema", type=_abspath, default=paths.swift_dir / "schema.py",
2727
help="input schema file (default %(default)s)")
2828
p.add_argument("--dbscheme", type=_abspath, default=paths.swift_dir / "ql/lib/swift.dbscheme",
2929
help="output file for dbscheme generation, input file for trap generation (default %(default)s)")

swift/codegen/generators/cppgen.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -70,16 +70,16 @@ def _get_class(self, name: str) -> cpp.Class:
7070
)
7171

7272
def get_classes(self):
73-
ret = {pathlib.Path(): []}
73+
ret = {'': []}
7474
for k, cls in self._classmap.items():
75-
ret.setdefault(cls.dir, []).append(self._get_class(cls.name))
75+
ret.setdefault(cls.group, []).append(self._get_class(cls.name))
7676
return ret
7777

7878

7979
def generate(opts, renderer):
8080
assert opts.cpp_output
81-
processor = Processor(schema.load(opts.schema).classes)
81+
processor = Processor(schema.load_file(opts.schema).classes)
8282
out = opts.cpp_output
8383
for dir, classes in processor.get_classes().items():
84-
include_parent = (dir != pathlib.Path())
85-
renderer.render(cpp.ClassList(classes, opts.schema, include_parent), out / dir / "TrapClasses")
84+
renderer.render(cpp.ClassList(classes, opts.schema,
85+
include_parent=bool(dir)), out / dir / "TrapClasses")

swift/codegen/generators/dbschemegen.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,11 @@
1414
The type hierarchy will be translated to corresponding `union` declarations.
1515
"""
1616

17-
import pathlib
18-
1917
import inflection
2018

2119
from swift.codegen.lib import schema
2220
from swift.codegen.lib.dbscheme import *
21+
from typing import Set, List
2322

2423
log = logging.getLogger(__name__)
2524

@@ -35,7 +34,7 @@ def cls_to_dbscheme(cls: schema.Class):
3534
""" Yield all dbscheme entities needed to model class `cls` """
3635
if cls.derived:
3736
yield Union(dbtype(cls.name), (dbtype(c) for c in cls.derived))
38-
dir = cls.dir if cls.dir != pathlib.Path() else None
37+
dir = pathlib.Path(cls.group) if cls.group else None
3938
# output a table specific to a class only if it is a leaf class or it has 1-to-1 properties
4039
# Leaf classes need a table to bind the `@` ids
4140
# 1-to-1 properties are added to a class specific table
@@ -104,7 +103,7 @@ def generate(opts, renderer):
104103
input = opts.schema
105104
out = opts.dbscheme
106105

107-
data = schema.load(input)
106+
data = schema.load_file(input)
108107

109108
dbscheme = Scheme(src=input.relative_to(opts.swift_dir),
110109
includes=get_includes(data, include_dir=input.parent, swift_dir=opts.swift_dir),

swift/codegen/generators/qlgen.py

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,10 @@ class RootElementHasChildren(Error):
5151
pass
5252

5353

54+
class NoClasses(Error):
55+
pass
56+
57+
5458
def get_ql_property(cls: schema.Class, prop: schema.Property, prev_child: str = "") -> ql.Property:
5559
args = dict(
5660
type=prop.type if not prop.is_predicate else "predicate",
@@ -103,7 +107,7 @@ def get_ql_class(cls: schema.Class, lookup: typing.Dict[str, schema.Class]):
103107
bases=cls.bases,
104108
final=not cls.derived,
105109
properties=properties,
106-
dir=cls.dir,
110+
dir=pathlib.Path(cls.group or ""),
107111
ipa=bool(cls.ipa),
108112
**pragmas,
109113
)
@@ -127,7 +131,7 @@ def get_ql_ipa_class_db(name: str) -> ql.Synth.FinalClassDb:
127131
def get_ql_ipa_class(cls: schema.Class):
128132
if cls.derived:
129133
return ql.Synth.NonFinalClass(name=cls.name, derived=sorted(cls.derived),
130-
root=(cls.name == schema.root_class_name))
134+
root=not cls.bases)
131135
if cls.ipa and cls.ipa.from_class is not None:
132136
source = cls.ipa.from_class
133137
get_ql_ipa_class_db(source).subtract_type(cls.name)
@@ -253,12 +257,14 @@ def generate(opts, renderer):
253257
existing |= {q for q in test_out.rglob("*.ql")}
254258
existing |= {q for q in test_out.rglob(missing_test_source_filename)}
255259

256-
data = schema.load(input)
260+
data = schema.load_file(input)
257261

258262
classes = {name: get_ql_class(cls, data.classes) for name, cls in data.classes.items()}
259-
# element root is absent in tests
260-
if schema.root_class_name in classes and classes[schema.root_class_name].has_children:
261-
raise RootElementHasChildren
263+
if not classes:
264+
raise NoClasses
265+
root = next(iter(classes.values()))
266+
if root.has_children:
267+
raise RootElementHasChildren(root)
262268

263269
imports = {}
264270

@@ -288,10 +294,10 @@ def generate(opts, renderer):
288294
for c in data.classes.values():
289295
if _should_skip_qltest(c, data.classes):
290296
continue
291-
test_dir = test_out / c.dir / c.name
297+
test_dir = test_out / c.group / c.name
292298
test_dir.mkdir(parents=True, exist_ok=True)
293299
if not any(test_dir.glob("*.swift")):
294-
log.warning(f"no test source in {c.dir / c.name}")
300+
log.warning(f"no test source in {test_dir.relative_to(test_out)}")
295301
renderer.render(ql.MissingTestInstructions(),
296302
test_dir / missing_test_source_filename)
297303
continue
@@ -308,12 +314,12 @@ def generate(opts, renderer):
308314
constructor_imports = []
309315
ipa_constructor_imports = []
310316
stubs = {}
311-
for cls in sorted(data.classes.values(), key=lambda cls: (cls.dir, cls.name)):
317+
for cls in sorted(data.classes.values(), key=lambda cls: (cls.group, cls.name)):
312318
ipa_type = get_ql_ipa_class(cls)
313319
if ipa_type.is_final:
314320
final_ipa_types.append(ipa_type)
315321
if ipa_type.has_params:
316-
stub_file = stub_out / cls.dir / f"{cls.name}Constructor.qll"
322+
stub_file = stub_out / cls.group / f"{cls.name}Constructor.qll"
317323
if not stub_file.is_file() or _is_generated_stub(stub_file):
318324
# stub rendering must be postponed as we might not have yet all subtracted ipa types in `ipa_type`
319325
stubs[stub_file] = ql.Synth.ConstructorStub(ipa_type)
@@ -326,7 +332,7 @@ def generate(opts, renderer):
326332

327333
for stub_file, data in stubs.items():
328334
renderer.render(data, stub_file)
329-
renderer.render(ql.Synth.Types(schema.root_class_name, final_ipa_types, non_final_ipa_types), out / "Synth.qll")
335+
renderer.render(ql.Synth.Types(root.name, final_ipa_types, non_final_ipa_types), out / "Synth.qll")
330336
renderer.render(ql.ImportList(constructor_imports), out / "SynthConstructors.qll")
331337
renderer.render(ql.ImportList(ipa_constructor_imports), out / "PureSynthConstructors.qll")
332338

swift/codegen/lib/schema/defs.py

Lines changed: 121 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,121 @@
1-
optional = list = int = string = boolean = predicate = include = group = child = synth = qltest = cpp = object()
1+
from typing import Callable as _Callable, Union as _Union
2+
from functools import singledispatch as _singledispatch
3+
from swift.codegen.lib import schema as _schema
4+
import inspect as _inspect
5+
6+
7+
class _ChildModifier(_schema.PropertyModifier):
8+
def modify(self, prop: _schema.Property):
9+
if prop.type is None or prop.type[0].islower():
10+
raise _schema.Error("Non-class properties cannot be children")
11+
prop.is_child = True
12+
13+
14+
def include(source: str):
15+
# add to `includes` variable in calling context
16+
_inspect.currentframe().f_back.f_locals.setdefault(
17+
"__includes", []).append(source)
18+
19+
20+
class _Pragma(_schema.PropertyModifier):
21+
""" A class or property pragma.
22+
For properties, it functions similarly to a `_PropertyModifier` with `|`, adding the pragma.
23+
For schema classes it acts as a python decorator with `@`.
24+
"""
25+
26+
def __init__(self, pragma):
27+
self.pragma = pragma
28+
29+
def modify(self, prop: _schema.Property):
30+
prop.pragmas.append(self.pragma)
31+
32+
def __call__(self, cls: type) -> type:
33+
""" use this pragma as a decorator on classes """
34+
if "pragmas" in cls.__dict__: # not using hasattr as we don't want to land on inherited pragmas
35+
cls.pragmas.append(self.pragma)
36+
else:
37+
cls.pragmas = [self.pragma]
38+
return cls
39+
40+
41+
class _Optionalizer(_schema.PropertyModifier):
42+
def modify(self, prop: _schema.Property):
43+
K = _schema.Property.Kind
44+
if prop.kind != K.SINGLE:
45+
raise _schema.Error(
46+
"Optional should only be applied to simple property types")
47+
prop.kind = K.OPTIONAL
48+
49+
50+
class _Listifier(_schema.PropertyModifier):
51+
def modify(self, prop: _schema.Property):
52+
K = _schema.Property.Kind
53+
if prop.kind == K.SINGLE:
54+
prop.kind = K.REPEATED
55+
elif prop.kind == K.OPTIONAL:
56+
prop.kind = K.REPEATED_OPTIONAL
57+
else:
58+
raise _schema.Error(
59+
"Repeated should only be applied to simple or optional property types")
60+
61+
62+
class _TypeModifier:
63+
""" Modifies types using get item notation """
64+
65+
def __init__(self, modifier: _schema.PropertyModifier):
66+
self.modifier = modifier
67+
68+
def __getitem__(self, item):
69+
return item | self.modifier
70+
71+
72+
class _Namespace:
73+
""" simple namespacing mechanism """
74+
75+
def __init__(self, **kwargs):
76+
self.__dict__.update(kwargs)
77+
78+
79+
_ClassDecorator = _Callable[[type], type]
80+
81+
82+
def _annotate(**kwargs) -> _ClassDecorator:
83+
def f(cls: type) -> type:
84+
for k, v in kwargs.items():
85+
setattr(cls, k, v)
86+
return cls
87+
88+
return f
89+
90+
91+
boolean = "boolean"
92+
int = "int"
93+
string = "string"
94+
95+
predicate = _schema.predicate_marker
96+
optional = _TypeModifier(_Optionalizer())
97+
list = _TypeModifier(_Listifier())
98+
99+
child = _ChildModifier()
100+
101+
qltest = _Namespace(
102+
skip=_Pragma("qltest_skip"),
103+
collapse_hierarchy=_Pragma("qltest_collapse_hierarchy"),
104+
uncollapse_hierarchy=_Pragma("qltest_uncollapse_hierarchy"),
105+
)
106+
107+
cpp = _Namespace(
108+
skip=_Pragma("cpp_skip"),
109+
)
110+
111+
112+
def group(name: str = "") -> _ClassDecorator:
113+
return _annotate(group=name)
114+
115+
116+
synth = _Namespace(
117+
from_class=lambda ref: _annotate(ipa=_schema.IpaInfo(
118+
from_class=_schema.get_type_name(ref))),
119+
on_arguments=lambda **kwargs: _annotate(
120+
ipa=_schema.IpaInfo(on_arguments={k: _schema.get_type_name(t) for k, t in kwargs.items()}))
121+
)

0 commit comments

Comments
 (0)