Skip to content

Commit 9232b28

Browse files
authored
Merge pull request #9891 from github/redsun82/swift-first-prototype-of-generated-ipa-layer
Swift: first prototype of a generated IPA layer
2 parents 8fb5714 + 9cd2ae2 commit 9232b28

File tree

568 files changed

+9821
-2054
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

568 files changed

+9821
-2054
lines changed

swift/codegen/generators/cppgen.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ def get_classes(self):
8383

8484
def generate(opts, renderer):
8585
assert opts.cpp_output
86-
processor = Processor({cls.name: cls for cls in schema.load(opts.schema).classes})
86+
processor = Processor(schema.load(opts.schema).classes)
8787
out = opts.cpp_output
8888
for dir, classes in processor.get_classes().items():
8989
include_parent = (dir != pathlib.Path())

swift/codegen/generators/dbschemegen.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ def cls_to_dbscheme(cls: schema.Class):
8888

8989

9090
def get_declarations(data: schema.Schema):
91-
return [d for cls in data.classes for d in cls_to_dbscheme(cls)]
91+
return [d for cls in data.classes.values() for d in cls_to_dbscheme(cls)]
9292

9393

9494
def get_includes(data: schema.Schema, include_dir: pathlib.Path, swift_dir: pathlib.Path):

swift/codegen/generators/qlgen.py

Lines changed: 122 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
#!/usr/bin/env python3
2+
# TODO this should probably be split in different generators now: ql, qltest, maybe qlipa
23

34
import logging
45
import pathlib
@@ -8,6 +9,7 @@
89
import itertools
910

1011
import inflection
12+
from toposort import toposort_flatten
1113

1214
from swift.codegen.lib import schema, ql
1315

@@ -27,59 +29,88 @@ class ModifiedStubMarkedAsGeneratedError(Error):
2729
pass
2830

2931

