Loading files
This file is pinned to the selected release. Download the original to open it in your design tool.
import CodeFile from "./CodeFile";
import CodeLocation from "./CodeLocation";
import CodeSpan from "./CodeSpan";
import ErrorCollector from "./ErrorCollector";
import {
IllegalStringEscapeSequenceLexingError,
InvalidNumberLiteralLexingError,
TooManyDotsInNumberLiteralLexingError,
TooManyEInNumberLiteralLexingError,
UnexpectedCharacterLexingError,
UnterminatedFilenameLexingError,
UnterminatedMultilineCommentLexingError,
UnterminatedStringLiteralLexingError,
} from "./errors/lexingErrors";
import {
ExtraToken,
MultiLineComment,
NewLineExtraToken,
SingleLineComment,
} from "./extraTokens";
import keywords from "./keywords";
import LiteralToken from "./LiteralToken";
import Token from "./Token";
import TokenType from "./TokenType";
/**
* The lexer is responsible for turning a string of characters into a stream of
* tokens. The tokens are then used by the parser to build an abstract syntax
* tree.
*
* The lexer handles parsing of string literals, digraphs (e.g. `<=`), and numbers.
* It also handles detecting keywords and identifiers.
*/
export default class Lexer {
protected start!: CodeLocation;
protected startWithWhitespace!: CodeLocation;
public tokens: Token[] = [];
protected currentExtraTokens: ExtraToken[] = [];
protected charOffset = 0;
protected lineOffset = 0;
protected colOffset = 0;
protected _currLocCache: CodeLocation | null = null;
constructor(
public codeFile: CodeFile,
public errorCollector: ErrorCollector
) {}
/**
* Scans the whole CodeFile and splits it into tokens.
* @throws LexingError
*/
scan(): Token[] {
this.start = this.getLoc();
this.startWithWhitespace = this.getLoc();
while (!this.isAtEnd()) {
this.start = this.getLoc();
this.scanToken();
}
this.start = this.getLoc();
this.addToken(TokenType.Eot);
return this.tokens;
}
protected scanToken() {
const c = this.advance();
switch (c) {
case "(":
this.addToken(TokenType.LeftParen);
break;
case ")":
this.addToken(TokenType.RightParen);
break;
case "{":
this.addToken(TokenType.LeftBrace);
break;
case "}":
this.addToken(TokenType.RightBrace);
break;
case "[":
this.addToken(TokenType.LeftBracket);
break;
case "]":
this.addToken(TokenType.RightBracket);
break;
case "+":
this.addToken(TokenType.Plus);
break;
case "-":
this.addToken(TokenType.Minus);
break;
case "%":
this.addToken(TokenType.Percent);
break;
case "*":
this.addToken(TokenType.Star);
break;
case "^":
this.addToken(TokenType.Caret);
break;
case "/":
if (this.match("/")) {
const comment = new SingleLineComment(this.getLoc(), "");
// consume a comment
while (this.peek() != "\n" && !this.isAtEnd()) {
comment.contents += this.advance();
}
this.currentExtraTokens.push(comment);
} else if (this.match("*")) {
const comment = new MultiLineComment(this.getLoc(), "");
// multiline comment
while (
!(this.peek() == "*" && this.peekNext() == "/") &&
!this.isAtEnd()
) {
comment.contents += this.advance();
}
if (this.isAtEnd()) {
throw this.errorCollector.reportError(
new UnterminatedMultilineCommentLexingError(this.getLoc())
);
}
this.currentExtraTokens.push(comment);
this.advance(); // advance the star
this.advance(); // advance the slash
} else {
this.addToken(TokenType.Slash);
}
break;
case ".":
// allow lexing of numbers without the leading 0
if (/[0-9]/.test(this.peek())) {
this.consumeNumberLiteral();
break;
}
this.addToken(TokenType.Dot);
break;
case ",":
this.addToken(TokenType.Comma);
break;
case ":":
this.addToken(TokenType.Colon);
break;
case "?":
this.addToken(TokenType.QuestionMark);
break;
case ";":
this.addToken(TokenType.Semicolon);
break;
case "#":
this.addToken(TokenType.Hash);
break;
case "!":
if (this.match("=")) {
this.addToken(TokenType.BangEqual);
} else {
this.addToken(TokenType.Bang);
}
break;
case "<":
if (this.match("<")) {
this.addToken(TokenType.ShiftLeft);
} else if (this.match("=")) {
this.addToken(TokenType.LessEqual);
} else {
this.addToken(TokenType.Less);
}
break;
case ">":
if (this.match(">")) {
this.addToken(TokenType.ShiftRight);
} else if (this.match("=")) {
this.addToken(TokenType.GreaterEqual);
} else {
this.addToken(TokenType.Greater);
}
break;
case "=":
if (this.match("=")) {
this.addToken(TokenType.EqualEqual);
} else {
this.addToken(TokenType.Equal);
}
break;
case "&":
if (this.match("&")) {
this.addToken(TokenType.AND);
} else {
this.addToken(TokenType.Ampersand);
}
break;
case "|":
if (this.match("|")) {
this.addToken(TokenType.OR);
} else {
this.addToken(TokenType.Pipe);
}
break;
case "~":
this.addToken(TokenType.Tilde);
break;
case "\n":
this.currentExtraTokens.push(new NewLineExtraToken(this.getLoc()));
break;
case "\r":
case " ":
case "\t":
break; // ignore whitespace
case '"':
this.consumeStringLiteral();
break;
default:
if (/[0-9]/.test(c)) {
this.consumeNumberOrIdentifierOrKeyword();
} else if (/[A-Za-z\$_]/.test(c)) {
this.consumeIdentifierOrKeyword();
} else {
throw this.errorCollector.reportError(
new UnexpectedCharacterLexingError(this.getLoc(), c)
);
}
}
}
protected consumeStringLiteral() {
let str = "";
while (this.peek() != '"' && !this.isAtEnd()) {
const c = this.advance();
// handle escape sequences
if (c == "\\") {
if (this.match('"')) {
str += '"';
} else if (this.match("\\")) {
str += "\\";
} else if (this.match("n")) {
str += "\n";
} else if (this.match("t")) {
str += "\t";
} else if (this.match("r")) {
str += "\r";
} else {
throw this.errorCollector.reportError(
new IllegalStringEscapeSequenceLexingError(this.getLoc(), `\\${c}`)
);
}
//TODO: Add unicode escape sequences handling
} else {
str += c;
}
}
if (this.isAtEnd()) {
throw this.errorCollector.reportError(
new UnterminatedStringLiteralLexingError(this.getLoc())
);
}
this.advance();
this.addToken(TokenType.StringLiteral, str);
}
protected consumeNumberLiteral() {
let ateDigit = /[0-9]/.test(this.codeFile.code[this.start.char]);
let ateDot = "." === this.codeFile.code[this.start.char];
let justAteExp = false;
while (
/[0-9]/.test(this.peek()) ||
(this.peek() == "." && /[0-9]/.test(this.peekNext())) ||
((this.peek() == "e" || this.peek() == "E") &&
/[0-9\-+]/.test(this.peekNext())) ||
(this.peek() == "-" && /[0-9]/.test(this.peekNext()) && justAteExp) ||
(this.peek() == "+" && /[0-9]/.test(this.peekNext()) && justAteExp) ||
(this.peek() == "." && ateDigit && !ateDot)
) {
ateDigit = ateDigit || /[0-9]/.test(this.peek());
ateDot = ateDot || this.peek() == ".";
justAteExp = this.peek() == "e" || this.peek() == "E";
this.advance();
}
const lexeme = this.codeFile.code.substring(
this.start.char,
this.charOffset
);
if ((lexeme.match(/\./g) || []).length > 1) {
throw this.errorCollector.reportError(
new TooManyDotsInNumberLiteralLexingError(this.getLoc(), lexeme)
);
}
if ((lexeme.match(/e/g) || []).length > 1) {
throw this.errorCollector.reportError(
new TooManyEInNumberLiteralLexingError(this.getLoc(), lexeme)
);
}
const value = parseFloat(lexeme);
if (isNaN(value) || !isFinite(value)) {
throw this.errorCollector.reportError(
new InvalidNumberLiteralLexingError(this.getLoc(), lexeme)
);
}
this.addToken(TokenType.NumberLiteral, value);
}
protected consumeHexNumberLiteral() {
this.advance(); // 0
this.advance(); // x
while (/[0-9a-fA-F]/.test(this.peek())) {
this.advance();
}
const lexeme = this.codeFile.code.substring(
this.start.char,
this.charOffset
);
this.addToken(TokenType.NumberLiteral, parseInt(lexeme.slice(2), 16));
}
protected consumeIdentifierOrKeyword() {
while (/[A-Za-z0-9_\$]/.test(this.peek()) && !this.isAtEnd()) {
this.advance();
}
const lexeme = this.codeFile.code.substring(
this.start.char,
this.charOffset
);
if (lexeme in keywords) {
const keywordType = keywords[lexeme];
this.addToken(keywordType);
// check if we need to lex a filename
if (keywordType === TokenType.Use || keywordType === TokenType.Include) {
this.consumeFileNameInChevrons();
}
return;
}
this.addToken(TokenType.Identifier, lexeme);
}
protected consumeNumberOrIdentifierOrKeyword() {
// OpenSCAD does accept identifiers starting with a digit.
// `9e9e9=1;echo(9e9e9);` is a valid code, `9e9=1;` produces a syntax error.
// Docs don't specify how conflicts are resolved, but from experiments
// it seems like a number is chosen unless an identifier is a longer match.
// That would be consistent with how lex/flex generated lexers work.
let wordLength = 1;
while (
this.start.char + wordLength < this.codeFile.code.length &&
/[0-9a-zA-Z_\$]/.test(this.codeFile.code[this.start.char + wordLength])
) {
wordLength++;
}
// Only lowercase "0x" is a hex prefix - "0X1A", "0x1g", "0x" fall
// through to being lexed as an identifier.
const hexMatch = this.peekRegex(/^0x[0-9a-fA-F]+/);
if (hexMatch.length >= wordLength) {
return this.consumeHexNumberLiteral();
}
const possibleNumberStarts = [
this.peekRegex(/^[0-9]+/),
this.peekRegex(/^[0-9]+[.]/),
this.peekRegex(/^[0-9]+[eE][+-]?[0-9]+/),
];
const numberLength = Math.max(...possibleNumberStarts.map((x) => x.length));
// If number is longer or same length as an indentifier - number wins.
if (numberLength >= wordLength) {
return this.consumeNumberLiteral();
} else {
return this.consumeIdentifierOrKeyword();
}
}
protected consumeFileNameInChevrons() {
this.startWithWhitespace = this.getLoc();
while (!this.isAtEnd()) {
this.start = this.getLoc();
if (
this.match("\n") ||
this.match("\t") ||
this.match("\r") ||
this.match(" ")
)
continue; // ignore whitespace
if (this.match("<")) break;
// The openscad parser does not allow putting comments like this: `use /* ddd*/ <file.scad>`
// We must check that and report an error
throw this.errorCollector.reportError(
new UnexpectedCharacterLexingError(this.getLoc(), this.advance())
);
}
if (this.isAtEnd()) {
throw this.errorCollector.reportError(
new UnterminatedFilenameLexingError(this.getLoc())
);
}
let filename = "";
let didEnd = false;
while (!this.isAtEnd()) {
const c = this.advance();
if (c === ">") {
didEnd = true;
break;
}
filename += c;
}
if (!didEnd) {
throw this.errorCollector.reportError(
new UnterminatedFilenameLexingError(this.getLoc())
);
}
this.addToken(TokenType.FilenameInChevrons, filename);
}
/**
* Adds a token to the token list. If a value is provieded a LiteralToken is pushed.
*
* Additionally it handles clearing and attaching the extra tokens.
*/
protected addToken<TValue = any>(
tokenType: TokenType,
value: TValue | null = null
) {
const lexeme = this.codeFile.code.substring(
this.start.char,
this.charOffset
);
let token;
if (value != null) {
token = new LiteralToken(
tokenType,
new CodeSpan(this.start, this.getLoc()),
lexeme,
value
);
} else {
token = new Token(
tokenType,
new CodeSpan(this.start, this.getLoc()),
lexeme
);
}
token.extraTokens = this.currentExtraTokens;
token.startWithWhitespace = this.startWithWhitespace;
this.startWithWhitespace = this.getLoc();
this.currentExtraTokens = [];
this.tokens.push(token);
}
protected isAtEnd() {
return this.charOffset >= this.codeFile.code.length;
}
protected match(expected: string) {
if (this.isAtEnd()) return false;
if (this.codeFile.code[this.charOffset] !== expected) return false;
this.advance();
return true;
}
protected advance() {
const c = this.codeFile.code[this.charOffset];
this.charOffset++;
if (c === "\n") {
this.lineOffset++;
this.colOffset = 0;
} else {
this.colOffset++;
}
this._currLocCache = null;
return c;
}
protected getLoc() {
if (!this._currLocCache) {
this._currLocCache = new CodeLocation(
this.codeFile,
this.charOffset,
this.lineOffset,
this.colOffset
);
}
return this._currLocCache;
}
protected peek() {
if (this.isAtEnd()) return "\0";
return this.codeFile.code[this.charOffset];
}
protected peekNext() {
if (this.charOffset + 1 >= this.codeFile.code.length) return "\0";
return this.codeFile.code[this.charOffset + 1];
}
protected peekRegex(regex: RegExp) {
const text = this.codeFile.code.slice(this.start.char);
const match = regex.exec(text);
return !match || match.index !== 0 ? "" : match[0];
}
}
2d56448eaThis file is pinned to the selected release. Download the original to open it in your design tool.