@erml/parser
Version:
Parser implementation of ERML
163 lines (138 loc) • 4.68 kB
text/typescript
import {
isValidIdentifier,
isValidReference,
isDuplicateIdentifier,
} from "./identifiers"
import { Token, Tokens, stringLiteralRegexp } from "../lexer"
import { Delimiters } from "."
export type PipelineFunction = (
token: Token,
tokenIndex: number
) => number | void
export type ParsingPipeline = PipelineFunction[]
export function assertToken(
token: Token,
expectedValues: readonly string[],
callback?: (matchedIndex: number) => void
) {
const matchedIndex = expectedValues.indexOf(token.value)
if (matchedIndex < 0) {
throw new SyntaxError(
`Expected to find ${expectedValues
.map((value) => `'${value}'`)
.join(", ")} at position ${token.position}, line ${
token.line
}. Instead found '${token.value}'`
)
}
if (callback) {
callback(matchedIndex)
}
}
export function processNumber(
token: Token,
range: [number, number],
callback: (numberValue: number) => void
) {
const numberValue = Number(token.value)
if (isNaN(numberValue)) {
throw new TypeError(
`'${token.value}' at position ${token.position}, line ${token.line} is not a valid number`
)
} else if (numberValue < range[0] || numberValue > range[1]) {
throw new RangeError(
`'${token.value}' at position ${token.position}, line ${token.line} doesn't fall in the range of [${range[0]}, ${range[1]}]`
)
}
callback(numberValue)
}
export function processStringLiteral(
token: Token,
callback: (stringValue: string) => void
) {
const match = stringLiteralRegexp.exec(token.value)
if (match === null) {
throw new TypeError(
`'${token.value}' at position ${token.position}, line ${token.line} is not a valid string`
)
}
const stringValue = match[1].replace(/\\\\|\\/g, (match) =>
match === "\\\\" ? "\\" : ""
)
callback(stringValue)
}
export function processIdentifier(
token: Token,
isReference: boolean,
callback: () => void
) {
if (isValidIdentifier(token.value) === false) {
throw new SyntaxError(
`'${token.value}' at position ${token.position}, line ${token.line} is not a valid identifier`
)
} else if (isReference && isValidReference(token.value) === false) {
throw new ReferenceError(
`'${token.value}' at position ${token.position}, line ${token.line} is not defined before`
)
} else if (isReference === false && isDuplicateIdentifier(token.value)) {
throw new SyntaxError(
`'${token.value}' at position ${token.position}, line ${token.line} is already defined`
)
}
callback()
}
export function processBody(
tokens: Tokens,
tokenIndex: number,
callback: (bodyStart: number, bodyEnd: number) => void
) {
assertToken(tokens[tokenIndex], [Delimiters.OPENING_BRACE])
const closingBracePosition = bracesMatchAt(tokens, tokenIndex)
if (closingBracePosition === null) {
throw new SyntaxError(
`Grouping symbols ("${Delimiters.OPENING_BRACE}" and "${Delimiters.CLOSING_BRACE}") don't match after "${Delimiters.OPENING_BRACE}" at position ${tokens[tokenIndex].position}, line ${tokens[tokenIndex].line}`
)
}
const bodyStart = tokenIndex + 1
const bodyEnd = closingBracePosition - 1
if (bodyStart > bodyEnd) {
throw new SyntaxError(
`Body can't be empty at position ${tokens[tokenIndex].position}, line ${tokens[tokenIndex].line}`
)
}
callback(bodyStart, bodyEnd)
return closingBracePosition
}
export function walkPipeline(
parsingPipeline: ParsingPipeline,
tokens: Tokens,
currentTokenIndex: number
): number {
for (const process of parsingPipeline) {
if (tokens[currentTokenIndex] === undefined) {
const previousToken = tokens[currentTokenIndex - 1]
throw new SyntaxError(
`Didn't expect to reach the end after token '${previousToken.value}' at position ${previousToken.position}, line ${previousToken.line}`
)
}
const bodyEnd = process(tokens[currentTokenIndex], currentTokenIndex)
currentTokenIndex = (bodyEnd ? bodyEnd : currentTokenIndex) + 1
}
return currentTokenIndex
}
function bracesMatchAt(tokens: Tokens, currentPosition: number) {
let scales = 0
do {
scales +=
tokens[currentPosition].value === Delimiters.OPENING_BRACE
? 1
: tokens[currentPosition].value === Delimiters.CLOSING_BRACE
? -1
: 0
if (scales === 0) {
return currentPosition
}
} while (++currentPosition < tokens.length)
return null
}
export const testables = { bracesMatchAt }