Skip to main content
Glama
parse.ts5.33 kB
// SPDX-FileCopyrightText: Copyright Orangebot, Inc. and Medplum contributors // SPDX-License-Identifier: Apache-2.0 import type { TypedValue } from '../types'; import type { Token } from './tokenize'; export interface AtomContext { parent?: AtomContext; variables: Record<string, TypedValue>; } export interface Atom { eval(context: AtomContext, input: TypedValue[]): TypedValue[]; toString(): string; } export abstract class PrefixOperatorAtom implements Atom { readonly operator: string; readonly child: Atom; constructor(operator: string, child: Atom) { this.operator = operator; this.child = child; } abstract eval(context: AtomContext, input: TypedValue[]): TypedValue[]; toString(): string { return `${this.operator}(${this.child.toString()})`; } } export abstract class InfixOperatorAtom implements Atom { readonly operator: string; readonly left: Atom; readonly right: Atom; constructor(operator: string, left: Atom, right: Atom) { this.operator = operator; this.left = left; this.right = right; } abstract eval(context: AtomContext, input: TypedValue[]): TypedValue[]; toString(): string { return `(${this.left.toString()} ${this.operator} ${this.right.toString()})`; } } export interface PrefixParselet { parse(parser: Parser, token: Token): Atom; } export interface InfixParselet { precedence: number; parse?(parser: Parser, left: Atom, token: Token): Atom; } export class ParserBuilder { private readonly prefixParselets: Record<string, PrefixParselet> = {}; private readonly infixParselets: Record<string, InfixParselet> = {}; public registerInfix(tokenType: string, parselet: InfixParselet): this { this.infixParselets[tokenType] = parselet; return this; } public registerPrefix(tokenType: string, parselet: PrefixParselet): this { this.prefixParselets[tokenType] = parselet; return this; } public prefix(tokenType: string, precedence: number, builder: (token: Token, right: Atom) => Atom): this { return this.registerPrefix(tokenType, { parse(parser, token) { const right = parser.consumeAndParse(precedence); return builder(token, right); }, }); } public infixLeft( tokenType: string, precedence: number, builder: (left: Atom, token: Token, right: Atom) => Atom ): this { return this.registerInfix(tokenType, { parse(parser, left, token) { const right = parser.consumeAndParse(precedence); return builder(left, token, right); }, precedence, }); } public construct(input: Token[]): Parser { return new Parser(input, this.prefixParselets, this.infixParselets); } } export class Parser { private tokens: Token[]; private readonly prefixParselets: Record<string, PrefixParselet>; private readonly infixParselets: Record<string, InfixParselet>; constructor( tokens: Token[], prefixParselets: Record<string, PrefixParselet>, infixParselets: Record<string, InfixParselet> ) { this.tokens = tokens; this.prefixParselets = prefixParselets; this.infixParselets = infixParselets; } hasMore(): boolean { return this.tokens.length > 0; } match(expected: string): boolean { const token = this.peek(); if (token?.id !== expected) { return false; } this.consume(); return true; } consumeAndParse(precedence = Infinity): Atom { const token = this.consume(); const prefix = this.prefixParselets[token.id]; if (!prefix) { throw new Error( `Parse error at "${token.value}" (line ${token.line}, column ${token.column}). No matching prefix parselet.` ); } let left = prefix.parse(this, token); while (precedence > this.getPrecedence()) { const next = this.consume(); const infix = this.getInfixParselet(next) as InfixParselet; left = (infix.parse as (parser: Parser, left: Atom, token: Token) => Atom)(this, left, next); } return left; } getPrecedence(): number { const nextToken = this.peek(); if (!nextToken) { return Infinity; } const parser = this.getInfixParselet(nextToken); if (parser) { return parser.precedence; } return Infinity; } consume(expectedId?: string, expectedValue?: string): Token { if (!this.tokens.length) { throw new Error('Cant consume unknown more tokens.'); } if (expectedId && this.peek()?.id !== expectedId) { const actual = this.peek() as Token; throw new Error( `Expected ${expectedId} but got "${actual.id}" (${actual.value}) at line ${actual.line} column ${actual.column}.` ); } if (expectedValue && this.peek()?.value !== expectedValue) { const actual = this.peek() as Token; throw new Error( `Expected "${expectedValue}" but got "${actual.value}" at line ${actual.line} column ${actual.column}.` ); } return this.tokens.shift() as Token; } peek(): Token | undefined { return this.tokens.length > 0 ? this.tokens[0] : undefined; } removeComments(): void { this.tokens = this.tokens.filter((t) => t.id !== 'Comment'); } getInfixParselet(token: Token): InfixParselet | undefined { return this.infixParselets[token.id === 'Symbol' ? token.value : token.id]; } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/medplum/medplum'

If you have feedback or need assistance with the MCP directory API, please join our Discord server