diff options
Diffstat (limited to 'ARMeilleure/IntermediateRepresentation')
| -rw-r--r-- | ARMeilleure/IntermediateRepresentation/BasicBlock.cs | 83 | ||||
| -rw-r--r-- | ARMeilleure/IntermediateRepresentation/Instruction.cs | 79 | ||||
| -rw-r--r-- | ARMeilleure/IntermediateRepresentation/Intrinsic.cs | 138 | ||||
| -rw-r--r-- | ARMeilleure/IntermediateRepresentation/IntrinsicOperation.cs | 12 | ||||
| -rw-r--r-- | ARMeilleure/IntermediateRepresentation/MemoryOperand.cs | 25 | ||||
| -rw-r--r-- | ARMeilleure/IntermediateRepresentation/Multiplier.cs | 10 | ||||
| -rw-r--r-- | ARMeilleure/IntermediateRepresentation/Node.cs | 163 | ||||
| -rw-r--r-- | ARMeilleure/IntermediateRepresentation/Operand.cs | 124 | ||||
| -rw-r--r-- | ARMeilleure/IntermediateRepresentation/OperandHelper.cs | 68 | ||||
| -rw-r--r-- | ARMeilleure/IntermediateRepresentation/OperandKind.cs | 12 | ||||
| -rw-r--r-- | ARMeilleure/IntermediateRepresentation/OperandType.cs | 51 | ||||
| -rw-r--r-- | ARMeilleure/IntermediateRepresentation/Operation.cs | 40 | ||||
| -rw-r--r-- | ARMeilleure/IntermediateRepresentation/PhiNode.cs | 22 | ||||
| -rw-r--r-- | ARMeilleure/IntermediateRepresentation/Register.cs | 43 | ||||
| -rw-r--r-- | ARMeilleure/IntermediateRepresentation/RegisterType.cs | 9 |
15 files changed, 879 insertions, 0 deletions
diff --git a/ARMeilleure/IntermediateRepresentation/BasicBlock.cs b/ARMeilleure/IntermediateRepresentation/BasicBlock.cs new file mode 100644 index 00000000..06839f30 --- /dev/null +++ b/ARMeilleure/IntermediateRepresentation/BasicBlock.cs @@ -0,0 +1,83 @@ +using System.Collections.Generic; + +namespace ARMeilleure.IntermediateRepresentation +{ + class BasicBlock + { + public int Index { get; set; } + + public LinkedListNode<BasicBlock> Node { get; set; } + + public LinkedList<Node> Operations { get; } + + private BasicBlock _next; + private BasicBlock _branch; + + public BasicBlock Next + { + get => _next; + set => _next = AddSuccessor(_next, value); + } + + public BasicBlock Branch + { + get => _branch; + set => _branch = AddSuccessor(_branch, value); + } + + public List<BasicBlock> Predecessors { get; } + + public HashSet<BasicBlock> DominanceFrontiers { get; } + + public BasicBlock ImmediateDominator { get; set; } + + public BasicBlock() + { + Operations = new LinkedList<Node>(); + + Predecessors = new List<BasicBlock>(); + + DominanceFrontiers = new HashSet<BasicBlock>(); + + Index = -1; + } + + public BasicBlock(int index) : this() + { + Index = index; + } + + private BasicBlock AddSuccessor(BasicBlock oldBlock, BasicBlock newBlock) + { + oldBlock?.Predecessors.Remove(this); + newBlock?.Predecessors.Add(this); + + return newBlock; + } + + public void Append(Node node) + { + // If the branch block is not null, then the list of operations + // should end with a branch instruction. We insert the new operation + // before this branch. + if (_branch != null || (Operations.Last != null && IsLeafBlock())) + { + Operations.AddBefore(Operations.Last, node); + } + else + { + Operations.AddLast(node); + } + } + + private bool IsLeafBlock() + { + return _branch == null && _next == null; + } + + public Node GetLastOp() + { + return Operations.Last?.Value; + } + } +}
\ No newline at end of file diff --git a/ARMeilleure/IntermediateRepresentation/Instruction.cs b/ARMeilleure/IntermediateRepresentation/Instruction.cs new file mode 100644 index 00000000..4c4ecb8f --- /dev/null +++ b/ARMeilleure/IntermediateRepresentation/Instruction.cs @@ -0,0 +1,79 @@ +namespace ARMeilleure.IntermediateRepresentation +{ + enum Instruction + { + Add, + BitwiseAnd, + BitwiseExclusiveOr, + BitwiseNot, + BitwiseOr, + Branch, + BranchIfFalse, + BranchIfTrue, + ByteSwap, + Call, + CompareAndSwap128, + CompareEqual, + CompareGreater, + CompareGreaterOrEqual, + CompareGreaterOrEqualUI, + CompareGreaterUI, + CompareLess, + CompareLessOrEqual, + CompareLessOrEqualUI, + CompareLessUI, + CompareNotEqual, + ConditionalSelect, + ConvertI64ToI32, + ConvertToFP, + ConvertToFPUI, + Copy, + CountLeadingZeros, + Divide, + DivideUI, + Load, + Load16, + Load8, + LoadArgument, + Multiply, + Multiply64HighSI, + Multiply64HighUI, + Negate, + Return, + RotateRight, + ShiftLeft, + ShiftRightSI, + ShiftRightUI, + SignExtend16, + SignExtend32, + SignExtend8, + StackAlloc, + Store, + Store16, + Store8, + Subtract, + VectorCreateScalar, + VectorExtract, + VectorExtract16, + VectorExtract8, + VectorInsert, + VectorInsert16, + VectorInsert8, + VectorOne, + VectorZero, + VectorZeroUpper64, + VectorZeroUpper96, + ZeroExtend16, + ZeroExtend32, + ZeroExtend8, + + Clobber, + CpuId, + Extended, + Fill, + LoadFromContext, + Spill, + SpillArg, + StoreToContext + } +}
\ No newline at end of file diff --git a/ARMeilleure/IntermediateRepresentation/Intrinsic.cs b/ARMeilleure/IntermediateRepresentation/Intrinsic.cs new file mode 100644 index 00000000..1fe29e85 --- /dev/null +++ b/ARMeilleure/IntermediateRepresentation/Intrinsic.cs @@ -0,0 +1,138 @@ +namespace ARMeilleure.IntermediateRepresentation +{ + enum Intrinsic + { + X86Addpd, + X86Addps, + X86Addsd, + X86Addss, + X86Andnpd, + X86Andnps, + X86Cmppd, + X86Cmpps, + X86Cmpsd, + X86Cmpss, + X86Comisdeq, + X86Comisdge, + X86Comisdlt, + X86Comisseq, + X86Comissge, + X86Comisslt, + X86Cvtdq2pd, + X86Cvtdq2ps, + X86Cvtpd2dq, + X86Cvtpd2ps, + X86Cvtps2dq, + X86Cvtps2pd, + X86Cvtsd2si, + X86Cvtsd2ss, + X86Cvtss2sd, + X86Divpd, + X86Divps, + X86Divsd, + X86Divss, + X86Haddpd, + X86Haddps, + X86Maxpd, + X86Maxps, + X86Maxsd, + X86Maxss, + X86Minpd, + X86Minps, + X86Minsd, + X86Minss, + X86Movhlps, + X86Movlhps, + X86Mulpd, + X86Mulps, + X86Mulsd, + X86Mulss, + X86Paddb, + X86Paddd, + X86Paddq, + X86Paddw, + X86Pand, + X86Pandn, + X86Pavgb, + X86Pavgw, + X86Pblendvb, + X86Pcmpeqb, + X86Pcmpeqd, + X86Pcmpeqq, + X86Pcmpeqw, + X86Pcmpgtb, + X86Pcmpgtd, + X86Pcmpgtq, + X86Pcmpgtw, + X86Pmaxsb, + X86Pmaxsd, + X86Pmaxsw, + X86Pmaxub, + X86Pmaxud, + X86Pmaxuw, + X86Pminsb, + X86Pminsd, + X86Pminsw, + X86Pminub, + X86Pminud, + X86Pminuw, + X86Pmovsxbw, + X86Pmovsxdq, + X86Pmovsxwd, + X86Pmovzxbw, + X86Pmovzxdq, + X86Pmovzxwd, + X86Pmulld, + X86Pmullw, + X86Popcnt, + X86Por, + X86Pshufb, + X86Pslld, + X86Pslldq, + X86Psllq, + X86Psllw, + X86Psrad, + X86Psraw, + X86Psrld, + X86Psrlq, + X86Psrldq, + X86Psrlw, + X86Psubb, + X86Psubd, + X86Psubq, + X86Psubw, + X86Punpckhbw, + X86Punpckhdq, + X86Punpckhqdq, + X86Punpckhwd, + X86Punpcklbw, + X86Punpckldq, + X86Punpcklqdq, + X86Punpcklwd, + X86Pxor, + X86Rcpps, + X86Rcpss, + X86Roundpd, + X86Roundps, + X86Roundsd, + X86Roundss, + X86Rsqrtps, + X86Rsqrtss, + X86Shufpd, + X86Shufps, + X86Sqrtpd, + X86Sqrtps, + X86Sqrtsd, + X86Sqrtss, + X86Subpd, + X86Subps, + X86Subsd, + X86Subss, + X86Unpckhpd, + X86Unpckhps, + X86Unpcklpd, + X86Unpcklps, + X86Xorpd, + X86Xorps + } +}
\ No newline at end of file diff --git a/ARMeilleure/IntermediateRepresentation/IntrinsicOperation.cs b/ARMeilleure/IntermediateRepresentation/IntrinsicOperation.cs new file mode 100644 index 00000000..34781b70 --- /dev/null +++ b/ARMeilleure/IntermediateRepresentation/IntrinsicOperation.cs @@ -0,0 +1,12 @@ +namespace ARMeilleure.IntermediateRepresentation +{ + class IntrinsicOperation : Operation + { + public Intrinsic Intrinsic { get; } + + public IntrinsicOperation(Intrinsic intrin, Operand dest, params Operand[] sources) : base(Instruction.Extended, dest, sources) + { + Intrinsic = intrin; + } + } +}
\ No newline at end of file diff --git a/ARMeilleure/IntermediateRepresentation/MemoryOperand.cs b/ARMeilleure/IntermediateRepresentation/MemoryOperand.cs new file mode 100644 index 00000000..742842fa --- /dev/null +++ b/ARMeilleure/IntermediateRepresentation/MemoryOperand.cs @@ -0,0 +1,25 @@ +namespace ARMeilleure.IntermediateRepresentation +{ + class MemoryOperand : Operand + { + public Operand BaseAddress { get; set; } + public Operand Index { get; set; } + + public Multiplier Scale { get; } + + public int Displacement { get; } + + public MemoryOperand( + OperandType type, + Operand baseAddress, + Operand index = null, + Multiplier scale = Multiplier.x1, + int displacement = 0) : base(OperandKind.Memory, type) + { + BaseAddress = baseAddress; + Index = index; + Scale = scale; + Displacement = displacement; + } + } +}
\ No newline at end of file diff --git a/ARMeilleure/IntermediateRepresentation/Multiplier.cs b/ARMeilleure/IntermediateRepresentation/Multiplier.cs new file mode 100644 index 00000000..23582072 --- /dev/null +++ b/ARMeilleure/IntermediateRepresentation/Multiplier.cs @@ -0,0 +1,10 @@ +namespace ARMeilleure.IntermediateRepresentation +{ + enum Multiplier + { + x1 = 0, + x2 = 1, + x4 = 2, + x8 = 3 + } +}
\ No newline at end of file diff --git a/ARMeilleure/IntermediateRepresentation/Node.cs b/ARMeilleure/IntermediateRepresentation/Node.cs new file mode 100644 index 00000000..167acd07 --- /dev/null +++ b/ARMeilleure/IntermediateRepresentation/Node.cs @@ -0,0 +1,163 @@ +using System; +using System.Collections.Generic; + +namespace ARMeilleure.IntermediateRepresentation +{ + class Node + { + public Operand Destination + { + get + { + return _destinations.Length != 0 ? GetDestination(0) : null; + } + set + { + if (value != null) + { + SetDestinations(new Operand[] { value }); + } + else + { + SetDestinations(new Operand[0]); + } + } + } + + private Operand[] _destinations; + private Operand[] _sources; + + private LinkedListNode<Node>[] _asgUseNodes; + private LinkedListNode<Node>[] _srcUseNodes; + + public int DestinationsCount => _destinations.Length; + public int SourcesCount => _sources.Length; + + public Node(Operand destination, int sourcesCount) + { + Destination = destination; + + _sources = new Operand[sourcesCount]; + + _srcUseNodes = new LinkedListNode<Node>[sourcesCount]; + } + + public Node(Operand[] destinations, int sourcesCount) + { + SetDestinations(destinations ?? throw new ArgumentNullException(nameof(destinations))); + + _sources = new Operand[sourcesCount]; + + _srcUseNodes = new LinkedListNode<Node>[sourcesCount]; + } + + public Operand GetDestination(int index) + { + return _destinations[index]; + } + + public Operand GetSource(int index) + { + return _sources[index]; + } + + public void SetDestination(int index, Operand destination) + { + Operand oldOp = _destinations[index]; + + if (oldOp != null && oldOp.Kind == OperandKind.LocalVariable) + { + oldOp.Assignments.Remove(_asgUseNodes[index]); + } + + if (destination != null && destination.Kind == OperandKind.LocalVariable) + { + _asgUseNodes[index] = destination.Assignments.AddLast(this); + } + + _destinations[index] = destination; + } + + public void SetSource(int index, Operand source) + { + Operand oldOp = _sources[index]; + + if (oldOp != null && oldOp.Kind == OperandKind.LocalVariable) + { + oldOp.Uses.Remove(_srcUseNodes[index]); + } + + if (source != null && source.Kind == OperandKind.LocalVariable) + { + _srcUseNodes[index] = source.Uses.AddLast(this); + } + + _sources[index] = source; + } + + public void SetDestinations(Operand[] destinations) + { + if (_destinations != null) + { + for (int index = 0; index < _destinations.Length; index++) + { + Operand oldOp = _destinations[index]; + + if (oldOp != null && oldOp.Kind == OperandKind.LocalVariable) + { + oldOp.Assignments.Remove(_asgUseNodes[index]); + } + } + + _destinations = destinations; + } + else + { + _destinations = new Operand[destinations.Length]; + } + + _asgUseNodes = new LinkedListNode<Node>[destinations.Length]; + + for (int index = 0; index < destinations.Length; index++) + { + Operand newOp = destinations[index]; + + _destinations[index] = newOp; + + if (newOp.Kind == OperandKind.LocalVariable) + { + _asgUseNodes[index] = newOp.Assignments.AddLast(this); + } + } + } + + public void SetSources(Operand[] sources) + { + for (int index = 0; index < _sources.Length; index++) + { + Operand oldOp = _sources[index]; + + if (oldOp != null && oldOp.Kind == OperandKind.LocalVariable) + { + oldOp.Uses.Remove(_srcUseNodes[index]); + } + } + + _sources = new Operand[sources.Length]; + + _srcUseNodes = new LinkedListNode<Node>[sources.Length]; + + for (int index = 0; index < sources.Length; index++) + { + Operand newOp = sources[index]; + + _sources[index] = newOp; + + if (newOp.Kind == OperandKind.LocalVariable) + { + _srcUseNodes[index] = newOp.Uses.AddLast(this); + } + } + } + } +}
\ No newline at end of file diff --git a/ARMeilleure/IntermediateRepresentation/Operand.cs b/ARMeilleure/IntermediateRepresentation/Operand.cs new file mode 100644 index 00000000..2df6256f --- /dev/null +++ b/ARMeilleure/IntermediateRepresentation/Operand.cs @@ -0,0 +1,124 @@ +using System; +using System.Collections.Generic; + +namespace ARMeilleure.IntermediateRepresentation +{ + class Operand + { + public OperandKind Kind { get; } + + public OperandType Type { get; } + + public ulong Value { get; private set; } + + public LinkedList<Node> Assignments { get; } + public LinkedList<Node> Uses { get; } + + private Operand() + { + Assignments = new LinkedList<Node>(); + Uses = new LinkedList<Node>(); + } + + public Operand(OperandKind kind, OperandType type = OperandType.None) : this() + { + Kind = kind; + Type = type; + } + + public Operand(int value) : this(OperandKind.Constant, OperandType.I32) + { + Value = (uint)value; + } + + public Operand(uint value) : this(OperandKind.Constant, OperandType.I32) + { + Value = (uint)value; + } + + public Operand(long value) : this(OperandKind.Constant, OperandType.I64) + { + Value = (ulong)value; + } + + public Operand(ulong value) : this(OperandKind.Constant, OperandType.I64) + { + Value = value; + } + + public Operand(float value) : this(OperandKind.Constant, OperandType.FP32) + { + Value = (ulong)BitConverter.SingleToInt32Bits(value); + } + + public Operand(double value) : this(OperandKind.Constant, OperandType.FP64) + { + Value = (ulong)BitConverter.DoubleToInt64Bits(value); + } + + public Operand(int index, RegisterType regType, OperandType type) : this() + { + Kind = OperandKind.Register; + Type = type; + + Value = (ulong)((int)regType << 24 | index); + } + + public Register GetRegister() + { + return new Register((int)Value & 0xffffff, (RegisterType)(Value >> 24)); + } + + public byte AsByte() + { + return (byte)Value; + } + + public short AsInt16() + { + return (short)Value; + } + + public int AsInt32() + { + return (int)Value; + } + + public long AsInt64() + { + return (long)Value; + } + + public float AsFloat() + { + return BitConverter.Int32BitsToSingle((int)Value); + } + + public double AsDouble() + { + return BitConverter.Int64BitsToDouble((long)Value); + } + + internal void NumberLocal(int number) + { + if (Kind != OperandKind.LocalVariable) + { + throw new InvalidOperationException("The operand is not a local variable."); + } + + Value = (ulong)number; + } + + public override int GetHashCode() + { + if (Kind == OperandKind.LocalVariable) + { + return base.GetHashCode(); + } + else + { + return (int)Value ^ ((int)Kind << 16) ^ ((int)Type << 20); + } + } + } +}
\ No newline at end of file diff --git a/ARMeilleure/IntermediateRepresentation/OperandHelper.cs b/ARMeilleure/IntermediateRepresentation/OperandHelper.cs new file mode 100644 index 00000000..4a930e03 --- /dev/null +++ b/ARMeilleure/IntermediateRepresentation/OperandHelper.cs @@ -0,0 +1,68 @@ +using ARMeilleure.State; +using System; + +namespace ARMeilleure.IntermediateRepresentation +{ + static class OperandHelper + { + public static Operand Const(OperandType type, long value) + { + return type == OperandType.I32 ? new Operand((int)value) : new Operand(value); + } + + public static Operand Const(bool value) + { + return new Operand(value ? 1 : 0); + } + + public static Operand Const(int value) + { + return new Operand(value); + } + + public static Operand Const(uint value) + { + return new Operand(value); + } + + public static Operand Const(long value) + { + return new Operand(value); + } + + public static Operand Const(ulong value) + { + return new Operand(value); + } + + public static Operand ConstF(float value) + { + return new Operand(value); + } + + public static Operand ConstF(double value) + { + return new Operand(value); + } + + public static Operand Label() + { + return new Operand(OperandKind.Label); + } + + public static Operand Local(OperandType type) + { + return new Operand(OperandKind.LocalVariable, type); + } + + public static Operand Register(int index, RegisterType regType, OperandType type) + { + return new Operand(index, regType, type); + } + + public static Operand Undef() + { + return new Operand(OperandKind.Undefined); + } + } +}
\ No newline at end of file diff --git a/ARMeilleure/IntermediateRepresentation/OperandKind.cs b/ARMeilleure/IntermediateRepresentation/OperandKind.cs new file mode 100644 index 00000000..57618353 --- /dev/null +++ b/ARMeilleure/IntermediateRepresentation/OperandKind.cs @@ -0,0 +1,12 @@ +namespace ARMeilleure.IntermediateRepresentation +{ + enum OperandKind + { + Constant, + Label, + LocalVariable, + Memory, + Register, + Undefined + } +}
\ No newline at end of file diff --git a/ARMeilleure/IntermediateRepresentation/OperandType.cs b/ARMeilleure/IntermediateRepresentation/OperandType.cs new file mode 100644 index 00000000..bfdf5130 --- /dev/null +++ b/ARMeilleure/IntermediateRepresentation/OperandType.cs @@ -0,0 +1,51 @@ +using System; + +namespace ARMeilleure.IntermediateRepresentation +{ + enum OperandType + { + None, + I32, + I64, + FP32, + FP64, + V128 + } + + static class OperandTypeExtensions + { + public static bool IsInteger(this OperandType type) + { + return type == OperandType.I32 || + type == OperandType.I64; + } + + public static RegisterType ToRegisterType(this OperandType type) + { + switch (type) + { + case OperandType.FP32: return RegisterType.Vector; + case OperandType.FP64: return RegisterType.Vector; + case OperandType.I32: return RegisterType.Integer; + case OperandType.I64: return RegisterType.Integer; + case OperandType.V128: return RegisterType.Vector; + } + + throw new InvalidOperationException($"Invalid operand type \"{type}\"."); + } + + public static int GetSizeInBytes(this OperandType type) + { + switch (type) + { + case OperandType.FP32: return 4; + case OperandType.FP64: return 8; + case OperandType.I32: return 4; + case OperandType.I64: return 8; + case OperandType.V128: return 16; + } + + throw new InvalidOperationException($"Invalid operand type \"{type}\"."); + } + } +}
\ No newline at end of file diff --git a/ARMeilleure/IntermediateRepresentation/Operation.cs b/ARMeilleure/IntermediateRepresentation/Operation.cs new file mode 100644 index 00000000..620bf3f6 --- /dev/null +++ b/ARMeilleure/IntermediateRepresentation/Operation.cs @@ -0,0 +1,40 @@ +namespace ARMeilleure.IntermediateRepresentation +{ + class Operation : Node + { + public Instruction Instruction { get; private set; } + + public Operation( + Instruction instruction, + Operand destination, + params Operand[] sources) : base(destination, sources.Length) + { + Instruction = instruction; + + for (int index = 0; index < sources.Length; index++) + { + SetSource(index, sources[index]); + } + } + + public Operation( + Instruction instruction, + Operand[] destinations, + Operand[] sources) : base(destinations, sources.Length) + { + Instruction = instruction; + + for (int index = 0; index < sources.Length; index++) + { + SetSource(index, sources[index]); + } + } + + public void TurnIntoCopy(Operand source) + { + Instruction = Instruction.Copy; + + SetSources(new Operand[] { source }); + } + } +}
\ No newline at end of file diff --git a/ARMeilleure/IntermediateRepresentation/PhiNode.cs b/ARMeilleure/IntermediateRepresentation/PhiNode.cs new file mode 100644 index 00000000..30fc4d38 --- /dev/null +++ b/ARMeilleure/IntermediateRepresentation/PhiNode.cs @@ -0,0 +1,22 @@ +namespace ARMeilleure.IntermediateRepresentation +{ + class PhiNode : Node + { + private BasicBlock[] _blocks; + + public PhiNode(Operand destination, int predecessorsCount) : base(destination, predecessorsCount) + { + _blocks = new BasicBlock[predecessorsCount]; + } + + public BasicBlock GetBlock(int index) + { + return _blocks[index]; + } + + public void SetBlock(int index, BasicBlock block) + { + _blocks[index] = block; + } + } +}
\ No newline at end of file diff --git a/ARMeilleure/IntermediateRepresentation/Register.cs b/ARMeilleure/IntermediateRepresentation/Register.cs new file mode 100644 index 00000000..745b3153 --- /dev/null +++ b/ARMeilleure/IntermediateRepresentation/Register.cs @@ -0,0 +1,43 @@ +using System; + +namespace ARMeilleure.IntermediateRepresentation +{ + struct Register : IEquatable<Register> + { + public int Index { get; } + + public RegisterType Type { get; } + + public Register(int index, RegisterType type) + { + Index = index; + Type = type; + } + + public override int GetHashCode() + { + return (ushort)Index | ((int)Type << 16); + } + + public static bool operator ==(Register x, Register y) + { + return x.Equals(y); + } + + public static bool operator !=(Register x, Register y) + { + return !x.Equals(y); + } + + public override bool Equals(object obj) + { + return obj is Register reg && Equals(reg); + } + + public bool Equals(Register other) + { + return other.Index == Index && + other.Type == Type; + } + } +}
\ No newline at end of file diff --git a/ARMeilleure/IntermediateRepresentation/RegisterType.cs b/ARMeilleure/IntermediateRepresentation/RegisterType.cs new file mode 100644 index 00000000..e71795cb --- /dev/null +++ b/ARMeilleure/IntermediateRepresentation/RegisterType.cs @@ -0,0 +1,9 @@ +namespace ARMeilleure.IntermediateRepresentation +{ + enum RegisterType + { + Integer, + Vector, + Flag + } +}
\ No newline at end of file |
