Skip to content

Commit

Permalink
feat: Implement parsing of function expression
Browse files Browse the repository at this point in the history
This allows us to parse:
```js
a = function OPT_NAME()
{
   // BODY
}
```

Which will create an FunctionExpression in our AST.
  • Loading branch information
PrestonLTaylor committed Apr 18, 2024
1 parent 0976ee7 commit 38ce2d0
Show file tree
Hide file tree
Showing 3 changed files with 107 additions and 2 deletions.
60 changes: 60 additions & 0 deletions JSS.Lib.UnitTests/ParserTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1132,6 +1132,66 @@ public void Parse_ReturnsFunctionDeclaration_WhenProvidingFunctionDeclaration_Wi
parameters[0].Name.Should().Be(expectedParameterIdentifier);
}

// NOTE: Function expressions cannot be expression statements, so we have to also have an a node that parses an expression
[Test]
public void Parse_ReturnsAssignmentExpression_WithFunctionExpression_WhenProvidingAssignmentWithAFunctionExpression()
{
// Arrange
var parser = new Parser($"a = function() {{}}");

// Act
var parsedProgram = ParseScript(parser);
var rootNodes = parsedProgram.ScriptCode;

// Assert
rootNodes.Should().HaveCount(1);

var expressionStatement = rootNodes[0] as ExpressionStatement;
expressionStatement.Should().NotBeNull();

var assignmentExpression = expressionStatement!.Expression as BasicAssignmentExpression;
assignmentExpression.Should().NotBeNull();

var functionExpression = assignmentExpression!.Rhs as FunctionExpression;
functionExpression.Should().NotBeNull();
functionExpression!.Identifier.Should().BeNull();
functionExpression.Body.Statements.Should().BeEmpty();

var parameters = functionExpression.Parameters;
parameters.Should().BeEmpty();
}

[Test]
public void Parse_ReturnsAssignmentExpression_WithFunctionExpression_WithAName_WhenProvidingAssignmentWithAFunctionExpression_WithAName()
{
// Arrange
const string expectedFunctionIdentifier = "expectedIdentifier";
const string expectedParameterIdentifier = "expectedFirstParameter";
var parser = new Parser($"a = function {expectedFunctionIdentifier}({expectedParameterIdentifier}) {{}}");

// Act
var parsedProgram = ParseScript(parser);
var rootNodes = parsedProgram.ScriptCode;

// Assert
rootNodes.Should().HaveCount(1);

var expressionStatement = rootNodes[0] as ExpressionStatement;
expressionStatement.Should().NotBeNull();

var assignmentExpression = expressionStatement!.Expression as BasicAssignmentExpression;
assignmentExpression.Should().NotBeNull();

var functionExpression = assignmentExpression!.Rhs as FunctionExpression;
functionExpression.Should().NotBeNull();
functionExpression!.Identifier.Should().Be(expectedFunctionIdentifier);
functionExpression.Body.Statements.Should().BeEmpty();

var parameters = functionExpression.Parameters;
parameters.Should().HaveCount(1);
parameters[0].Name.Should().Be(expectedParameterIdentifier);
}

// Tests for 15.7 Class Definitions, https://tc39.es/ecma262/#sec-class-definitions
[Test]
public void Parse_ReturnsEmptyClassDeclaration_WhenProvidingEmptyClass()
Expand Down
18 changes: 18 additions & 0 deletions JSS.Lib/AST/FunctionExpression.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
namespace JSS.Lib.AST;

// 15.2 Function Definitions, https://tc39.es/ecma262/#sec-function-definitions
internal sealed class FunctionExpression : IExpression
{
public FunctionExpression(string? identifier, List<Identifier> parameters, StatementList body)
{
Identifier = identifier;
Parameters = parameters;
Body = body;
}

// FIXME: 15.2.6 Runtime Semantics: Evaluation, https://tc39.es/ecma262/#sec-function-definitions-runtime-semantics-evaluation

public string? Identifier { get; }
public IReadOnlyList<Identifier> Parameters { get; }
public StatementList Body { get; }
}
31 changes: 29 additions & 2 deletions JSS.Lib/Parser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1262,6 +1262,11 @@ private bool TryParsePrimaryExpression(out IExpression? parsedExpression)
parsedExpression = ParseParenthesizedExpression();
return true;
}
if (IsFunctionDeclaration())
{
parsedExpression = ParseFunctionExpression();
return true;
}

parsedExpression = null;
return false;
Expand Down Expand Up @@ -1492,7 +1497,7 @@ private EmptyStatement ParseEmptyStatement()
// 14.5 Expression Statement, https://tc39.es/ecma262/#prod-ExpressionStatement
private bool TryParseExpressionStatement(out INode? parsedExpressionStatement)
{
if (IsAmbigiousForExpressionStatement())
if (IsAmbiguousForExpressionStatement())
{
parsedExpressionStatement = null;
return false;
Expand All @@ -1508,7 +1513,8 @@ private bool TryParseExpressionStatement(out INode? parsedExpressionStatement)
return true;
}

private bool IsAmbigiousForExpressionStatement()
// For [lookahead ∉ { {, function, async [no LineTerminator here] function, class, let [ }]
private bool IsAmbiguousForExpressionStatement()
{
return _consumer.CanConsume() && _consumer.Peek().type switch
{
Expand Down Expand Up @@ -1945,6 +1951,27 @@ private FunctionDeclaration ParseFunctionDeclaration()
return new FunctionDeclaration(identifier.Name, parameters, body);
}

private FunctionExpression ParseFunctionExpression()
{
_consumer.ConsumeTokenOfType(TokenType.Function);

Identifier? identifier = IsIdentifier() ? ParseIdentifier() : null;

_consumer.ConsumeTokenOfType(TokenType.OpenParen);

var parameters = ParseFormalParameters();

_consumer.ConsumeTokenOfType(TokenType.ClosedParen);

_consumer.ConsumeTokenOfType(TokenType.OpenBrace);

var body = ParseFunctionBody();

_consumer.ConsumeTokenOfType(TokenType.ClosedBrace);

return new FunctionExpression(identifier?.Name, parameters, body);
}

private StatementList ParseFunctionBody()
{
return ParseStatementListWhile(() => _consumer.CanConsume() && !_consumer.IsTokenOfType(TokenType.ClosedBrace));
Expand Down

0 comments on commit 38ce2d0

Please sign in to comment.