Skip to content

Commit

Permalink
Expose variable scope information tracked by the parser to consumers …
Browse files Browse the repository at this point in the history
…via the ParserOptions.OnNode callback
  • Loading branch information
adams85 committed Jun 22, 2024
1 parent bb3683e commit 7ccea95
Show file tree
Hide file tree
Showing 17 changed files with 542 additions and 172 deletions.
1 change: 1 addition & 0 deletions Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
<NoWarn>$(NoWarn);CA1716</NoWarn> <!-- Identifiers should not match keywords -->
<NoWarn>$(NoWarn);CA1510</NoWarn> <!-- Use ArgumentNullException.ThrowIfNull -->
<NoWarn>$(NoWarn);CA1711</NoWarn> <!-- Identifiers should not have incorrect suffix -->
<NoWarn>$(NoWarn);CA1863</NoWarn> <!-- Cache a 'CompositeFormat' for repeated use in this formatting operation -->
</PropertyGroup>

</Project>
2 changes: 1 addition & 1 deletion src/Acornima.Extras/Acornima.Extras.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

<PropertyGroup>
<RootNamespace>Acornima</RootNamespace>
<TargetFrameworks>net6.0;net462;netstandard2.0;netstandard2.1</TargetFrameworks>
<TargetFrameworks>net8.0;net6.0;net462;netstandard2.1;netstandard2.0</TargetFrameworks>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>

<AssemblyTitle>Acornima.Extras</AssemblyTitle>
Expand Down
50 changes: 39 additions & 11 deletions src/Acornima.Extras/ParserOptionsExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,27 +1,55 @@
using System;
using Acornima.Ast;

namespace Acornima;

public static class ParserOptionsExtensions
{
private static readonly OnNodeHandler s_parentSetter = node =>
public static TOptions RecordParentNodeInUserData<TOptions>(this TOptions options, bool enable = true)
where TOptions : ParserOptions
{
foreach (var child in node.ChildNodes)
var helper = options._onNode?.Target as OnNodeHelper;
if (enable)
{
(helper ?? new OnNodeHelper()).EnableParentNodeRecoding(options);
}
else
{
child.UserData = node;
helper?.DisableParentNodeRecoding(options);
}
};

public static TOptions RecordParentNodeInUserData<TOptions>(this TOptions options, bool enable = true)
where TOptions : ParserOptions
return options;
}

private sealed class OnNodeHelper : IOnNodeHandlerWrapper
{
options._onNode = (OnNodeHandler?)Delegate.RemoveAll(options._onNode, s_parentSetter);
private OnNodeHandler? _onNode;
public OnNodeHandler? OnNode { get => _onNode; set => _onNode = value; }

if (enable)
public void EnableParentNodeRecoding(ParserOptions options)
{
options._onNode += s_parentSetter;
if (!ReferenceEquals(options._onNode?.Target, this))
{
_onNode = options._onNode;
options._onNode = SetParentNode;
}
}

return options;
public void DisableParentNodeRecoding(ParserOptions options)
{
if (options._onNode == SetParentNode)
{
options._onNode = _onNode;
}
}

private void SetParentNode(Node node, OnNodeContext context)
{
foreach (var child in node.ChildNodes)
{
child.UserData = node;
}

_onNode?.Invoke(node, context);
}
}
}
2 changes: 1 addition & 1 deletion src/Acornima/Acornima.csproj
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>net6.0;net462;netstandard2.0;netstandard2.1</TargetFrameworks>
<TargetFrameworks>net8.0;net6.0;net462;netstandard2.1;netstandard2.0</TargetFrameworks>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>

