Skip to content

Commit

Permalink
Precompute scope references (Parent, VarScope, ThisScope) during pars…
Browse files Browse the repository at this point in the history
…ing + make parent node and scope info tracking composable
  • Loading branch information
adams85 committed Jun 1, 2024
1 parent a0bc794 commit 4e7921c
Show file tree
Hide file tree
Showing 11 changed files with 330 additions and 133 deletions.
1 change: 0 additions & 1 deletion samples/Acornima.Cli/Commands/PrintScopesCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,6 @@ public int OnExecute()
_ => throw new InvalidOperationException()
};


var treePrinter = new TreePrinter(_console);
treePrinter.Print(new[] { rootNode },
node => node
Expand Down
167 changes: 133 additions & 34 deletions src/Acornima.Extras/ParserOptionsExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,67 +1,166 @@
using System;
using Acornima.Ast;
using Acornima.Helpers;

namespace Acornima;

public static class ParserOptionsExtensions
{
private static readonly OnNodeHandler s_parentSetter = (Node node, in Scope _, ReadOnlySpan<Scope> _) =>
public static TOptions RecordParentNodeInUserData<TOptions>(this TOptions options, bool enable = true)
where TOptions : ParserOptions
{
foreach (var child in node.ChildNodes)
var helper = options._onNode?.GetInvocationList() is { } invocationList
? (OnNodeHelper?)Array.Find(invocationList, invocation => invocation.Target is OnNodeHelper)?.Target
: null;

if (enable)
{
child.UserData = node;
(helper ??= new OnNodeHelper()).EnableParentNodeRecoding(options);
}
};
else
{
helper?.DisableParentNodeRecoding(options);
}

return options;
}

private static readonly OnNodeHandler s_scopeInfoSetter = (Node node, in Scope scope, ReadOnlySpan<Scope> scopeStack) =>
public static TOptions RecordScopeInfoInUserData<TOptions>(this TOptions options, bool enable = true)
where TOptions : ParserOptions
{
if (!Scope.IsNullRef(scope))
var helper = options._onNode?.GetInvocationList() is { } invocationList
? (OnNodeHelper?)Array.Find(invocationList, invocation => invocation.Target is OnNodeHelper)?.Target
: null;

if (enable)
{
node.UserData = new ScopeInfo(node, scope, scopeStack);
(helper ??= new OnNodeHelper()).EnableScopeInfoRecoding(options);
}
else
{
helper?.DisableScopeInfoRecoding(options);
}

foreach (var child in node.ChildNodes)
return options;
}

private sealed class OnNodeHelper
{
private OnNodeHandler? _handler;
private ArrayList<ScopeInfo> _scopes;

private void ReleaseLargeBuffers()
{
if (child.UserData is ScopeInfo scopeInfo)
_scopes.Clear();
if (_scopes.Capacity > 64)
{
scopeInfo.AssociatedNodeParent = node;
_scopes.Capacity = 64;
}
else
}

public void EnableParentNodeRecoding(ParserOptions options)
{
if (_handler is null)
{
child.UserData = node;
options._onNode += _handler = SetParentNode;
options._onReleaseLargeBuffers += ReleaseLargeBuffers;
}
else if (_handler == SetScopeInfo)
{
options._onNode -= _handler;
options._onNode += _handler = SetParentNodeAndScopeInfo;
}
}
};

/// <remarks>
/// WARNING: Enabling this together with <see cref="RecordScopeInfoInUserData"/> is an undefined behavior.
/// </remarks>
public static TOptions RecordParentNodeInUserData<TOptions>(this TOptions options, bool enable = true)
where TOptions : ParserOptions
{
options._onNode = (OnNodeHandler?)Delegate.RemoveAll(options._onNode, s_parentSetter);
public void DisableParentNodeRecoding(ParserOptions options)
{
if (_handler == SetParentNodeAndScopeInfo)
{
options._onNode -= _handler;
options._onNode += _handler = SetScopeInfo;
}
else if (_handler == SetParentNode)
{
ReleaseLargeBuffers();
options._onReleaseLargeBuffers -= ReleaseLargeBuffers;
options._onNode -= _handler;
}
}

if (enable)
public void EnableScopeInfoRecoding(ParserOptions options)
{
options._onNode += s_parentSetter;
if (_handler is null)
{
options._onNode += _handler = SetScopeInfo;
options._onReleaseLargeBuffers += ReleaseLargeBuffers;
}
else if (_handler == SetParentNode)
{
options._onNode -= _handler;
options._onNode += _handler = SetParentNodeAndScopeInfo;
}
}

return options;
}
public void DisableScopeInfoRecoding(ParserOptions options)
{
if (_handler == SetParentNodeAndScopeInfo)
{
options._onNode -= _handler;
options._onNode += _handler = SetParentNode;
}
else if (_handler == SetScopeInfo)
{
ReleaseLargeBuffers();
options._onReleaseLargeBuffers -= ReleaseLargeBuffers;
options._onNode -= _handler;
}
}

/// <remarks>
/// WARNING: Enabling this together with <see cref="RecordParentNodeInUserData"/> is an undefined behavior.
/// </remarks>
public static TOptions RecordScopeInfoInUserData<TOptions>(this TOptions options, bool enable = true)
where TOptions : ParserOptions
{
options._onNode = (OnNodeHandler?)Delegate.RemoveAll(options.OnNode, s_scopeInfoSetter);
private void SetParentNode(Node node, in Scope scope, ReadOnlySpan<Scope> scopeStack)
{
foreach (var child in node.ChildNodes)
{
child.UserData = node;
}
}

if (enable)
private void SetScopeInfo(Node node, in Scope scope, ReadOnlySpan<Scope> scopeStack)
{
options._onNode += s_scopeInfoSetter;
if (!Scope.IsNullRef(scope))
{
for (var n = scope.Id - _scopes.Count; n >= 0; n--)
{
ref var scopeInfoRef = ref _scopes.PushRef();
scopeInfoRef ??= new ScopeInfo();
}

var scopeInfo = _scopes[scope.Id];
scopeInfo.Initialize(node,
parent: scope.Id != scopeStack[0].Id ? _scopes[scopeStack.Last().Id] : null,
varScope: scope.CurrentVarScopeIndex == scopeStack.Length ? scopeInfo : _scopes[scopeStack[scope.CurrentVarScopeIndex].Id],
thisScope: scope.CurrentThisScopeIndex == scopeStack.Length ? scopeInfo : _scopes[scopeStack[scope.CurrentThisScopeIndex].Id],
varNames: scope.VarNames,
lexicalNames: scope.LexicalNames,
functionNames: scope.FunctionNames);
node.UserData = scopeInfo;
}
}

return options;
private void SetParentNodeAndScopeInfo(Node node, in Scope scope, ReadOnlySpan<Scope> scopeStack)
{
SetScopeInfo(node, scope, scopeStack);

foreach (var child in node.ChildNodes)
{
if (child.UserData is ScopeInfo scopeInfo)
{
scopeInfo.UserData = node;
}
else
{
child.UserData = node;
}
}
}
}
}
95 changes: 59 additions & 36 deletions src/Acornima.Extras/ScopeInfo.cs
Original file line number Diff line number Diff line change
@@ -1,53 +1,76 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using Acornima.Ast;

