thi-ng/umbrella

View on GitHub
packages/sexpr/src/parse.ts

Summary

Maintainability
A
0 mins
Test Coverage
import { isString } from "@thi.ng/checks/is-string";
import {
    DEFAULT_SYNTAX,
    type ASTNode,
    type Expression,
    type Root,
    type SyntaxOpts,
    type Token,
} from "./api.js";
import { tokenize } from "./tokenize.js";

export class ParseError extends Error {
    line: number;
    col: number;

    constructor(msg: string, line: number, col: number) {
        super(msg);
        this.line = line;
        this.col = col;
    }
}

/**
 * Takes a `src` string or {@link Token} iteratable and parses it into
 * an AST, then returns tree's root node. Throws {@link ParseError} if
 * the token order causes illegal nesting. The error includes `line` and
 * `column` information of the offending token.
 *
 * @param src -
 * @param opts -
 */
export const parse = (
    src: string | Iterable<Token>,
    opts?: Partial<SyntaxOpts>
) => {
    const { scopes } = { ...DEFAULT_SYNTAX, ...opts };
    const scopeOpen = scopes.map((x) => x[0]);
    const scopeClose = scopes.map((x) => x[1]);
    const tree: ASTNode[] = [{ type: "root", children: [] }];
    let currScope = -1;
    for (let token of isString(src) ? tokenize(src, opts) : src) {
        const t = token.value;
        let tmp: number;
        if ((tmp = scopeOpen.indexOf(t)) !== -1) {
            tree.push({ type: "expr", value: t, children: [] });
            currScope = tmp;
        } else if ((tmp = scopeClose.indexOf(t)) !== -1) {
            if (tree.length < 2 || currScope !== tmp) {
                throw new ParseError(`unmatched '${t}'`, token.line, token.col);
            }
            (<Expression>tree[tree.length - 2]).children!.push(tree.pop()!);
            currScope = scopeOpen.indexOf(
                (<Expression>tree[tree.length - 1]).value
            );
        } else {
            let node: ASTNode;
            let value: number;
            if (t.startsWith('"')) {
                node = { type: "str", value: t.substring(1, t.length - 1) };
            } else if (
                (t.startsWith("0x") &&
                    !isNaN((value = parseInt(t.substring(2), 16)))) ||
                !isNaN((value = parseFloat(t)))
            ) {
                node = { type: "num", value };
            } else {
                node = { type: "sym", value: t };
            }
            (<Expression>tree[tree.length - 1]).children!.push(node);
        }
    }
    if (tree.length > 1) {
        throw new ParseError("unclosed s-expression", -1, -1);
    }
    return <Root>tree[0];
};