diff --git a/TinyTokenizer.Tests/GreenNodeEqualityTests.cs b/TinyTokenizer.Tests/GreenNodeEqualityTests.cs new file mode 100644 index 0000000..817b298 --- /dev/null +++ b/TinyTokenizer.Tests/GreenNodeEqualityTests.cs @@ -0,0 +1,99 @@ +using System.Collections.Immutable; +using TinyTokenizer.Ast; + +namespace TinyTokenizer.Tests; + +[Trait("Category", "Equality")] +public class GreenNodeEqualityTests +{ + [Fact] + public void GreenLeaf_EquivalentConstructions_AreEqual_AndHashMatches() + { + var leading = ImmutableArray.Create(GreenTrivia.Whitespace(" ")); + var trailing = ImmutableArray.Create(GreenTrivia.Newline("\n")); + + var a = new GreenLeaf(NodeKind.Ident, "x", leading, trailing); + + var b = new GreenLeaf(NodeKind.Ident, "x") + .WithLeadingTrivia(leading) + .WithTrailingTrivia(trailing); + + Assert.True(a.Equals(b)); + Assert.Equal(a.GetHashCode(), b.GetHashCode()); + } + + [Fact] + public void GreenLeaf_CacheAndNonCachePaths_BehaveConsistently() + { + // No trivia -> cached + var cached1 = GreenNodeCache.GetOrCreate(NodeKind.Symbol, "{"); + var cached2 = GreenNodeCache.CreateDelimiter('{'); + + Assert.Same(cached1, cached2); + Assert.True(cached1.Equals(cached2)); + Assert.Equal(cached1.GetHashCode(), cached2.GetHashCode()); + + // Trailing space -> cached "with space" + var spaceTrivia = ImmutableArray.Create(GreenTrivia.Whitespace(" ")); + var cachedWithSpace = GreenNodeCache.GetOrCreateWithTrailingSpace(NodeKind.Symbol, "{"); + var viaFactory = GreenNodeCache.Create(NodeKind.Symbol, "{", ImmutableArray.Empty, spaceTrivia); + + Assert.Same(cachedWithSpace, viaFactory); + Assert.Equal(cachedWithSpace.GetHashCode(), viaFactory.GetHashCode()); + AssertHasWhitespaceButNoNewlineOrComment(cachedWithSpace); + } + + [Fact] + public void StructuralSharing_WithMethods_DoNotMutateOriginals() + { + var leaf = new GreenLeaf(NodeKind.Ident, "x"); + var changed = leaf.WithText("y"); + + Assert.Equal("x", leaf.Text); + Assert.Equal("y", changed.Text); + Assert.False(leaf.Equals(changed)); + + var child1 = new GreenLeaf(NodeKind.Ident, "a"); + var child2 = new GreenLeaf(NodeKind.Ident, "b"); + var block = GreenBlock.Create('{', ImmutableArray.Create(child1, child2)); + + var inserted = block.WithInsert(2, ImmutableArray.Create(new GreenLeaf(NodeKind.Ident, "c"))); + + Assert.Equal(4, block.SlotCount); // opener + 2 children + closer + Assert.Equal(5, inserted.SlotCount); // opener + 3 children + closer + + // Original is unchanged and still shares children + Assert.Same(child1, block.GetSlot(1)); + Assert.Same(child2, block.GetSlot(2)); + + // Inserted block shares originals too + Assert.Same(child1, inserted.GetSlot(1)); + Assert.Same(child2, inserted.GetSlot(3)); + + Assert.False(block.Equals(inserted)); + } + + [Fact] + public void OnlyContentChanges_CauseInequality() + { + // Same kind/text/trivia should be equal (baseline) + var a = new GreenLeaf(NodeKind.Ident, "x"); + var b = new GreenLeaf(NodeKind.Ident, "x"); + Assert.True(a.Equals(b)); + + // Different text -> not equal + var c = new GreenLeaf(NodeKind.Ident, "y"); + Assert.False(a.Equals(c)); + + // Different kind -> not equal + var d = new GreenLeaf(NodeKind.Numeric, "x"); + Assert.False(a.Equals(d)); + } + + private static void AssertHasWhitespaceButNoNewlineOrComment(GreenLeaf leaf) + { + Assert.True((leaf.Flags & GreenNodeFlags.ContainsWhitespaceTrivia) != 0); + Assert.True((leaf.Flags & GreenNodeFlags.ContainsNewlineTrivia) == 0); + Assert.True((leaf.Flags & GreenNodeFlags.ContainsCommentTrivia) == 0); + } +} diff --git a/TinyTokenizer.Tests/GreenNodeFlagsTests.cs b/TinyTokenizer.Tests/GreenNodeFlagsTests.cs new file mode 100644 index 0000000..e0a8973 --- /dev/null +++ b/TinyTokenizer.Tests/GreenNodeFlagsTests.cs @@ -0,0 +1,222 @@ +using System.Collections.Immutable; +using TinyTokenizer.Ast; + +namespace TinyTokenizer.Tests; + +[Trait("Category", "Flags")] +public class GreenNodeFlagsTests +{ + #region Helpers + + private static ImmutableArray ParseLeaves(string source, TokenizerOptions? options = null) + { + var lexer = new GreenLexer(options ?? TokenizerOptions.Default); + var nodes = lexer.ParseToGreenNodes(source); + return CollectLeaves(nodes); + } + + private static ImmutableArray CollectLeaves(ImmutableArray nodes) + { + var builder = ImmutableArray.CreateBuilder(); + foreach (var node in nodes) + { + CollectLeavesRecursive(node, builder); + } + return builder.ToImmutable(); + } + + private static void CollectLeavesRecursive(GreenNode node, ImmutableArray.Builder builder) + { + if (node is GreenLeaf leaf) + { + builder.Add(leaf); + return; + } + + for (int i = 0; i < node.SlotCount; i++) + { + var child = node.GetSlot(i); + if (child != null) + CollectLeavesRecursive(child, builder); + } + } + + private static GreenLeaf FindLeaf(ImmutableArray leaves, string text) => + Assert.Single(leaves.Where(l => l.Text == text)); + + private static void AssertHas(GreenNodeFlags flags, GreenNodeFlags expected) + { + Assert.True((flags & expected) == expected, $"Expected flags to include {expected} but was {flags}"); + } + + private static void AssertNotHas(GreenNodeFlags flags, GreenNodeFlags unexpected) + { + Assert.True((flags & unexpected) == 0, $"Expected flags to NOT include {unexpected} but was {flags}"); + } + + #endregion + + [Fact] + public void GreenLeaf_Flags_ReflectLeadingAndTrailingTriviaKinds() + { + var leading = ImmutableArray.Create( + GreenTrivia.Whitespace(" "), + GreenTrivia.SingleLineComment("// c"), + GreenTrivia.Newline("\n")); + + var trailing = ImmutableArray.Create( + GreenTrivia.Whitespace("\t"), + GreenTrivia.MultiLineComment("/* m */")); + + var leaf = new GreenLeaf(NodeKind.Ident, "x", leading, trailing); + + AssertHas(leaf.Flags, GreenNodeFlags.HasLeadingWhitespaceTrivia); + AssertHas(leaf.Flags, GreenNodeFlags.HasLeadingCommentTrivia); + AssertHas(leaf.Flags, GreenNodeFlags.HasLeadingNewlineTrivia); + + AssertHas(leaf.Flags, GreenNodeFlags.HasTrailingWhitespaceTrivia); + AssertHas(leaf.Flags, GreenNodeFlags.HasTrailingCommentTrivia); + AssertNotHas(leaf.Flags, GreenNodeFlags.HasTrailingNewlineTrivia); + + AssertHas(leaf.Flags, GreenNodeFlags.ContainsWhitespaceTrivia); + AssertHas(leaf.Flags, GreenNodeFlags.ContainsCommentTrivia); + AssertHas(leaf.Flags, GreenNodeFlags.ContainsNewlineTrivia); + } + + [Fact] + public void GreenLeaf_Flags_IncludeKindDerivedSubtreeBits() + { + var keywordKind = NodeKindExtensions.KeywordKind(0); + + var keywordLeaf = new GreenLeaf(keywordKind, "if"); + AssertHas(keywordLeaf.Flags, GreenNodeFlags.ContainsKeyword); + AssertNotHas(keywordLeaf.Flags, GreenNodeFlagMasks.Boundary | GreenNodeFlags.ContainsWhitespaceTrivia | GreenNodeFlags.ContainsCommentTrivia | GreenNodeFlags.ContainsNewlineTrivia); + + var taggedLeaf = new GreenLeaf(NodeKind.TaggedIdent, "#define"); + AssertHas(taggedLeaf.Flags, GreenNodeFlags.ContainsTaggedIdent); + + var errorLeaf = new GreenLeaf(NodeKind.Error, ""); + AssertHas(errorLeaf.Flags, GreenNodeFlags.ContainsErrorNode); + + var normalLeaf = new GreenLeaf(NodeKind.Ident, "x"); + Assert.Equal(GreenNodeFlags.None, normalLeaf.Flags); + } + + [Fact] + public void GreenBlock_Flags_AggregateContains_AndUseOpenerLeadingPlusCloserTrailingAsBoundary() + { + var opener = new GreenLeaf( + NodeKind.Symbol, + "{", + leadingTrivia: ImmutableArray.Create(GreenTrivia.Newline("\n")), + trailingTrivia: ImmutableArray.Create(GreenTrivia.Whitespace(" "))); + + var closer = new GreenLeaf( + NodeKind.Symbol, + "}", + leadingTrivia: ImmutableArray.Create(GreenTrivia.Newline("\n")), + trailingTrivia: ImmutableArray.Create(GreenTrivia.Whitespace(" "))); + + var inner = new GreenLeaf( + NodeKind.Ident, + "x", + trailingTrivia: ImmutableArray.Create(GreenTrivia.SingleLineComment("// c"))); + + var block = new GreenBlock(opener, closer, ImmutableArray.Create(inner)); + + // Boundary behavior + AssertHas(block.Flags, GreenNodeFlags.HasLeadingNewlineTrivia); + AssertHas(block.Flags, GreenNodeFlags.HasTrailingWhitespaceTrivia); + + // Not from opener trailing / closer leading + AssertNotHas(block.Flags, GreenNodeFlags.HasLeadingWhitespaceTrivia); + AssertNotHas(block.Flags, GreenNodeFlags.HasTrailingNewlineTrivia); + + // Subtree contains aggregation + AssertHas(block.Flags, GreenNodeFlags.ContainsNewlineTrivia); + AssertHas(block.Flags, GreenNodeFlags.ContainsWhitespaceTrivia); + AssertHas(block.Flags, GreenNodeFlags.ContainsCommentTrivia); + } + + [Fact] + public void GreenList_Flags_UseFirstLeadingAndLastTrailingAsBoundary() + { + var first = new GreenLeaf(NodeKind.Ident, "a", leadingTrivia: ImmutableArray.Create(GreenTrivia.Whitespace(" "))); + var middle = new GreenLeaf(NodeKind.Ident, "b", trailingTrivia: ImmutableArray.Create(GreenTrivia.SingleLineComment("// c"))); + var last = new GreenLeaf(NodeKind.Ident, "c", trailingTrivia: ImmutableArray.Create(GreenTrivia.Newline("\n"))); + + var list = new GreenList(ImmutableArray.Create(first, middle, last)); + + AssertHas(list.Flags, GreenNodeFlags.HasLeadingWhitespaceTrivia); + AssertHas(list.Flags, GreenNodeFlags.HasTrailingNewlineTrivia); + + AssertHas(list.Flags, GreenNodeFlags.ContainsWhitespaceTrivia); + AssertHas(list.Flags, GreenNodeFlags.ContainsCommentTrivia); + AssertHas(list.Flags, GreenNodeFlags.ContainsNewlineTrivia); + } + + [Fact] + public void GreenSyntaxNode_Flags_AggregateLikeList() + { + var kind = NodeKindExtensions.SemanticKind(0); + + var first = new GreenLeaf(NodeKind.Ident, "a", leadingTrivia: ImmutableArray.Create(GreenTrivia.Whitespace(" "))); + var last = new GreenLeaf(NodeKind.Ident, "b", trailingTrivia: ImmutableArray.Create(GreenTrivia.Newline("\n"))); + + var node = new GreenSyntaxNode(kind, first, last); + + AssertHas(node.Flags, GreenNodeFlags.HasLeadingWhitespaceTrivia); + AssertHas(node.Flags, GreenNodeFlags.HasTrailingNewlineTrivia); + AssertHas(node.Flags, GreenNodeFlags.ContainsWhitespaceTrivia); + AssertHas(node.Flags, GreenNodeFlags.ContainsNewlineTrivia); + } + + [Fact] + public void ParsedNewlineOwnership_ReflectedInFlags_CommonCase() + { + var leaves = ParseLeaves("a\nb"); + + var a = FindLeaf(leaves, "a"); + var b = FindLeaf(leaves, "b"); + + AssertHas(a.Flags, GreenNodeFlags.HasTrailingNewlineTrivia); + AssertHas(a.Flags, GreenNodeFlags.ContainsNewlineTrivia); + + AssertNotHas(b.Flags, GreenNodeFlags.HasLeadingNewlineTrivia); + } + + [Fact] + public void ParsedSameLineCommentOwnership_ReflectedInFlags() + { + var options = TokenizerOptions.Default.WithCommentStyles(CommentStyle.CStyleSingleLine); + var leaves = ParseLeaves("a // c\nb", options); + + var a = FindLeaf(leaves, "a"); + var b = FindLeaf(leaves, "b"); + + AssertHas(a.Flags, GreenNodeFlags.HasTrailingCommentTrivia); + AssertHas(a.Flags, GreenNodeFlags.HasTrailingNewlineTrivia); + AssertHas(a.Flags, GreenNodeFlags.ContainsCommentTrivia); + AssertHas(a.Flags, GreenNodeFlags.ContainsNewlineTrivia); + + Assert.Empty(b.LeadingTrivia); + AssertNotHas(b.Flags, GreenNodeFlags.HasLeadingCommentTrivia | GreenNodeFlags.HasLeadingNewlineTrivia); + } + + [Fact] + public void ParsedOwnLineCommentOwnership_ReflectedInFlags() + { + var options = TokenizerOptions.Default.WithCommentStyles(CommentStyle.CStyleSingleLine); + var leaves = ParseLeaves("a\n// c\nb", options); + + var a = FindLeaf(leaves, "a"); + var b = FindLeaf(leaves, "b"); + + AssertHas(a.Flags, GreenNodeFlags.HasTrailingNewlineTrivia); + + AssertHas(b.Flags, GreenNodeFlags.HasLeadingCommentTrivia); + AssertHas(b.Flags, GreenNodeFlags.HasLeadingNewlineTrivia); + AssertHas(b.Flags, GreenNodeFlags.ContainsCommentTrivia); + AssertHas(b.Flags, GreenNodeFlags.ContainsNewlineTrivia); + } +} diff --git a/TinyTokenizer/Ast/GreenBlock.cs b/TinyTokenizer/Ast/GreenBlock.cs index 968dc1e..115f71d 100644 --- a/TinyTokenizer/Ast/GreenBlock.cs +++ b/TinyTokenizer/Ast/GreenBlock.cs @@ -24,10 +24,14 @@ internal sealed record GreenBlock : GreenContainer private readonly ImmutableArray _children; private readonly int _width; + private readonly GreenNodeFlags _flags; private readonly int[]? _childOffsets; // Pre-computed for ≥10 children (O(1) lookup) /// public override NodeKind Kind { get; } + + /// + internal override GreenNodeFlags Flags => _flags; /// The opening delimiter node (e.g., '{', '[', '(') with its trivia. public GreenLeaf OpenerNode { get; } @@ -75,6 +79,19 @@ public GreenBlock( childrenWidth += child.Width; _width = OpenerNode.Width + childrenWidth + CloserNode.Width; + + // Flags + // - Boundary comes only from the first/last leaf boundaries (opener leading, closer trailing). + // - Subtree contains flags are ORed across opener/inner/closer (excluding boundary bits). + var boundary = + (OpenerNode.Flags & GreenNodeFlagMasks.LeadingBoundary) | + (CloserNode.Flags & GreenNodeFlagMasks.TrailingBoundary); + + var contains = (OpenerNode.Flags | CloserNode.Flags) & GreenNodeFlagMasks.Contains; + foreach (var child in _children) + contains |= child.Flags & GreenNodeFlagMasks.Contains; + + _flags = boundary | contains; // Pre-compute child offsets for large blocks if (_children.Length >= 10) diff --git a/TinyTokenizer/Ast/GreenLeaf.cs b/TinyTokenizer/Ast/GreenLeaf.cs index a53f030..bc1f0a6 100644 --- a/TinyTokenizer/Ast/GreenLeaf.cs +++ b/TinyTokenizer/Ast/GreenLeaf.cs @@ -17,9 +17,13 @@ internal sealed record GreenLeaf : GreenNode $"{Kind}[{Width}] \"{Truncate(Text, 20)}\""; private readonly int _width; + private readonly GreenNodeFlags _flags; /// public override NodeKind Kind { get; } + + /// + internal override GreenNodeFlags Flags => _flags; /// The text content of this token (excluding trivia). public string Text { get; } @@ -61,10 +65,31 @@ public GreenLeaf( Text = text; LeadingTrivia = leadingTrivia.IsDefault ? ImmutableArray.Empty : leadingTrivia; TrailingTrivia = trailingTrivia.IsDefault ? ImmutableArray.Empty : trailingTrivia; - - LeadingTriviaWidth = ComputeTriviaWidth(LeadingTrivia); - TrailingTriviaWidth = ComputeTriviaWidth(TrailingTrivia); + + LeadingTriviaWidth = ComputeTriviaWidthAndFlags( + LeadingTrivia, + isLeading: true, + out var leadingBoundaryFlags, + out var leadingContainsFlags); + + TrailingTriviaWidth = ComputeTriviaWidthAndFlags( + TrailingTrivia, + isLeading: false, + out var trailingBoundaryFlags, + out var trailingContainsFlags); + _width = LeadingTriviaWidth + Text.Length + TrailingTriviaWidth; + + // Subtree flags based on node kind + var kindFlags = GreenNodeFlags.None; + if (kind == NodeKind.Error) + kindFlags |= GreenNodeFlags.ContainsErrorNode; + if (kind == NodeKind.TaggedIdent) + kindFlags |= GreenNodeFlags.ContainsTaggedIdent; + if (kind.IsKeyword()) + kindFlags |= GreenNodeFlags.ContainsKeyword; + + _flags = leadingBoundaryFlags | trailingBoundaryFlags | leadingContainsFlags | trailingContainsFlags | kindFlags; } /// @@ -114,11 +139,56 @@ public GreenLeaf WithTrailingTrivia(ImmutableArray trivia) public GreenLeaf WithText(string text) => new(Kind, text, LeadingTrivia, TrailingTrivia); - private static int ComputeTriviaWidth(ImmutableArray trivia) + private static int ComputeTriviaWidthAndFlags( + ImmutableArray trivia, + bool isLeading, + out GreenNodeFlags boundaryFlags, + out GreenNodeFlags containsFlags) { + boundaryFlags = GreenNodeFlags.None; + containsFlags = GreenNodeFlags.None; + int width = 0; + bool hasNewline = false; + bool hasWhitespace = false; + bool hasComment = false; + foreach (var t in trivia) + { width += t.Width; + switch (t.Kind) + { + case TriviaKind.Newline: + hasNewline = true; + break; + case TriviaKind.Whitespace: + hasWhitespace = true; + break; + case TriviaKind.SingleLineComment: + case TriviaKind.MultiLineComment: + hasComment = true; + break; + } + } + + if (hasNewline) + { + boundaryFlags |= isLeading ? GreenNodeFlags.HasLeadingNewlineTrivia : GreenNodeFlags.HasTrailingNewlineTrivia; + containsFlags |= GreenNodeFlags.ContainsNewlineTrivia; + } + + if (hasWhitespace) + { + boundaryFlags |= isLeading ? GreenNodeFlags.HasLeadingWhitespaceTrivia : GreenNodeFlags.HasTrailingWhitespaceTrivia; + containsFlags |= GreenNodeFlags.ContainsWhitespaceTrivia; + } + + if (hasComment) + { + boundaryFlags |= isLeading ? GreenNodeFlags.HasLeadingCommentTrivia : GreenNodeFlags.HasTrailingCommentTrivia; + containsFlags |= GreenNodeFlags.ContainsCommentTrivia; + } + return width; } } diff --git a/TinyTokenizer/Ast/GreenList.cs b/TinyTokenizer/Ast/GreenList.cs index cccc566..c6a3ab5 100644 --- a/TinyTokenizer/Ast/GreenList.cs +++ b/TinyTokenizer/Ast/GreenList.cs @@ -18,6 +18,7 @@ internal sealed record GreenList : GreenContainer private readonly ImmutableArray _children; private readonly int _width; + private readonly GreenNodeFlags _flags; private readonly int[]? _childOffsets; /// @@ -25,6 +26,9 @@ internal sealed record GreenList : GreenContainer /// public override int Width => _width; + + /// + internal override GreenNodeFlags Flags => _flags; /// public override ImmutableArray Children => _children; @@ -41,6 +45,27 @@ public GreenList(ImmutableArray children) foreach (var child in _children) width += child.Width; _width = width; + + // Flags + if (_children.Length == 0) + { + _flags = GreenNodeFlags.None; + } + else + { + var first = _children[0]; + var last = _children[^1]; + + var boundary = + (first.Flags & GreenNodeFlagMasks.LeadingBoundary) | + (last.Flags & GreenNodeFlagMasks.TrailingBoundary); + + var contains = GreenNodeFlags.None; + foreach (var child in _children) + contains |= child.Flags & GreenNodeFlagMasks.Contains; + + _flags = boundary | contains; + } // Pre-compute offsets for large lists if (_children.Length >= 10) diff --git a/TinyTokenizer/Ast/GreenNode.cs b/TinyTokenizer/Ast/GreenNode.cs index b563cae..a9e65e5 100644 --- a/TinyTokenizer/Ast/GreenNode.cs +++ b/TinyTokenizer/Ast/GreenNode.cs @@ -36,6 +36,12 @@ protected static string Truncate(string text, int maxLength) /// The kind of this node. public abstract NodeKind Kind { get; } + + /// + /// Cached flags describing trivia/content properties for fast queries. + /// Concrete green node types override this once flags are computed. + /// + internal virtual GreenNodeFlags Flags => GreenNodeFlags.None; /// /// Total character width of this node, including any trivia. diff --git a/TinyTokenizer/Ast/GreenNodeFlags.cs b/TinyTokenizer/Ast/GreenNodeFlags.cs new file mode 100644 index 0000000..89124a7 --- /dev/null +++ b/TinyTokenizer/Ast/GreenNodeFlags.cs @@ -0,0 +1,55 @@ +using System; + +namespace TinyTokenizer.Ast; + +/// +/// Bitflags describing trivia and content properties of a green node. +/// Intended for O(1) query checks and subtree pruning. +/// +[Flags] +internal enum GreenNodeFlags : uint +{ + None = 0, + + // Boundary trivia (left/right edge of the node's text span) + HasLeadingNewlineTrivia = 1u << 0, + HasTrailingNewlineTrivia = 1u << 1, + + HasLeadingWhitespaceTrivia = 1u << 2, + HasTrailingWhitespaceTrivia = 1u << 3, + + HasLeadingCommentTrivia = 1u << 4, + HasTrailingCommentTrivia = 1u << 5, + + // Subtree flags (anywhere within the node's subtree) + ContainsNewlineTrivia = 1u << 8, + ContainsWhitespaceTrivia = 1u << 9, + ContainsCommentTrivia = 1u << 10, + + ContainsErrorNode = 1u << 11, + ContainsKeyword = 1u << 12, + ContainsTaggedIdent = 1u << 13, +} + +internal static class GreenNodeFlagMasks +{ + public const GreenNodeFlags LeadingBoundary = + GreenNodeFlags.HasLeadingNewlineTrivia | + GreenNodeFlags.HasLeadingWhitespaceTrivia | + GreenNodeFlags.HasLeadingCommentTrivia; + + public const GreenNodeFlags TrailingBoundary = + GreenNodeFlags.HasTrailingNewlineTrivia | + GreenNodeFlags.HasTrailingWhitespaceTrivia | + GreenNodeFlags.HasTrailingCommentTrivia; + + public const GreenNodeFlags Boundary = LeadingBoundary | TrailingBoundary; + + public const GreenNodeFlags Contains = + GreenNodeFlags.ContainsNewlineTrivia | + GreenNodeFlags.ContainsWhitespaceTrivia | + GreenNodeFlags.ContainsCommentTrivia | + GreenNodeFlags.ContainsErrorNode | + GreenNodeFlags.ContainsKeyword | + GreenNodeFlags.ContainsTaggedIdent; +} diff --git a/TinyTokenizer/Ast/GreenSyntaxNode.cs b/TinyTokenizer/Ast/GreenSyntaxNode.cs index 8ace7d0..ed21515 100644 --- a/TinyTokenizer/Ast/GreenSyntaxNode.cs +++ b/TinyTokenizer/Ast/GreenSyntaxNode.cs @@ -24,6 +24,7 @@ internal sealed record GreenSyntaxNode : GreenContainer private readonly ImmutableArray _children; private readonly NodeKind _kind; private readonly int _width; + private readonly GreenNodeFlags _flags; /// /// Creates a green syntax node wrapping the specified children. @@ -42,6 +43,27 @@ public GreenSyntaxNode(NodeKind kind, ImmutableArray children) width += child.Width; } _width = width; + + // Flags + if (_children.Length == 0) + { + _flags = GreenNodeFlags.None; + } + else + { + var first = _children[0]; + var last = _children[^1]; + + var boundary = + (first.Flags & GreenNodeFlagMasks.LeadingBoundary) | + (last.Flags & GreenNodeFlagMasks.TrailingBoundary); + + var contains = GreenNodeFlags.None; + foreach (var child in _children) + contains |= child.Flags & GreenNodeFlagMasks.Contains; + + _flags = boundary | contains; + } } /// @@ -57,6 +79,9 @@ public GreenSyntaxNode(NodeKind kind, params GreenNode[] children) /// public override int Width => _width; + + /// + internal override GreenNodeFlags Flags => _flags; /// public override ImmutableArray Children => _children;