From d40f6d315107e70d50f497e24920a038fa047c31 Mon Sep 17 00:00:00 2001 From: Ryan Nowak Date: Thu, 27 Oct 2016 10:46:05 -0700 Subject: [PATCH] Add abstractions for Razor IR This is an API skeleton for the IR data model that we'll be using as a spiritual continuation of the 'chunks' API. Currently missing a lot of detail which will be filled in as needed. --- .../Intermediate/DefaultIRBuilder.cs | 75 +++++++++ .../Intermediate/IRBuilder.cs | 23 +++ .../Intermediate/IRDocument.cs | 31 ++++ .../Intermediate/IRNode.cs | 20 +++ .../Intermediate/IRNodeVisitor.cs | 22 +++ .../Intermediate/IRNodeVisitorOfT.cs | 23 +++ .../Intermediate/IRNodeWalker.cs | 18 +++ .../Properties/Resources.Designer.cs | 16 ++ .../Resources.resx | 3 + .../Intermediate/DefaultIRBuilderTest.cs | 145 ++++++++++++++++++ .../Intermediate/IRBuilderTest.cs | 20 +++ .../Intermediate/IRNodeWalkerTest.cs | 93 +++++++++++ 12 files changed, 489 insertions(+) create mode 100644 src/Microsoft.AspNetCore.Razor.Evolution/Intermediate/DefaultIRBuilder.cs create mode 100644 src/Microsoft.AspNetCore.Razor.Evolution/Intermediate/IRBuilder.cs create mode 100644 src/Microsoft.AspNetCore.Razor.Evolution/Intermediate/IRDocument.cs create mode 100644 src/Microsoft.AspNetCore.Razor.Evolution/Intermediate/IRNode.cs create mode 100644 src/Microsoft.AspNetCore.Razor.Evolution/Intermediate/IRNodeVisitor.cs create mode 100644 src/Microsoft.AspNetCore.Razor.Evolution/Intermediate/IRNodeVisitorOfT.cs create mode 100644 src/Microsoft.AspNetCore.Razor.Evolution/Intermediate/IRNodeWalker.cs create mode 100644 test/Microsoft.AspNetCore.Razor.Evolution.Test/Intermediate/DefaultIRBuilderTest.cs create mode 100644 test/Microsoft.AspNetCore.Razor.Evolution.Test/Intermediate/IRBuilderTest.cs create mode 100644 test/Microsoft.AspNetCore.Razor.Evolution.Test/Intermediate/IRNodeWalkerTest.cs diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/Intermediate/DefaultIRBuilder.cs b/src/Microsoft.AspNetCore.Razor.Evolution/Intermediate/DefaultIRBuilder.cs new file mode 100644 index 0000000000..71cc105d67 --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.Evolution/Intermediate/DefaultIRBuilder.cs @@ -0,0 +1,75 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; + +namespace Microsoft.AspNetCore.Razor.Evolution.Intermediate +{ + internal class DefaultIRBuilder : IRBuilder + { + private readonly List _stack; + private int _depth; + + public DefaultIRBuilder() + { + _stack = new List(); + } + + public override IRNode Current + { + get + { + return _depth > 0 ? _stack[_depth - 1] : null; + } + } + + public override void Add(IRNode node) + { + if (node == null) + { + throw new ArgumentNullException(nameof(node)); + } + + Push(node); + Pop(); + } + + public override IRNode Pop() + { + if (_depth == 0) + { + throw new InvalidOperationException(Resources.FormatIRBuilder_PopInvalid(nameof(Pop))); + } + + var node = _stack[--_depth]; + return node; + } + + public override void Push(IRNode node) + { + if (node == null) + { + throw new ArgumentNullException(nameof(node)); + } + + if (_depth >= _stack.Count) + { + _stack.Add(node); + } + else + { + _stack[_depth] = node; + } + + if (_depth > 0) + { + var parent = _stack[_depth - 1]; + node.Parent = parent; + parent.Children.Add(node); + } + + _depth++; + } + } +} diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/Intermediate/IRBuilder.cs b/src/Microsoft.AspNetCore.Razor.Evolution/Intermediate/IRBuilder.cs new file mode 100644 index 0000000000..9df1b17bef --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.Evolution/Intermediate/IRBuilder.cs @@ -0,0 +1,23 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Razor.Evolution.Intermediate +{ + public abstract class IRBuilder + { + public static IRBuilder Document() + { + var builder = new DefaultIRBuilder(); + builder.Push(new IRDocument()); + return builder; + } + + public abstract IRNode Current { get; } + + public abstract void Add(IRNode node); + + public abstract void Push(IRNode node); + + public abstract IRNode Pop(); + } +} diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/Intermediate/IRDocument.cs b/src/Microsoft.AspNetCore.Razor.Evolution/Intermediate/IRDocument.cs new file mode 100644 index 0000000000..4cd40c46ba --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.Evolution/Intermediate/IRDocument.cs @@ -0,0 +1,31 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; + +namespace Microsoft.AspNetCore.Razor.Evolution.Intermediate +{ + public sealed class IRDocument : IRNode + { + // Only allow creation of documents through the builder API because + // they can't be nested. + internal IRDocument() + { + Children = new List(); + } + + public override IList Children { get; } + + public override IRNode Parent { get; set; } + + public override void Accept(IRNodeVisitor visitor) + { + visitor.VisitDocument(this); + } + + public override TResult Accept(IRNodeVisitor visitor) + { + return visitor.VisitDocument(this); + } + } +} diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/Intermediate/IRNode.cs b/src/Microsoft.AspNetCore.Razor.Evolution/Intermediate/IRNode.cs new file mode 100644 index 0000000000..ed596676e8 --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.Evolution/Intermediate/IRNode.cs @@ -0,0 +1,20 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; + +namespace Microsoft.AspNetCore.Razor.Evolution.Intermediate +{ + public abstract class IRNode + { + internal static readonly IRNode[] EmptyArray = new IRNode[0]; + + public abstract IList Children { get; } + + public abstract IRNode Parent { get; set; } + + public abstract void Accept(IRNodeVisitor visitor); + + public abstract TResult Accept(IRNodeVisitor visitor); + } +} diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/Intermediate/IRNodeVisitor.cs b/src/Microsoft.AspNetCore.Razor.Evolution/Intermediate/IRNodeVisitor.cs new file mode 100644 index 0000000000..8e5b1df86c --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.Evolution/Intermediate/IRNodeVisitor.cs @@ -0,0 +1,22 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Razor.Evolution.Intermediate +{ + public abstract class IRNodeVisitor + { + public virtual void Visit(IRNode node) + { + node.Accept(this); + } + + public virtual void VisitDefault(IRNode node) + { + } + + public virtual void VisitDocument(IRDocument node) + { + VisitDefault(node); + } + } +} diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/Intermediate/IRNodeVisitorOfT.cs b/src/Microsoft.AspNetCore.Razor.Evolution/Intermediate/IRNodeVisitorOfT.cs new file mode 100644 index 0000000000..9fa0e1a246 --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.Evolution/Intermediate/IRNodeVisitorOfT.cs @@ -0,0 +1,23 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Razor.Evolution.Intermediate +{ + public abstract class IRNodeVisitor + { + public virtual TResult Visit(IRNode node) + { + return node.Accept(this); + } + + public virtual TResult VisitDefault(IRNode node) + { + return default(TResult); + } + + public virtual TResult VisitDocument(IRDocument node) + { + return VisitDefault(node); + } + } +} diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/Intermediate/IRNodeWalker.cs b/src/Microsoft.AspNetCore.Razor.Evolution/Intermediate/IRNodeWalker.cs new file mode 100644 index 0000000000..2ea7a60224 --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.Evolution/Intermediate/IRNodeWalker.cs @@ -0,0 +1,18 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Razor.Evolution.Intermediate +{ + public abstract class IRNodeWalker : IRNodeVisitor + { + public override void VisitDefault(IRNode node) + { + var children = node.Children; + for (var i = 0; i < node.Children.Count; i++) + { + var child = children[i]; + Visit(child); + } + } + } +} diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/Properties/Resources.Designer.cs b/src/Microsoft.AspNetCore.Razor.Evolution/Properties/Resources.Designer.cs index 4fbc0eb223..549494a35c 100644 --- a/src/Microsoft.AspNetCore.Razor.Evolution/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNetCore.Razor.Evolution/Properties/Resources.Designer.cs @@ -10,6 +10,22 @@ namespace Microsoft.AspNetCore.Razor.Evolution private static readonly ResourceManager _resourceManager = new ResourceManager("Microsoft.AspNetCore.Razor.Evolution.Resources", typeof(Resources).GetTypeInfo().Assembly); + /// + /// The '{0}' operation is not valid when the builder is empty. + /// + internal static string IRBuilder_PopInvalid + { + get { return GetString("IRBuilder_PopInvalid"); } + } + + /// + /// The '{0}' operation is not valid when the builder is empty. + /// + internal static string FormatIRBuilder_PopInvalid(object p0) + { + return string.Format(CultureInfo.CurrentCulture, GetString("IRBuilder_PopInvalid"), p0); + } + /// /// The '{0}' phase requires a '{1}' provided by the '{2}'. /// diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/Resources.resx b/src/Microsoft.AspNetCore.Razor.Evolution/Resources.resx index 55420a29df..9fd6aa5d07 100644 --- a/src/Microsoft.AspNetCore.Razor.Evolution/Resources.resx +++ b/src/Microsoft.AspNetCore.Razor.Evolution/Resources.resx @@ -117,6 +117,9 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + The '{0}' operation is not valid when the builder is empty. + The '{0}' phase requires a '{1}' provided by the '{2}'. diff --git a/test/Microsoft.AspNetCore.Razor.Evolution.Test/Intermediate/DefaultIRBuilderTest.cs b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Intermediate/DefaultIRBuilderTest.cs new file mode 100644 index 0000000000..7395be5a6b --- /dev/null +++ b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Intermediate/DefaultIRBuilderTest.cs @@ -0,0 +1,145 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using Microsoft.AspNetCore.Testing; +using Xunit; + +namespace Microsoft.AspNetCore.Razor.Evolution.Intermediate +{ + public class DefaultIRBuilderTest + { + [Fact] + public void Ctor_CreatesEmptyBuilder() + { + // Arrange & Act + var builder = new DefaultIRBuilder(); + var current = builder.Current; + + // Assert + Assert.Null(current); + } + + [Fact] + public void Push_WhenEmpty_AddsNode() + { + // Arrange + var builder = new DefaultIRBuilder(); + var node = new BasicIRNode(); + + // Act + builder.Push(node); + + // Assert + Assert.Same(node, builder.Current); + Assert.Null(node.Parent); + } + + [Fact] + public void Push_WhenNonEmpty_SetsUpParentAndChild() + { + // Arrange + var builder = new DefaultIRBuilder(); + + var parent = new BasicIRNode(); + builder.Push(parent); + + var node = new BasicIRNode(); + + // Act + builder.Push(node); + + // Assert + Assert.Same(node, builder.Current); + Assert.Same(parent, node.Parent); + Assert.Collection(parent.Children, n => Assert.Same(node, n)); + } + + [Fact] + public void Pop_ThrowsWhenEmpty() + { + // Arrange + var builder = new DefaultIRBuilder(); + + // Act & Assert + ExceptionAssert.Throws( + () => builder.Pop(), + "The 'Pop' operation is not valid when the builder is empty."); + } + + [Fact] + public void Pop_SingleNodeDepth_RemovesAndReturnsNode() + { + // Arrange + var builder = new DefaultIRBuilder(); + + var node = new BasicIRNode(); + builder.Push(node); + + // Act + var result = builder.Pop(); + + // Assert + Assert.Same(node, result); + Assert.Null(builder.Current); + } + + [Fact] + public void Pop_MultipleNodeDepth_RemovesAndReturnsNode() + { + // Arrange + var builder = new DefaultIRBuilder(); + + var parent = new BasicIRNode(); + builder.Push(parent); + + var node = new BasicIRNode(); + builder.Push(node); + + // Act + var result = builder.Pop(); + + // Assert + Assert.Same(node, result); + Assert.Same(parent, builder.Current); + } + + [Fact] + public void Add_DoesPushAndPop() + { + // Arrange + var builder = new DefaultIRBuilder(); + + var parent = new BasicIRNode(); + builder.Push(parent); + + var node = new BasicIRNode(); + + // Act + builder.Add(node); + + // Assert + Assert.Same(parent, builder.Current); + Assert.Same(parent, node.Parent); + Assert.Collection(parent.Children, n => Assert.Same(node, n)); + } + + private class BasicIRNode : IRNode + { + public override IList Children { get; } = new List(); + + public override IRNode Parent { get; set; } + + public override void Accept(IRNodeVisitor visitor) + { + throw new NotImplementedException(); + } + + public override TResult Accept(IRNodeVisitor visitor) + { + throw new NotImplementedException(); + } + } + } +} diff --git a/test/Microsoft.AspNetCore.Razor.Evolution.Test/Intermediate/IRBuilderTest.cs b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Intermediate/IRBuilderTest.cs new file mode 100644 index 0000000000..44a919f28a --- /dev/null +++ b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Intermediate/IRBuilderTest.cs @@ -0,0 +1,20 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Xunit; + +namespace Microsoft.AspNetCore.Razor.Evolution.Intermediate +{ + public class IRBuilderTest + { + [Fact] + public void Document_CreatesDocumentNode() + { + // Arrange & Act + var builder = IRBuilder.Document(); + + // Assert + Assert.IsType(builder.Current); + } + } +} diff --git a/test/Microsoft.AspNetCore.Razor.Evolution.Test/Intermediate/IRNodeWalkerTest.cs b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Intermediate/IRNodeWalkerTest.cs new file mode 100644 index 0000000000..25e66b9432 --- /dev/null +++ b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Intermediate/IRNodeWalkerTest.cs @@ -0,0 +1,93 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using Xunit; + +namespace Microsoft.AspNetCore.Razor.Evolution.Intermediate +{ + public class IRNodeWalkerTest + { + [Fact] + public void IRNodeWalker_Visit_TraversesEntireGraph() + { + // Arrange + var walker = new DerivedIRNodeWalker(); + + var nodes = new IRNode[] + { + new BasicIRNode("Root"), + new BasicIRNode("Root->A"), + new BasicIRNode("Root->B"), + new BasicIRNode("Root->B->1"), + new BasicIRNode("Root->B->2"), + new BasicIRNode("Root->C"), + }; + + var builder = new DefaultIRBuilder(); + builder.Push(nodes[0]); + builder.Add(nodes[1]); + builder.Push(nodes[2]); + builder.Add(nodes[3]); + builder.Add(nodes[4]); + builder.Pop(); + builder.Add(nodes[5]); + + var root = builder.Pop(); + + // Act + walker.Visit(root); + + // Assert + Assert.Equal(nodes, walker.Visited.ToArray()); + } + + private class DerivedIRNodeWalker : IRNodeWalker + { + public List Visited { get; } = new List(); + + public override void VisitDefault(IRNode node) + { + Visited.Add(node); + + base.VisitDefault(node); + } + + public virtual void VisitBasic(BasicIRNode node) + { + VisitDefault(node); + } + } + + + private class BasicIRNode : IRNode + { + public BasicIRNode(string name) + { + Name = name; + } + + public string Name { get; } + + public override IList Children { get; } = new List(); + + public override IRNode Parent { get; set; } + + public override void Accept(IRNodeVisitor visitor) + { + ((DerivedIRNodeWalker)visitor).VisitBasic(this); + } + + public override TResult Accept(IRNodeVisitor visitor) + { + throw new NotImplementedException(); + } + + public override string ToString() + { + return Name; + } + } + } +}