Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Semicolon as statement ender #7

Merged
merged 13 commits into from
Jul 6, 2023
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ npm run mtsc ./tests/singleVar.ts
## Exercises

- [x] Add EmptyStatement (https://github.com/imteekay/mini-typescript/pull/2).
- [ ] Make semicolon a statement ender, not statement separator.
- [x] Make semicolon a statement ender, not statement separator.
- Hint: You'll need a predicate to peek at the next token and decide if it's the start of an element.
- Bonus: Switch from semicolon to newline as statement ender.
- [x] Add string literals (https://github.com/imteekay/mini-typescript/pull/4).
Expand Down
2 changes: 1 addition & 1 deletion baselines/reference/emptyStatement.tree.baseline
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
}
},
{
"kind": "EmptyStatement"
"kind": "EndOfFile"
}
]
}
2 changes: 1 addition & 1 deletion baselines/reference/redeclare.tree.baseline
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
}
},
{
"kind": "EmptyStatement"
"kind": "EndOfFile"
}
]
}
2 changes: 1 addition & 1 deletion baselines/reference/singleIdentifier.tree.baseline
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
}
},
{
"kind": "EmptyStatement"
"kind": "EndOfFile"
}
]
}
2 changes: 1 addition & 1 deletion baselines/reference/singleTypedVar.tree.baseline
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
}
},
{
"kind": "EmptyStatement"
"kind": "EndOfFile"
}
]
}
2 changes: 1 addition & 1 deletion baselines/reference/singleVar.tree.baseline
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
}
},
{
"kind": "EmptyStatement"
"kind": "EndOfFile"
}
]
}
2 changes: 1 addition & 1 deletion baselines/reference/stringLiteral.tree.baseline
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@
}
},
{
"kind": "EmptyStatement"
"kind": "EndOfFile"
}
]
}
1 change: 1 addition & 0 deletions baselines/reference/terminator.errors.baseline
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[]
1 change: 1 addition & 0 deletions baselines/reference/terminator.js.baseline
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"var x = 1;\nvar y = 2;\nvar z = 3;\nx;\ny;\nz"
81 changes: 81 additions & 0 deletions baselines/reference/terminator.tree.baseline
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
{
"locals": {
"x": [
{
"kind": "Var",
"pos": 3
}
],
"y": [
{
"kind": "Var",
"pos": 14
}
],
"z": [
{
"kind": "Var",
"pos": 25
}
]
},
"statements": [
{
"kind": "Var",
"name": {
"kind": "Identifier",
"text": "x"
},
"init": {
"kind": "NumericLiteral",
"value": 1
}
},
{
"kind": "Var",
"name": {
"kind": "Identifier",
"text": "y"
},
"init": {
"kind": "NumericLiteral",
"value": 2
}
},
{
"kind": "Var",
"name": {
"kind": "Identifier",
"text": "z"
},
"init": {
"kind": "NumericLiteral",
"value": 3
}
},
{
"kind": "ExpressionStatement",
"expr": {
"kind": "Identifier",
"text": "x"
}
},
{
"kind": "ExpressionStatement",
"expr": {
"kind": "Identifier",
"text": "y"
}
},
{
"kind": "ExpressionStatement",
"expr": {
"kind": "Identifier",
"text": "z"
}
},
{
"kind": "EndOfFile"
}
]
}
2 changes: 1 addition & 1 deletion baselines/reference/twoStatements.tree.baseline
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
}
},
{
"kind": "EmptyStatement"
"kind": "EndOfFile"
}
]
}
2 changes: 1 addition & 1 deletion baselines/reference/twoTypedStatements.tree.baseline
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
}
},
{
"kind": "EmptyStatement"
"kind": "EndOfFile"
}
]
}
2 changes: 1 addition & 1 deletion baselines/reference/typeAlias.tree.baseline
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@
}
},
{
"kind": "EmptyStatement"
"kind": "EndOfFile"
}
]
}
3 changes: 3 additions & 0 deletions src/check.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const stringType: Type = { id: 'string' };
const numberType: Type = { id: 'number' };
const errorType: Type = { id: 'error' };
const empty: Type = { id: 'empty' };
const eof: Type = { id: 'eof' };
imteekay marked this conversation as resolved.
Show resolved Hide resolved

function typeToString(type: Type) {
return type.id;
Expand Down Expand Up @@ -44,6 +45,8 @@ export function check(module: Module) {
return checkType(statement.typename);
case Node.EmptyStatement:
return empty;
case Node.EndOfFile:
return eof;
}
}

Expand Down
2 changes: 2 additions & 0 deletions src/emit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ function emitStatement(statement: Statement): string {
return `type ${statement.name.text} = ${statement.typename.text}`;
case Node.EmptyStatement:
return '';
case Node.EndOfFile:
return '';
}
}

Expand Down
26 changes: 19 additions & 7 deletions src/lex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,21 +22,17 @@ export function lex(s: string): Lexer {
};

