Skip to content

Commit

Permalink
Move from antlr4ts to ANTLR's official TypeScript target (#184)
Browse files Browse the repository at this point in the history
  • Loading branch information
rkalis committed Jan 10, 2024
1 parent c39f140 commit f74b8ab
Show file tree
Hide file tree
Showing 15 changed files with 1,556 additions and 1,668 deletions.
2 changes: 1 addition & 1 deletion .eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ module.exports = {
'max-len': ['error', { code: 120, ignoreStrings: true, ignoreTemplateLiterals: true }],
'import/no-cycle': 0, // Needed for AST -> AstVisitor -> AST
'class-methods-use-this': 0, // I don't like this rule
'no-underscore-dangle': 0, // antlr4ts automatically uses this
'no-underscore-dangle': 0, // antlr automatically uses this
'no-param-reassign': 0, // Makes visitors returning the node object easier
'@typescript-eslint/lines-between-class-members': [ // Makes defining interfaces / abstract classes easier
'error',
Expand Down
27 changes: 27 additions & 0 deletions DEVELOPMENT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
## cashc

### Prerequisites

In order to use the `antlr` command line tool when updating CashScript's grammar file, you need to have it installed.

On macOS you can install it using Homebrew:

```bash
brew install antlr
```

On Linux you can install it using apt:

```bash
sudo apt install antlr4
```

For other platforms, refer to the [Antlr website](https://www.antlr.org/).

### Updating the grammar

When updating the grammar file in `src/grammar/CashScript.g4`, we also need to make sure that the generated parser is updated. To do this, run the following command in the `packages/cashc` directory:

```bash
yarn antlr
``````
5 changes: 2 additions & 3 deletions packages/cashc/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
"test": "test"
},
"scripts": {
"antlr": "antlr4ts -visitor -no-listener src/grammar/CashScript.g4",
"antlr": "antlr -Dlanguage=TypeScript -visitor -no-listener src/grammar/CashScript.g4",
"postantlr": "find src/grammar -type f -name '*.ts' | xargs sed -i '' 's|\\(import .* \".*/.*\\)\";|\\1\\.js\";|g'",
"build": "yarn clean && yarn compile",
"build:test": "yarn clean:test && yarn compile:test && cpy './test/**/*.cash' ./dist-test/test",
Expand All @@ -51,15 +51,14 @@
"dependencies": {
"@bitauth/libauth": "^2.0.0-alpha.8",
"@cashscript/utils": "^0.9.2",
"antlr4ts": "^0.5.0-alpha.4",
"antlr4": "^4.13.1-patch-1",
"commander": "^7.1.0",
"semver": "^7.3.4"
},
"devDependencies": {
"@jest/globals": "^29.4.1",
"@types/node": "^18.11.18",
"@types/semver": "^7.3.4",
"antlr4ts-cli": "^0.5.0-alpha.4",
"cpy-cli": "^4.2.0",
"eslint": "^7.20.0",
"jest": "^29.4.1",
Expand Down
100 changes: 55 additions & 45 deletions packages/cashc/src/ast/AstBuilder.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import { ParseTree, ParseTreeVisitor } from 'antlr4';
import { hexToBin } from '@bitauth/libauth';
import { parseType } from '@cashscript/utils';
import { AbstractParseTreeVisitor } from 'antlr4ts/tree/AbstractParseTreeVisitor.js';
import { ParseTree } from 'antlr4ts/tree/ParseTree.js';
import { TerminalNode } from 'antlr4ts/tree/TerminalNode.js';
import semver from 'semver';
import {
Node,
Expand Down Expand Up @@ -46,7 +44,6 @@ import type {
FunctionCallContext,
CastContext,
LiteralContext,
NumberLiteralContext,
SourceFileContext,
BlockContext,
TimeOpStatementContext,
Expand All @@ -63,8 +60,9 @@ import type {
InstantiationContext,
NullaryOpContext,
UnaryIntrospectionOpContext,
StatementContext,
} from '../grammar/CashScriptParser.js';
import type { CashScriptVisitor } from '../grammar/CashScriptVisitor.js';
import CashScriptVisitor from '../grammar/CashScriptVisitor.js';
import { Location } from './Location.js';
import {
NumberUnit,
Expand All @@ -75,7 +73,7 @@ import { version } from '../index.js';
import { ParseError, VersionError } from '../Errors.js';

export default class AstBuilder
extends AbstractParseTreeVisitor<Node>
extends ParseTreeVisitor<Node>
implements CashScriptVisitor<Node> {
constructor(private tree: ParseTree) {
super();
Expand All @@ -90,7 +88,7 @@ export default class AstBuilder
}

visitSourceFile(ctx: SourceFileContext): SourceFileNode {
ctx.pragmaDirective().forEach((pragma) => {
ctx.pragmaDirective_list().forEach((pragma) => {
this.processPragma(pragma);
});

Expand All @@ -101,34 +99,34 @@ export default class AstBuilder
}

processPragma(ctx: PragmaDirectiveContext): void {
const pragmaName = getPragmaName(ctx.pragmaName().text);
const pragmaName = getPragmaName(ctx.pragmaName().getText());
if (pragmaName !== PragmaName.CASHSCRIPT) throw new Error(); // Shouldn't happen

// Strip any -beta tags
const actualVersion = version.replace(/-.*/g, '');

ctx.pragmaValue().versionConstraint().forEach((constraint) => {
ctx.pragmaValue().versionConstraint_list().forEach((constraint) => {
const op = getVersionOpFromCtx(constraint.versionOperator());
const versionConstraint = `${op}${constraint.VersionLiteral().text}`;
const versionConstraint = `${op}${constraint.VersionLiteral().getText()}`;
if (!semver.satisfies(actualVersion, versionConstraint)) {
throw new VersionError(actualVersion, versionConstraint);
}
});
}

visitContractDefinition(ctx: ContractDefinitionContext): ContractNode {
const name = ctx.Identifier().text;
const parameters = ctx.parameterList().parameter().map((p) => this.visit(p) as ParameterNode);
const functions = ctx.functionDefinition().map((f) => this.visit(f) as FunctionDefinitionNode);
const name = ctx.Identifier().getText();
const parameters = ctx.parameterList().parameter_list().map((p) => this.visit(p) as ParameterNode);
const functions = ctx.functionDefinition_list().map((f) => this.visit(f) as FunctionDefinitionNode);
const contract = new ContractNode(name, parameters, functions);
contract.location = Location.fromCtx(ctx);
return contract;
}

visitFunctionDefinition(ctx: FunctionDefinitionContext): FunctionDefinitionNode {
const name = ctx.Identifier().text;
const parameters = ctx.parameterList().parameter().map((p) => this.visit(p) as ParameterNode);
const statements = ctx.statement().map((s) => this.visit(s) as StatementNode);
const name = ctx.Identifier().getText();
const parameters = ctx.parameterList().parameter_list().map((p) => this.visit(p) as ParameterNode);
const statements = ctx.statement_list().map((s) => this.visit(s) as StatementNode);
const block = new BlockNode(statements);
block.location = Location.fromCtx(ctx);

Expand All @@ -138,17 +136,22 @@ export default class AstBuilder
}

visitParameter(ctx: ParameterContext): ParameterNode {
const type = parseType(ctx.typeName().text);
const name = ctx.Identifier().text;
const type = parseType(ctx.typeName().getText());
const name = ctx.Identifier().getText();
const parameter = new ParameterNode(type, name);
parameter.location = Location.fromCtx(ctx);
return parameter;
}

visitStatement(ctx: StatementContext): StatementNode {
// Statement nodes only have a single child, so we can just visit that child
return this.visit(ctx.getChild(0));
}

visitVariableDefinition(ctx: VariableDefinitionContext): VariableDefinitionNode {
const type = parseType(ctx.typeName().text);
const modifiers = ctx.modifier().map((modifier) => modifier.text);
const name = ctx.Identifier().text;
const type = parseType(ctx.typeName().getText());
const modifiers = ctx.modifier_list().map((modifier) => modifier.getText());
const name = ctx.Identifier().getText();
const expression = this.visit(ctx.expression());
const variableDefinition = new VariableDefinitionNode(type, modifiers, name, expression);
variableDefinition.location = Location.fromCtx(ctx);
Expand All @@ -157,18 +160,19 @@ export default class AstBuilder

visitTupleAssignment(ctx: TupleAssignmentContext): TupleAssignmentNode {
const expression = this.visit(ctx.expression());
const names = ctx.Identifier();
const types = ctx.typeName();
const [var1, var2] = names.map((name, i) => (
{ name: name.text, type: parseType(types[i].text) }
));
const names = ctx.Identifier_list();
const types = ctx.typeName_list();
const [var1, var2] = names.map((name, i) => ({
name: name.getText(),
type: parseType(types[i].getText()),
}));
const tupleAssignment = new TupleAssignmentNode(var1, var2, expression);
tupleAssignment.location = Location.fromCtx(ctx);
return tupleAssignment;
}

visitAssignStatement(ctx: AssignStatementContext): AssignNode {
const identifier = new IdentifierNode(ctx.Identifier().text);
const identifier = new IdentifierNode(ctx.Identifier().getText());
identifier.location = Location.fromToken(ctx.Identifier().symbol);

const expression = this.visit(ctx.expression());
Expand All @@ -179,7 +183,7 @@ export default class AstBuilder

visitTimeOpStatement(ctx: TimeOpStatementContext): TimeOpNode {
const expression = this.visit(ctx.expression());
const timeOp = new TimeOpNode(ctx.TxVar().text as TimeOp, expression);
const timeOp = new TimeOpNode(ctx.TxVar().getText() as TimeOp, expression);
timeOp.location = Location.fromCtx(ctx);

return timeOp;
Expand All @@ -202,7 +206,7 @@ export default class AstBuilder
}

visitBlock(ctx: BlockContext): BlockNode {
const statements = ctx.statement().map((s) => this.visit(s) as StatementNode);
const statements = ctx.statement_list().map((s) => this.visit(s) as StatementNode);
const block = new BlockNode(statements);
block.location = Location.fromCtx(ctx);
return block;
Expand All @@ -213,7 +217,7 @@ export default class AstBuilder
}

visitCast(ctx: CastContext): CastNode {
const type = parseType(ctx.typeName().text);
const type = parseType(ctx.typeName().getText());
const expression = this.visit(ctx._castable);
const size = ctx._size && this.visit(ctx._size);
const cast = new CastNode(type, expression, size);
Expand All @@ -226,33 +230,33 @@ export default class AstBuilder
}

visitFunctionCall(ctx: FunctionCallContext): FunctionCallNode {
const identifier = new IdentifierNode(ctx.Identifier().text as string);
const identifier = new IdentifierNode(ctx.Identifier().getText());
identifier.location = Location.fromToken(ctx.Identifier().symbol);
const parameters = ctx.expressionList().expression().map((e) => this.visit(e));
const parameters = ctx.expressionList().expression_list().map((e) => this.visit(e));
const functionCall = new FunctionCallNode(identifier, parameters);
functionCall.location = Location.fromCtx(ctx);
return functionCall;
}

visitInstantiation(ctx: InstantiationContext): InstantiationNode {
const identifier = new IdentifierNode(ctx.Identifier().text as string);
const identifier = new IdentifierNode(ctx.Identifier().getText());
identifier.location = Location.fromToken(ctx.Identifier().symbol);
const parameters = ctx.expressionList().expression().map((e) => this.visit(e));
const parameters = ctx.expressionList().expression_list().map((e) => this.visit(e));
const instantiation = new InstantiationNode(identifier, parameters);
instantiation.location = Location.fromCtx(ctx);
return instantiation;
}

visitTupleIndexOp(ctx: TupleIndexOpContext): TupleIndexOpNode {
const tuple = this.visit(ctx.expression());
const index = parseInt(ctx._index.text as string, 10);
const index = parseInt(ctx._index.text, 10);
const tupleIndexOp = new TupleIndexOpNode(tuple, index);
tupleIndexOp.location = Location.fromCtx(ctx);
return tupleIndexOp;
}

visitNullaryOp(ctx: NullaryOpContext): NullaryOpNode {
const operator = ctx.text as NullaryOperator;
const operator = ctx.getText() as NullaryOperator;
const nullaryOp = new NullaryOpNode(operator);
nullaryOp.location = Location.fromCtx(ctx);
return nullaryOp;
Expand Down Expand Up @@ -284,14 +288,14 @@ export default class AstBuilder
}

visitArray(ctx: ArrayContext): ArrayNode {
const elements = ctx.expression().map((e) => this.visit(e));
const elements = ctx.expression_list().map((e) => this.visit(e));
const array = new ArrayNode(elements);
array.location = Location.fromCtx(ctx);
return array;
}

visitIdentifier(ctx: IdentifierContext): IdentifierNode {
const identifier = new IdentifierNode((ctx.Identifier() as TerminalNode).text);
const identifier = new IdentifierNode(ctx.Identifier().getText());
identifier.location = Location.fromCtx(ctx);
return identifier;
}
Expand Down Expand Up @@ -325,25 +329,25 @@ export default class AstBuilder
}

createBooleanLiteral(ctx: LiteralContext): BoolLiteralNode {
const boolString = (ctx.BooleanLiteral() as TerminalNode).text;
const boolString = ctx.BooleanLiteral().getText();
const boolValue = boolString === 'true';
const booleanLiteral = new BoolLiteralNode(boolValue);
booleanLiteral.location = Location.fromCtx(ctx);
return booleanLiteral;
}

createIntLiteral(ctx: LiteralContext): IntLiteralNode {
const numberCtx = ctx.numberLiteral() as NumberLiteralContext;
const numberString = numberCtx.NumberLiteral().text;
const numberUnit = numberCtx.NumberUnit()?.text;
const numberCtx = ctx.numberLiteral();
const numberString = numberCtx.NumberLiteral().getText();
const numberUnit = numberCtx.NumberUnit()?.getText();
const numberValue = BigInt(numberString) * BigInt(numberUnit ? NumberUnit[numberUnit.toUpperCase()] : 1);
const intLiteral = new IntLiteralNode(numberValue);
intLiteral.location = Location.fromCtx(ctx);
return intLiteral;
}

createStringLiteral(ctx: LiteralContext): StringLiteralNode {
const rawString = (ctx.StringLiteral() as TerminalNode).text;
const rawString = ctx.StringLiteral().getText();
const stringValue = rawString.substring(1, rawString.length - 1);
const quote = rawString.substring(0, 1);
const stringLiteral = new StringLiteralNode(stringValue, quote);
Expand All @@ -352,7 +356,7 @@ export default class AstBuilder
}

createDateLiteral(ctx: LiteralContext): IntLiteralNode {
const rawString = (ctx.DateLiteral() as TerminalNode).text;
const rawString = ctx.DateLiteral().getText();
const stringValue = rawString.substring(6, rawString.length - 2).trim();

if (!/^\d\d\d\d-\d\d-\d\dT\d\d:\d\d:\d\d$/.test(stringValue)) {
Expand All @@ -371,10 +375,16 @@ export default class AstBuilder
}

createHexLiteral(ctx: LiteralContext): HexLiteralNode {
const hexString = (ctx.HexLiteral() as TerminalNode).text;
const hexString = ctx.HexLiteral().getText();
const hexValue = hexToBin(hexString.substring(2));
const hexLiteral = new HexLiteralNode(hexValue);
hexLiteral.location = Location.fromCtx(ctx);
return hexLiteral;
}

// For safety reasons, we throw an error when the "default" visitChildren is called. *All* nodes
// must have a custom visit method, so that we can be sure that we've covered all cases.
visitChildren(): Node {
throw new Error('Safety Warning: Unhandled node in AST builder');
}
}
11 changes: 5 additions & 6 deletions packages/cashc/src/ast/Location.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import type { ParserRuleContext } from 'antlr4ts/ParserRuleContext.js';
import type { Token } from 'antlr4ts';
import type { ParserRuleContext, Token } from 'antlr4';

export class Location {
constructor(public start: Point, public end: Point) {}
Expand All @@ -8,17 +7,17 @@ export class Location {
const stop = ctx.stop?.text ? ctx.stop : ctx.start;
const textLength = (stop.text ?? '').length;

const start = new Point(ctx.start.line, ctx.start.charPositionInLine);
const end = new Point(stop.line, stop.charPositionInLine + textLength);
const start = new Point(ctx.start.line, ctx.start.column);
const end = new Point(stop.line, stop.column + textLength);

return new Location(start, end);
}

static fromToken(token: Token): Location | undefined {
const textLength = (token.text ?? '').length;

const start = new Point(token.line, token.charPositionInLine);
const end = new Point(token.line, token.charPositionInLine + textLength);
const start = new Point(token.line, token.column);
const end = new Point(token.line, token.column + textLength);

return new Location(start, end);
}
Expand Down
4 changes: 2 additions & 2 deletions packages/cashc/src/ast/Pragma.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { VersionOperatorContext } from '../grammar/CashScriptParser.js';
import type { VersionOperatorContext } from '../grammar/CashScriptParser.js';

export enum PragmaName {
CASHSCRIPT = 'cashscript',
Expand All @@ -19,5 +19,5 @@ export function getPragmaName(name: string): PragmaName {
}

export function getVersionOpFromCtx(ctx?: VersionOperatorContext): VersionOp {
return <VersionOp>(ctx ? ctx.text : '=');
return <VersionOp>(ctx ? ctx.getText() : '=');
}
Loading

1 comment on commit f74b8ab

@vercel
Copy link

@vercel vercel bot commented on f74b8ab Jan 10, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.