namespace Acornima;

public sealed class ScopeInfo
{
private readonly int _id;
private readonly int _currentVarScopeId;
private readonly int _currentThisScopeId;
private VariableNameCollection _varNames;
private VariableNameCollection _lexicalNames;
private VariableNameCollection _functionNames;

public ScopeInfo(Node node, in Scope scope, ReadOnlySpan<Scope> scopeStack)
public static ScopeInfo From(Node associatedNode,
ScopeInfo? parent, ScopeInfo? varScope, ScopeInfo? thisScope,
ReadOnlySpan<string> varNames, ReadOnlySpan<string> lexicalNames, ReadOnlySpan<string> functionNames)
{
AssociatedNode = node;
_id = scope.Id;
_currentVarScopeId = scope.CurrentVarScopeIndex == scopeStack.Length ? scope.Id : scopeStack[scope.CurrentVarScopeIndex].Id;
_currentThisScopeId = scope.CurrentThisScopeIndex == scopeStack.Length ? scope.Id : scopeStack[scope.CurrentThisScopeIndex].Id;
VarNames = scope.VarNames.ToArray();
LexicalNames = scope.LexicalNames.ToArray();
FunctionNames = scope.FunctionNames.ToArray();
var scope = new ScopeInfo();
scope.Initialize(associatedNode ?? throw new ArgumentNullException(nameof(associatedNode)),
parent, varScope ?? scope, thisScope ?? scope,
varNames, lexicalNames, functionNames);
return scope;
}

public Node AssociatedNode { get; }
public Node? AssociatedNodeParent { get; internal set; }
internal ScopeInfo()
{
AssociatedNode = null!;
VarScope = ThisScope = this;
}

internal void Initialize(Node associatedNode, ScopeInfo? parent, ScopeInfo varScope, ScopeInfo thisScope,
ReadOnlySpan<string> varNames, ReadOnlySpan<string> lexicalNames, ReadOnlySpan<string> functionNames)
{
AssociatedNode = associatedNode;
Parent = parent;
VarScope = varScope;
ThisScope = thisScope;
_varNames = new VariableNameCollection(varNames);
_lexicalNames = new VariableNameCollection(lexicalNames);
_functionNames = new VariableNameCollection(functionNames);
}

public string[] VarNames { get; }
public string[] LexicalNames { get; }
public string[] FunctionNames { get; }
public Node AssociatedNode { [MethodImpl(MethodImplOptions.AggressiveInlining)] get; private set; }

// These lookups could as well be cached.
public ScopeInfo? Parent => FindAncestor(_ => true);
public ScopeInfo? VarScope => FindAncestor(scope => scope._id == _currentVarScopeId);
public ScopeInfo? ThisScope => FindAncestor(scope => scope._id == _currentThisScopeId);
public ScopeInfo? Parent { [MethodImpl(MethodImplOptions.AggressiveInlining)] get; private set; }
public ScopeInfo VarScope { [MethodImpl(MethodImplOptions.AggressiveInlining)] get; private set; }
public ScopeInfo ThisScope { [MethodImpl(MethodImplOptions.AggressiveInlining)] get; private set; }

private ScopeInfo? FindAncestor(Func<ScopeInfo, bool> predicate)
/// <summary>
/// A list of distinct var-declared names sorted in ascending order in the current lexical scope.
/// </summary>
public VariableNameCollection VarNames { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => _varNames; }

/// <summary>
/// A list of distinct lexically-declared names sorted in ascending order in the current lexical scope.
/// </summary>
public VariableNameCollection LexicalNames { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => _lexicalNames; }

/// <summary>
/// A list of distinct lexically-declared <see cref="FunctionDeclaration"/> names sorted in ascending order in the current lexical scope.
/// </summary>
public VariableNameCollection FunctionNames { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => _functionNames; }

/// <summary>
/// Gets or sets the arbitrary, user-defined data object associated with the current <see cref="ScopeInfo"/>.
/// </summary>
/// <remarks>
/// The operation is not guaranteed to be thread-safe. In case concurrent access or update is possible, the necessary synchronization is caller's responsibility.
/// </remarks>
public object? UserData
{
var node = AssociatedNode;
while ((node = GetParentNode(node!)!) is not null)
{
if (node.UserData is ScopeInfo scopeInfo && predicate(scopeInfo))
{
return scopeInfo;
}
}

return null;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
set;
}

private static Node? GetParentNode(Node node)
=> node.UserData is ScopeInfo scopeInfo ? scopeInfo.AssociatedNodeParent : (Node?)node.UserData;
}
61 changes: 61 additions & 0 deletions src/Acornima.Extras/VariableNameCollection.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Runtime.CompilerServices;
using Acornima.Helpers;