function scan() {
// scan forward all
// \t - tabs
// \b - empty strings at the beginning and end of a word
// \n - newline char
scanForward((c) => /[ \t\b\n]/.test(c));
scanForward(isEmptyStrings);
const start = pos;

if (pos === s.length) {
token = Token.EOF;
} else if (/[0-9]/.test(s.charAt(pos))) {
scanForward((c) => /[0-9]/.test(c));
scanForward(isNumber);
text = s.slice(start, pos);
token = Token.NumericLiteral;
} else if (/[_a-zA-Z]/.test(s.charAt(pos))) {
scanForward((c) => /[_a-zA-Z0-9]/.test(c));
scanForward(isAlphanumerical);
text = s.slice(start, pos);
token =
text in keywords
Expand Down Expand Up @@ -65,6 +61,22 @@ export function lex(s: string): Lexer {
}
}

function isEmptyStrings(c: string) {
// scan forward all
// \t - tabs
// \b - empty strings at the beginning and end of a word
// \n - newline char
return /[ \t\b\n]/.test(c);
}

function isNumber(c: string) {
return /[0-9]/.test(c);
}

function isAlphanumerical(c: string) {
return /[_a-zA-Z0-9]/.test(c);
}

function scanForward(pred: (x: string) => boolean) {
while (pos < s.length && pred(s.charAt(pos))) pos++;
}
Expand Down
29 changes: 17 additions & 12 deletions src/parse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,15 @@ import {
} from './types';
import { error } from './error';

const emptyTokens = [Token.EOF, Token.Semicolon];

export function parse(lexer: Lexer): Module {
lexer.scan();
return parseModule();

function parseModule(): Module {
const statements = parseSeparated(parseStatement, () =>
tryParseToken(Token.Semicolon),
);
parseExpected(Token.EOF);
return { statements, locals: new Map() };
return {
statements: parseStatements(parseStatement),
locals: new Map(),
};
}

function parseExpression(): Expression {
Expand Down Expand Up @@ -65,7 +62,10 @@ export function parse(lexer: Lexer): Module {

function parseStatement(): Statement {
const pos = lexer.pos();
if (tryParseToken(Token.Var)) {

if (tryParseToken(Token.EOF)) {
return { kind: Node.EndOfFile };
} else if (tryParseToken(Token.Var)) {
const name = parseIdentifier();
const typename = tryParseToken(Token.Colon)
? parseIdentifier()
Expand All @@ -78,8 +78,13 @@ export function parse(lexer: Lexer): Module {
parseExpected(Token.Equals);
const typename = parseIdentifier();
return { kind: Node.TypeAlias, name, typename, pos };
} else if (emptyTokens.includes(lexer.token())) {
return { kind: Node.EmptyStatement };
} else if (tryParseToken(Token.Semicolon)) {
// if a semicolon is followed by another semicolon,
// it should return an empty statement
if (lexer.token() === Token.Semicolon) {
return { kind: Node.EmptyStatement };
}
return parseStatement();
}
return { kind: Node.ExpressionStatement, expr: parseExpression(), pos };
}
Expand All @@ -103,9 +108,9 @@ export function parse(lexer: Lexer): Module {
}
}

function parseSeparated<T>(element: () => T, separator: () => unknown) {
function parseStatements<T>(element: () => T) {
const list = [element()];
while (separator()) {
while (lexer.token() !== Token.EOF) {
Copy link

Choose a reason for hiding this comment

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

I think this function should be the one checking for semicolons, not parseStatement.
Although it would be best to pass in a predicate like separator, except named terminator or something.

Copy link
Owner Author

@imteekay imteekay May 16, 2023

Choose a reason for hiding this comment

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

@sandersn Not sure if I got the exercise right this time. imteekay/mini-typescript@b74b6fc

The whole idea was

  • Add a predicate function (terminator) to handle semicolons
  • after a semicolon token, we can have
    • another semicolon: I considered that an empty statement (Allow var to have multiple declarations mini-typescript#2)
      • just added a peek function to see the next token and if it's indeed a semicolon, call the parse statement to get the EmptyStatement node
    • the rest of the tokens (string, number, eof): just parse a statement

Does that make sense? Maybe it can be improved for this particular case.

Choose a reason for hiding this comment

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

Here's the way Typescript does it, as I recall:

const list = []
while (peek()) {
  list.push(element())
  terminator()
}

where peek returns true if the current token could be the start of a new Statement, and terminator doesn't consume a token if terminators are optional. This relies on the first token of statements to be distinguishable from from non-statements, but the only non-statement that can come after a statement list is Token.EOF, so peek is just going to be () => lexer.token !== Token.EOF

This way is a bit simpler than yours, and generalises to other kinds of lists, like parameter lists, arrays, etc.

Copy link
Owner Author

Choose a reason for hiding this comment

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

Simplified it, the implementation seems much better now! ✨ imteekay/mini-typescript@cad1fec

list.push(element());
}
return list;
Expand Down
2 changes: 2 additions & 0 deletions src/transform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ function typescript(statements: Statement[]) {
return [];
case Node.EmptyStatement:
return [];
case Node.EndOfFile:
return [];
}
}
}
12 changes: 11 additions & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export enum Node {
TypeAlias,
StringLiteral,
EmptyStatement,
EndOfFile,
}

export type Error = {
Expand Down Expand Up @@ -72,7 +73,12 @@ export type Assignment = Location & {
value: Expression;
};

export type Statement = ExpressionStatement | Var | TypeAlias | EmptyStatement;
export type Statement =
| ExpressionStatement
| Var
| TypeAlias
| EmptyStatement
| EndOfFile;
imteekay marked this conversation as resolved.
Show resolved Hide resolved

export type ExpressionStatement = Location & {
kind: Node.ExpressionStatement;
Expand All @@ -96,6 +102,10 @@ export type EmptyStatement = {
kind: Node.EmptyStatement;
};

export type EndOfFile = {
kind: Node.EndOfFile;
};

export type Declaration = Var | TypeAlias; // plus others, like function

export type Symbol = {
Expand Down
4 changes: 4 additions & 0 deletions tests/terminator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
var x = 1;
var y = 2;
var z = 3;
x;y;z;