Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[swift2objc] Filtering Support #1730

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
23 changes: 17 additions & 6 deletions pkgs/swift2objc/lib/src/config.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import 'package:path/path.dart' as path;

import 'ast/_core/interfaces/declaration.dart';

const defaultTempDirPrefix = 'swift2objc_temp_';
const symbolgraphFileSuffix = '.symbols.json';

Expand Down Expand Up @@ -32,12 +34,21 @@ class Config {
/// intermediate files after generating the wrapper
final Uri? tempDir;

const Config({
required this.input,
required this.outputFile,
this.tempDir,
this.preamble,
});
/// Filter function to filter APIs
///
/// APIs can be filtered by name
///
/// Includes all declarations by default
final bool Function(Declaration declaration)? include;
nikeokoronkwo marked this conversation as resolved.
Show resolved Hide resolved

static bool _defaultInclude(_) => true;

const Config(
{required this.input,
required this.outputFile,
this.tempDir,
this.preamble,
this.include = Config._defaultInclude});
}

/// Used to specify the inputs in the `config` object.
Expand Down
4 changes: 2 additions & 2 deletions pkgs/swift2objc/lib/src/generate_wrapper.dart
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,8 @@ Future<void> generateWrapper(Config config) async {
};

final declarations = parseAst(symbolgraphJson);
final transformedDeclarations = transform(declarations);