<AssemblyTitle>Acornima</AssemblyTitle>
Expand Down
6 changes: 3 additions & 3 deletions src/Acornima/Helpers/ArrayList.cs
Original file line number Diff line number Diff line change
Expand Up @@ -74,9 +74,9 @@ namespace Acornima.Helpers;
[DebuggerDisplay($"{nameof(Count)} = {{{nameof(Count)}}}, {nameof(Capacity)} = {{{nameof(Capacity)}}}, Version = {{{nameof(_localVersion)}}}")]
[DebuggerTypeProxy(typeof(ArrayList<>.DebugView))]
#endif
internal partial struct ArrayList<T> : IList<T>
internal partial struct ArrayList<T> : IList<T>, IReadOnlyList<T>
{
private const int MinAllocatedCount = 4;
internal const int MinAllocatedCount = 4;

private T[]? _items;
private int _count;
Expand Down Expand Up @@ -239,7 +239,7 @@ internal readonly ref T GetItemRef(int index)
internal readonly ref T LastItemRef() => ref GetItemRef(_count - 1);

[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static long GrowCapacity(int capacity)
internal static long GrowCapacity(int capacity)
{
// NOTE: Using a growth factor of 3/2 yields better benchmark results than 2.
// It also results in less excess when the underlying array is returned directly wrapped in a NodeList, Span, etc.
Expand Down
38 changes: 38 additions & 0 deletions src/Acornima/Helpers/ReadOnlyRef.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;

namespace Acornima.Helpers;

/// <summary>
/// A struct that can store a read-only managed reference.
/// </summary>
internal readonly ref struct ReadOnlyRef<T>
{
#if NET7_0_OR_GREATER
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ReadOnlyRef(ref readonly T value)
{
Value = ref value;
}

public readonly ref readonly T Value;
#else
private readonly ReadOnlySpan<T> _value;

[MethodImpl(MethodImplOptions.AggressiveInlining)]
#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP2_1_OR_GREATER
public ReadOnlyRef(ref readonly T value)
{
_value = MemoryMarshal.CreateReadOnlySpan(ref Unsafe.AsRef(in value), 1);
}
#else
public ReadOnlyRef(ReadOnlySpan<T> span, int index)
{
_value = span.Slice(index, 1);
}
#endif

public ref readonly T Value { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => ref MemoryMarshal.GetReference(_value); }
#endif
}
37 changes: 37 additions & 0 deletions src/Acornima/OnNodeContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
using System;
using System.Runtime.CompilerServices;
using Acornima.Helpers;

namespace Acornima;

using static ExceptionHelper;

public readonly ref struct OnNodeContext
{
internal readonly ReadOnlyRef<Scope> _scope;

[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal OnNodeContext(ReadOnlyRef<Scope> scope, ArrayList<Scope> scopeStack)
{
_scope = scope;
ScopeStack = scopeStack.AsReadOnlySpan();
}

public bool HasScope { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => !Unsafe.IsNullRef(ref Unsafe.AsRef(in _scope.Value)); }

public ref readonly Scope Scope
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get
{
ref readonly var scope = ref _scope.Value;
if (Unsafe.IsNullRef(ref Unsafe.AsRef(in scope)))
{
ThrowInvalidOperationException<object>();
}
return ref scope;
}
}

public ReadOnlySpan<Scope> ScopeStack { [MethodImpl(MethodImplOptions.AggressiveInlining)] get; }
}
25 changes: 12 additions & 13 deletions src/Acornima/Parser.Expression.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1649,13 +1649,13 @@ private FunctionExpression ParseMethod(bool isGenerator, bool isAsync = false, b

NodeList<Node> parameters = ParseBindingList(close: TokenType.ParenRight, allowEmptyElement: false, allowTrailingComma: _tokenizerOptions._ecmaVersion >= EcmaVersion.ES8)!;
CheckYieldAwaitInDefaultParams();
var body = ParseFunctionBody(id: null, parameters, isArrowFunction: false, isMethod: true, ExpressionContext.Default, out _);
var scope = ParseFunctionBody(id: null, parameters, isArrowFunction: false, isMethod: true, ExpressionContext.Default, out _, out var body);

_yieldPosition = oldYieldPos;
_awaitPosition = oldAwaitPos;
_awaitIdentifierPosition = oldAwaitIdentPos;

return FinishNode(startMarker, new FunctionExpression(id: null, parameters, (FunctionBody)body, isGenerator, isAsync));
return FinishNode(startMarker, new FunctionExpression(id: null, parameters, (FunctionBody)body, isGenerator, isAsync), scope);
}

// Parse arrow function expression with given parameters.
Expand All @@ -1674,24 +1674,22 @@ private ArrowFunctionExpression ParseArrowExpression(in Marker startMarker, in N
EnterScope(FunctionFlags(isAsync, generator: false) | ScopeFlags.Arrow);

NodeList<Node> paramList = ToAssignableList(parameters!, isBinding: true, isParams: true)!;
var body = ParseFunctionBody(id: null, paramList, isArrowFunction: true, isMethod: false, context, out var expression);
var scope = ParseFunctionBody(id: null, paramList, isArrowFunction: true, isMethod: false, context, out var expression, out var body);

_yieldPosition = oldYieldPos;
_awaitPosition = oldAwaitPos;
_awaitIdentifierPosition = oldAwaitIdentPos;

return FinishNode(startMarker, new ArrowFunctionExpression(paramList, body, expression, isAsync));
return FinishNode(startMarker, new ArrowFunctionExpression(paramList, body, expression, isAsync), scope);
}

// Parse function body and check parameters.
private StatementOrExpression ParseFunctionBody(Identifier? id, in NodeList<Node> parameters,
bool isArrowFunction, bool isMethod, ExpressionContext context, out bool expression)
private ReadOnlyRef<Scope> ParseFunctionBody(Identifier? id, in NodeList<Node> parameters, bool isArrowFunction, bool isMethod, ExpressionContext context,
out bool expression, out StatementOrExpression body)
{
// https://github.com/acornjs/acorn/blob/8.11.3/acorn/src/expression.js > `pp.parseFunctionBody = function`

expression = isArrowFunction && _tokenizer._type != TokenType.BraceLeft;

StatementOrExpression body;
if (expression)
{
CheckParams(parameters, allowDuplicates: false);
Expand Down Expand Up @@ -1722,15 +1720,13 @@ private StatementOrExpression ParseFunctionBody(Identifier? id, in NodeList<Node
// flag (restore them to their old value afterwards).
var oldLabels = _labels;
_labels = new ArrayList<Label>();
ParseBlock(ref statements, createNewLexicalScope: false, exitStrict: strict && !oldStrict);
var scope = ParseBlock(ref statements, createNewLexicalScope: false, exitStrict: strict && !oldStrict);
_labels = oldLabels;

body = FinishNode(startMarker, new FunctionBody(NodeList.From(ref statements), strict));
body = FinishNode(startMarker, new FunctionBody(NodeList.From(ref statements), strict), scope);
}

ExitScope();

return body;
return ExitScope();
}

private static bool IsSimpleParamList(in NodeList<Node> parameters)
Expand Down Expand Up @@ -1761,6 +1757,9 @@ private void CheckParams(in NodeList<Node> parameters, bool allowDuplicates)
Debug.Assert(param is not null);
CheckLValInnerPattern(param!, BindingType.Var, checkClashes: nameHash);
}

ref var varList = ref CurrentScope._var;
varList.ParamCount = varList.Count;
}

// Parses a comma-separated list of expressions, and returns them as
Expand Down
16 changes: 10 additions & 6 deletions src/Acornima/Parser.Helpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,29 +25,33 @@ internal Marker StartNode()
return new Marker(_tokenizer._start, _tokenizer._startLocation);
}

internal T FinishNodeAt<T>(in Marker startMarker, in Marker endMarker, T node) where T : Node
[MethodImpl(MethodImplOptions.NoInlining)]
internal T FinishNodeAt<T>(in Marker startMarker, in Marker endMarker, T node, ReadOnlyRef<Scope> scope = default) where T : Node
{
// https://github.com/acornjs/acorn/blob/8.11.3/acorn/src/node.js > `function finishNodeAt`, `pp.finishNodeAt = function`

node._range = new Range(startMarker.Index, endMarker.Index);
node._location = new SourceLocation(startMarker.Position, endMarker.Position, _tokenizer._sourceFile);
_options._onNode?.Invoke(node);
_options._onNode?.Invoke(node, new OnNodeContext(scope, _scopeStack));
return node;
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal T FinishNode<T>(in Marker startMarker, T node) where T : Node
[MethodImpl(MethodImplOptions.NoInlining)]
internal T FinishNode<T>(in Marker startMarker, T node, ReadOnlyRef<Scope> scope = default) where T : Node
{
// https://github.com/acornjs/acorn/blob/8.11.3/acorn/src/node.js > `pp.finishNode = function`

return FinishNodeAt(startMarker, new Marker(_tokenizer._lastTokenEnd, _tokenizer._lastTokenEndLocation), node);
node._range = new Range(startMarker.Index, _tokenizer._lastTokenEnd);
node._location = new SourceLocation(startMarker.Position, _tokenizer._lastTokenEndLocation, _tokenizer._sourceFile);
_options._onNode?.Invoke(node, new OnNodeContext(scope, _scopeStack));
return node;
}

private T ReinterpretNode<T>(Node originalNode, T node) where T : Node
{
node._range = originalNode._range;
node._location = originalNode._location;
_options._onNode?.Invoke(node);
_options._onNode?.Invoke(node, new OnNodeContext(default, _scopeStack));
return node;
}

Expand Down
Loading

0 comments on commit 7ccea95

Please sign in to comment.