Skip to content

Commit

Permalink
Merge 13ec065 into cf2b709
Browse files Browse the repository at this point in the history
  • Loading branch information
simonseyock committed Feb 9, 2022
2 parents cf2b709 + 13ec065 commit 18a8a70
Show file tree
Hide file tree
Showing 15 changed files with 121 additions and 148 deletions.
94 changes: 45 additions & 49 deletions src/Parser.ts
Original file line number Diff line number Diff line change
@@ -1,40 +1,38 @@
import { EarlyEndOfParseError, NoParsletFoundError } from './errors'
import { TokenType } from './lexer/Token'
import { Lexer } from './lexer/Lexer'
import { Grammar } from './grammars/Grammar'
import { assertRootResult } from './assertTypes'
import { Precedence } from './Precedence'
import { RootResult } from './result/RootResult'
import { IntermediateResult } from './result/IntermediateResult'

interface ParserOptions {
grammar: Grammar
lexer?: Lexer
parent?: Parser
}
import { TokenType } from './lexer/Token'

export class Parser {
private readonly grammar: Grammar
private _lexer: Lexer
public readonly parent?: Parser

private readonly lexer: Lexer
private readonly parent?: Parser

constructor ({ grammar, lexer, parent }: ParserOptions) {
this.lexer = lexer ?? new Lexer()

constructor (grammar: Grammar, textOrLexer: string | Lexer, parent?: Parser) {
this.grammar = grammar
if (typeof textOrLexer === 'string') {
this._lexer = Lexer.create(textOrLexer)
} else {
this._lexer = textOrLexer
}
this.parent = parent
}

this.grammar = grammar
get lexer (): Lexer {
return this._lexer
}

/**
* Parses a given string and throws an error if the parse ended before the end of the string.
*/
parseText (text: string): RootResult {
this.lexer.lex(text)
parse (): RootResult {
const result = this.parseType(Precedence.ALL)
if (this.lexer.token().type !== 'EOF') {
throw new EarlyEndOfParseError(this.lexer.token())
if (this.lexer.current.type !== 'EOF') {
throw new EarlyEndOfParseError(this.lexer.current)
}
return result
}
Expand All @@ -46,28 +44,15 @@ export class Parser {
return assertRootResult(this.parseIntermediateType(precedence))
}

/**
* Tries to parse the current state with all parslets in the grammar and returns the first non null result.
*/
private tryParslets (precedence: Precedence, left: IntermediateResult | null): IntermediateResult | null {
for (const parslet of this.grammar) {
const result = parslet(this, precedence, left)
if (result !== null) {
return result
}
}
return null
}

/**
* The main parsing function. First it tries to parse the current state in the prefix step, and then it continues
* to parse the state in the infix step.
*/
public parseIntermediateType (precedence: Precedence): IntermediateResult {
const result = this.tryParslets(precedence, null)
const result = this.tryParslets(null, precedence)

if (result === null) {
throw new NoParsletFoundError(this.lexer.token())
throw new NoParsletFoundError(this.lexer.current)
}

return this.parseInfixIntermediateType(result, precedence)
Expand All @@ -77,37 +62,48 @@ export class Parser {
* In the infix parsing step the parser continues to parse the current state with all parslets until none returns
* a result.
*/
public parseInfixIntermediateType (result: IntermediateResult, precedence: Precedence): IntermediateResult {
let newResult = this.tryParslets(precedence, result)
public parseInfixIntermediateType (left: IntermediateResult, precedence: Precedence): IntermediateResult {
let result = this.tryParslets(left, precedence)

while (newResult !== null) {
result = newResult
newResult = this.tryParslets(precedence, result)
while (result !== null) {
left = result
result = this.tryParslets(left, precedence)
}

return result
return left
}

/**
* Tries to parse the current state with all parslets in the grammar and returns the first non null result.
*/
private tryParslets (left: IntermediateResult | null, precedence: Precedence): IntermediateResult | null {
for (const parslet of this.grammar) {
const result = parslet(this, precedence, left)
if (result !== null) {
return result
}
}
return null
}

/**
* If the given type equals the current type of the {@link Lexer} advance the lexer. Return true if the lexer was
* advanced.
*/
public consume (types: TokenType|TokenType[]): boolean {
public consume (types: TokenType | TokenType[]): boolean {
if (!Array.isArray(types)) {
types = [types]
}
if (!types.includes(this.lexer.token().type)) {

if (types.includes(this.lexer.current.type)) {
this._lexer = this.lexer.advance()
return true
} else {
return false
}
this.lexer.advance()
return true
}

getLexer (): Lexer {
return this.lexer
}

getParent (): Parser | undefined {
return this.parent
public acceptLexerState (parser: Parser): void {
this._lexer = parser.lexer
}
}
64 changes: 24 additions & 40 deletions src/lexer/Lexer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,56 +170,40 @@ const rules = [
]

export class Lexer {
private text: string = ''
private readonly text: string = ''
public readonly current: Token
public readonly next: Token
public readonly previous: Token | undefined

private current: Token | undefined
private next: Token | undefined
private previous: Token | undefined

lex (text: string): void {
this.text = text
this.current = undefined
this.next = undefined
this.advance()
}

token (): Token {
if (this.current === undefined) {
throw new Error('Lexer not lexing')
}
return this.current
}

peek (): Token {
if (this.next === undefined) {
this.next = this.read()
}
return this.next
}

last (): Token | undefined {
return this.previous
public static create (text: string): Lexer {
const current = this.read(text)
text = current.text
const next = this.read(text)
text = next.text
return new Lexer(text, undefined, current.token, next.token)
}

advance (): void {
this.previous = this.current
if (this.next !== undefined) {
this.current = this.next
this.next = undefined
return
}
this.current = this.read()
private constructor (text: string, previous: Token | undefined, current: Token, next: Token) {
this.text = text
this.previous = previous
this.current = current
this.next = next
}

private read (): Token {
const text = this.text.trim()
private static read (text: string): { text: string, token: Token } {
text = text.trim()
for (const rule of rules) {
const token = rule(text)
if (token !== null) {
this.text = text.slice(token.text.length)
return token
text = text.slice(token.text.length)
return { text, token }
}
}
throw new Error('Unexpected Token ' + text)
}

advance (): Lexer {
const next = Lexer.read(this.text)
return new Lexer(next.text, this.current, this.next, next.token)
}
}
23 changes: 9 additions & 14 deletions src/parse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,25 +6,20 @@ import { RootResult } from './result/RootResult'

export type ParseMode = 'closure' | 'jsdoc' | 'typescript'

const parsers = {
jsdoc: new Parser({
grammar: jsdocGrammar
}),
closure: new Parser({
grammar: closureGrammar
}),
typescript: new Parser({
grammar: typescriptGrammar
})
}

/**
* This function parses the given expression in the given mode and produces a {@link RootResult}.
* @param expression
* @param mode
*/
export function parse (expression: string, mode: ParseMode): RootResult {
return parsers[mode].parseText(expression)
switch (mode) {
case 'closure':
return (new Parser(closureGrammar, expression)).parse()
case 'jsdoc':
return (new Parser(jsdocGrammar, expression)).parse()
case 'typescript':
return (new Parser(typescriptGrammar, expression)).parse()
}
}

/**
Expand All @@ -38,7 +33,7 @@ export function tryParse (expression: string, modes: ParseMode[] = ['typescript'
let error
for (const mode of modes) {
try {
return parsers[mode].parseText(expression)
return parse(expression, mode)
} catch (e) {
error = e
}
Expand Down
2 changes: 1 addition & 1 deletion src/parslets/FunctionParslet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export function createFunctionParslet ({ allowNamedParameters, allowNoReturnType
parsePrefix: parser => {
parser.consume('function')

const hasParenthesis = parser.getLexer().token().type === '('
const hasParenthesis = parser.lexer.current.type === '('

if (!hasParenthesis) {
if (!allowWithoutParenthesis) {
Expand Down
17 changes: 12 additions & 5 deletions src/parslets/KeyValueParslet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,20 +27,24 @@ export function createKeyValueParslet ({ allowKeyTypes, allowReadonly, allowOpti
}

// object parslet uses a special grammar and for the value we want to switch back to the parent
parser = parser.getParent() ?? parser
const parentParser = parser.parent ?? parser
parentParser.acceptLexerState(parser)

if (left.type === 'JsdocTypeNumber' || left.type === 'JsdocTypeName' || left.type === 'JsdocTypeStringValue') {
parser.consume(':')
parentParser.consume(':')

let quote
if (left.type === 'JsdocTypeStringValue') {
quote = left.meta.quote
}

const right = parentParser.parseType(Precedence.KEY_VALUE)
parser.acceptLexerState(parentParser)

return {
type: 'JsdocTypeKeyValue',
key: left.value.toString(),
right: parser.parseType(Precedence.KEY_VALUE),
right: right,
optional: optional,
readonly: readonlyProperty,
meta: {
Expand All @@ -53,12 +57,15 @@ export function createKeyValueParslet ({ allowKeyTypes, allowReadonly, allowOpti
throw new UnexpectedTypeError(left)
}

parser.consume(':')
parentParser.consume(':')

const right = parentParser.parseType(Precedence.KEY_VALUE)
parser.acceptLexerState(parentParser)

return {
type: 'JsdocTypeKeyValue',
left: assertRootResult(left),
right: parser.parseType(Precedence.KEY_VALUE),
right: right,
meta: {
hasLeftSideExpression: true
}
Expand Down
2 changes: 1 addition & 1 deletion src/parslets/NameParslet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export function createNameParslet ({ allowedAdditionalTokens }: {
name: 'nameParslet',
accept: type => type === 'Identifier' || type === 'this' || type === 'new' || allowedAdditionalTokens.includes(type),
parsePrefix: parser => {
const { type, text } = parser.getLexer().token()
const { type, text } = parser.lexer.current
parser.consume(type)

return {
Expand Down
12 changes: 5 additions & 7 deletions src/parslets/NamePathParslet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ export function createNamePathParslet ({ allowJsdocNamePaths, pathGrammar }: {
if ((left == null) || precedence >= Precedence.NAME_PATH) {
return null
}
const type = parser.getLexer().token().type
const next = parser.getLexer().peek().type
const type = parser.lexer.current.type
const next = parser.lexer.next.type

const accept = (type === '.' && next !== '<') ||
(type === '[' && left.type === 'JsdocTypeName') ||
Expand All @@ -42,13 +42,11 @@ export function createNamePathParslet ({ allowJsdocNamePaths, pathGrammar }: {
}

const pathParser = pathGrammar !== null
? new Parser({
grammar: pathGrammar,
lexer: parser.getLexer()
})
? new Parser(pathGrammar, parser.lexer, parser)
: parser

const parsed = pathParser.parseIntermediateType(Precedence.NAME_PATH)
parser.acceptLexerState(pathParser)
let right: PropertyResult | SpecialNamePath<'event'>

switch (parsed.type) {
Expand Down Expand Up @@ -91,7 +89,7 @@ export function createNamePathParslet ({ allowJsdocNamePaths, pathGrammar }: {
}

if (brackets && !parser.consume(']')) {
const token = parser.getLexer().token()
const token = parser.lexer.current
throw new Error(`Unterminated square brackets. Next token is '${token.type}' ` +
`with text '${token.text}'`)
}
Expand Down
4 changes: 2 additions & 2 deletions src/parslets/NullableParslets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import { isQuestionMarkUnknownType } from './isQuestionMarkUnkownType'
import { assertRootResult } from '../assertTypes'

export const nullableParslet: ParsletFunction = (parser, precedence, left) => {
const type = parser.getLexer().token().type
const next = parser.getLexer().peek().type
const type = parser.lexer.current.type
const next = parser.lexer.next.type

const accept = ((left == null) && type === '?' && !isQuestionMarkUnknownType(next)) ||
((left != null) && type === '?')
Expand Down
2 changes: 1 addition & 1 deletion src/parslets/NumberParslet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ export const numberParslet = composeParslet({
name: 'numberParslet',
accept: type => type === 'Number',
parsePrefix: parser => {
const value = parseFloat(parser.getLexer().token().text)
const value = parseFloat(parser.lexer.current.text)
parser.consume('Number')
return {
type: 'JsdocTypeNumber',
Expand Down
Loading

0 comments on commit 18a8a70

Please sign in to comment.