final transformedDeclarations =
transform(declarations, filter: config.include);
final wrapperCode = generate(
transformedDeclarations,
moduleName: sourceModule,
Expand Down
198 changes: 198 additions & 0 deletions pkgs/swift2objc/lib/src/transformer/_core/dependencies.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
import '../../ast/_core/interfaces/declaration.dart';
import '../../ast/_core/interfaces/enum_declaration.dart';
import '../../ast/_core/interfaces/function_declaration.dart';
import '../../ast/_core/interfaces/variable_declaration.dart';
import '../../ast/_core/shared/parameter.dart';
import '../../ast/_core/shared/referred_type.dart';
import '../../ast/declarations/compounds/class_declaration.dart';
import '../../ast/declarations/compounds/members/initializer_declaration.dart';
import '../../ast/declarations/compounds/protocol_declaration.dart';
import '../../ast/declarations/compounds/struct_declaration.dart';

/// Gets the type name from a string type by removing other characters like
Set<String> _getTypeNames(String type) {
// Remove optional markers (?) and square brackets ([])
type = type.replaceAll(RegExp(r'\?|!|[\[\]]'), '');

// Remove annotations (words starting with @)
type = type.replaceAll(RegExp(r'@\w+'), '');

// Extract unique type names using regex
final matches = RegExp(r'\b\w+\b').allMatches(type);

// Return unique type names as a set
return matches.map((match) => match.group(0)!).toSet();
}

// TODO: Type restrictions have not yet been implemented in system
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODOs should link to a bug. If there isn't one, file one first, then link to the bug like TODO(https://github.com/dart-lang/native/issues/...): Type restrictions...

Same below

class DependencyVisitor {
Set<String> visitDeclaration(Declaration decl, [Set<String>? context]) {
final cont = context ??= {};

// switch between declarations
if (decl is ClassDeclaration)
visitClass(decl, cont);
else if (decl is ProtocolDeclaration)
visitProtocol(decl, cont);
else if (decl is StructDeclaration)
visitStruct(decl, cont);
else if (decl is FunctionDeclaration)
visitFunction(decl, cont);
else if (decl is VariableDeclaration)
visitVariable(decl, cont);
else if (decl is EnumDeclaration) visitEnum(decl, cont);

return cont;
}

Set<String> visitEnum(EnumDeclaration decl, [Set<String>? context]) {
final cont = context ??= {};

// TODO: what of raw values of enums?

// visit nested declarations
decl.nestedDeclarations.forEach((n) => visitDeclaration(n, cont));

// visit protocols
decl.conformedProtocols.forEach((p) => visitProtocol(p.declaration, cont));

// ensure generic types do not enter
cont.removeWhere((t) => decl.typeParams.map((type) => type.name).contains(t));

return cont;
}

Set<String> visitStruct(StructDeclaration decl, [Set<String>? context]) {
final cont = context ??= {};

// visit variables
decl.properties.forEach((d) => visitVariable(d, cont));

// visit methods
decl.methods.forEach((m) => visitFunction(m, cont));

// visit initializers
decl.initializers.forEach((i) => visitInitializer(i, cont));

// visit nested declarations
decl.nestedDeclarations.forEach((n) => visitDeclaration(n, cont));

// visit protocols
decl.conformedProtocols.forEach((p) => visitProtocol(p.declaration, cont));

// ensure generic types do not enter
cont.removeWhere((t) => decl.typeParams.map((type) => type.name).contains(t));

return cont;
}

Set<String> visitClass(ClassDeclaration decl, [Set<String>? context]) {
final cont = context ??= {};

// visit variables
decl.properties.forEach((d) => visitVariable(d, cont));

// visit methods
decl.methods.forEach((m) => visitFunction(m, cont));

// visit initializers
decl.initializers.forEach((i) => visitInitializer(i, cont));

// visit super if any
if (decl.superClass != null)
visitDeclaration(decl.superClass!.declaration, cont);

// visit nested declarations
decl.nestedDeclarations.forEach((n) => visitDeclaration(n, cont));

// visit protocols
decl.conformedProtocols.forEach((p) => visitProtocol(p.declaration, cont));

// ensure generic types do not enter
cont.removeWhere((t) => decl.typeParams.map((type) => type.name).contains(t));

return cont;
}

Set<String> visitProtocol(ProtocolDeclaration decl, [Set<String>? context]) {
final cont = context ??= {};

// visit variables
decl.properties.forEach((d) => visitVariable(d, cont));

// visit methods
decl.methods.forEach((m) => visitFunction(m, cont));

// visit initializers
decl.initializers.forEach((i) => visitInitializer(i, cont));

// visit nested declarations
decl.nestedDeclarations.forEach((n) => visitDeclaration(n, cont));

// visit protocols
decl.conformedProtocols.forEach((p) => visitProtocol(p.declaration, cont));

// ensure generic types do not enter
cont.removeWhere((t) => decl.typeParams.map((type) => type.name).contains(t));

return cont;
}

Set<String> visitInitializer(InitializerDeclaration decl,
[Set<String>? context]) {
final cont = context ??= {};

// similar to `visitMethod`, except no return type
decl.params.forEach((p) => visitParameter(p, cont));

return cont;
}

Set<String> visitFunction(FunctionDeclaration decl, [Set<String>? context]) {
final cont = context ??= {};

// visit parameters
decl.params.forEach((p) => visitParameter(p, cont));

// ensure generic types do not enter
cont.removeWhere((t) => decl.typeParams.map((type) => type.name).contains(t));

// visit return type
visitType(decl.returnType, cont);

// TODO: what of type restrictions (`... where T.Element: CustomStringConvertible`)

return cont;
}

Set<String> visitParameter(Parameter decl, [Set<String>? context]) {
final cont = context ??= {};

// just visit type of parameter
visitType(decl.type, cont);

return cont;
}

Set<String> visitVariable(VariableDeclaration decl, [Set<String>? context]) {
final cont = context ??= {};

// just return property type
visitType(decl.type, cont);

return cont;
}

Set<String> visitType(ReferredType type, [Set<String>? context]) {
final cont = context ??= {};

// we need to confirm the types located
// at the moment, we can perform simple regex to clean up text characters

// since we are making such visitations on normal declarations in a file,
// we do not need to filter out primitives at the moment
cont.addAll(_getTypeNames(type.swiftType));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You shouldn't have to re-parse these types from strings. We did all the parsing already, and now have a nice clean AST to work with. In this case you can just check if the type is a DeclaredType, and get its id or declaration or whatever. Also need to handle the OptionalType case.


return cont;
}
}
41 changes: 36 additions & 5 deletions pkgs/swift2objc/lib/src/transformer/transform.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,24 +8,55 @@ import '../ast/_core/interfaces/nestable_declaration.dart';
import '../ast/declarations/compounds/class_declaration.dart';
import '../ast/declarations/compounds/struct_declaration.dart';
import '../ast/declarations/globals/globals.dart';
import '_core/dependencies.dart';
import '_core/unique_namer.dart';
import 'transformers/transform_compound.dart';
import 'transformers/transform_globals.dart';

typedef TransformationMap = Map<Declaration, Declaration>;

List<Declaration> transform(List<Declaration> declarations) {
Set<Declaration> generateDependencies(Iterable<Declaration> decls, {Iterable<Declaration>? allDecls}) {
final dependencies = <Declaration>{};
final dependencyVisitor = DependencyVisitor();

var _d = decls;

while (true) {
final deps = _d.fold<Set<String>>(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This algorithm is pretty hard to follow. A simpler approach would be to give DependencyVisitor a member Set that it outputs all the dependencies into. generateDependencies would just construct a DependencyVisitor, then iterate over every element of decls and call dependencyVisitor.visitDeclaration, then return the Set that dependencyVisitor constructed.

DependencyVisitor.visitDeclaration would just DFS from the given element to any child element not in that Set. This would also mean you don't have to pass around the context variable through all those visit methods in DependencyVisitor, because the context would be replaced by this member Set.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reason for recursiveness is because of at the moment, the algorithm generates bindings for all parts of all dependencies of filtered types, and since these parts (e.g methods and properties) may also require types as well, those are generated as well recursively.

What can be done, is to pass the declarations to DependencyVisitor, allow it to perform the recursive visiting, and then return the output.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, all that recursion should happen inside the visitor

{},
(previous, element) =>
previous.union(dependencyVisitor.visitDeclaration(element)));
final depDecls =
(allDecls ?? decls).where((d) => deps.contains(d.name));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Names aren't unique (eg two nested types, nested in different parent types, can have the same name). Use the declaration id instead.

Better yet, why not just have the dependency visitor construct a Set<Declaration> directly, rather than constructing a Set<String> and then looking up the declaration from the string?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Most of the types do not reference declarations directly (like function parameters, variable types), which therefore means that declarations need to be looked up eventually.

The lookup is done afterwards to prevent multiple lookups and just have a single lookup per visitation.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You shouldn't ever need to look up types by name or id, after parsing is complete. The AST already has references to the declarations. Check if the ReferredType is a DeclaredType.

if (depDecls.isEmpty || (dependencies.union(depDecls.toSet()).length) == dependencies.length) {
break;
} else {
dependencies.addAll(depDecls);
_d = depDecls;
}
}

return dependencies;
}

/// Transforms the given declarations into the desired ObjC wrapped declarations
List<Declaration> transform(List<Declaration> declarations,
{bool Function(Declaration)? filter}) {
nikeokoronkwo marked this conversation as resolved.
Show resolved Hide resolved
final transformationMap = <Declaration, Declaration>{};

final _declarations =
declarations.where(filter ?? (declaration) => true).toSet();
_declarations.addAll(generateDependencies(_declarations, allDecls: declarations));

final globalNamer = UniqueNamer(
declarations.map((declaration) => declaration.name),
_declarations.map((declaration) => declaration.name),
);

final globals = Globals(
functions: declarations.whereType<GlobalFunctionDeclaration>().toList(),
variables: declarations.whereType<GlobalVariableDeclaration>().toList(),
functions: _declarations.whereType<GlobalFunctionDeclaration>().toList(),
variables: _declarations.whereType<GlobalVariableDeclaration>().toList(),
);
final nonGlobals = declarations
final nonGlobals = _declarations
.where(
(declaration) =>
declaration is! GlobalFunctionDeclaration &&
Expand Down
Loading
Loading