diff --git a/src/Swift.Bindings/src/Demangler/IReduction.cs b/src/Swift.Bindings/src/Demangler/IReduction.cs new file mode 100644 index 00000000000..e5c30b52729 --- /dev/null +++ b/src/Swift.Bindings/src/Demangler/IReduction.cs @@ -0,0 +1,50 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace BindingsGeneration.Demangling; + +/// +/// Represents the result of reducing a Node or tree of nodes +/// +public interface IReduction { + string Symbol { get; init; } +} + +/// +/// Represents an error in an attempted reduction +/// +public class ReductionError : IReduction { + public required string Symbol { get; init; } + /// + /// Returns an error message describing the error + /// + public required string Message { get; init; } +} + +/// +/// Represents a reduction that reduces to a single type +/// +public class TypeSpecReduction : IReduction { + public required string Symbol { get; init; } + /// + /// Returns a TypeSpec for the type that this node represents + /// + public required TypeSpec TypeSpec { get; init; } +} + +/// +/// Represents a reduction that reduces to a Swift function +/// +public class FunctionReduction : IReduction { + public required string Symbol { get; init; } + /// + /// Returns a function that this type represents + /// + public required SwiftFunction Function { get; init; } +} + +public class ProtocolWitnessTableReduction : IReduction { + public required string Symbol { get; init; } + public required NamedTypeSpec ImplementingType { get; init; } + public required NamedTypeSpec ProtocolType { get; init;} +} \ No newline at end of file diff --git a/src/Swift.Bindings/src/Demangler/MatchRule.cs b/src/Swift.Bindings/src/Demangler/MatchRule.cs new file mode 100644 index 00000000000..ff67df170df --- /dev/null +++ b/src/Swift.Bindings/src/Demangler/MatchRule.cs @@ -0,0 +1,137 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Diagnostics; + +namespace BindingsGeneration.Demangling; + +/// +/// MatchRule represents a tree reducing rule for a demangled Swift symbol. +/// Given a tree of nodes, a match rule contains conditions for the match, +/// which include: +/// - If and how the node content should be matched +/// - If the number of children should match +/// - Rules to run on the children +/// - One or more NodeKinds to match +/// If a match occurs, a reducer function can be run on the node. +/// +[DebuggerDisplay("{ToString()}")] +internal class MatchRule { + /// + /// The name of this rule - useful for debugging + /// + public required string Name { get; init; } + + /// + /// A list of node kinds to match. + /// + public List NodeKindList { get; set; } = new List(); + + /// + /// A convenience accessor to match on a single NodeKind + /// + public NodeKind NodeKind { + get { + if (NodeKindList.Count != 1) { + throw new InvalidOperationException($"NodeKind is invalid when NodeKindList has {NodeKindList.Count} entries."); + } + return NodeKindList[0]; + } + set { + NodeKindList = new List { value }; + } + } + + /// + /// If and how to match the content of the Node + /// + public MatchNodeContentType MatchContentType { get; init; } = MatchNodeContentType.None; + + /// + /// Rules to apply to children node. + /// + public List ChildRules { get; init; } = new List(); + + /// + /// Whether or not the total number of children should match + /// + public bool MatchChildCount { get; init; } = false; + + /// + /// A reducer to apply if the node matches + /// + public required Func Reducer { get; init; } + + /// + /// Returns true if and only if the given node matches this rule + /// + /// a node to match on + /// + public bool Matches(Node n) + { + return NodeKindMatches(n) && ContentTypeMatches(n) && ChildrenMatches(n); + } + + /// + /// Returns true if and only if the NodeKind matches + /// + /// a node to match on + /// + bool NodeKindMatches(Node n) + { + return NodeKindList.Contains(n.Kind); + } + + /// + /// Returns true if and only if the content of the given node matches + /// + /// a node to match on + /// + /// + bool ContentTypeMatches(Node n) + { + // Only care about the content type not its value + switch (MatchContentType) + { + case MatchNodeContentType.AlwaysMatch: + return true; + case MatchNodeContentType.Index: + return n.HasIndex; + case MatchNodeContentType.Text: + return n.HasText; + case MatchNodeContentType.None: + return !n.HasIndex && !n.HasText; + default: + throw new InvalidOperationException ($"Unknown match instruction {MatchContentType} in match rule."); + } + } + + /// + /// Returns true if and only if the children rules matches the given node's children + /// + /// + /// + bool ChildrenMatches (Node n) + { + // if the rule says the child count matters, apply + if (MatchChildCount && n.Children.Count != ChildRules.Count) + return false; + + // match up to the minimum of each list + // if MatchChileCount is true, min is the size of both lists + int minimumChildCount = Math.Min (n.Children.Count, ChildRules.Count); + for (var i = 0; i < minimumChildCount; i++) { + var childRule = ChildRules [i]; + // recurse + if (!childRule.Matches (n.Children [i])) + return false; + } + return true; + } + + /// + /// Creates a simple string representation of this rule + /// + /// a string representation of the rule + public override string ToString() => Name; +} diff --git a/src/Swift.Bindings/src/Demangler/RuleRunner.cs b/src/Swift.Bindings/src/Demangler/RuleRunner.cs new file mode 100644 index 00000000000..49b05123404 --- /dev/null +++ b/src/Swift.Bindings/src/Demangler/RuleRunner.cs @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace BindingsGeneration.Demangling; + +/// +/// RuleRunner contains a a collection of rules that get run to reduce nodes +/// +internal class RuleRunner { + string mangledName; + List rules = new List(); + + /// + /// Constructs a new rules runner initialized with the give rule set + /// + /// Rules to test against a node + public RuleRunner(IEnumerable rules, string mangledName) + { + this.mangledName = mangledName; + this.rules.AddRange(rules); + } + + /// + /// Run a set of rules on the given node and return a reduction on that node. If there was no match + /// or there was an error, this will return a IReduction of type ReductionError + /// + /// A node to attempt to match + /// A name used for the reduction + /// + public IReduction RunRules(Node node, string? name) + { + var rule = rules.FirstOrDefault (r => r.Matches(node)); + + if (rule is null) + return new ReductionError() { Symbol = mangledName, Message = $"No rule for node {node.Kind}" }; + return rule.Reducer(node, name); + } +} diff --git a/src/Swift.Bindings/src/Demangler/Swift5Demangler.cs b/src/Swift.Bindings/src/Demangler/Swift5Demangler.cs index 9460764cf4d..a48e5af5c50 100644 --- a/src/Swift.Bindings/src/Demangler/Swift5Demangler.cs +++ b/src/Swift.Bindings/src/Demangler/Swift5Demangler.cs @@ -41,20 +41,35 @@ public Swift5Demangler (string mangledName, ulong offset = 0) slice.Advance (GetManglingPrefixLength (originalIdentifier)); } - public void Run () + public IReduction Run () { Node topLevelNode = DemangleType (null); - // if (topLevelNode != null && topLevelNode.IsAttribute () && nodeStack.Count >= 1) { - // var attribute = topLevelNode.ExtractAttribute (); - // var tld = Run (); - // if (tld != null && tld is TLFunction tlf) { - // tlf.Signature.Attributes.Add (attribute); - // } - // return tld; - // } else { - // Swift5NodeToTLDefinition converter = new Swift5NodeToTLDefinition (originalIdentifier, offset); - // return converter.Convert (topLevelNode); - // } + if (topLevelNode is not null && topLevelNode.IsAttribute() && nodeStack.Count >= 1) { + var attribute = ExtractAttribute (topLevelNode); + var nextReduction = Run (); + if (nextReduction is not null && nextReduction is TypeSpecReduction ts) { + ts.TypeSpec.Attributes.Add (attribute); + } + return nextReduction; + } else if (topLevelNode is not null) { + var reducer = new Swift5Reducer (originalIdentifier); + return reducer.Convert (topLevelNode); + } else { + return new ReductionError () {Symbol = originalIdentifier, Message = $"Unable to demangle {originalIdentifier}" }; + } + } + + static TypeSpecAttribute ExtractAttribute (Node node) + { + switch (node.Kind) { + case NodeKind.ObjCAttribute: return new TypeSpecAttribute ("ObjC"); + case NodeKind.DynamicAttribute: return new TypeSpecAttribute ("Dynamic"); + case NodeKind.NonObjCAttribute: return new TypeSpecAttribute ("NonObjC"); + case NodeKind.ImplFunctionAttribute: return new TypeSpecAttribute ("ImplFunction"); + case NodeKind.DirectMethodReferenceAttribute: return new TypeSpecAttribute ("DirectMethodReference"); + default: + throw new NotSupportedException ($"{node.Kind} is not a supported attribute."); + } } bool NextIf (string str) diff --git a/src/Swift.Bindings/src/Demangler/Swift5Reducer.cs b/src/Swift.Bindings/src/Demangler/Swift5Reducer.cs new file mode 100644 index 00000000000..93458db8102 --- /dev/null +++ b/src/Swift.Bindings/src/Demangler/Swift5Reducer.cs @@ -0,0 +1,132 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Net.Sockets; + +namespace BindingsGeneration.Demangling; + +/// +/// Swift5Reducer takes a tree of Node and attempts to reduce it completely to an easier to +/// to manipulate data structure. It does this by matching patterns in the tree and on match +/// applying a reduction function on the matched Node. +/// +internal class Swift5Reducer { + string mangledName; + RuleRunner ruleRunner; + List rules; + + /// + /// Construct Swift5Reducer to operate on a tree of nodes build from the given mangled name + /// + public Swift5Reducer (string mangledName) + { + this.mangledName = mangledName; + rules = BuildMatchRules (); + ruleRunner = new RuleRunner (rules, mangledName); + } + + /// + /// Build a set of match rules for Node reduction + /// + List BuildMatchRules () => + new List() { + new MatchRule() { + Name = "Global", NodeKind = NodeKind.Global, Reducer = ConvertFirstChild + }, + new MatchRule() { + Name = "ProtocolWitnessTable", NodeKind = NodeKind.ProtocolWitnessTable, Reducer = ConvertProtocolWitnessTable + }, + new MatchRule() { + Name = "Type", NodeKind = NodeKind.Type, Reducer = ConvertFirstChild + }, + new MatchRule() { + Name = "Nominal", NodeKindList = new List() { NodeKind.Class, NodeKind.Structure, NodeKind.Protocol, NodeKind.Enum }, + Reducer = ConvertNominal + } + }; + + /// + /// Convert a Node into an IReduction. On failure, the IReduction will be type ReductionError + /// + public IReduction Convert (Node node) + { + return ruleRunner.RunRules (node, null); + } + + /// + /// Given a ProtocolWitnessTable node, convert to a ProtocolWitnessTable reduction + /// + IReduction ConvertProtocolWitnessTable (Node node, string? name) + { + // What to expect here: + // ProtocolConformance + // Type + // Type + var child = node.Children [0]; + if (child.Kind != NodeKind.ProtocolConformance) { + return ReductionError (ExpectedButGot ("ProtocolConformance", node.Kind.ToString ())); + } + var grandchild = Convert (child.Children [0]); + if (grandchild is ReductionError) return grandchild; + var implementingType = grandchild as TypeSpecReduction; + if (implementingType is null) { + return ReductionError (ExpectedButGot ("Nominal type implementing protocol", grandchild.GetType().Name)); + } + + grandchild = Convert (child.Children [1]); + if (grandchild is ReductionError) return grandchild; + var protocolType = grandchild as TypeSpecReduction; + if (protocolType is null) { + return ReductionError (ExpectedButGot ("Nominal type protocol", grandchild.GetType().Name)); + } + + var impNamed = (NamedTypeSpec)implementingType.TypeSpec; + var protoNamed = (NamedTypeSpec)protocolType.TypeSpec; + + return new ProtocolWitnessTableReduction() { Symbol = mangledName, ImplementingType = impNamed, ProtocolType = protoNamed }; + } + + /// + /// Convert a nominal node into TypeSpecReduction + /// + IReduction ConvertNominal (Node node, string? name) + { + // What to expect here: + // Class/Structure/Protocol/Enum + // Module Name + // Identifier Name + var kind = node.Kind; + if (kind == NodeKind.Class || kind == NodeKind.Structure || kind == NodeKind.Enum || kind == NodeKind.Protocol) { + var moduleName = node.Children [0].Text; + if (node.Children[1].Kind != NodeKind.Identifier) + return ReductionError(ExpectedButGot("Identifier", node.Children[1].Kind.ToString())); + var typeName = node.Children [1].Text; + return new TypeSpecReduction() { Symbol = mangledName, TypeSpec = new NamedTypeSpec ($"{moduleName}.{typeName}")}; + } + return ReductionError(ExpectedButGot("Class/Struct/Enum/Protocol", kind.ToString())); + } + + /// + /// Recurce on Convert with the first child + /// + IReduction ConvertFirstChild (Node node, string? name) + { + return Convert (node.Children [0]); + } + + /// + /// Return a string in the form mangledName: expected xxxx but got yyyy + /// + string ExpectedButGot (string expected, string butGot) + { + return $"Demangling {mangledName}: expected {expected} but got {butGot}"; + } + + /// + /// Convenience factory for reduction errors + /// + ReductionError ReductionError (string message) + { + return new ReductionError() { Symbol = mangledName, Message = message }; + } +} \ No newline at end of file diff --git a/src/Swift.Bindings/src/Model/Provenance.cs b/src/Swift.Bindings/src/Model/Provenance.cs new file mode 100644 index 00000000000..d3ff9616af7 --- /dev/null +++ b/src/Swift.Bindings/src/Model/Provenance.cs @@ -0,0 +1,111 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Diagnostics.CodeAnalysis; + +namespace BindingsGeneration; + +/// +/// Represents the origin of a particular function +/// +public class Provenance { + private Provenance() { } + + /// + /// If the provenance is in instance, return the type + /// + public NamedTypeSpec? InstanceType { get; private set; } + + /// + /// If the provenance is an extension, return type it is an extension on + /// + public NamedTypeSpec? ExtensionOn { get; private set; } + + /// + /// If the provenance is top level, return the module + /// + public string? Module {get; private set; } + + /// + /// Construct a new instance provenance from the given NamedTypeSpec + /// + /// + /// + public static Provenance Instance (NamedTypeSpec ns) { + return new Provenance () { InstanceType = ns }; + } + + /// + /// Construct a new extension provenance from the given NamedTypeSpec + /// + /// + /// + public static Provenance Extension (NamedTypeSpec ns) { + return new Provenance () { ExtensionOn = ns }; + } + + /// + /// Construct a new top-level provenance from the given module name + /// + /// + /// + public static Provenance TopLevel (string module) { + return new Provenance () { Module = module };; + } + + /// + /// Returns true if and only if the provenance is an instance + /// + [MemberNotNullWhen(true, nameof(InstanceType))] + public bool IsInstance => InstanceType is not null; + + /// + /// Returns true if and only if the provenance is an extension + /// + [MemberNotNullWhen(true, nameof(ExtensionOn))] + public bool IsExtension => ExtensionOn is not null; + + /// + /// Returns true if and only if the provenance is top level + /// + [MemberNotNullWhen(true, nameof(Module))] + public bool IsTopLevel => Module is not null; + + /// + /// Returns true if o is a Provenance and equals this one + /// + /// + /// + public override bool Equals(object? o) + { + if (o is Provenance other) { + return (IsInstance && InstanceType.Equals (other.InstanceType)) || + (IsExtension && ExtensionOn.Equals (other.ExtensionOn)) || + (IsTopLevel && Module == other.Module); + } + return false; + } + + /// + /// Returns a hashcode for this object + /// + /// + public override int GetHashCode () => ToString ().GetHashCode (); + + /// + /// Returns a string representation of the provenance + /// + /// + /// + public override string ToString() + { + if (IsInstance) { + return InstanceType.ToString (); + } else if (IsExtension) { + return ExtensionOn.ToString (); + } else if (IsTopLevel) { + return Module.ToString (); + } + throw new NotImplementedException("Unknown provenance"); + } +} \ No newline at end of file diff --git a/src/Swift.Bindings/src/Model/SwiftFunction.cs b/src/Swift.Bindings/src/Model/SwiftFunction.cs new file mode 100644 index 00000000000..c6a8895e67a --- /dev/null +++ b/src/Swift.Bindings/src/Model/SwiftFunction.cs @@ -0,0 +1,59 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Diagnostics; + +namespace BindingsGeneration; + +/// +/// Represents a Swift function signature and its provenance. +/// +[DebuggerDisplay("{ToString()}")] +public class SwiftFunction { + /// + /// Gets the name of the function + /// + public required string Name { get; init; } + + /// + /// Gets the provenance of the function + /// + public required Provenance Provenance { get; init; } + + /// + /// Gets the parameter list of the function as a tuple + /// + public required TupleTypeSpec ParameterList { get; init; } + + /// + /// Gets the return type of the function + /// + public required TypeSpec Return { get; init; } + + /// + /// Returns true if the give object is a SwiftFunction and matches this + /// + /// + /// true if this equals the supplied object + public override bool Equals(object? o) + { + if (o is SwiftFunction other) { + return Name == other.Name && Provenance.Equals (other.Provenance) && + ParameterList.Equals (other.ParameterList) && Return.Equals (other.Return); + } else { + return false; + } + } + + /// + /// Returns a hashcode for the function + /// + /// + public override int GetHashCode() => ToString ().GetHashCode (); + + /// + /// Returns a string representation of the function + /// + /// a string representation of the function + public override string ToString () => $"{Provenance}.{Name}{ParameterList} -> {Return}"; +} \ No newline at end of file diff --git a/src/Swift.Bindings/tests/DemanglerTests/BasicDemanglingTests.cs b/src/Swift.Bindings/tests/DemanglerTests/BasicDemanglingTests.cs new file mode 100644 index 00000000000..3c9ba9e6b49 --- /dev/null +++ b/src/Swift.Bindings/tests/DemanglerTests/BasicDemanglingTests.cs @@ -0,0 +1,71 @@ +using BindingsGeneration.Demangling; +using Xunit; + +namespace BindingsGeneration.Tests; + +public class BasicDemanglingTests : IClassFixture +{ + private readonly TestFixture _fixture; + + public BasicDemanglingTests(TestFixture fixture) + { + _fixture = fixture; + } + + public class TestFixture + { + static TestFixture() + { + } + + private static void InitializeResources() + { + } + } + + [Fact] + public void TestProtocolWitnessTable() + { + var symbol = "_$s20GenericTestFramework6ThingyCAA7StanleyAAWP"; + var demangler = new Swift5Demangler (symbol); + var result = demangler.Run (); + var protoWitnessReduction = result as ProtocolWitnessTableReduction; + Assert.NotNull(protoWitnessReduction); + Assert.Equal("GenericTestFramework.Thingy", protoWitnessReduction.ImplementingType.Name); + Assert.Equal("GenericTestFramework.Stanley", protoWitnessReduction.ProtocolType.Name); + } + + [Fact] + public void TestOtherProtocolWitnessTable() + { + var symbol = "_$s20GenericTestFramework6ThingyCAA8IsItRealAAWP"; + var demangler = new Swift5Demangler (symbol); + var result = demangler.Run (); + var protoWitnessReduction = result as ProtocolWitnessTableReduction; + Assert.NotNull(protoWitnessReduction); + Assert.Equal("GenericTestFramework.Thingy", protoWitnessReduction.ImplementingType.Name); + Assert.Equal("GenericTestFramework.IsItReal", protoWitnessReduction.ProtocolType.Name); + } + + [Fact] + public void TestFailDemangleNonsense() + { + var symbol = "_$ThisIsJustGarbage"; + var demangler = new Swift5Demangler (symbol); + var result = demangler.Run (); + var err = result as ReductionError; + Assert.NotNull(err); + Assert.Equal("No rule for node FirstElementMarker", err.Message); + } + + [Fact] + public void TestFailMetadataAccessor() + { + var symbol = "_$s20GenericTestFramework6ThingyCMa"; + var demangler = new Swift5Demangler (symbol); + var result = demangler.Run (); + var err = result as ReductionError; + Assert.NotNull(err); + Assert.Equal("No rule for node TypeMetadataAccessFunction", err.Message); + } +} \ No newline at end of file