Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Fetching contributors…

Cannot retrieve contributors at this time

505 lines (432 sloc) 16.352 kb
using Nemerle;
using Nemerle.Collections;
using Nemerle.Compiler;
using Nemerle.Compiler.Parsetree;
using Nemerle.Text;
using Nemerle.Utility;
using System;
using System.Collections.Generic;
namespace Nemerle.Extensions
{
/// Implements Equals and related methods, using the concept of
/// http://everything2.com/title/structural+equality
[MacroUsage(MacroPhase.BeforeInheritance, MacroTargets.Class, Inherited = false, AllowMultiple = false)]
public macro StructuralEquality(tb : TypeBuilder, params _options : list[PExpr])
{
StructuralEqualityImpl.RunBeforeInheritance(tb);
}
[MacroUsage(MacroPhase.WithTypedMembers, MacroTargets.Class, Inherited = false, AllowMultiple = false)]
public macro StructuralEquality(tb : TypeBuilder, params options : list[PExpr])
{
StructuralEqualityImpl.RunWithTypedMembers(tb, options);
}
[MacroUsage(MacroPhase.BeforeInheritance, MacroTargets.Field, Inherited = false, AllowMultiple = false)]
public macro EqualsIgnore(tb : TypeBuilder, field : ParsedField)
{
StructuralEqualityImpl.Ignore(tb, field);
}
[MacroUsage(MacroPhase.BeforeInheritance, MacroTargets.Property, Inherited = false, AllowMultiple = false)]
public macro EqualsIgnore(tb : TypeBuilder, prop : ParsedProperty)
{
StructuralEqualityImpl.Ignore(tb, prop);
}
module StructuralEqualityImpl
{
public IndexOf[T](this source : list[T], elem : T) : int
{
def loop(source, elem, index)
{
match (source)
{
| head :: _ when head.Equals(elem) => index
| _ :: tail => loop(tail, elem, index + 1)
| _ => -1
}
}
loop(source, elem, 0)
}
public NOfTypeRev[TIn, TFilter](this source : list[TIn]) : list[TFilter]
{
def loop(listIn : list[TIn], listOut : list[TFilter])
{
match (listIn)
{
| head :: tail =>
match (head)
{
| x is TFilter => loop(tail, x :: listOut)
| _ => loop(tail, listOut)
}
| _ => listOut
}
}
loop(source, [])
}
// used as keys in UserData
public IgnoredFieldsLabel : string = "StructuralEquality.IgnoredFields";
public IgnoredPropertiesLabel : string = "StructuralEquality.IgnoredProperties";
public IsEquatableImplementedLabel : string = "StructuralEquality.IsEquatableImplemented";
public IsStructuralEquatableImplementedLabel : string = "StructuralEquality.IsStructuralEquatableImplemented";
// implements interfaces
public RunBeforeInheritance(tb : TypeBuilder) : void
{
def type = GetTypeName(tb);
if (tb.IsValueType || tb.IsSealed)
tb.Define(<[ decl:
private EqualsImpl($("other" : dyn) : $type) : bool
{
} ]>);
else
tb.Define(<[ decl:
protected virtual EqualsImpl($("other" : dyn) : $type) : bool
{
} ]>);
when (tb.Ast is TopDeclaration.Variant)
foreach (vo in tb.GetVariantOptions())
vo.Ast.AddCustomAttribute(<[ StructuralEquality ]>);
// Nemerle doesn't build if Tuples from stdlib are changed
unless (tb.IsVariantOption)
{
tb.AddImplementedInterface(<[ System.IEquatable.[$type] ]>);
tb.UserData.Add(IsEquatableImplementedLabel, true);
// only .NET 4.0+ supports this
when (tb.Manager.NameTree.LookupExactType("System.Collections.IStructuralEquatable").IsSome)
{
tb.AddImplementedInterface(<[ System.Collections.IStructuralEquatable ]>);
tb.UserData.Add(IsStructuralEquatableImplementedLabel, true);
}
}
}
// parses options, defines methods
public RunWithTypedMembers(tb : TypeBuilder, options_expr : list[PExpr]) : void
{
//assert2(false);
def options = SEOptions.Parse(options_expr);
def get_relevant_fields(tb)
{
def all_fields = tb.GetFields(BindingFlags.Public %|
BindingFlags.NonPublic %|
BindingFlags.Instance %|
BindingFlags.DeclaredOnly);
// retrieve all ignored fields
def ignore_fields =
if (tb.UserData.Contains(IgnoredFieldsLabel))
tb.UserData[IgnoredFieldsLabel] :> List[string]
else
List();
// ignored properties
when (tb.UserData.Contains(IgnoredPropertiesLabel))
{
def prop_list = tb.UserData[IgnoredPropertiesLabel] :> List[string];
foreach (prop in prop_list)
{
match (tb.LookupMember(prop).Find(x => x is IProperty))
{
| Some(builder is PropertyBuilder) =>
match (builder.AutoPropertyField)
{
| Some(field) => ignore_fields.Add(field.Name)
| _ => Message.Warning(builder.Location, $"$prop is not an autoproperty. No need to use EqualsIgnore")
}
| _ => Message.Error($"Property $prop not found.")
}
}
}
ignore_fields.AddRange(options.IgnoreFields);
ignore_fields.Sort();
// remove ignored fields and return result
all_fields.Filter(x => ignore_fields.BinarySearch(x.Name) < 0);
}
// fields that are not ignored when evaluating structural equality
def relevant_fields = get_relevant_fields(tb);
// true if strict type equality is needed, i. e. no subtypes are allowed;
def typecheck_needed = !tb.IsSealed && !tb.IsVariantOption && !tb.IsValueType && options.CheckTypes;
//assert2(false);
DefineEquality(tb, relevant_fields, typecheck_needed);
DefineHashCode(tb, relevant_fields);
DefineOperators(tb);
DefineStructural(tb);
}
// adds a field to ignore list
public Ignore(tb : TypeBuilder, field : ClassMember.Field) : void
{
unless (tb.UserData.Contains(IgnoredFieldsLabel))
tb.UserData.Add(IgnoredFieldsLabel, List.[string]());
def lst = tb.UserData[IgnoredFieldsLabel] :> List[string];
unless (lst.Contains(field.Name))
lst.Add(field.Name);
}
// adds a property to ignore list
public Ignore(tb : TypeBuilder, prop : ClassMember.Property) : void
{
unless (tb.UserData.Contains(IgnoredPropertiesLabel)) tb.UserData.Add(IgnoredPropertiesLabel, List.[string]());
def lst = tb.UserData[IgnoredPropertiesLabel] :> List[string];
unless (lst.Contains(prop.Name)) lst.Add(prop.Name);
}
// represents macro options
[Record]
struct SEOptions
{
[Accessor] _ignoreFields : list[string];
[Accessor] _checkTypes : bool;
[Accessor] static _default : SEOptions = SEOptions([], true);
public static Parse(options : list[PExpr]) : SEOptions
{
mutable check_types = true;
mutable ignore_fields = [];
foreach (opt in options)
{
| <[ CheckTypes = true ]> => check_types = true;
| <[ CheckTypes = false ]> => check_types = false;
| <[ Ignore = [..$flds] ]>
| <[ Ignore = $fld ]> with flds = [fld] =>
// add field names as strings
ignore_fields += flds.MapFiltered(_ is PExpr.Ref, x => (x :> PExpr.Ref).name.Id)
| _ => Message.Error("Unknown options for StructuralEquality.")
}
SEOptions(ignore_fields, check_types)
}
}
GetEqualsImpl(typeInfoToLookup : TypeInfo, paramType : FixedType.Class) : option[IMethod]
{
def baseEqualsImpl = typeInfoToLookup
.LookupMember("EqualsImpl")
.NOfTypeRev.[_, IMethod]()
.Find(m => m.Header.Parameters.Length == 1 && m.Header.Parameters.Head.Type.Equals(paramType));
baseEqualsImpl
}
IsParentImplement_EqualsImpl(tb : TypeBuilder) : bool
{
GetEqualsImpl(tb.BaseType, tb.BaseClass).IsSome
}
HasTypedEquals(type : FixedType) : bool
{
| Class(ti, _) =>
def result = ti
.LookupMember("Equals")
.NOfTypeRev.[_, IMethod]()
.Find(m => m.Header.Parameters.Length == 1 && m.Header.Parameters.Head.Type.Equals(type));
result.IsSome
| _ => false
}
DefineEquality(tb : TypeBuilder, fields : Seq[IField], _check_types : bool) : void
{
assert2(tb.BaseType.LookupMemberAvailable);
def defineMember(member : ClassMember) : void { _ = tb.DefineWithSource(member); }
// generates comparison code for a single field
def invokeEquals(x : IField)
{
def type = x.GetMemType();
def value = <[ $(x.Name : usesite) ]>;
if (type.IsPrimitive)
<[ this.$value == other.$value ]> // primitive magic
else if (type.Equals(tb.InternalType.String))
<[ string.Equals(this.$value, other.$value) ]>
else if (type.IsValueType && !type.CanBeNull && HasTypedEquals(type))
<[ this.$value.Equals(other.$value) ]> // no null-checks
// <[ if ($value.HasValue) other.$value.HasValue && this.$value.Value.Equals(other.$value.Value); else !other.$value.HasValue; ]> // For T?
//else if (type is FixedType.StaticTypeVarRef) // for type parameters
// <[ EqualityComparer.Default.Equals($value, other.$value) ]>
else
<[ EqualityComparer.Default.Equals(this.$value, other.$value) ]>;
}
def type = GetTypeName(tb);
//def type_checker =
// if (check_types)
// <[ other.GetType().Equals(this.GetType()) ]>
// else
// <[ true ]>;
def isParentImplement_EqualsImpl = IsParentImplement_EqualsImpl(tb);
def parentType = tb.BaseClass;
def baseCall =
if (isParentImplement_EqualsImpl)
<[ base.EqualsImpl(other : $(parentType : typed)) ]>
else
<[ true ]>;
// core comparison code (type checker + comparison for each field)
def body = fields.Fold(baseCall, (f, acc) => <[ $(invokeEquals(f)) && $acc ]> );
// no null-check for structs
def fun_body = if (tb.GetMemType().CanBeNull) <[ match (other) { | null => false | _ => $body } ]> else body;
def fun_body = <[
_ = other; // shut the compiler up if body degrades to "true"
$fun_body
]>;
match (GetEqualsImpl(tb, tb.GetMemType()))
{
| Some(method is MethodBuilder) =>
method.Body = fun_body;
//assert2(false);
method.Ast.Body = fun_body;
tb.TyManager.GenerateFakeSourceCode(method.Ast);
| _ => Message.FatalError($"Can't find EqualsImpl(other : $(tb.GetMemType())) in $tb");
}
def implementsEquatable = AskUserData(tb, IsEquatableImplementedLabel);
if (implementsEquatable)
defineMember(<[ decl:
public Equals(other : $type) : bool implements System.IEquatable.[$type].Equals
{
EqualsImpl(other)
} ]>);
else
defineMember(<[ decl:
public Equals(other : $type) : bool
{
EqualsImpl(other)
} ]>);
when (isParentImplement_EqualsImpl)
defineMember(<[ decl:
protected override EqualsImpl(other : $(parentType : typed)) : bool
{
| x is $type => EqualsImpl(x)
| _ => false
}
]>);
// implements object.Equals
defineMember(<[ decl:
public override Equals(other : System.Object) : bool
{
| x is $type => EqualsImpl(x)
| _ => false
}
]>);
}
// uses http://en.wikipedia.org/wiki/Jenkins_hash_function to implement GetHashCode
DefineHashCode(tb : TypeBuilder, fields : Seq[IField]) : void
{
def callBase = IsParentImplement_EqualsImpl(tb);
def hash_body = fields.Map(
fun (f)
{
def type = f.GetMemType();
def value = <[ $(f.Name : usesite) ]>;
def gethashcode =
if (type.Equals(tb.InternalType.Int32))
value
else if (type.Equals(tb.InternalType.UInt32))
<[ unchecked ($value :> int) ]>
else if (type.IsValueType)
if (type.CanBeNull)
<[ EqualityComparer.Default.GetHashCode($value) ]> // можно оптимизировать
else
<[ this.$value.GetHashCode() ]>
else if (type.Equals(tb.InternalType.Int32))
<[ this.$value ]>
else if (type is FixedType.StaticTypeVarRef)
<[ EqualityComparer.Default.GetHashCode($value) ]>
else
<[ this.$value?.GetHashCode() ]>;
<[
hash += $gethashcode;
hash += (hash << 10);
hash ^= (hash >> 6);
]>
});
def variantOptionHash =
if (tb.IsVariantOption)
{
def optionNumber = tb.GetVariantOptionParent().GetVariantOptions().IndexOf(tb) + 1;
assert(optionNumber > 0);
<[ hash ^= $(optionNumber : int); ]>
}
else
<[ () ]>;
def baseHash =
if (callBase)
<[ hash ^= base.GetHashCode(); ]>
else
<[ () ]>;
def body =
if (hash_body.IsEmpty)
if (callBase) <[ base.GetHashCode() ]> else <[ 0 ]>
else
<[
unchecked
{
mutable hash : int;
{ ..$hash_body }
//hash += (hash << 3);
//hash ^= (hash >> 11);
//hash += (hash << 15);
$baseHash;
$variantOptionHash;
hash
}
]>;
_ = tb.DefineWithSource(<[ decl: public override GetHashCode() : int { $body } ]>);
}
DefineOperators(tb : TypeBuilder) : void
{
def type = GetTypeName(tb);
if (tb.IsValueType)
{
tb.Define(<[ decl:
public static @==(first : $type, second : $type) : bool
{
first.Equals(second)
}
]>);
}
else
{
tb.Define(<[ decl:
public static @==(first : $type, second : $type) : bool
{
if (first is null) second is null else first.Equals(second)
}
]>);
}
tb.Define(<[ decl:
public static @!= (first : $type, second : $type) : bool
{
!(first == second)
}
]>);
}
DefineStructural(tb : TypeBuilder) : void
{
when (AskUserData(tb, IsStructuralEquatableImplementedLabel) == true)
{
tb.Define(<[ decl:
public Equals(other : object, _comparer : System.Collections.IEqualityComparer) : bool
{
Equals(other);
}
]>);
tb.Define(<[ decl:
public GetHashCode(_comparer : System.Collections.IEqualityComparer) : int
{
GetHashCode();
}
]>);
}
}
//MarkCompilerGenerated(cm : ClassMember) : ClassMember
//{
// cm.AddCustomAttribute(<[System.Runtime.CompilerServices.CompilerGenerated]>);
// cm
//}
// no api to get type name with params; 'this' keyword in this context is bugged
GetTypeName(tb : TypeBuilder) : PExpr
{
//<[ $(tb.GetMemType() : typed) ]>
def splicable_to_ref(s : Splicable)
{
| Name(n)
| HalfId(n) => PExpr.Ref(n)
| Expression(e) => e
}
def qname = PExpr.FromQualifiedIdentifier(tb.Manager, tb.Ast.FullQualifiedName);
if (tb.Ast.TypeParameters is null)
<[ $qname ]>
else
{
def args = tb.Ast.TypeParameters.tyvars.Map(splicable_to_ref);
<[ $qname.[..$args] ]>
}
}
AskUserData(tb : TypeBuilder, question : string, defaultAnswer : bool = false) : bool
{
if (!tb.UserData.Contains(question)) defaultAnswer else tb.UserData[question] :> bool
}
}
}
Jump to Line
Something went wrong with that request. Please try again.