const {
DIAGNOSTIC_CODES,
ParseError,
} = require('../diagnostic/ParseError.js');
class Lexer {
constructor(input, maxTokenBytes = Number.POSITIVE_INFINITY) {
this.input = input;
this.maxTokenBytes = maxTokenBytes;
this.pos = 0;
this.line = 1;
this.column = 1;
this.offset = 0;
}
current() {
const codePoint = this.input.codePointAt(this.pos);
return codePoint === undefined ? undefined : String.fromCodePoint(codePoint);
}
advance() {
if (this.pos < this.input.length) {
const c = this.current();
this.pos += c.length;
this.offset += Buffer.byteLength(c, 'utf8');
if (c === '\n') {
this.line++;
this.column = 1;
} else {
this.column++;
}
}
}
startLocation() {
return { line: this.line, column: this.column, offset: this.offset };
}
makeToken(type, start, end) {
return { type, span: { start, end } };
}
readStringDouble() {
let result = '';
while (this.current() !== undefined) {
const c = this.current();
if (c === '"') {
this.advance();
break;
}
if (c === '\\') {
this.advance();
const escaped = this.current();
if (escaped) {
switch (escaped) {
case 'n': result += '\n'; break;
case 'r': result += '\r'; break;
case 't': result += '\t'; break;
case '\\': result += '\\'; break;
case '"': result += '"'; break;
default: result += '\\' + escaped;
}
this.advance();
}
} else if (c === '\n') {
break;
} else {
result += c;
this.advance();
}
}
return result;
}
readStringSingle() {
let result = '';
while (this.current() !== undefined) {
const c = this.current();
if (c === "'") {
this.advance();
break;
}
if (c === '\\') {
this.advance();
const escaped = this.current();
if (escaped) {
switch (escaped) {
case "'": result += "'"; break;
default: result += '\\' + escaped;
}
this.advance();
}
} else if (c === '\n') {
break;
} else {
result += c;
this.advance();
}
}
return result;
}
readNumber() {
const start = this.pos;
let hasDot = false;
let hasE = false;
if (this.current() === '-') {
this.advance();
}
while (this.current() !== undefined) {
const c = this.current();
if (c >= '0' && c <= '9') {
this.advance();
} else if (c === '.' && !hasDot && !hasE) {
hasDot = true;
this.advance();
} else if ((c === 'e' || c === 'E') && !hasE) {
hasE = true;
this.advance();
if (this.current() === '+' || this.current() === '-') {
this.advance();
}
} else {
break;
}
}
const numStr = this.input.slice(start, this.pos);
const value = parseFloat(numStr);
return { value: isNaN(value) ? 0 : value };
}
readIdent() {
const start = this.pos;
while (this.current() !== undefined) {
const c = this.current();
if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c === '_' || (c >= '0' && c <= '9')) {
this.advance();
} else {
break;
}
}
const name = this.input.slice(start, this.pos);
return { name };
}
tokenize() {
const tokens = [];
while (this.current() !== undefined) {
const start = this.startLocation();
const startPos = this.pos;
const c = this.current();
switch (c) {
case '{':
this.advance();
tokens.push(this.makeToken('LeftBrace', start, this.startLocation()));
break;
case '}':
this.advance();
tokens.push(this.makeToken('RightBrace', start, this.startLocation()));
break;
case '[':
this.advance();
tokens.push(this.makeToken('LeftBracket', start, this.startLocation()));
break;
case ']':
this.advance();
tokens.push(this.makeToken('RightBracket', start, this.startLocation()));
break;
case '(':
this.advance();
tokens.push(this.makeToken('LeftParen', start, this.startLocation()));
break;
case ')':
this.advance();
tokens.push(this.makeToken('RightParen', start, this.startLocation()));
break;
case '=':
this.advance();
tokens.push(this.makeToken('Equal', start, this.startLocation()));
break;
case '+':
this.advance();
if (this.current() === '=') {
this.advance();
tokens.push(this.makeToken('PlusEqual', start, this.startLocation()));
}
break;
case ':':
this.advance();
tokens.push(this.makeToken('Colon', start, this.startLocation()));
break;
case ',':
this.advance();
tokens.push(this.makeToken('Comma', start, this.startLocation()));
break;
case '"':
this.advance();
const s = this.readStringDouble();
tokens.push({ type: 'String', value: s, span: { start, end: this.startLocation() } });
break;
case "'":
this.advance();
const s2 = this.readStringSingle();
tokens.push({ type: 'String', value: s2, span: { start, end: this.startLocation() } });
break;
case '#':
case '/':
this.advance();
while (this.current() !== undefined && this.current() !== '\n') {
this.advance();
}
tokens.push(this.makeToken('Comment', start, this.startLocation()));
break;
case '\n':
case '\r':
this.advance();
tokens.push(this.makeToken('Newline', start, this.startLocation()));
break;
case ' ':
case '\t':
this.advance();
break;
default:
if ((c >= '0' && c <= '9') || c === '-' || c === '.') {
const { value } = this.readNumber();
tokens.push({ type: 'Number', value, span: { start, end: this.startLocation() } });
} else if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c === '_') {
const { name } = this.readIdent();
let type = 'Ident';
if (name === 'true') type = 'True';
else if (name === 'false') type = 'False';
else if (name === 'null') type = 'Null';
tokens.push({ type, value: name, span: { start, end: this.startLocation() } });
} else {
this.advance();
}
}
const tokenBytes = Buffer.byteLength(this.input.slice(startPos, this.pos), 'utf8');
if (tokenBytes > this.maxTokenBytes) {
throw new ParseError(
DIAGNOSTIC_CODES.TOKEN_BYTES_LIMIT,
`ACL parse limit exceeded: token is longer than ${this.maxTokenBytes} bytes`,
{ start, end: this.startLocation() }
);
}
}
tokens.push({ type: 'Eof', span: { start: this.startLocation(), end: this.startLocation() } });
return tokens;
}
}
module.exports = { Lexer };