diff --git a/Directory.Build.props b/Directory.Build.props index f0818df..2d83b6f 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -15,6 +15,7 @@ $(NoWarn);CA1716 $(NoWarn);CA1510 $(NoWarn);CA1711 + $(NoWarn);CA1863 diff --git a/src/Acornima.Extras/Acornima.Extras.csproj b/src/Acornima.Extras/Acornima.Extras.csproj index f0df537..df3abba 100644 --- a/src/Acornima.Extras/Acornima.Extras.csproj +++ b/src/Acornima.Extras/Acornima.Extras.csproj @@ -2,7 +2,7 @@ Acornima - net6.0;net462;netstandard2.0;netstandard2.1 + net8.0;net6.0;net462;netstandard2.1;netstandard2.0 true Acornima.Extras diff --git a/src/Acornima.Extras/ParserOptionsExtensions.cs b/src/Acornima.Extras/ParserOptionsExtensions.cs index eab18e8..81ebddd 100644 --- a/src/Acornima.Extras/ParserOptionsExtensions.cs +++ b/src/Acornima.Extras/ParserOptionsExtensions.cs @@ -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(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(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); + } } } diff --git a/src/Acornima/Acornima.csproj b/src/Acornima/Acornima.csproj index 91a36d4..7376464 100644 --- a/src/Acornima/Acornima.csproj +++ b/src/Acornima/Acornima.csproj @@ -1,7 +1,7 @@ - net6.0;net462;netstandard2.0;netstandard2.1 + net8.0;net6.0;net462;netstandard2.1;netstandard2.0 true Acornima diff --git a/src/Acornima/Helpers/ArrayList.cs b/src/Acornima/Helpers/ArrayList.cs index 3a7b64f..b03a084 100644 --- a/src/Acornima/Helpers/ArrayList.cs +++ b/src/Acornima/Helpers/ArrayList.cs @@ -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 : IList +internal partial struct ArrayList : IList, IReadOnlyList { - private const int MinAllocatedCount = 4; + internal const int MinAllocatedCount = 4; private T[]? _items; private int _count; @@ -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. diff --git a/src/Acornima/Helpers/ReadOnlyRef.cs b/src/Acornima/Helpers/ReadOnlyRef.cs new file mode 100644 index 0000000..1aa5ad1 --- /dev/null +++ b/src/Acornima/Helpers/ReadOnlyRef.cs @@ -0,0 +1,38 @@ +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace Acornima.Helpers; + +/// +/// A struct that can store a read-only managed reference. +/// +internal readonly ref struct ReadOnlyRef +{ +#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 _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 span, int index) + { + _value = span.Slice(index, 1); + } +#endif + + public ref readonly T Value { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => ref MemoryMarshal.GetReference(_value); } +#endif +} diff --git a/src/Acornima/OnNodeContext.cs b/src/Acornima/OnNodeContext.cs new file mode 100644 index 0000000..771bbc2 --- /dev/null +++ b/src/Acornima/OnNodeContext.cs @@ -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; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal OnNodeContext(ReadOnlyRef scope, ArrayList 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(); + } + return ref scope; + } + } + + public ReadOnlySpan ScopeStack { [MethodImpl(MethodImplOptions.AggressiveInlining)] get; } +} diff --git a/src/Acornima/Parser.Expression.cs b/src/Acornima/Parser.Expression.cs index 075f549..47df5f7 100644 --- a/src/Acornima/Parser.Expression.cs +++ b/src/Acornima/Parser.Expression.cs @@ -1649,13 +1649,13 @@ private FunctionExpression ParseMethod(bool isGenerator, bool isAsync = false, b NodeList 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. @@ -1674,24 +1674,22 @@ private ArrowFunctionExpression ParseArrowExpression(in Marker startMarker, in N EnterScope(FunctionFlags(isAsync, generator: false) | ScopeFlags.Arrow); NodeList 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 parameters, - bool isArrowFunction, bool isMethod, ExpressionContext context, out bool expression) + private ReadOnlyRef ParseFunctionBody(Identifier? id, in NodeList 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); @@ -1722,15 +1720,13 @@ private StatementOrExpression ParseFunctionBody(Identifier? id, in NodeList(); - 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 parameters) @@ -1761,6 +1757,9 @@ private void CheckParams(in NodeList 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 diff --git a/src/Acornima/Parser.Helpers.cs b/src/Acornima/Parser.Helpers.cs index 73e80b9..3d233d6 100644 --- a/src/Acornima/Parser.Helpers.cs +++ b/src/Acornima/Parser.Helpers.cs @@ -25,29 +25,33 @@ internal Marker StartNode() return new Marker(_tokenizer._start, _tokenizer._startLocation); } - internal T FinishNodeAt(in Marker startMarker, in Marker endMarker, T node) where T : Node + [MethodImpl(MethodImplOptions.NoInlining)] + internal T FinishNodeAt(in Marker startMarker, in Marker endMarker, T node, ReadOnlyRef 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(in Marker startMarker, T node) where T : Node + [MethodImpl(MethodImplOptions.NoInlining)] + internal T FinishNode(in Marker startMarker, T node, ReadOnlyRef 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(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; } diff --git a/src/Acornima/Parser.LVal.cs b/src/Acornima/Parser.LVal.cs index c3c4b87..1a44c1c 100644 --- a/src/Acornima/Parser.LVal.cs +++ b/src/Acornima/Parser.LVal.cs @@ -469,7 +469,7 @@ private void CheckLValSimple(Node expr, BindingType bindingType = BindingType.No if (bindingType != BindingType.Outside) { - DeclareName(identifier.Name, bindingType, identifier.Start); + DeclareName(identifier, bindingType); } } break; @@ -561,19 +561,20 @@ private void CheckLValInnerPattern(Node pattern, BindingType bindingType = Bindi } } - private void DeclareName(string name, BindingType bindingType, int pos) + private void DeclareName(Identifier id, BindingType bindingType) { // https://github.com/acornjs/acorn/blob/8.11.3/acorn/src/scope.js > `pp.declareName = function` var redeclared = false; + var name = id.Name; ref var scope = ref NullRef(); switch (bindingType) { case BindingType.Lexical: scope = ref CurrentScope; - redeclared = scope.Lexical.IndexOf(name) >= 0 || scope.Functions.IndexOf(name) >= 0 || scope.Var.IndexOf(name) >= 0; - scope.Lexical.Add(name); - if (_inModule && (scope.Flags & ScopeFlags.Top) != 0) + redeclared = scope._lexical.Contains(name) || scope._functions.Contains(name) || scope._var.Contains(name); + scope._lexical.Add(id); + if (_inModule && (scope._flags & ScopeFlags.Top) != 0) { _undefinedExports!.Remove(name); } @@ -581,34 +582,34 @@ private void DeclareName(string name, BindingType bindingType, int pos) case BindingType.SimpleCatch: scope = ref CurrentScope; - scope.Lexical.Add(name); + scope._lexical.Add(id); break; case BindingType.Function: scope = ref CurrentScope; - redeclared = (scope.Flags & _functionsAsVarInScopeFlags) != 0 - ? scope.Lexical.IndexOf(name) >= 0 - : scope.Lexical.IndexOf(name) >= 0 || scope.Var.IndexOf(name) >= 0; - scope.Functions.Add(name); + redeclared = (scope._flags & _functionsAsVarInScopeFlags) != 0 + ? scope._lexical.Contains(name) + : scope._lexical.Contains(name) || scope._var.Contains(name); + scope._functions.Add(id); break; default: for (var i = _scopeStack.Count - 1; i >= 0; --i) { scope = ref _scopeStack.GetItemRef(i); - if (scope.Lexical.IndexOf(name) >= 0 && !((scope.Flags & ScopeFlags.SimpleCatch) != 0 && scope.Lexical[0] == name) - || (scope.Flags & _functionsAsVarInScopeFlags) == 0 && scope.Functions.IndexOf(name) >= 0) + if (scope._lexical.Contains(name) && !((scope._flags & ScopeFlags.SimpleCatch) != 0 && scope._lexical[0] == name) + || (scope._flags & _functionsAsVarInScopeFlags) == 0 && scope._functions.Contains(name)) { redeclared = true; break; } - scope.Var.Add(name); - if (_inModule && (scope.Flags & ScopeFlags.Top) != 0) + scope._var.Add(id); + if (_inModule && (scope._flags & ScopeFlags.Top) != 0) { _undefinedExports!.Remove(name); } - if ((scope.Flags & ScopeFlags.Var) != 0) + if ((scope._flags & ScopeFlags.Var) != 0) { break; } @@ -618,8 +619,8 @@ private void DeclareName(string name, BindingType bindingType, int pos) if (redeclared) { - // RaiseRecoverable(pos, $"Identifier '{name}' has already been declared"); // original acornjs error reporting - Raise(pos, VarRedeclaration, new object[] { name }); + // RaiseRecoverable(id.Start, $"Identifier '{name}' has already been declared"); // original acornjs error reporting + Raise(id.Start, VarRedeclaration, new object[] { name }); } } @@ -629,8 +630,8 @@ private void CheckLocalExport(Identifier id) ref readonly var rootScope = ref _scopeStack.GetItemRef(0); // scope.functions must be empty as Module code is always strict. - if (rootScope.Lexical.IndexOf(id.Name) < 0 - && rootScope.Var.IndexOf(id.Name) < 0) + if (!rootScope._lexical.Contains(id.Name) + && !rootScope._var.Contains(id.Name)) { _undefinedExports![id.Name] = id.Start; } diff --git a/src/Acornima/Parser.State.cs b/src/Acornima/Parser.State.cs index 0edd8f7..5e91077 100644 --- a/src/Acornima/Parser.State.cs +++ b/src/Acornima/Parser.State.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Generic; using System.Diagnostics; using System.Runtime.CompilerServices; @@ -36,6 +35,7 @@ public partial class Parser private Dictionary? _undefinedExports; // Scope tracking for duplicate variable names + private int _scopeId; private ArrayList _scopeStack; // The following functions keep track of declared variables in the current scope in order to detect duplicate variable names. @@ -105,6 +105,7 @@ internal void Reset(string input, int start, int length, SourceType sourceType, _labels.Clear(); + _scopeId = 0; _scopeStack.Clear(); EnterScope(ScopeFlags.Top); @@ -173,24 +174,30 @@ private void EnterScope(ScopeFlags flags) if ((flags & ScopeFlags.Var) != 0) { currentVarScopeIndex = _scopeStack.Count; - currentThisScopeIndex = (flags & ScopeFlags.Arrow) == 0 ? _scopeStack.Count : _scopeStack.PeekRef().CurrentThisScopeIndex; + currentThisScopeIndex = (flags & ScopeFlags.Arrow) == 0 ? _scopeStack.Count : _scopeStack.PeekRef()._currentThisScopeIndex; } else { ref readonly var currentScope = ref _scopeStack.PeekRef(); - currentVarScopeIndex = currentScope.CurrentVarScopeIndex; - currentThisScopeIndex = currentScope.CurrentThisScopeIndex; + currentVarScopeIndex = currentScope._currentVarScopeIndex; + currentThisScopeIndex = currentScope._currentThisScopeIndex; } - _scopeStack.PushRef().Reset(flags, currentVarScopeIndex, currentThisScopeIndex); + _scopeStack.PushRef().Reset(_scopeId++, flags, currentVarScopeIndex, currentThisScopeIndex); } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void ExitScope() + private ReadOnlyRef ExitScope() { // https://github.com/acornjs/acorn/blob/8.11.3/acorn/src/scope.js > `pp.exitScope = function` +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP2_1_OR_GREATER + return new ReadOnlyRef(ref _scopeStack.PopRef()); +#else + var scope = Scope.GetScopeRef(_scopeStack, _scopeStack.Count - 1); _scopeStack.PopRef(); + return scope; +#endif } // https://github.com/acornjs/acorn/blob/8.11.3/acorn/src/scope.js > `pp.currentScope = function` @@ -203,7 +210,7 @@ private ref Scope CurrentVarScope(out int index) // NOTE: to improve performance, we calculate and store the index of the current var scope on the fly // instead of looking it up at every call as it's done in acornjs (see also `EnterScope`). - index = _scopeStack.PeekRef().CurrentVarScopeIndex; + index = _scopeStack.PeekRef()._currentVarScopeIndex; return ref _scopeStack.GetItemRef(index); } @@ -215,7 +222,7 @@ private ref Scope CurrentThisScope(out int index) // NOTE: to improve performance, we calculate and store the index of the current this scope on the fly // instead of looking it up at every call as it's done in acornjs (see also `EnterScope`). - index = _scopeStack.PeekRef().CurrentThisScopeIndex; + index = _scopeStack.PeekRef()._currentThisScopeIndex; return ref _scopeStack.GetItemRef(index); } @@ -224,7 +231,7 @@ private bool InFunction() { // https://github.com/acornjs/acorn/blob/8.11.3/acorn/src/state.js > `get inFunction` - return (CurrentVarScope(out _).Flags & ScopeFlags.Function) != 0; + return (CurrentVarScope(out _)._flags & ScopeFlags.Function) != 0; } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -232,7 +239,7 @@ private bool InGenerator() { // https://github.com/acornjs/acorn/blob/8.11.3/acorn/src/state.js > `get inGenerator` - return (CurrentVarScope(out _).Flags & (ScopeFlags.Generator | ScopeFlags.InClassFieldInit)) == ScopeFlags.Generator; + return (CurrentVarScope(out _)._flags & (ScopeFlags.Generator | ScopeFlags.InClassFieldInit)) == ScopeFlags.Generator; } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -240,7 +247,7 @@ private bool InAsync() { // https://github.com/acornjs/acorn/blob/8.11.3/acorn/src/state.js > `get inAsync` - return (CurrentVarScope(out _).Flags & (ScopeFlags.Async | ScopeFlags.InClassFieldInit)) == ScopeFlags.Async; + return (CurrentVarScope(out _)._flags & (ScopeFlags.Async | ScopeFlags.InClassFieldInit)) == ScopeFlags.Async; } private bool CanAwait() @@ -251,14 +258,14 @@ private bool CanAwait() { ref readonly var scope = ref _scopeStack.GetItemRef(i); - if ((scope.Flags & (ScopeFlags.InClassFieldInit | ScopeFlags.ClassStaticBlock)) != 0) + if ((scope._flags & (ScopeFlags.InClassFieldInit | ScopeFlags.ClassStaticBlock)) != 0) { return false; } - if ((scope.Flags & ScopeFlags.Function) != 0) + if ((scope._flags & ScopeFlags.Function) != 0) { - return (scope.Flags & ScopeFlags.Async) != 0; + return (scope._flags & ScopeFlags.Async) != 0; } } @@ -270,7 +277,7 @@ private bool AllowSuper() { // https://github.com/acornjs/acorn/blob/8.11.3/acorn/src/state.js > `get allowSuper` - return _options._allowSuperOutsideMethod || (CurrentThisScope(out _).Flags & (ScopeFlags.Super | ScopeFlags.InClassFieldInit)) != 0; + return _options._allowSuperOutsideMethod || (CurrentThisScope(out _)._flags & (ScopeFlags.Super | ScopeFlags.InClassFieldInit)) != 0; } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -278,7 +285,7 @@ private bool AllowDirectSuper() { // https://github.com/acornjs/acorn/blob/8.11.3/acorn/src/state.js > `get allowDirectSuper` - return (CurrentThisScope(out _).Flags & ScopeFlags.DirectSuper) != 0; + return (CurrentThisScope(out _)._flags & ScopeFlags.DirectSuper) != 0; } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -290,7 +297,7 @@ private bool TreatFunctionsAsVar() // NOTE: to improve performance, we calculate and store this flag on the fly // instead of recalculating it at every call as it's done in acornjs. - return (CurrentScope.Flags & _functionsAsVarInScopeFlags) != 0; + return (CurrentScope._flags & _functionsAsVarInScopeFlags) != 0; } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -299,7 +306,7 @@ private bool AllowNewDotTarget() // https://github.com/acornjs/acorn/blob/8.11.3/acorn/src/state.js > `get allowNewDotTarget` return _options.AllowNewTargetOutsideFunction - || (CurrentThisScope(out _).Flags & (ScopeFlags.Function | ScopeFlags.ClassStaticBlock | ScopeFlags.InClassFieldInit)) != 0; + || (CurrentThisScope(out _)._flags & (ScopeFlags.Function | ScopeFlags.ClassStaticBlock | ScopeFlags.InClassFieldInit)) != 0; } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -307,13 +314,13 @@ private bool InClassStaticBlock() { // https://github.com/acornjs/acorn/blob/8.11.3/acorn/src/state.js > `get inClassStaticBlock` - return (CurrentVarScope(out _).Flags & ScopeFlags.ClassStaticBlock) != 0; + return (CurrentVarScope(out _)._flags & ScopeFlags.ClassStaticBlock) != 0; } [MethodImpl(MethodImplOptions.AggressiveInlining)] private bool InClassFieldInit() { - return (CurrentThisScope(out _).Flags & ScopeFlags.InClassFieldInit) != 0; + return (CurrentThisScope(out _)._flags & ScopeFlags.InClassFieldInit) != 0; } private enum LabelKind : byte @@ -341,65 +348,6 @@ public void Reset(LabelKind kind, string? name = null, int statementStart = 0) public int StatementStart; } - // Each scope gets a bitset that may contain these flags - [Flags] - private enum ScopeFlags : ushort - { - // https://github.com/acornjs/acorn/blob/8.11.3/acorn/src/scopeflags.js - - None = 0, - Top = 1 << 0, - Function = 1 << 1, - Async = 1 << 2, - Generator = 1 << 3, - Arrow = 1 << 4, - SimpleCatch = 1 << 5, - Super = 1 << 6, - DirectSuper = 1 << 7, - ClassStaticBlock = 1 << 8, - - Var = Top | Function | ClassStaticBlock, - - // A switch to disallow the identifier reference 'arguments' - InClassFieldInit = 1 << 15, - } - - private struct Scope - { - // https://github.com/acornjs/acorn/blob/8.11.3/acorn/src/scope.js > `class Scope` - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void Reset(ScopeFlags flags, int currentVarScopeIndex, int currentThisScopeIndex) - { - Flags = flags; - CurrentVarScopeIndex = currentVarScopeIndex; - CurrentThisScopeIndex = currentThisScopeIndex; - Var.Clear(); - Lexical.Clear(); - Functions.Clear(); - } - - public ScopeFlags Flags; - - public int CurrentVarScopeIndex; - public int CurrentThisScopeIndex; - - /// - /// A list of var-declared names in the current lexical scope. - /// - public ArrayList Var; - - /// - /// A list of lexically-declared names in the current lexical scope. - /// - public ArrayList Lexical; - - /// - /// A list of lexically-declared FunctionDeclaration names in the current lexical scope. - /// - public ArrayList Functions; - } - private struct PrivateNameStatus { [MethodImpl(MethodImplOptions.AggressiveInlining)] diff --git a/src/Acornima/Parser.Statement.cs b/src/Acornima/Parser.Statement.cs index fa55eea..d65fbff 100644 --- a/src/Acornima/Parser.Statement.cs +++ b/src/Acornima/Parser.Statement.cs @@ -693,10 +693,10 @@ private SwitchStatement ParseSwitchStatement(in Marker startMarker) cases.Add(current); } - ExitScope(); + var scope = ExitScope(); _labels.PopRef(); - return FinishNode(startMarker, new SwitchStatement(discriminant, NodeList.From(ref cases))); + return FinishNode(startMarker, new SwitchStatement(discriminant, NodeList.From(ref cases)), scope); } private ThrowStatement ParseThrowStatement(in Marker startMarker) @@ -737,7 +737,11 @@ private Node ParseCatchClauseParam() } EnterScope(scopeFlags); + CheckLValPattern(param, bindingType); + ref var lexicalList = ref CurrentScope._lexical; + lexicalList.ParamCount = lexicalList.Count; + Expect(TokenType.ParenRight); return param; @@ -776,9 +780,9 @@ private TryStatement ParseTryStatement(in Marker startMarker) blockStartMarker = StartNode(); var body = ParseBlockStatement(blockStartMarker, createNewLexicalScope: false); - ExitScope(); + var scope = ExitScope(); - handler = FinishNode(clauseStartMarker, new CatchClause(param, body)); + handler = FinishNode(clauseStartMarker, new CatchClause(param, body), scope); } NestedBlockStatement? finalizer; @@ -911,12 +915,12 @@ private NestedBlockStatement ParseBlockStatement(in Marker startMarker, bool cre Expect(TokenType.BraceLeft); var statements = new ArrayList(); - ParseBlock(ref statements, createNewLexicalScope); + var scope = ParseBlock(ref statements, createNewLexicalScope); - return FinishNode(startMarker, new NestedBlockStatement(NodeList.From(ref statements))); + return FinishNode(startMarker, new NestedBlockStatement(NodeList.From(ref statements)), scope); } - private void ParseBlock(ref ArrayList body, bool createNewLexicalScope = true, bool exitStrict = false) + private ReadOnlyRef ParseBlock(ref ArrayList body, bool createNewLexicalScope = true, bool exitStrict = false) { // https://github.com/acornjs/acorn/blob/8.11.3/acorn/src/statement.js > `pp.parseBlock = function` @@ -933,10 +937,7 @@ private void ParseBlock(ref ArrayList body, bool createNewLexicalScop _strict = _strict && !exitStrict; - if (createNewLexicalScope) - { - ExitScope(); - } + return createNewLexicalScope ? ExitScope() : default; } // Parse a regular `for` loop. The disambiguation code in @@ -956,10 +957,10 @@ private ForStatement ParseFor(in Marker startMarker, StatementOrExpression? init var body = ParseStatement(StatementContext.For); - ExitScope(); + var scope = ExitScope(); _labels.PopRef(); - return FinishNode(startMarker, new ForStatement(init, test, update, body)); + return FinishNode(startMarker, new ForStatement(init, test, update, body), scope); } // Parse a `for`/`in` and `for`/`of` loop, which are almost @@ -993,12 +994,13 @@ private Statement ParseForInOf(in Marker startMarker, bool isForIn, bool await, var body = ParseStatement(StatementContext.For); - ExitScope(); + var scope = ExitScope(); _labels.PopRef(); return FinishNode(startMarker, isForIn ? new ForInStatement(left, right, body) - : new ForOfStatement(left, right, body, await)); + : new ForOfStatement(left, right, body, await), + scope); } // Parse a list of variable declarations. @@ -1107,15 +1109,16 @@ private StatementOrExpression ParseFunction(in Marker startMarker, FunctionOrCla } var parameters = ParseFunctionParams(); - var body = (FunctionBody)ParseFunctionBody(id, parameters, isArrowFunction: false, isMethod: false, context, out _); + var scope = ParseFunctionBody(id, parameters, isArrowFunction: false, isMethod: false, context, out _, out var body); _yieldPosition = oldYieldPos; _awaitPosition = oldAwaitPos; _awaitIdentifierPosition = oldAwaitIdentPos; return FinishNode(startMarker, isStatement - ? new FunctionDeclaration(id, parameters, body, generator, isAsync) - : new FunctionExpression(id, parameters, body, generator, isAsync)); + ? new FunctionDeclaration(id, parameters, (FunctionBody)body, generator, isAsync) + : new FunctionExpression(id, parameters, (FunctionBody)body, generator, isAsync), + scope); } private NodeList ParseFunctionParams() @@ -1140,6 +1143,11 @@ private StatementOrExpression ParseClass(in Marker startMarker, FunctionOrClassF _strict = true; var id = ParseClassId(flags); + + // Original acornjs implementation doesn't create a scope for classes, however, as Acornima exposes scope information, + // it's necessary to create one so consumers can store and look up the class name, which is visible in the scope of the class. + EnterScope(ScopeFlags.None); + var superClass = ParseClassSuper(); var privateNameStatusIndex = EnterClassBody(); @@ -1187,11 +1195,14 @@ private StatementOrExpression ParseClass(in Marker startMarker, FunctionOrClassF var classBody = FinishNode(classBodyStartMarker, new ClassBody(NodeList.From(ref body))); ExitClassBody(); + var scope = ExitScope(); + var isStatement = (flags & FunctionOrClassFlags.Statement) != 0; return FinishNode(startMarker, isStatement ? new ClassDeclaration(id, superClass, classBody, NodeList.From(ref _decorators)) - : new ClassExpression(id, superClass, classBody, NodeList.From(ref _decorators))); + : new ClassExpression(id, superClass, classBody, NodeList.From(ref _decorators)), + scope); } private Node ParseClassElement(bool constructorAllowsSuper) @@ -1429,13 +1440,13 @@ private ClassProperty ParseClassField(in Marker startMarker, Expression key, boo { // To raise SyntaxError if 'arguments' exists in the initializer. ref var scope = ref CurrentThisScope(out var thisScopeIndex); - var oldScopeFlags = scope.Flags; - scope.Flags |= ScopeFlags.InClassFieldInit; + var oldScopeFlags = scope._flags; + scope._flags |= ScopeFlags.InClassFieldInit; value = ParseMaybeAssign(ref NullRef()); scope = _scopeStack.GetItemRef(thisScopeIndex); - scope.Flags = oldScopeFlags; + scope._flags = oldScopeFlags; } else { @@ -1464,10 +1475,10 @@ private StaticBlock ParseClassStaticBlock(in Marker startMarker) body.Add(statement); } - ExitScope(); + var scope = ExitScope(); _labels = oldLabels; - return FinishNode(startMarker, new StaticBlock(NodeList.From(ref body))); + return FinishNode(startMarker, new StaticBlock(NodeList.From(ref body)), scope); } private Identifier? ParseClassId(FunctionOrClassFlags flags) diff --git a/src/Acornima/Parser.cs b/src/Acornima/Parser.cs index 5f6a1af..10132c0 100644 --- a/src/Acornima/Parser.cs +++ b/src/Acornima/Parser.cs @@ -53,7 +53,7 @@ public Script ParseScript(string input, int start, int length, string? sourceFil var body = ParseTopLevel(); Debug.Assert(_tokenizer._type == TokenType.EOF); - return FinishNodeAt(startMarker, new Marker(_tokenizer._start, _tokenizer._startLocation), new Script(body, _strict)); + return FinishNodeAt(startMarker, new Marker(_tokenizer._start, _tokenizer._startLocation), new Script(body, _strict), Scope.GetScopeRef(_scopeStack, 0)); } finally { @@ -80,7 +80,7 @@ public Module ParseModule(string input, int start, int length, string? sourceFil var body = ParseTopLevel(); Debug.Assert(_tokenizer._type == TokenType.EOF); - return FinishNodeAt(startMarker, new Marker(_tokenizer._start, _tokenizer._startLocation), new Module(body)); + return FinishNodeAt(startMarker, new Marker(_tokenizer._start, _tokenizer._startLocation), new Module(body), Scope.GetScopeRef(_scopeStack, 0)); } finally { diff --git a/src/Acornima/ParserOptions.cs b/src/Acornima/ParserOptions.cs index d3be781..e1ce502 100644 --- a/src/Acornima/ParserOptions.cs +++ b/src/Acornima/ParserOptions.cs @@ -13,7 +13,12 @@ namespace Acornima; public delegate void OnTrailingCommaHandler(int lastTokenEnd, Position lastTokenEndLocation); -public delegate void OnNodeHandler(Node node); +public delegate void OnNodeHandler(Node node, OnNodeContext context); + +internal interface IOnNodeHandlerWrapper +{ + OnNodeHandler? OnNode { get; set; } +} public record class ParserOptions { @@ -210,7 +215,7 @@ public ParseErrorHandler ErrorHandler /// This callback allows you to make changes to the nodes created by the parser. /// E.g. you can use it to store a reference to the parent node for later use: /// - /// OnNode = node => + /// OnNode = (node, _) => /// { /// foreach (var child in node.ChildNodes) /// { @@ -221,5 +226,19 @@ public ParseErrorHandler ErrorHandler /// Please note that the callback is also executed on nodes which are reinterpreted /// later during parsing, that is, on nodes which won't become a part of the final AST. /// - public OnNodeHandler? OnNode { get => _onNode; init => _onNode = value; } + public OnNodeHandler? OnNode + { + get => _onNode?.Target is IOnNodeHandlerWrapper wrapper ? wrapper.OnNode : _onNode; + init + { + if (_onNode?.Target is IOnNodeHandlerWrapper wrapper) + { + wrapper.OnNode = value; + } + else + { + _onNode = value; + } + } + } } diff --git a/src/Acornima/Scope.cs b/src/Acornima/Scope.cs new file mode 100644 index 0000000..e79de32 --- /dev/null +++ b/src/Acornima/Scope.cs @@ -0,0 +1,216 @@ +using System; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using Acornima.Ast; +using Acornima.Helpers; + +namespace Acornima; + +using static ExceptionHelper; + +// https://github.com/acornjs/acorn/blob/8.11.3/acorn/src/scope.js > `class Scope` + +/// +/// Stores variable scope information. +/// +/// +/// Scopes are created for exactly the following types of AST nodes: +/// +/// +/// (be aware that no separate scopes are created for the parameter list and the body of the function, see also ) +/// +/// +/// +/// (be aware that no separate scopes are created for the parameter list and the body of the catch clause, see also ) +/// , , (a separate scope is created for the initialization part of the statement) +/// (a scope is created for the body of the statement; be aware that the discriminant expression is not part of this scope) +/// +/// +public struct Scope +{ + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static ReadOnlyRef GetScopeRef(ArrayList scopeStack, int index) + { +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP2_1_OR_GREATER + return new ReadOnlyRef(ref scopeStack.GetItemRef(index)); +#else + return new ReadOnlyRef(scopeStack.AsReadOnlySpan(), index); +#endif + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal void Reset(int id, ScopeFlags flags, int currentVarScopeIndex, int currentThisScopeIndex) + { + _id = id; + _flags = flags; + _currentVarScopeIndex = currentVarScopeIndex; + _currentThisScopeIndex = currentThisScopeIndex; + _var.Reset(); + _lexical.Reset(); + _functions.Reset(); + } + + internal int _id; + /// + /// It is guaranteed that values are assigned sequentially, starting with zero (assigned to the root scope). + /// + public readonly int Id { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => _id; } + + internal ScopeFlags _flags; + + internal int _currentVarScopeIndex; + public readonly int CurrentVarScopeIndex { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => _currentVarScopeIndex; } + + internal int _currentThisScopeIndex; + public readonly int CurrentThisScopeIndex { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => _currentThisScopeIndex; } + + internal VariableList _var; + + /// + /// A list of var-declared variables in the current lexical scope. In the case of function scopes, also includes parameters (listed at the beginning of the span). + /// + /// + /// Variables declared in a nested statement block are hoisted, meaning that the variable identifier will be included in the span of + /// the parent scopes, up to the root var scope (i.e. the scope introduced by the closest , or node). + /// + public readonly ReadOnlySpan VarVariables { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => _var.AsReadOnlySpan(); } + + /// + /// The number of parameter names at the beginning of the span. + /// + public readonly int VarParamCount { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => _var.ParamCount; } + + internal VariableList _lexical; + + /// + /// A list of lexically-declared variables in the current lexical scope. In the case of catch clause scopes, also includes parameters (listed at the beginning of the span). + /// + public readonly ReadOnlySpan LexicalVariables { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => _lexical.AsReadOnlySpan(); } + + /// + /// The number of parameter names at the beginning of the span. + /// + public readonly int LexicalParamCount { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => _lexical.ParamCount; } + + internal VariableList _functions; + + /// + /// A list of lexically-declared s in the current lexical scope. + /// + /// + /// Functions declared in a nested statement block are not hoisted, meaning that the function identifiers will not be included in the span of + /// the parent scopes. (This is relevant only in non-strict contexts as strict mode prevents functions from being hoisted out of the scope in they are declared.) + /// + public readonly ReadOnlySpan Functions { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => _functions.AsReadOnlySpan(); } + + // This is a heavily slimmed down version of ArrayList for collecting variable and parameter names. + // Ideally, we'd just use an ArrayList for this purpose. However, we also need to store an extra 32-bit integer + // that indicates the number of parameters in the case of function and catch clause scopes. + // The layout of the Scope struct is so unfortunate that adding the two extra int fields to it would increase its size by 8 bytes + // while there'd be 3*4=12 bytes of wasted space as ArrayLists need only 12 bytes but are padded to 16 bytes on x64 architectures. + // Unfortunately, there seems to be no cleaner and safer way to utilize that wasted space than duplicating some code of ArrayList. +#if DEBUG + [DebuggerDisplay($"{nameof(Count)} = {{{nameof(Count)}}}")] + [DebuggerTypeProxy(typeof(DebugView))] +#endif + internal struct VariableList + { + private Identifier[]? _items; + private int _count; + + public int ParamCount; + + public readonly string this[int index] + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + // Following trick can reduce the range check by one + if ((uint)index >= (uint)_count) + { + return ThrowIndexOutOfRangeException(); + } + + return _items![index].Name; + } + } + + public readonly int Count + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => _count; + } + + public void Add(Identifier item) + { + var capacity = _items?.Length ?? 0; + + if (_count == capacity) + { + Array.Resize(ref _items, Math.Max(checked((int)ArrayList.GrowCapacity(capacity)), ArrayList.MinAllocatedCount)); + } + + Debug.Assert(_items is not null); + _items![_count++] = item; + } + + public void Reset() + { + if (_count != 0) + { + Array.Clear(_items!, 0, _count); + _count = 0; + } + ParamCount = 0; + } + + public readonly bool Contains(string name) + { + for (var i = 0; i < _count; i++) + { + if (_items![i].Name == name) + { + return true; + } + } + return false; + } + + /// + /// WARNING: Items should not be added or removed from the while the returned is in use. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal readonly ReadOnlySpan AsReadOnlySpan() + { + return new ReadOnlySpan(_items, 0, _count); + } + +#if DEBUG + public readonly Identifier[] ToArray() + { + if (_count == 0) + { + return Array.Empty(); + } + + var array = new Identifier[_count]; + Array.Copy(_items!, 0, array, 0, _count); + return array; + } + + [DebuggerNonUserCode] + private sealed class DebugView + { + private readonly VariableList _list; + + public DebugView(VariableList list) + { + _list = list; + } + + [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)] + public Identifier[] Items => _list.ToArray(); + } +#endif + } +} diff --git a/src/Acornima/ScopeFlags.cs b/src/Acornima/ScopeFlags.cs new file mode 100644 index 0000000..d4ad371 --- /dev/null +++ b/src/Acornima/ScopeFlags.cs @@ -0,0 +1,26 @@ +using System; + +namespace Acornima; + +// https://github.com/acornjs/acorn/blob/8.11.3/acorn/src/scopeflags.js + +// Each scope gets a bitset that may contain these flags +[Flags] +internal enum ScopeFlags +{ + None = 0, + Top = 1 << 0, + Function = 1 << 1, + Async = 1 << 2, + Generator = 1 << 3, + Arrow = 1 << 4, + SimpleCatch = 1 << 5, + Super = 1 << 6, + DirectSuper = 1 << 7, + ClassStaticBlock = 1 << 8, + + Var = Top | Function | ClassStaticBlock, + + // A switch to disallow the identifier reference 'arguments' + InClassFieldInit = 1 << 15, +} diff --git a/test/Acornima.Tests/ParserTests.cs b/test/Acornima.Tests/ParserTests.cs index 9bc41d5..f660ebb 100644 --- a/test/Acornima.Tests/ParserTests.cs +++ b/test/Acornima.Tests/ParserTests.cs @@ -3,10 +3,8 @@ using System.Globalization; using System.Linq; using System.Numerics; -using System.Text.RegularExpressions; using Acornima.Ast; using Acornima.Helpers; -using Acornima.Tests.Acorn; using Xunit; namespace Acornima.Tests; @@ -90,9 +88,9 @@ public void CanHandleDeepRecursion() var parser = new Parser(); #if DEBUG - const int depth = 400; + const int depth = 395; #else - const int depth = 895; + const int depth = 1000; #endif var input = $"if ({new string('(', depth)}true{new string(')', depth)}) {{ }}"; parser.ParseScript(input); @@ -214,13 +212,50 @@ public void CanReuseParser() Assert.Equal(0, comments[0].Range.Start); } - [Fact] - public void RecordsParentNodeInUserDataCorrectly() + [Theory] + [InlineData(false)] + [InlineData(true)] + public void RecordsParentNodeInUserDataCorrectly(bool registerUserHandler) { - var parser = new Parser(new ParserOptions().RecordParentNodeInUserData(true)); + var userHandlerCalled = false; + + var options = (registerUserHandler ? new ParserOptions { OnNode = delegate { userHandlerCalled = true; } } : new ParserOptions()) + .RecordParentNodeInUserData(); + + var parser = new Parser(options); var script = parser.ParseScript("function toObj(a, b) { return { a, b() { return b } }; }"); - new ParentNodeChecker().Check(script); + Func parentGetter = node => (Node?)node.UserData; + + new ParentNodeChecker(parentGetter).Check(script); + + Assert.Equal(registerUserHandler, userHandlerCalled); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void ShouldPreserveUserOnNodeHandler(bool registerUserHandler) + { + const string code = "function toObj(a, b) { return { a, b: x => { let y = 2; return x * y } }; }"; + + var userHandlerCalled = false; + OnNodeHandler? userHandler = registerUserHandler ? delegate { userHandlerCalled = true; } : null; + + var options = new ParserOptions { OnNode = userHandler }; + Assert.Same(userHandler, options.OnNode); + + options = options.RecordParentNodeInUserData(); + Assert.Same(userHandler, options.OnNode); + + options = options.RecordParentNodeInUserData(enable: false); + Assert.Same(userHandler, options.OnNode); + + var parser = new Parser(options); + var script = parser.ParseScript(code); + + Assert.Empty(script.DescendantNodesAndSelf().Where(node => node.UserData is not null)); + Assert.Equal(registerUserHandler, userHandlerCalled); } [Theory] @@ -582,16 +617,23 @@ public void ShouldParseCommentsWithinSliceOnly() private sealed class ParentNodeChecker : AstVisitor { + private readonly Func _parentGetter; + + public ParentNodeChecker(Func parentGetter) + { + _parentGetter = parentGetter; + } + public void Check(Node node) { - Assert.Null(node.UserData); + Assert.Null(_parentGetter(node)); base.Visit(node); } public override object? Visit(Node node) { - var parent = (Node?)node.UserData; + var parent = _parentGetter(node); Assert.NotNull(parent); Assert.Contains(node, parent!.ChildNodes);