145 lines
3.9 KiB
JavaScript
145 lines
3.9 KiB
JavaScript
(() => {
|
|
"use strict";
|
|
|
|
// ------------------ S-EXPRESSIONS ------------------
|
|
const main = () => {
|
|
const expr = [
|
|
"((data \"quoted data\" 123 4.5)",
|
|
" (data (!@# (4.5) \"(more\" \"data)\")))"
|
|
]
|
|
.join("\n");
|
|
|
|
const [parse, residue] = parseExpr(
|
|
tokenized(expr)
|
|
);
|
|
|
|
return 0 < residue.length
|
|
? `Unparsed tokens: ${JSON.stringify(residue)}`
|
|
: 0 < parse.length
|
|
? [
|
|
JSON.stringify(parse, null, 2),
|
|
"Reserialized from parse:",
|
|
parse.map(serialized).join(" ")
|
|
]
|
|
.join("\n\n")
|
|
: "Could not be parsed";
|
|
};
|
|
|
|
// ---------------- EXPRESSION PARSER ----------------
|
|
|
|
// parseExpr [String] -> ([Expr], [String])
|
|
const parseExpr = tokens =>
|
|
// A tuple of (parsed trees, residual tokens)
|
|
// derived from a list of tokens.
|
|
until(finished)(readToken)([
|
|
[], tokens
|
|
]);
|
|
|
|
|
|
// finished :: ([Expr], [String]) -> Bool
|
|
const finished = ([, tokens]) =>
|
|
// True if no tokens remain, or the next
|
|
// closes a sub-expression.
|
|
0 === tokens.length || ")" === tokens[0];
|
|
|
|
|
|
// readToken :: ([Expr], [String]) -> ([Expr], [String])
|
|
const readToken = ([xs, tokens]) => {
|
|
// A tuple of enriched expressions and
|
|
// depleted tokens.
|
|
const [token, ...ts] = tokens;
|
|
|
|
// An open bracket introduces recursion over
|
|
// a sub-expression to define a sub-list.
|
|
return "(" === token
|
|
? (() => {
|
|
const [expr, rest] = parseExpr(ts);
|
|
|
|
return [xs.concat([expr]), rest.slice(1)];
|
|
})()
|
|
: ")" === token
|
|
? [xs, token]
|
|
: [xs.concat(atom(token)), ts];
|
|
};
|
|
|
|
// ------------------- ATOM PARSER -------------------
|
|
|
|
// atom :: String -> Expr
|
|
const atom = s =>
|
|
0 < s.length
|
|
? isNaN(s)
|
|
? "\"'".includes(s[0])
|
|
? s.slice(1, -1)
|
|
: {name: s}
|
|
: parseFloat(s, 10)
|
|
: "";
|
|
|
|
|
|
// ------------------ TOKENIZATION -------------------
|
|
|
|
// tokenized :: String -> [String]
|
|
const tokenized = s =>
|
|
// Brackets and quoted or unquoted atomic strings.
|
|
quoteTokens("\"")(s).flatMap(
|
|
segment => "\"" !== segment[0]
|
|
? segment.replace(/([()])/gu, " $1 ")
|
|
.split(/\s+/u)
|
|
.filter(Boolean)
|
|
: [segment]
|
|
);
|
|
|
|
|
|
// quoteTokens :: Char -> String -> [String]
|
|
const quoteTokens = q =>
|
|
// Alternating unquoted and quoted segments.
|
|
s => s.split(q).flatMap(
|
|
(k, i) => even(i)
|
|
? 0 < k.length
|
|
? [k]
|
|
: []
|
|
: [`${q}${k}${q}`]
|
|
);
|
|
|
|
// ------------------ SERIALIZATION ------------------
|
|
|
|
// serialized :: Expr -> String
|
|
const serialized = e => {
|
|
const t = typeof e;
|
|
|
|
return "number" === t
|
|
? `${e}`
|
|
: "string" === t
|
|
? `"${e}"`
|
|
: "object" === t
|
|
? Array.isArray(e)
|
|
? `(${e.map(serialized).join(" ")})`
|
|
: e.name
|
|
: "?";
|
|
};
|
|
|
|
|
|
// --------------------- GENERIC ---------------------
|
|
|
|
// even :: Int -> Bool
|
|
const even = n =>
|
|
// True if 2 is a factor of n.
|
|
0 === n % 2;
|
|
|
|
|
|
// until :: (a -> Bool) -> (a -> a) -> a -> a
|
|
const until = p =>
|
|
// The value resulting from repeated applications
|
|
// of f to the seed value x, terminating when
|
|
// that result returns true for the predicate p.
|
|
f => {
|
|
const go = x =>
|
|
p(x)
|
|
? x
|
|
: go(f(x));
|
|
|
|
return go;
|
|
};
|
|
|
|
return main();
|
|
})();
|