30-
def get_ql_property(cls: schema.Class, prop: schema.Property):
31-
common_args = dict(
32+
def get_ql_property(cls: schema.Class, prop: schema.Property) -> ql.Property:
33+
args = dict(
3234
type=prop.type if not prop.is_predicate else "predicate",
3335
qltest_skip="qltest_skip" in prop.pragmas,
3436
is_child=prop.is_child,
3537
is_optional=prop.is_optional,
3638
is_predicate=prop.is_predicate,
3739
)
3840
if prop.is_single:
39-
return ql.Property(
40-
**common_args,
41+
args.update(
4142
singular=inflection.camelize(prop.name),
4243
tablename=inflection.tableize(cls.name),
43-
tableparams=[
44-
"this"] + ["result" if p is prop else "_" for p in cls.properties if p.is_single],
44+
tableparams=["this"] + ["result" if p is prop else "_" for p in cls.properties if p.is_single],
4545
)
4646
elif prop.is_repeated:
47-
return ql.Property(
48-
**common_args,
47+
args.update(
4948
singular=inflection.singularize(inflection.camelize(prop.name)),
5049
plural=inflection.pluralize(inflection.camelize(prop.name)),
5150
tablename=inflection.tableize(f"{cls.name}_{prop.name}"),
5251
tableparams=["this", "index", "result"],
5352
)
5453
elif prop.is_optional:
55-
return ql.Property(
56-
**common_args,
54+
args.update(
5755
singular=inflection.camelize(prop.name),
5856
tablename=inflection.tableize(f"{cls.name}_{prop.name}"),
5957
tableparams=["this", "result"],
6058
)
6159
elif prop.is_predicate:
62-
return ql.Property(
63-
**common_args,
64-
singular=inflection.camelize(
65-
prop.name, uppercase_first_letter=False),
60+
args.update(
61+
singular=inflection.camelize(prop.name, uppercase_first_letter=False),
6662
tablename=inflection.underscore(f"{cls.name}_{prop.name}"),
6763
tableparams=["this"],
6864
)
65+
else:
66+
raise ValueError(f"unknown property kind for {prop.name} from {cls.name}")
67+
return ql.Property(**args)
6968

7069

71-
def get_ql_class(cls: schema.Class):
70+
def get_ql_class(cls: schema.Class, lookup: typing.Dict[str, schema.Class]):
7271
pragmas = {k: True for k in cls.pragmas if k.startswith("ql")}
7372
return ql.Class(
7473
name=cls.name,
7574
bases=cls.bases,
7675
final=not cls.derived,
7776
properties=[get_ql_property(cls, p) for p in cls.properties],
7877
dir=cls.dir,
78+
ipa=bool(cls.ipa),
7979
**pragmas,
8080
)
8181

8282

83+
def _to_db_type(x: str) -> str:
84+
if x[0].isupper():
85+
return "Raw::" + x
86+
return x
87+
88+
89+
_final_db_class_lookup = {}
90+
91+
92+
def get_ql_ipa_class_db(name: str) -> ql.Synth.FinalClassDb:
93+
return _final_db_class_lookup.setdefault(name, ql.Synth.FinalClassDb(name=name,
94+
params=[
95+
ql.Synth.Param("id", _to_db_type(name))]))
96+
97+
98+
def get_ql_ipa_class(cls: schema.Class):
99+
if cls.derived:
100+
return ql.Synth.NonFinalClass(name=cls.name, derived=sorted(cls.derived),
101+
root=(cls.name == schema.root_class_name))
102+
if cls.ipa and cls.ipa.from_class is not None:
103+
source = cls.ipa.from_class
104+
get_ql_ipa_class_db(source).subtract_type(cls.name)
105+
return ql.Synth.FinalClassDerivedIpa(name=cls.name,
106+
params=[ql.Synth.Param("id", _to_db_type(source))])
107+
if cls.ipa and cls.ipa.on_arguments is not None:
108+
return ql.Synth.FinalClassFreshIpa(name=cls.name,
109+
params=[ql.Synth.Param(k, _to_db_type(v))
110+
for k, v in cls.ipa.on_arguments.items()])
111+
return get_ql_ipa_class_db(cls.name)
112+
113+
83114
def get_import(file: pathlib.Path, swift_dir: pathlib.Path):
84115
stem = file.relative_to(swift_dir / "ql/lib").with_suffix("")
85116
return str(stem).replace("/", ".")
@@ -96,10 +127,10 @@ def get_classes_used_by(cls: ql.Class):
96127
return sorted(set(t for t in get_types_used_by(cls) if t[0].isupper()))
97128

98129

99-
_generated_stub_re = re.compile(r"private import .*\n\nclass \w+ extends \w+ \{[ \n]\}", re.MULTILINE)
130+
_generated_stub_re = re.compile(r"\n*private import .*\n+class \w+ extends \w+ \{[ \n]?\}", re.MULTILINE)
100131

101132

102-
def _is_generated_stub(file):
133+
def _is_generated_stub(file: pathlib.Path) -> bool:
103134
with open(file) as contents:
104135
for line in contents:
105136
if not line.startswith("// generated"):
@@ -108,12 +139,14 @@ def _is_generated_stub(file):
108139
else:
109140
# no lines
110141
return False
111-
# one line already read, if we can read 5 other we are past the normal stub generation
112-
line_threshold = 5
113-
first_lines = list(itertools.islice(contents, line_threshold))
114-
if len(first_lines) == line_threshold or not _generated_stub_re.match("".join(first_lines)):
115-
raise ModifiedStubMarkedAsGeneratedError(
116-
f"{file.name} stub was modified but is still marked as generated")
142+
# we still do not detect modified synth constructors
143+
if not file.name.endswith("Constructor.qll"):
144+
# one line already read, if we can read 5 other we are past the normal stub generation
145+
line_threshold = 5
146+
first_lines = list(itertools.islice(contents, line_threshold))
147+
if len(first_lines) == line_threshold or not _generated_stub_re.match("".join(first_lines)):
148+
raise ModifiedStubMarkedAsGeneratedError(
149+
f"{file.name} stub was modified but is still marked as generated")
117150
return True
118151

119152

@@ -129,45 +162,53 @@ def format(codeql, files):
129162
log.debug(line.strip())
130163

131164

132-
def _get_all_properties(cls: ql.Class, lookup: typing.Dict[str, ql.Class]) -> typing.Iterable[
133-
typing.Tuple[ql.Class, ql.Property]]:
134-
for b in cls.bases:
165+
def _get_all_properties(cls: schema.Class, lookup: typing.Dict[str, schema.Class],
166+
already_seen: typing.Optional[typing.Set[int]] = None) -> \
167+
typing.Iterable[typing.Tuple[schema.Class, schema.Property]]:
168+
# deduplicate using ids
169+
if already_seen is None:
170+
already_seen = set()
171+
for b in sorted(cls.bases):
135172
base = lookup[b]
136-
for item in _get_all_properties(base, lookup):
173+
for item in _get_all_properties(base, lookup, already_seen):
137174
yield item
138175
for p in cls.properties:
139-
yield cls, p
176+
if id(p) not in already_seen:
177+
already_seen.add(id(p))
178+
yield cls, p
140179

141180

142-
def _get_all_properties_to_be_tested(cls: ql.Class, lookup: typing.Dict[str, ql.Class]) -> typing.Iterable[
143-
ql.PropertyForTest]:
144-
# deduplicate using id
145-
already_seen = set()
181+
def _get_all_properties_to_be_tested(cls: schema.Class, lookup: typing.Dict[str, schema.Class]) -> \
182+
typing.Iterable[ql.PropertyForTest]:
146183
for c, p in _get_all_properties(cls, lookup):
147-
if not (c.qltest_skip or p.qltest_skip or id(p) in already_seen):
148-
already_seen.add(id(p))
184+
if not ("qltest_skip" in c.pragmas or "qltest_skip" in p.pragmas):
185+
# TODO here operations are duplicated, but should be better if we split ql and qltest generation
186+
p = get_ql_property(c, p)
149187
yield ql.PropertyForTest(p.getter, p.type, p.is_single, p.is_predicate, p.is_repeated)
150188

151189

190+
def _partition_iter(x, pred):
191+
x1, x2 = itertools.tee(x)
192+
return filter(pred, x1), itertools.filterfalse(pred, x2)
193+
194+
152195
def _partition(l, pred):
153196
""" partitions a list according to boolean predicate """
154-
res = ([], [])
155-
for x in l:
156-
res[not pred(x)].append(x)
157-
return res
197+
return map(list, _partition_iter(l, pred))
158198

159199

160-
def _is_in_qltest_collapsed_hierachy(cls: ql.Class, lookup: typing.Dict[str, ql.Class]):
161-
return cls.qltest_collapse_hierarchy or _is_under_qltest_collapsed_hierachy(cls, lookup)
200+
def _is_in_qltest_collapsed_hierachy(cls: schema.Class, lookup: typing.Dict[str, schema.Class]):
201+
return "qltest_collapse_hierarchy" in cls.pragmas or _is_under_qltest_collapsed_hierachy(cls, lookup)
162202

163203

164-
def _is_under_qltest_collapsed_hierachy(cls: ql.Class, lookup: typing.Dict[str, ql.Class]):
165-
return not cls.qltest_uncollapse_hierarchy and any(
204+
def _is_under_qltest_collapsed_hierachy(cls: schema.Class, lookup: typing.Dict[str, schema.Class]):
205+
return "qltest_uncollapse_hierarchy" not in cls.pragmas and any(
166206
_is_in_qltest_collapsed_hierachy(lookup[b], lookup) for b in cls.bases)
167207

168208

169-
def _should_skip_qltest(cls: ql.Class, lookup: typing.Dict[str, ql.Class]):
170-
return cls.qltest_skip or not (cls.final or cls.qltest_collapse_hierarchy) or _is_under_qltest_collapsed_hierachy(
209+
def _should_skip_qltest(cls: schema.Class, lookup: typing.Dict[str, schema.Class]):
210+
return "qltest_skip" in cls.pragmas or not (
211+
cls.final or "qltest_collapse_hierarchy" in cls.pragmas) or _is_under_qltest_collapsed_hierachy(
171212
cls, lookup)
172213

173214

@@ -185,15 +226,18 @@ def generate(opts, renderer):
185226

186227
data = schema.load(input)
187228

188-
classes = [get_ql_class(cls) for cls in data.classes]
189-
lookup = {cls.name: cls for cls in classes}
190-
classes.sort(key=lambda cls: (cls.dir, cls.name))
229+
classes = {name: get_ql_class(cls, data.classes) for name, cls in data.classes.items()}
191230
imports = {}
192231

193-
for c in classes:
232+
inheritance_graph = {name: cls.bases for name, cls in data.classes.items()}
233+
db_classes = [classes[name] for name in toposort_flatten(inheritance_graph) if not classes[name].ipa]
234+
renderer.render(ql.DbClasses(db_classes), out / "Raw.qll")
235+
236+
classes_by_dir_and_name = sorted(classes.values(), key=lambda cls: (cls.dir, cls.name))
237+
for c in classes_by_dir_and_name:
194238
imports[c.name] = get_import(stub_out / c.path, opts.swift_dir)
195239

196-
for c in classes:
240+
for c in classes.values():
197241
qll = out / c.path.with_suffix(".qll")
198242
c.imports = [imports[t] for t in get_classes_used_by(c)]
199243
renderer.render(c, qll)
@@ -207,27 +251,49 @@ def generate(opts, renderer):
207251
include_file = stub_out.with_suffix(".qll")
208252
renderer.render(ql.ImportList(list(imports.values())), include_file)
209253

210-
renderer.render(ql.GetParentImplementation(
211-
classes), out / 'GetImmediateParent.qll')
254+
renderer.render(ql.GetParentImplementation(classes_by_dir_and_name), out / 'GetImmediateParent.qll')
212255

213-
for c in classes:
214-
if _should_skip_qltest(c, lookup):
256+
for c in data.classes.values():
257+
if _should_skip_qltest(c, data.classes):
215258
continue
216-
test_dir = test_out / c.path
259+
test_dir = test_out / c.dir / c.name
217260
test_dir.mkdir(parents=True, exist_ok=True)
218261
if not any(test_dir.glob("*.swift")):
219-
log.warning(f"no test source in {c.path}")
262+
log.warning(f"no test source in {c.dir / c.name}")
220263
renderer.render(ql.MissingTestInstructions(),
221264
test_dir / missing_test_source_filename)
222265
continue
223-
total_props, partial_props = _partition(_get_all_properties_to_be_tested(c, lookup),
266+
total_props, partial_props = _partition(_get_all_properties_to_be_tested(c, data.classes),
224267
lambda p: p.is_single or p.is_predicate)
225268
renderer.render(ql.ClassTester(class_name=c.name,
226269
properties=total_props), test_dir / f"{c.name}.ql")
227270
for p in partial_props:
228271
renderer.render(ql.PropertyTester(class_name=c.name,
229272
property=p), test_dir / f"{c.name}_{p.getter}.ql")
230273

274+
final_ipa_types = []
275+
non_final_ipa_types = []
276+
constructor_imports = []
277+
ipa_constructor_imports = []
278+
for cls in sorted(data.classes.values(), key=lambda cls: (cls.dir, cls.name)):
279+
ipa_type = get_ql_ipa_class(cls)
280+
if ipa_type.is_final:
281+
final_ipa_types.append(ipa_type)
282+
if ipa_type.has_params:
283+
stub_file = stub_out / cls.dir / f"{cls.name}Constructor.qll"
284+
if not stub_file.is_file() or _is_generated_stub(stub_file):
285+
renderer.render(ql.Synth.ConstructorStub(ipa_type), stub_file)
286+
constructor_import = get_import(stub_file, opts.swift_dir)
287+
constructor_imports.append(constructor_import)
288+
if ipa_type.is_ipa:
289+
ipa_constructor_imports.append(constructor_import)
290+
else:
291+
non_final_ipa_types.append(ipa_type)
292+
293+
renderer.render(ql.Synth.Types(schema.root_class_name, final_ipa_types, non_final_ipa_types), out / "Synth.qll")
294+
renderer.render(ql.ImportList(constructor_imports), out / "SynthConstructors.qll")
295+
renderer.render(ql.ImportList(ipa_constructor_imports), out / "PureSynthConstructors.qll")
296+
231297
renderer.cleanup(existing)
232298
if opts.ql_format:
233299
format(opts.codeql_binary, renderer.written)

0 commit comments

Comments
 (0)