namespace Acornima;

[DebuggerDisplay($"{nameof(Count)} = {{{nameof(Count)}}}")]
[DebuggerTypeProxy(typeof(DebugView))]
public readonly struct VariableNameCollection : IReadOnlyCollection<string>
{
private readonly ArrayList<string> _names;

public VariableNameCollection(ReadOnlySpan<string> names)
{
if (names.Length > 0)
{
_names = new ArrayList<string>(initialCapacity: names.Length);
for (var i = 0; i < names.Length; i++)
{
var name = names[i];
var index = _names.AsReadOnlySpan().BinarySearch(name);
if (index < 0)
{
_names.Insert(~index, name);
}
}
_names.TrimExcess();
}
}

public int Count { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => _names.Count; }

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool Contains(string name)
{
return _names.AsReadOnlySpan().BinarySearch(name) >= 0;
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public IEnumerator<string> GetEnumerator() => _names.GetEnumerator();

IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();

[DebuggerNonUserCode]
private sealed class DebugView
{
private readonly VariableNameCollection _collection;

public DebugView(VariableNameCollection collection)
{
_collection = collection;
}

[DebuggerBrowsable(DebuggerBrowsableState.RootHidden)]
public string[] Items => _collection.ToArray();
}
}
2 changes: 1 addition & 1 deletion src/Acornima/Helpers/ArrayList.cs
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ namespace Acornima.Helpers;
[DebuggerDisplay($"{nameof(Count)} = {{{nameof(Count)}}}, {nameof(Capacity)} = {{{nameof(Capacity)}}}, Version = {{{nameof(_localVersion)}}}")]
[DebuggerTypeProxy(typeof(ArrayList<>.DebugView))]
#endif
internal struct ArrayList<T> : IList<T>
internal struct ArrayList<T> : IList<T>, IReadOnlyList<T>
{
private const int MinAllocatedCount = 4;

Expand Down
Loading

0 comments on commit 4e7921c

Please sign in to comment.